mirror of https://github.com/jumpserver/jumpserver
Dev2 (#1766)
* [Update] 初始化操作日志 * [Feature] 完成操作日志记录 * [Update] 修改mfa失败提示 * [Update] 修改增加created by内容 * [Update] 增加改密日志 * [Update] 登录日志迁移到日志审计中 * [Update] change block user logic, if login success, clean block limit * [Update] 更新中/英文翻译(ALL) (#1662) * Revert "授权页面分页问题" * 增加命令导出 (#1566) * [Update] gunicorn不使用eventlet * [Update] 添加eventlet * 替换淘宝IP查询接口 * [Feature] 添加命令记录下载功能 (#1559) * [Feature] 添加命令记录下载功能 * [Update] 文案修改,导出记录、提交,取消全部命令导出 * [Update] 命令导出,修复时间问题 * [Update] paramiko => 2.4.1 * [Update] 修改settings * [Update] 修改权限判断 * Dev (#1646) * [Update] 添加org * [Update] 修改url * [Update] 完成基本框架 * [Update] 修改一些逻辑 * [Update] 修改用户view * [Update] 修改资产 * [Update] 修改asset api * [Update] 修改协议小问题 * [Update] stash it * [Update] 修改约束 * [Update] 修改外键为org_id * [Update] 删掉Premiddleware * [Update] 修改Node * [Update] 修改get_current_org 为 proxy对象 current_org * [Bugfix] 解决Node.root() 死循环,移动AdminRequired到permission中 (#1571) * [Update] 修改permission (#1574) * Tmp org (#1579) * [Update] 添加org api, 升级到django 2.0 * [Update] fix some bug * [Update] 修改一些bug * [Update] 添加授权规则org (#1580) * [Update] 修复创建授权规则,显示org_name不是有效UUID的bug * [Update] 更新org之间隔离授权规则,解决QuerySet与Manager问题;修复创建用户,显示org_name不是有效UUID之bug; * Tmp org (#1583) * [Update] 修改一些内容 * [Update] 修改datatable 支持process * [Bugfix] 修复asset queryset 没有valid方法的bug * [Update] 在线/历史/命令model添加org;修复命令记录保存org失败bug (#1584) * [Update] 修复创建授权规则,显示org_name不是有效UUID的bug * [Update] 更新org之间隔离授权规则,解决QuerySet与Manager问题;修复创建用户,显示org_name不是有效UUID之bug; * [Update] 在线/历史/命令model添加org * [Bugfix] 修复命令记录,保存org不成功bug * [Update] Org功能修改 * [Bugfix] 修复merge带来的问题 * [Update] org admin显示资产详情右侧选项卡;修复资产授权添加用户,会显示其他org用户的bug (#1594) * [Bugfix] 修复资产授权添加用户,显示其他org的用户bug * [Update] org admin 显示资产详情右侧选项卡 * Tmp org (#1596) * [Update] 修改index view * [Update] 修改nav * [Update] 修改profile * [Bugfix] 修复org下普通用户打开web终端看不到已被授权的资产和节点bug * [Update] 修改get_all_assets * [Bugfix] 修复节点前面有个空目录 * [Bugfix] 修复merge引起的bug * [Update] Add init * [Update] Node get_all_assets 过滤游离资产,条件nodes_key=None -> nodes=None * [Update] 恢复原来的api地址 * [Update] 修改api * [Bugfix] 修复org下用户查看我的资产不显示已授权节点/资产的bug * [Bugfix] Fix perm name unique * [Bugfix] 修复校验失败api * [Update] Merge with org * [Merge] 修改一下bug * [Update] 暂时修改一些url * [Update] 修改url 为django 2.0 path * [Update] 优化datatable 和显示组织优化 * [Update] 升级url * [Bugfix] 修复coco启动失败(load_config_from_server)、硬件刷新,测试连接,str 没有 decode(… (#1613) * [Bugfix] 修复coco启动失败(load_config_from_server)、硬件刷新,测试连接,str 没有 decode() method的bug * [Bugfix] (task任务系统)修复资产连接性测试、硬件刷新和系统用户连接性测试失败等bug * [Bugfix] 修复一些bug * [Bugfix] 修复一些bug * [Update] 更新org下普通用户的资产详情 (#1619) * [Update] 更新org下普通用户查看资产详情,只显示数据 * [Update] 优化org下普通用户查看资产详情前端代码 * [Update] 创建/更新用户的role选项;密码强度提示信息中英文; (#1623) * [Update] 修改 超级管理员/组织管理员 在 创建/更新 用户时role的选项 问题 * [Update] 用户密码强度提示信息支持中英文 * [Update] 修改token返回 * [Update] Asset返回org name * [Update] 修改支持xpack * [Update] 修改url * [Bugfix] 修复不登录就能查看资产的bug * [Update] 用户修改 * [Bugfix] ... * [Bugfix] 修复跳转错误的问题 * [Update] xpack/orgs组织添加删除功能-js; 修复Label继承Org后bug; (#1644) * [Update] 更新xpack下orgs的翻译信息 * [Update] 更新model Label,继承OrgModelMixin; * [Update] xpack/orgs组织添加删除功能-js; 修复Label继承Org后bug; * [Bugfix] 修复小bug * [Update] 优化一些api * [Update] 优化用户资产页面 * [Update] 更新 xpack/orgs 删除功能:限制在当前org下删除当前org (#1645) * [Update] 修改版本号 * [Update] 添加功能: 语言切换(中/英);修改 header_bar <商业支持、文档>显示方式 * [Update] 中/英切换文案修改;修改django_language key 从 settings 中获取 * [Update] 修改Dashboard页面文案,支持英文 * [Update] 更新中/英文翻译(ALL) * [Update] 解决翻译文件冲突 * [Update] 系统用户支持单独隋松 * [Update] 重置用户MFA * [Update] 设置session空闲时间 * [Update] 加密setting配置 * [Update] 修改单独推送和测试资产可连接性 * [Update] 添加功能:用户个人详情页添加 更改MFA操作 (#1748) * [Update] 添加功能:用户个人详情页添加 更改MFA操作 * [Update] 删除print * [Bugfix] 添加部分views的权限控制;从组织移除用户,同时从授权规则和用户组中移除此用户。 (#1746) * [Bugfix] 修复上传command log 为空 * [Update] 修复执行任务的bug * [Bugfix] 修复将用户从组内移除,其依然具有之前的组权限的bug, perms and user_groups * [Bugfix] 修复组管理员可以访问部分url-views的bug(如: /settings/)添加views权限控制 * [Update] 修改日志滚动 * [Bugfix] 修复组织权限控制的bug (#1763) * [Bugfix] 修复将用户从组内移除,其依然具有之前的组权限的bug, perms and user_groups * [Bugfix] 修复组管理员可以访问部分url-views的bug(如: /settings/)添加views权限控制pull/1770/head
parent
dc918c031c
commit
fe45d839fb
|
@ -55,7 +55,7 @@ class NodeViewSet(viewsets.ModelViewSet):
|
|||
post_value = request.data.get('value')
|
||||
if node_value != post_value:
|
||||
return Response(
|
||||
{"msg": _("You cant update the root node name")},
|
||||
{"msg": _("You can't update the root node name")},
|
||||
status=400
|
||||
)
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
@ -218,7 +218,8 @@ class RefreshNodeHardwareInfoApi(APIView):
|
|||
node_id = kwargs.get('pk')
|
||||
node = get_object_or_404(self.model, id=node_id)
|
||||
assets = node.assets.all()
|
||||
task_name = _("更新节点资产硬件信息: {}".format(node.name))
|
||||
# task_name = _("更新节点资产硬件信息: {}".format(node.name))
|
||||
task_name = _("Update node asset hardware information: {}").format(node.name)
|
||||
task = update_assets_hardware_info_util.delay(assets, task_name=task_name)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
@ -231,6 +232,7 @@ class TestNodeConnectiveApi(APIView):
|
|||
node_id = kwargs.get('pk')
|
||||
node = get_object_or_404(self.model, id=node_id)
|
||||
assets = node.assets.all()
|
||||
task_name = _("测试节点下资产是否可连接: {}".format(node.name))
|
||||
# task_name = _("测试节点下资产是否可连接: {}".format(node.name))
|
||||
task_name = _("Test if the assets under the node are connectable: {}".format(node.name))
|
||||
task = test_asset_connectability_util.delay(assets, task_name=task_name)
|
||||
return Response({"task": task.id})
|
||||
|
|
|
@ -13,22 +13,27 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import generics
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
|
||||
from ..models import SystemUser
|
||||
from ..models import SystemUser, Asset
|
||||
from .. import serializers
|
||||
from ..tasks import push_system_user_to_assets_manual, \
|
||||
test_system_user_connectability_manual
|
||||
test_system_user_connectability_manual, push_system_user_a_asset_manual, \
|
||||
test_system_user_connectability_a_asset
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'SystemUserViewSet', 'SystemUserAuthInfoApi',
|
||||
'SystemUserPushApi', 'SystemUserTestConnectiveApi'
|
||||
'SystemUserPushApi', 'SystemUserTestConnectiveApi',
|
||||
'SystemUserAssetsListView', 'SystemUserPushToAssetApi',
|
||||
'SystemUserTestAssetConnectabilityApi',
|
||||
]
|
||||
|
||||
|
||||
|
@ -82,3 +87,43 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView):
|
|||
system_user = self.get_object()
|
||||
task = test_system_user_connectability_manual.delay(system_user)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
class SystemUserAssetsListView(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.AssetSerializer
|
||||
pagination_class = LimitOffsetPagination
|
||||
filter_fields = ("hostname", "ip")
|
||||
search_fields = filter_fields
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
return get_object_or_404(SystemUser, pk=pk)
|
||||
|
||||
def get_queryset(self):
|
||||
system_user = self.get_object()
|
||||
return system_user.assets.all()
|
||||
|
||||
|
||||
class SystemUserPushToAssetApi(generics.RetrieveAPIView):
|
||||
queryset = SystemUser.objects.all()
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
system_user = self.get_object()
|
||||
asset_id = self.kwargs.get('aid')
|
||||
asset = get_object_or_404(Asset, id=asset_id)
|
||||
task = push_system_user_a_asset_manual.delay(system_user, asset)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
class SystemUserTestAssetConnectabilityApi(generics.RetrieveAPIView):
|
||||
queryset = SystemUser.objects.all()
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
system_user = self.get_object()
|
||||
asset_id = self.kwargs.get('aid')
|
||||
asset = get_object_or_404(Asset, id=asset_id)
|
||||
task = test_system_user_connectability_a_asset.delay(system_user, asset)
|
||||
return Response({"task": task.id})
|
|
@ -52,7 +52,8 @@ class Cluster(models.Model):
|
|||
contact=forgery_py.name.full_name(),
|
||||
phone=forgery_py.address.phone(),
|
||||
address=forgery_py.address.city() + forgery_py.address.street_address(),
|
||||
operator=choice(['北京联通', '北京电信', 'BGP全网通']),
|
||||
# operator=choice(['北京联通', '北京电信', 'BGP全网通']),
|
||||
operator=choice([_('Beijing unicom'), _('Beijing telecom'), _('BGP full netcom')]),
|
||||
comment=forgery_py.lorem_ipsum.sentence(),
|
||||
created_by='Fake')
|
||||
try:
|
||||
|
|
|
@ -20,6 +20,9 @@ class Domain(OrgModelMixin):
|
|||
date_created = models.DateTimeField(auto_now_add=True, null=True,
|
||||
verbose_name=_('Date created'))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Domain")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
@ -53,3 +56,4 @@ class Gateway(AssetUser):
|
|||
|
||||
class Meta:
|
||||
unique_together = [('name', 'org_id')]
|
||||
verbose_name = _("Gateway")
|
||||
|
|
|
@ -24,6 +24,9 @@ class Node(OrgModelMixin):
|
|||
is_node = True
|
||||
_full_value_cache_key_prefix = '_NODE_VALUE_{}'
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Node")
|
||||
|
||||
def __str__(self):
|
||||
return self.full_value
|
||||
|
||||
|
|
|
@ -93,8 +93,8 @@ def update_assets_hardware_info_util(assets, task_name=None):
|
|||
"""
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
if task_name is None:
|
||||
# task_name = _("Update some assets hardware info")
|
||||
task_name = _("更新资产硬件信息")
|
||||
task_name = _("Update some assets hardware info")
|
||||
# task_name = _("更新资产硬件信息")
|
||||
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
|
||||
hostname_list = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
|
||||
if not hostname_list:
|
||||
|
@ -113,8 +113,8 @@ def update_assets_hardware_info_util(assets, task_name=None):
|
|||
|
||||
@shared_task
|
||||
def update_asset_hardware_info_manual(asset):
|
||||
# task_name = _("Update asset hardware info")
|
||||
task_name = _("更新资产硬件信息")
|
||||
task_name = _("Update asset hardware info")
|
||||
# task_name = _("更新资产硬件信息")
|
||||
return update_assets_hardware_info_util([asset], task_name=task_name)
|
||||
|
||||
|
||||
|
@ -132,8 +132,8 @@ def update_assets_hardware_info_period():
|
|||
return
|
||||
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
# task_name = _("Update assets hardware info period")
|
||||
task_name = _("定期更新资产硬件信息")
|
||||
task_name = _("Update assets hardware info period")
|
||||
# task_name = _("定期更新资产硬件信息")
|
||||
hostname_list = [
|
||||
asset.fullname for asset in Asset.objects.all()
|
||||
if asset.is_active and asset.is_unixlike()
|
||||
|
@ -210,15 +210,15 @@ def test_admin_user_connectability_period():
|
|||
|
||||
admin_users = AdminUser.objects.all()
|
||||
for admin_user in admin_users:
|
||||
# task_name = _("Test admin user connectability period: {}".format(admin_user.name))
|
||||
task_name = _("定期测试管理账号可连接性: {}".format(admin_user.name))
|
||||
task_name = _("Test admin user connectability period: {}".format(admin_user.name))
|
||||
# task_name = _("定期测试管理账号可连接性: {}".format(admin_user.name))
|
||||
test_admin_user_connectability_util(admin_user, task_name)
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_admin_user_connectability_manual(admin_user):
|
||||
# task_name = _("Test admin user connectability: {}").format(admin_user.name)
|
||||
task_name = _("测试管理行号可连接性: {}").format(admin_user.name)
|
||||
task_name = _("Test admin user connectability: {}").format(admin_user.name)
|
||||
# task_name = _("测试管理行号可连接性: {}").format(admin_user.name)
|
||||
return test_admin_user_connectability_util(admin_user, task_name)
|
||||
|
||||
|
||||
|
@ -227,8 +227,8 @@ def test_asset_connectability_util(assets, task_name=None):
|
|||
from ops.utils import update_or_create_ansible_task
|
||||
|
||||
if task_name is None:
|
||||
# task_name = _("Test assets connectability")
|
||||
task_name = _("测试资产可连接性")
|
||||
task_name = _("Test assets connectability")
|
||||
# task_name = _("测试资产可连接性")
|
||||
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
|
||||
if not hosts:
|
||||
logger.info("No hosts, passed")
|
||||
|
@ -272,15 +272,16 @@ def set_system_user_connectablity_info(result, **kwargs):
|
|||
|
||||
|
||||
@shared_task
|
||||
def test_system_user_connectability_util(system_user, task_name):
|
||||
def test_system_user_connectability_util(system_user, assets, task_name):
|
||||
"""
|
||||
Test system cant connect his assets or not.
|
||||
:param system_user:
|
||||
:param assets:
|
||||
:param task_name:
|
||||
:return:
|
||||
"""
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
assets = system_user.get_assets()
|
||||
# assets = system_user.get_assets()
|
||||
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
|
||||
tasks = const.TEST_SYSTEM_USER_CONN_TASKS
|
||||
if not hosts:
|
||||
|
@ -299,7 +300,16 @@ def test_system_user_connectability_util(system_user, task_name):
|
|||
@shared_task
|
||||
def test_system_user_connectability_manual(system_user):
|
||||
task_name = _("Test system user connectability: {}").format(system_user)
|
||||
return test_system_user_connectability_util(system_user, task_name)
|
||||
assets = system_user.get_assets()
|
||||
return test_system_user_connectability_util(system_user, assets, task_name)
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_system_user_connectability_a_asset(system_user, asset):
|
||||
task_name = _("Test system user connectability: {} => {}").format(
|
||||
system_user, asset
|
||||
)
|
||||
return test_system_user_connectability_util(system_user, [asset], task_name)
|
||||
|
||||
|
||||
@shared_task
|
||||
|
@ -313,8 +323,8 @@ def test_system_user_connectability_period():
|
|||
|
||||
system_users = SystemUser.objects.all()
|
||||
for system_user in system_users:
|
||||
# task_name = _("Test system user connectability period: {}".format(system_user))
|
||||
task_name = _("定期测试系统用户可连接性: {}".format(system_user))
|
||||
task_name = _("Test system user connectability period: {}".format(system_user))
|
||||
# task_name = _("定期测试系统用户可连接性: {}".format(system_user))
|
||||
test_system_user_connectability_util(system_user, task_name)
|
||||
|
||||
|
||||
|
@ -393,13 +403,23 @@ def push_system_user_util(system_users, assets, task_name):
|
|||
@shared_task
|
||||
def push_system_user_to_assets_manual(system_user):
|
||||
assets = system_user.get_assets()
|
||||
task_name = "推送系统用户到入资产: {}".format(system_user.name)
|
||||
# task_name = "推送系统用户到入资产: {}".format(system_user.name)
|
||||
task_name = _("Push system users to assets: {}").format(system_user.name)
|
||||
return push_system_user_util([system_user], assets, task_name=task_name)
|
||||
|
||||
|
||||
@shared_task
|
||||
def push_system_user_a_asset_manual(system_user, asset):
|
||||
task_name = _("Push system users to asset: {} => {}").format(
|
||||
system_user.name, asset.fullname
|
||||
)
|
||||
return push_system_user_util([system_user], [asset], task_name=task_name)
|
||||
|
||||
|
||||
@shared_task
|
||||
def push_system_user_to_assets(system_user, assets):
|
||||
task_name = _("推送系统用户到入资产: {}").format(system_user.name)
|
||||
# task_name = _("推送系统用户到入资产: {}").format(system_user.name)
|
||||
task_name = _("Push system users to assets: {}").format(system_user.name)
|
||||
return push_system_user_util.delay([system_user], assets, task_name)
|
||||
|
||||
|
||||
|
|
|
@ -5,8 +5,11 @@
|
|||
|
||||
{% block help_message %}
|
||||
<div class="alert alert-info help-message">
|
||||
管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。
|
||||
Windows或其它硬件可以随意设置一个
|
||||
{# 管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。#}
|
||||
{# Windows或其它硬件可以随意设置一个#}
|
||||
{% trans 'Admin users are asset (charged server) on the root, or have NOPASSWD: ALL sudo permissions users, '%}
|
||||
{% trans 'Jumpserver users of the system using the user to `push system user`, `get assets hardware information`, etc. '%}
|
||||
{% trans 'You can set any one for Windows or other hardware.' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
|
||||
{% block form %}
|
||||
<div class="ydxbd" id="formlists" style="display: block;">
|
||||
<p id="tags_p" class="mgl-5 c02">选择需要修改属性</p>
|
||||
<p id="tags_p" class="mgl-5 c02">{% trans 'Select properties that need to be modified' %}</p>
|
||||
<div class="tagBtnList">
|
||||
<a class="label label-primary" id="change_all" value="1">全选</a>
|
||||
<a class="label label-primary" id="change_all" value="1">{% trans 'Select all' %}</a>
|
||||
{% for field in form %}
|
||||
{% if field.name != 'assets' %}
|
||||
<a data-id="{{ field.id_for_label }}" class="label label-default label-primary field-tag" value="1">{{ field.label }}</a>
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
|
||||
{% block help_message %}
|
||||
<div class="alert alert-info help-message">
|
||||
左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产
|
||||
{# 左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产#}
|
||||
{% trans 'The left side is the asset tree, right click to create, delete, and change the tree node, authorization asset is also organized as a node, and the right side is the asset under that node' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -627,6 +628,7 @@ $(document).ready(function(){
|
|||
text: "{% trans 'This will delete the selected assets !!!' %}",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonText: "{% trans 'Cancel' %}",
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: false
|
||||
|
|
|
@ -120,8 +120,8 @@ $(document).ready(function(){
|
|||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: "GET",
|
||||
success_message: "可连接",
|
||||
fail_message: "连接失败"
|
||||
success_message: "{% trans 'Can be connected' %}",
|
||||
fail_message: "{% trans 'The connection fails' %}"
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -4,8 +4,11 @@
|
|||
|
||||
{% block help_message %}
|
||||
<div class="alert alert-info help-message">
|
||||
网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过网关服务器进行跳转登录。<br>
|
||||
JMS => 网域网关 => 目标资产
|
||||
{# 网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过网关服务器进行跳转登录。<br>#}
|
||||
{# JMS => 网域网关 => 目标资产#}
|
||||
{% trans 'The domain function is added to address the fact that some environments (such as the hybrid cloud) cannot be connected directly by jumping on the gateway server.' %}
|
||||
<br>
|
||||
{% trans 'JMS => Domain gateway => Target assets' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
<th>{% trans 'IP' %}</th>
|
||||
<th>{% trans 'Port' %}</th>
|
||||
<th>{% trans 'Reachable' %}</th>
|
||||
<th>{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -67,14 +68,6 @@
|
|||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td width="50%">{% trans 'Push system user now' %}:</td>
|
||||
<td>
|
||||
<span style="float: right">
|
||||
<button type="button" class="btn btn-primary btn-xs btn-push" style="width: 54px">{% trans 'Push' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">{% trans 'Test assets connective' %}:</td>
|
||||
<td>
|
||||
<span style="float: right">
|
||||
|
@ -82,6 +75,52 @@
|
|||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% if system_user.auto_push %}
|
||||
<tr>
|
||||
<td width="50%">{% trans 'Push system user now' %}:</td>
|
||||
<td>
|
||||
<span style="float: right">
|
||||
<button type="button" class="btn btn-primary btn-xs btn-push" style="width: 54px">{% trans 'Push' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Nodes' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table node_edit" id="add-asset2group">
|
||||
<tbody>
|
||||
<form>
|
||||
<tr>
|
||||
<td colspan="2" class="no-borders">
|
||||
<select data-placeholder="{% trans 'Add to node' %}" id="node_selected" class="select2" style="width: 100%" multiple="" tabindex="4">
|
||||
{% for node in nodes_remain %}
|
||||
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="no-borders">
|
||||
<button type="button" class="btn btn-info btn-sm" id="btn-add-to-node">{% trans 'Confirm' %}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
|
||||
{% for node in system_user.nodes.all %}
|
||||
<tr>
|
||||
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node }}</b></td>
|
||||
<td>
|
||||
<button class="btn btn-danger pull-right btn-xs btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -96,7 +135,7 @@
|
|||
{% block custom_foot_js %}
|
||||
<script>
|
||||
function initAssetsTable() {
|
||||
var unreachable = {{ system_user.unreachable_assets|safe}};
|
||||
var unreachable = {{ system_user.unreachable_assets|safe }};
|
||||
var options = {
|
||||
ele: $('#system_user_list'),
|
||||
buttons: [],
|
||||
|
@ -112,27 +151,64 @@ function initAssetsTable() {
|
|||
} else {
|
||||
$(td).html('<i class="fa fa-check text-navy"></i>')
|
||||
}
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
var push_btn = '';
|
||||
{% if system_user.auto_push %}
|
||||
push_btn = '<a class="btn btn-xs btn-primary btn-push-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Push" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
|
||||
{% endif %}
|
||||
var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
|
||||
{#var unbound_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-asset-unbound" data-uid="{{ DEFAULT_PK }}">{% trans "Unbound" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);#}
|
||||
$(td).html(push_btn + test_btn);
|
||||
}}
|
||||
],
|
||||
ajax_url: '{% url "api-assets:asset-list" %}?system_user_id={{ system_user.id }}',
|
||||
columns: [{data: "hostname" }, {data: "ip" }, {data: "port" }, {data: "hostname" }],
|
||||
ajax_url: '{% url "api-assets:system-user-assets" pk=system_user.id %}',
|
||||
columns: [{data: "hostname" }, {data: "ip" }, {data: "port" }, {data: "hostname" }, {data: "id"}],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
jumpserver.initServerSideDataTable(options);
|
||||
}
|
||||
|
||||
function updateSystemUserNode(nodes) {
|
||||
var the_url = "{% url 'api-assets:system-user-detail' pk=system_user.id %}";
|
||||
var body = {
|
||||
nodes: Object.assign([], nodes)
|
||||
};
|
||||
var success = function(data) {
|
||||
// remove all the selected groups from select > option and rendered ul element;
|
||||
$('.select2-selection__rendered').empty();
|
||||
$('#node_selected').val('');
|
||||
$.map(jumpserver.nodes_selected, function(node_name, index) {
|
||||
$('#opt_' + index).remove();
|
||||
// change tr html of user groups.
|
||||
$('.node_edit tbody').append(
|
||||
'<tr>' +
|
||||
'<td><b class="bdg_node" data-gid="' + index + '">' + node_name + '</b></td>' +
|
||||
'<td><button class="btn btn-danger btn-xs pull-right btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button></td>' +
|
||||
'</tr>'
|
||||
)
|
||||
});
|
||||
// clear jumpserver.nodes_selected
|
||||
jumpserver.nodes_selected = {};
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
success: success
|
||||
});
|
||||
}
|
||||
jumpserver.nodes_selected = {};
|
||||
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2()
|
||||
.on("select2:select", function (evt) {
|
||||
var data = evt.params.data;
|
||||
jumpserver.assets_selected[data.id] = data.text;
|
||||
jumpserver.asset_groups_selected[data.id] = data.text;
|
||||
})
|
||||
.on('select2:unselect', function(evt) {
|
||||
var data = evt.params.data;
|
||||
delete jumpserver.assets_selected[data.id];
|
||||
delete jumpserver.asset_groups_selected[data.id];
|
||||
});
|
||||
.on('select2:select', function(evt) {
|
||||
var data = evt.params.data;
|
||||
jumpserver.nodes_selected[data.id] = data.text;
|
||||
})
|
||||
.on('select2:unselect', function(evt) {
|
||||
var data = evt.params.data;
|
||||
delete jumpserver.nodes_selected[data.id];
|
||||
});
|
||||
initAssetsTable();
|
||||
})
|
||||
.on('click', '.btn-push', function () {
|
||||
|
@ -140,11 +216,16 @@ $(document).ready(function () {
|
|||
var error = function (data) {
|
||||
alert(data)
|
||||
};
|
||||
var success = function (data) {
|
||||
var task_id = data.task;
|
||||
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
|
||||
window.open(url, '', 'width=800,height=600,left=400,top=400')
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
error: error,
|
||||
method: 'GET',
|
||||
success_message: "{% trans "Task has been send, Go to ops task list seen result" %}"
|
||||
success: success
|
||||
});
|
||||
})
|
||||
.on('click', '.btn-test-connective', function () {
|
||||
|
@ -152,15 +233,85 @@ $(document).ready(function () {
|
|||
var error = function (data) {
|
||||
alert(data)
|
||||
};
|
||||
var success = function (data) {
|
||||
var task_id = data.task;
|
||||
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
|
||||
window.open(url, '', 'width=800,height=600,left=400,top=400')
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
error: error,
|
||||
method: 'GET',
|
||||
success_message: "{% trans "Task has been send, seen left assets status" %}"
|
||||
success: success
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
.on('click', '.btn-remove-from-node', function() {
|
||||
var $this = $(this);
|
||||
var $tr = $this.closest('tr');
|
||||
var $badge = $tr.find('.bdg_node');
|
||||
var gid = $badge.data('gid');
|
||||
var node_name = $badge.html() || $badge.text();
|
||||
$('#groups_selected').append(
|
||||
'<option value="' + gid + '" id="opt_' + gid + '">' + node_name + '</option>'
|
||||
);
|
||||
$tr.remove();
|
||||
var nodes = $('.bdg_node').map(function () {
|
||||
return $(this).data('gid');
|
||||
}).get();
|
||||
updateSystemUserNode(nodes);
|
||||
})
|
||||
.on('click', '#btn-add-to-node', function() {
|
||||
if (Object.keys(jumpserver.nodes_selected).length === 0) {
|
||||
return false;
|
||||
}
|
||||
var nodes = $('.bdg_node').map(function() {
|
||||
return $(this).data('gid');
|
||||
}).get();
|
||||
$.map(jumpserver.nodes_selected, function(value, index) {
|
||||
nodes.push(index);
|
||||
});
|
||||
updateSystemUserNode(nodes);
|
||||
})
|
||||
.on('click', '.btn-push-asset', function () {
|
||||
var $this = $(this);
|
||||
var asset_id = $this.data('uid');
|
||||
var the_url = "{% url 'api-assets:system-user-push-to-asset' pk=object.id aid=DEFAULT_PK %}";
|
||||
the_url = the_url.replace("{{ DEFAULT_PK }}", asset_id);
|
||||
var success = function (data) {
|
||||
var task_id = data.task;
|
||||
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
|
||||
window.open(url, '', 'width=800,height=600,left=400,top=400')
|
||||
};
|
||||
var error = function (data) {
|
||||
alert(data)
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: 'GET',
|
||||
success: success,
|
||||
error: error
|
||||
})
|
||||
})
|
||||
.on('click', '.btn-test-asset', function () {
|
||||
var $this = $(this);
|
||||
var asset_id = $this.data('uid');
|
||||
var the_url = "{% url 'api-assets:system-user-test-to-asset' pk=object.id aid=DEFAULT_PK %}";
|
||||
the_url = the_url.replace("{{ DEFAULT_PK }}", asset_id);
|
||||
var success = function (data) {
|
||||
var task_id = data.task;
|
||||
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
|
||||
window.open(url, '', 'width=800,height=600,left=400,top=400')
|
||||
};
|
||||
var error = function (data) {
|
||||
alert(data)
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: 'GET',
|
||||
success: success,
|
||||
error: error
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
<li class="active">
|
||||
<a href="{% url 'assets:system-user-detail' pk=system_user.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||
</li>
|
||||
{# <li>#}
|
||||
{# <a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center">#}
|
||||
{# <i class="fa fa-bar-chart-o"></i> {% trans 'Attached assets' %}#}
|
||||
{# </a>#}
|
||||
{# </li>#}
|
||||
<li>
|
||||
<a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center">
|
||||
<i class="fa fa-bar-chart-o"></i> {% trans 'Assets' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-default" href="{% url 'assets:system-user-update' pk=system_user.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
|
||||
</li>
|
||||
|
@ -152,63 +152,46 @@
|
|||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{# <tr>#}
|
||||
{# <td width="50%">{% trans 'Clear auth' %}:</td>#}
|
||||
{# <td>#}
|
||||
{# <span style="float: right">#}
|
||||
{# <button type="button" class="btn btn-primary btn-xs btn-clear-auth" style="width: 54px">{% trans 'Clear' %}</button>#}
|
||||
{# </span>#}
|
||||
{# </td>#}
|
||||
{# </tr>#}
|
||||
|
||||
{# <tr>#}
|
||||
{# <td width="50%">{% trans 'Change auth period' %}:</td>#}
|
||||
{# <td>#}
|
||||
{# <span style="float: right">#}
|
||||
{# <button type="button" class="btn btn-primary btn-xs" style="width: 54px">{% trans 'Reset' %}</button>#}
|
||||
{# </span>#}
|
||||
{# </td>#}
|
||||
{# </tr>#}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Nodes' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table node_edit" id="add-asset2group">
|
||||
<tbody>
|
||||
<form>
|
||||
<tr>
|
||||
<td colspan="2" class="no-borders">
|
||||
<select data-placeholder="{% trans 'Add to node' %}" id="node_selected" class="select2" style="width: 100%" multiple="" tabindex="4">
|
||||
{% for node in nodes_remain %}
|
||||
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="no-borders">
|
||||
<button type="button" class="btn btn-info btn-sm" id="btn-add-to-node">{% trans 'Confirm' %}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
|
||||
{% for node in system_user.nodes.all %}
|
||||
<tr>
|
||||
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node }}</b></td>
|
||||
<td>
|
||||
<button class="btn btn-danger pull-right btn-xs btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{# <div class="panel panel-info">#}
|
||||
{# <div class="panel-heading">#}
|
||||
{# <i class="fa fa-info-circle"></i> {% trans 'Nodes' %}#}
|
||||
{# </div>#}
|
||||
{# <div class="panel-body">#}
|
||||
{# <table class="table node_edit" id="add-asset2group">#}
|
||||
{# <tbody>#}
|
||||
{# <form>#}
|
||||
{# <tr>#}
|
||||
{# <td colspan="2" class="no-borders">#}
|
||||
{# <select data-placeholder="{% trans 'Add to node' %}" id="node_selected" class="select2" style="width: 100%" multiple="" tabindex="4">#}
|
||||
{# {% for node in nodes_remain %}#}
|
||||
{# <option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>#}
|
||||
{# {% endfor %}#}
|
||||
{# </select>#}
|
||||
{# </td>#}
|
||||
{# </tr>#}
|
||||
{# <tr>#}
|
||||
{# <td colspan="2" class="no-borders">#}
|
||||
{# <button type="button" class="btn btn-info btn-sm" id="btn-add-to-node">{% trans 'Confirm' %}</button>#}
|
||||
{# </td>#}
|
||||
{# </tr>#}
|
||||
{# </form>#}
|
||||
{##}
|
||||
{# {% for node in system_user.nodes.all %}#}
|
||||
{# <tr>#}
|
||||
{# <td ><b class="bdg_node" data-gid={{ node.id }}>{{ node }}</b></td>#}
|
||||
{# <td>#}
|
||||
{# <button class="btn btn-danger pull-right btn-xs btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button>#}
|
||||
{# </td>#}
|
||||
{# </tr>#}
|
||||
{# {% endfor %}#}
|
||||
{# </tbody>#}
|
||||
{# </table>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -338,13 +321,13 @@ $(document).ready(function () {
|
|||
var the_url = '{% url "api-assets:system-user-auth-info" pk=system_user.id %}';
|
||||
var name = '{{ system_user.name }}';
|
||||
swal({
|
||||
title: '你确定清除该系统用户的认证信息吗 ?',
|
||||
title: "{% trans 'Are you sure to remove authentication information for the system user ?' %}",
|
||||
text: " [" + name + "] ",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonText: '取消',
|
||||
cancelButtonText: "{% trans 'Cancel' %}",
|
||||
confirmButtonColor: "#ed5565",
|
||||
confirmButtonText: '确认',
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: true
|
||||
}, function () {
|
||||
APIUpdateAttr({
|
||||
|
|
|
@ -3,10 +3,13 @@
|
|||
|
||||
{% block help_message %}
|
||||
<div class="alert alert-info help-message">
|
||||
系统用户是 Jumpserver跳转登录资产时使用的用户,可以理解为登录资产用户,如 web, sa, dba(`ssh web@some-host`), 而不是使用某个用户的用户名跳转登录服务器(`ssh xiaoming@some-host`);
|
||||
简单来说是 用户使用自己的用户名登录Jumpserver, Jumpserver使用系统用户登录资产。
|
||||
系统用户创建时,如果选择了自动推送 Jumpserver会使用ansible自动推送系统用户到资产中,如果资产(交换机、windows)不支持ansible, 请手动填写账号密码。
|
||||
目前还不支持Windows的自动推送
|
||||
{# 系统用户是 Jumpserver跳转登录资产时使用的用户,可以理解为登录资产用户,如 web, sa, dba(`ssh web@some-host`), 而不是使用某个用户的用户名跳转登录服务器(`ssh xiaoming@some-host`);#}
|
||||
{# 简单来说是 用户使用自己的用户名登录Jumpserver, Jumpserver使用系统用户登录资产。#}
|
||||
{# 系统用户创建时,如果选择了自动推送 Jumpserver会使用ansible自动推送系统用户到资产中,如果资产(交换机、windows)不支持ansible, 请手动填写账号密码。#}
|
||||
{# 目前还不支持Windows的自动推送#}
|
||||
{% trans 'System user is Jumpserver jump login assets used by the users, can be understood as the user login assets, such as web, sa, the dba (` ssh web@some-host `), rather than using a user the username login server jump (` ssh xiaoming@some-host `); '%}
|
||||
{% trans 'In simple terms, users log into Jumpserver using their own username, and Jumpserver uses system users to log into assets. '%}
|
||||
{% trans 'When system users are created, if you choose auto push Jumpserver to use ansible push system users into the asset, if the asset (Switch, Windows) does not support ansible, please manually fill in the account password. Automatic push for Windows is not currently supported.' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -135,6 +138,7 @@ $(document).ready(function(){
|
|||
text: "{% trans 'This will delete the selected System Users !!!' %}",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonText: "{% trans 'Cancel' %}",
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: false
|
||||
|
|
|
@ -19,6 +19,8 @@ urlpatterns = [
|
|||
path('assets-bulk/', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'),
|
||||
path('system-user/<uuid:pk>/auth-info/',
|
||||
api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
|
||||
path('system-user/<uuid:pk>/assets/',
|
||||
api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
|
||||
path('assets/<uuid:pk>/refresh/',
|
||||
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'),
|
||||
path('assets/<uuid:pk>/alive/',
|
||||
|
@ -31,10 +33,16 @@ urlpatterns = [
|
|||
api.AdminUserAuthApi.as_view(), name='admin-user-auth'),
|
||||
path('admin-user/<uuid:pk>/connective/',
|
||||
api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'),
|
||||
|
||||
path('system-user/<uuid:pk>/push/',
|
||||
api.SystemUserPushApi.as_view(), name='system-user-push'),
|
||||
path('system-user/<uuid:pk>/asset/<uuid:aid>/push/',
|
||||
api.SystemUserPushToAssetApi.as_view(), name='system-user-push-to-asset'),
|
||||
path('system-user/<uuid:pk>/asset/<uuid:aid>/test/',
|
||||
api.SystemUserTestAssetConnectabilityApi.as_view(), name='system-user-test-to-asset'),
|
||||
path('system-user/<uuid:pk>/connective/',
|
||||
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
|
||||
|
||||
path('nodes/<uuid:pk>/children/',
|
||||
api.NodeChildrenApi.as_view(), name='node-children'),
|
||||
path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'),
|
||||
|
|
|
@ -94,6 +94,7 @@ class SystemUserAssetView(AdminUserRequiredMixin, DetailView):
|
|||
context = {
|
||||
'app': _('assets'),
|
||||
'action': _('System user asset'),
|
||||
'nodes_remain': Node.objects.exclude(systemuser=self.object)
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
|
|
@ -3,3 +3,6 @@ from django.apps import AppConfig
|
|||
|
||||
class AuditsConfig(AppConfig):
|
||||
name = 'audits'
|
||||
|
||||
def ready(self):
|
||||
from . import signals_handler
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from users.models import LoginLog
|
|
@ -4,6 +4,11 @@ from django.db import models
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins import OrgModelMixin
|
||||
from .hands import LoginLog
|
||||
|
||||
__all__ = [
|
||||
'FTPLog', 'OperateLog', 'PasswordChangeLog', 'UserLoginLog',
|
||||
]
|
||||
|
||||
|
||||
class FTPLog(OrgModelMixin):
|
||||
|
@ -16,3 +21,40 @@ class FTPLog(OrgModelMixin):
|
|||
filename = models.CharField(max_length=1024, verbose_name=_("Filename"))
|
||||
is_success = models.BooleanField(default=True, verbose_name=_("Success"))
|
||||
date_start = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class OperateLog(OrgModelMixin):
|
||||
ACTION_CREATE = 'create'
|
||||
ACTION_UPDATE = 'update'
|
||||
ACTION_DELETE = 'delete'
|
||||
ACTION_CHOICES = (
|
||||
(ACTION_CREATE, _("Create")),
|
||||
(ACTION_UPDATE, _("Update")),
|
||||
(ACTION_DELETE, _("Delete"))
|
||||
)
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
user = models.CharField(max_length=128, verbose_name=_('User'))
|
||||
action = models.CharField(max_length=16, choices=ACTION_CHOICES, verbose_name=_("Action"))
|
||||
resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type"))
|
||||
resource = models.CharField(max_length=128, verbose_name=_("Resource"))
|
||||
remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True)
|
||||
datetime = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return "<{}> {} <{}>".format(self.user, self.action, self.resource)
|
||||
|
||||
|
||||
class PasswordChangeLog(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
user = models.CharField(max_length=128, verbose_name=_('User'))
|
||||
change_by = models.CharField(max_length=128, verbose_name=_("Change by"))
|
||||
remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True)
|
||||
datetime = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return "{} change {}'s password".format(self.change_by, self.user)
|
||||
|
||||
|
||||
class UserLoginLog(LoginLog):
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.db import transaction
|
||||
|
||||
from jumpserver.utils import current_request
|
||||
from common.utils import get_request_ip
|
||||
from users.models import User
|
||||
from .models import OperateLog, PasswordChangeLog
|
||||
|
||||
|
||||
MODELS_NEED_RECORD = (
|
||||
'User', 'UserGroup', 'Asset', 'Node', 'AdminUser', 'SystemUser',
|
||||
'Domain', 'Gateway', 'Organization', 'AssetPermission',
|
||||
)
|
||||
|
||||
|
||||
def create_operate_log(action, sender, resource):
|
||||
user = current_request.user if current_request else None
|
||||
if not user or not user.is_authenticated:
|
||||
return
|
||||
model_name = sender._meta.object_name
|
||||
if model_name not in MODELS_NEED_RECORD:
|
||||
return
|
||||
resource_type = sender._meta.verbose_name
|
||||
remote_addr = get_request_ip(current_request)
|
||||
with transaction.atomic():
|
||||
OperateLog.objects.create(
|
||||
user=user, action=action, resource_type=resource_type,
|
||||
resource=resource, remote_addr=remote_addr
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, dispatch_uid="my_unique_identifier")
|
||||
def on_object_created_or_update(sender, instance=None, created=False, **kwargs):
|
||||
if created:
|
||||
action = OperateLog.ACTION_CREATE
|
||||
else:
|
||||
action = OperateLog.ACTION_UPDATE
|
||||
create_operate_log(action, sender, instance)
|
||||
|
||||
|
||||
@receiver(post_delete, dispatch_uid="my_unique_identifier")
|
||||
def on_object_delete(sender, instance=None, **kwargs):
|
||||
create_operate_log(OperateLog.ACTION_DELETE, sender, instance)
|
||||
|
||||
|
||||
@receiver(post_save, sender=User, dispatch_uid="my_unique_identifier")
|
||||
def on_user_change_password(sender, instance=None, **kwargs):
|
||||
if hasattr(instance, '_set_password'):
|
||||
if not current_request or not current_request.user.is_authenticated:
|
||||
return
|
||||
with transaction.atomic():
|
||||
PasswordChangeLog.objects.create(
|
||||
user=instance, change_by=current_request.user,
|
||||
remote_addr=get_request_ip(current_request),
|
||||
)
|
|
@ -66,7 +66,6 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block table_head %}
|
||||
<th class="text-center"></th>
|
||||
{# <th class="text-center">{% trans 'ID' %}</th>#}
|
||||
<th class="text-center">{% trans 'User' %}</th>
|
||||
<th class="text-center">{% trans 'Asset' %}</th>
|
||||
|
@ -82,7 +81,6 @@
|
|||
{% block table_body %}
|
||||
{% for object in object_list %}
|
||||
<tr class="gradeX">
|
||||
<td class="text-center"><input type="checkbox" value="{{ object.id }}"></td>
|
||||
{# <td class="text-center">#}
|
||||
{# <a href="{% url 'terminal:object-detail' pk=object.id %}">{{ forloop.counter }}</a>#}
|
||||
{# </td>#}
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
{% extends '_base_list.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load terminal_tags %}
|
||||
{% load common_tags %}
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
||||
<style>
|
||||
#search_btn {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_left_head %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block table_search %}
|
||||
<form id="search_form" method="get" action="" class="pull-right form-inline">
|
||||
<div class="form-group" id="date">
|
||||
<div class="input-daterange input-group" id="datepicker">
|
||||
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
|
||||
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d' }}">
|
||||
<span class="input-group-addon">to</span>
|
||||
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<select class="select2 form-control" name="user">
|
||||
<option value="">{% trans 'User' %}</option>
|
||||
{% for u in user_list %}
|
||||
<option value="{{ u }}" {% if u == user %} selected {% endif %}>{{ u }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<select class="select2 form-control" name="asset">
|
||||
<option value="">{% trans 'Action' %}</option>
|
||||
{% for k, v in actions.items %}
|
||||
<option value="{{ k }}" {% if k == action %} selected {% endif %}>{{ v }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<select class="select2 form-control" name="system_user">
|
||||
<option value="">{% trans 'Resource Type' %}</option>
|
||||
{% for r in resource_type_list %}
|
||||
<option value="{{ r }}" {% if r == resource_type %} selected {% endif %}>{{ r }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<div class="input-group-btn">
|
||||
<button id='search_btn' type="submit" class="btn btn-sm btn-primary">
|
||||
{% trans 'Search' %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block table_head %}
|
||||
<th class="text-center">{% trans 'User' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
<th class="text-center">{% trans 'Resource Type' %}</th>
|
||||
<th class="text-center">{% trans 'Resource' %}</th>
|
||||
<th class="text-center">{% trans 'Remote addr' %}</th>
|
||||
<th class="text-center">{% trans 'Datetime' %}</th>
|
||||
{% endblock %}
|
||||
|
||||
{% block table_body %}
|
||||
{% for object in object_list %}
|
||||
<tr class="gradeX">
|
||||
{# <td class="text-center">#}
|
||||
{# <a href="{% url 'terminal:object-detail' pk=object.id %}">{{ forloop.counter }}</a>#}
|
||||
{# </td>#}
|
||||
<td class="text-center">{{ object.user }}</td>
|
||||
<td class="text-center">{{ object.get_action_display }}</td>
|
||||
<td class="text-center">{{ object.resource_type }}</td>
|
||||
<td class="text-center">{{ object.resource }}</td>
|
||||
<td class="text-center">{{ object.remote_addr }}</td>
|
||||
<td class="text-center">{{ object.datetime }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content_bottom_left %}
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('table').DataTable({
|
||||
"searching": false,
|
||||
"paging": false,
|
||||
"bInfo" : false,
|
||||
"order": [],
|
||||
"language": jumpserver.language
|
||||
});
|
||||
$('.select2').select2({
|
||||
dropdownAutoWidth: true,
|
||||
width: "auto"
|
||||
});
|
||||
$('.input-daterange.input-group').datepicker({
|
||||
format: "yyyy-mm-dd",
|
||||
todayBtn: "linked",
|
||||
keyboardNavigation: false,
|
||||
forceParse: false,
|
||||
calendarWeeks: true,
|
||||
autoclose: true
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
{% extends '_base_list.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load terminal_tags %}
|
||||
{% load common_tags %}
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
||||
<style>
|
||||
#search_btn {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_left_head %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block table_search %}
|
||||
<form id="search_form" method="get" action="" class="pull-right form-inline">
|
||||
<div class="form-group" id="date">
|
||||
<div class="input-daterange input-group" id="datepicker">
|
||||
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
|
||||
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d' }}">
|
||||
<span class="input-group-addon">to</span>
|
||||
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<select class="select2 form-control" name="user">
|
||||
<option value="">{% trans 'User' %}</option>
|
||||
{% for u in user_list %}
|
||||
<option value="{{ u }}" {% if u == user %} selected {% endif %}>{{ u }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<div class="input-group-btn">
|
||||
<button id='search_btn' type="submit" class="btn btn-sm btn-primary">
|
||||
{% trans 'Search' %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block table_head %}
|
||||
<th class="text-center">{% trans 'User' %}</th>
|
||||
<th class="text-center">{% trans 'Change by' %}</th>
|
||||
<th class="text-center">{% trans 'Remote addr' %}</th>
|
||||
<th class="text-center">{% trans 'Datetime' %}</th>
|
||||
{% endblock %}
|
||||
|
||||
{% block table_body %}
|
||||
{% for object in object_list %}
|
||||
<tr class="gradeX">
|
||||
<td class="text-center">{{ object.user }}</td>
|
||||
<td class="text-center">{{ object.change_by }}</td>
|
||||
<td class="text-center">{{ object.remote_addr }}</td>
|
||||
<td class="text-center">{{ object.datetime }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content_bottom_left %}
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('table').DataTable({
|
||||
"searching": false,
|
||||
"paging": false,
|
||||
"bInfo" : false,
|
||||
"order": [],
|
||||
"language": jumpserver.language
|
||||
});
|
||||
$('.select2').select2({
|
||||
dropdownAutoWidth: true,
|
||||
width: "auto"
|
||||
});
|
||||
$('.input-daterange.input-group').datepicker({
|
||||
format: "yyyy-mm-dd",
|
||||
todayBtn: "linked",
|
||||
keyboardNavigation: false,
|
||||
forceParse: false,
|
||||
calendarWeeks: true,
|
||||
autoclose: true
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -9,5 +9,8 @@ __all__ = ["urlpatterns"]
|
|||
app_name = "audits"
|
||||
|
||||
urlpatterns = [
|
||||
path('login-log/', views.LoginLogListView.as_view(), name='login-log-list'),
|
||||
path('ftp-log/', views.FTPLogListView.as_view(), name='ftp-log-list'),
|
||||
path('operate-log/', views.OperateLogListView.as_view(), name='operate-log-list'),
|
||||
path('password-change-log/', views.PasswordChangeLogList.as_view(), name='password-change-log-list'),
|
||||
]
|
||||
|
|
|
@ -1,11 +1,26 @@
|
|||
from django.conf import settings
|
||||
from django.views.generic import ListView
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.db.models import Q
|
||||
|
||||
from common.mixins import DatetimeSearchMixin
|
||||
from common.permissions import AdminUserRequiredMixin
|
||||
|
||||
from .models import FTPLog
|
||||
from orgs.utils import current_org
|
||||
from .models import FTPLog, OperateLog, PasswordChangeLog, UserLoginLog
|
||||
|
||||
|
||||
def get_resource_type_list():
|
||||
from users.models import User, UserGroup
|
||||
from assets.models import Asset, Node, AdminUser, SystemUser, Domain, Gateway
|
||||
from orgs.models import Organization
|
||||
from perms.models import AssetPermission
|
||||
|
||||
models = [
|
||||
User, UserGroup, Asset, Node, AdminUser, SystemUser, Domain,
|
||||
Gateway, Organization, AssetPermission
|
||||
]
|
||||
return [model._meta.verbose_name for model in models]
|
||||
|
||||
|
||||
class FTPLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
|
||||
|
@ -53,3 +68,125 @@ class FTPLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
|
|||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class OperateLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
|
||||
model = OperateLog
|
||||
template_name = 'audits/operate_log_list.html'
|
||||
paginate_by = settings.DISPLAY_PER_PAGE
|
||||
user = action = resource_type = ''
|
||||
date_from = date_to = None
|
||||
actions_dict = dict(OperateLog.ACTION_CHOICES)
|
||||
|
||||
def get_queryset(self):
|
||||
self.queryset = super().get_queryset()
|
||||
self.user = self.request.GET.get('user')
|
||||
self.action = self.request.GET.get('action')
|
||||
self.resource_type = self.request.GET.get('resource_type')
|
||||
|
||||
filter_kwargs = dict()
|
||||
filter_kwargs['datetime__gt'] = self.date_from
|
||||
filter_kwargs['datetime__lt'] = self.date_to
|
||||
if self.user:
|
||||
filter_kwargs['user'] = self.user
|
||||
if self.action:
|
||||
filter_kwargs['action'] = self.action
|
||||
if self.resource_type:
|
||||
filter_kwargs['resource_type'] = self.resource_type
|
||||
if filter_kwargs:
|
||||
self.queryset = self.queryset.filter(**filter_kwargs).order_by('-datetime')
|
||||
return self.queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'user_list': current_org.get_org_users(),
|
||||
'actions': self.actions_dict,
|
||||
'resource_type_list': get_resource_type_list(),
|
||||
'date_from': self.date_from,
|
||||
'date_to': self.date_to,
|
||||
'user': self.user,
|
||||
'action': self.action,
|
||||
'resource_type': self.resource_type,
|
||||
"app": _("Audits"),
|
||||
"action": _("Operate log"),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class PasswordChangeLogList(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
|
||||
model = PasswordChangeLog
|
||||
template_name = 'audits/password_change_log_list.html'
|
||||
paginate_by = settings.DISPLAY_PER_PAGE
|
||||
user = ''
|
||||
date_from = date_to = None
|
||||
|
||||
def get_queryset(self):
|
||||
self.queryset = super().get_queryset()
|
||||
self.user = self.request.GET.get('user')
|
||||
|
||||
filter_kwargs = dict()
|
||||
filter_kwargs['datetime__gt'] = self.date_from
|
||||
filter_kwargs['datetime__lt'] = self.date_to
|
||||
if self.user:
|
||||
filter_kwargs['user'] = self.user
|
||||
if filter_kwargs:
|
||||
self.queryset = self.queryset.filter(**filter_kwargs).order_by('-datetime')
|
||||
return self.queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'user_list': current_org.get_org_users(),
|
||||
'date_from': self.date_from,
|
||||
'date_to': self.date_to,
|
||||
'user': self.user,
|
||||
"app": _("Audits"),
|
||||
"action": _("Password change log"),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
|
||||
template_name = 'audits/login_log_list.html'
|
||||
model = UserLoginLog
|
||||
paginate_by = settings.DISPLAY_PER_PAGE
|
||||
user = keyword = ""
|
||||
date_to = date_from = None
|
||||
|
||||
@staticmethod
|
||||
def get_org_users():
|
||||
users = current_org.get_org_users().values_list('username', flat=True)
|
||||
return users
|
||||
|
||||
def get_queryset(self):
|
||||
users = self.get_org_users()
|
||||
queryset = super().get_queryset().filter(username__in=users)
|
||||
self.user = self.request.GET.get('user', '')
|
||||
self.keyword = self.request.GET.get("keyword", '')
|
||||
|
||||
queryset = queryset.filter(
|
||||
datetime__gt=self.date_from, datetime__lt=self.date_to
|
||||
)
|
||||
if self.user:
|
||||
queryset = queryset.filter(username=self.user)
|
||||
if self.keyword:
|
||||
queryset = queryset.filter(
|
||||
Q(ip__contains=self.keyword) |
|
||||
Q(city__contains=self.keyword) |
|
||||
Q(username__contains=self.keyword)
|
||||
)
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Users'),
|
||||
'action': _('Login log'),
|
||||
'date_from': self.date_from,
|
||||
'date_to': self.date_to,
|
||||
'user': self.user,
|
||||
'keyword': self.keyword,
|
||||
'user_list': self.get_org_users(),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
|
@ -74,3 +74,10 @@ class EncryptCharField(EncryptMixin, models.CharField):
|
|||
kwargs['max_length'] = 2048
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class FormEncryptMixin:
|
||||
pass
|
||||
|
||||
|
||||
class FormEncryptCharField(FormEncryptMixin, forms.CharField):
|
||||
pass
|
||||
|
|
|
@ -4,66 +4,71 @@ import json
|
|||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.html import escape
|
||||
from django.db import transaction
|
||||
from django.conf import settings
|
||||
|
||||
from .models import Setting
|
||||
from .fields import DictField
|
||||
from .models import Setting, common_settings
|
||||
from .fields import DictField, FormEncryptCharField, FormEncryptMixin
|
||||
|
||||
|
||||
def to_model_value(value):
|
||||
try:
|
||||
return json.dumps(value)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def to_form_value(value):
|
||||
try:
|
||||
data = json.loads(value)
|
||||
if isinstance(data, dict):
|
||||
data = value
|
||||
return data
|
||||
except json.JSONDecodeError:
|
||||
return ""
|
||||
# def to_model_value(value):
|
||||
# try:
|
||||
# return json.dumps(value)
|
||||
# except json.JSONDecodeError:
|
||||
# return None
|
||||
#
|
||||
#
|
||||
# def to_form_value(value):
|
||||
# try:
|
||||
# data = json.loads(value)
|
||||
# if isinstance(data, dict):
|
||||
# data = value
|
||||
# return data
|
||||
# except json.JSONDecodeError:
|
||||
# return ""
|
||||
|
||||
|
||||
class BaseForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
db_settings = Setting.objects.all()
|
||||
# db_settings = Setting.objects.all()
|
||||
for name, field in self.fields.items():
|
||||
db_value = getattr(db_settings, name).value
|
||||
db_value = getattr(common_settings, name)
|
||||
django_value = getattr(settings, name) if hasattr(settings, name) else None
|
||||
|
||||
if db_value is False or db_value:
|
||||
field.initial = to_form_value(db_value)
|
||||
field.initial = db_value
|
||||
elif django_value is False or django_value:
|
||||
field.initial = to_form_value(to_model_value(django_value))
|
||||
field.initial = django_value
|
||||
|
||||
def save(self, category="default"):
|
||||
if not self.is_bound:
|
||||
raise ValueError("Form is not bound")
|
||||
|
||||
db_settings = Setting.objects.all()
|
||||
if self.is_valid():
|
||||
with transaction.atomic():
|
||||
for name, value in self.cleaned_data.items():
|
||||
field = self.fields[name]
|
||||
if isinstance(field.widget, forms.PasswordInput) and not value:
|
||||
continue
|
||||
if value == to_form_value(getattr(db_settings, name).value):
|
||||
continue
|
||||
|
||||
defaults = {
|
||||
'name': name,
|
||||
'category': category,
|
||||
'value': to_model_value(value)
|
||||
}
|
||||
Setting.objects.update_or_create(defaults=defaults, name=name)
|
||||
else:
|
||||
# db_settings = Setting.objects.all()
|
||||
if not self.is_valid():
|
||||
raise ValueError(self.errors)
|
||||
|
||||
with transaction.atomic():
|
||||
for name, value in self.cleaned_data.items():
|
||||
field = self.fields[name]
|
||||
if isinstance(field.widget, forms.PasswordInput) and not value:
|
||||
continue
|
||||
if value == getattr(common_settings, name):
|
||||
continue
|
||||
|
||||
encrypted = True if isinstance(field, FormEncryptMixin) else False
|
||||
try:
|
||||
setting = Setting.objects.get(name=name)
|
||||
except Setting.DoesNotExist:
|
||||
setting = Setting()
|
||||
setting.name = name
|
||||
setting.category = category
|
||||
setting.encrypted = encrypted
|
||||
setting.cleaned_value = value
|
||||
setting.save()
|
||||
return setting
|
||||
|
||||
|
||||
class BasicSettingForm(BaseForm):
|
||||
SITE_URL = forms.URLField(
|
||||
|
@ -88,7 +93,7 @@ class EmailSettingForm(BaseForm):
|
|||
EMAIL_HOST_USER = forms.CharField(
|
||||
max_length=128, label=_("SMTP user"), initial='noreply@jumpserver.org'
|
||||
)
|
||||
EMAIL_HOST_PASSWORD = forms.CharField(
|
||||
EMAIL_HOST_PASSWORD = FormEncryptCharField(
|
||||
max_length=1024, label=_("SMTP password"), widget=forms.PasswordInput,
|
||||
required=False, help_text=_("Some provider use token except password")
|
||||
)
|
||||
|
@ -109,7 +114,7 @@ class LDAPSettingForm(BaseForm):
|
|||
AUTH_LDAP_BIND_DN = forms.CharField(
|
||||
label=_("Bind DN"), initial='cn=admin,dc=jumpserver,dc=org'
|
||||
)
|
||||
AUTH_LDAP_BIND_PASSWORD = forms.CharField(
|
||||
AUTH_LDAP_BIND_PASSWORD = FormEncryptCharField(
|
||||
label=_("Password"), initial='',
|
||||
widget=forms.PasswordInput, required=False
|
||||
)
|
||||
|
@ -194,6 +199,14 @@ class SecuritySettingForm(BaseForm):
|
|||
"number of times, no login is allowed during this time interval."
|
||||
)
|
||||
)
|
||||
SECURITY_MAX_IDLE_TIME = forms.IntegerField(
|
||||
initial=30, required=False,
|
||||
label=_("Connection max idle time"),
|
||||
help_text=_(
|
||||
'If idle time more than it, disconnect connection(only ssh now) '
|
||||
'Unit: minute'
|
||||
),
|
||||
)
|
||||
# min length
|
||||
SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
|
||||
initial=6, label=_("Password minimum length"),
|
||||
|
@ -223,9 +236,10 @@ class SecuritySettingForm(BaseForm):
|
|||
'and resets must contain numeric characters')
|
||||
)
|
||||
# special char
|
||||
SECURITY_PASSWORD_SPECIAL_CHAR= forms.BooleanField(
|
||||
SECURITY_PASSWORD_SPECIAL_CHAR = forms.BooleanField(
|
||||
initial=False, required=False,
|
||||
label=_("Must contain special characters"),
|
||||
help_text=_('After opening, the user password changes '
|
||||
'and resets must contain special characters')
|
||||
)
|
||||
|
||||
|
|
|
@ -7,6 +7,10 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from django.conf import settings
|
||||
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
|
||||
|
||||
from .utils import get_signer
|
||||
|
||||
signer = get_signer()
|
||||
|
||||
|
||||
class SettingQuerySet(models.QuerySet):
|
||||
def __getattr__(self, item):
|
||||
|
@ -26,6 +30,7 @@ class Setting(models.Model):
|
|||
name = models.CharField(max_length=128, unique=True, verbose_name=_("Name"))
|
||||
value = models.TextField(verbose_name=_("Value"))
|
||||
category = models.CharField(max_length=128, default="default")
|
||||
encrypted = models.BooleanField(default=False)
|
||||
enabled = models.BooleanField(verbose_name=_("Enabled"), default=True)
|
||||
comment = models.TextField(verbose_name=_("Comment"))
|
||||
|
||||
|
@ -34,10 +39,21 @@ class Setting(models.Model):
|
|||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __getattr__(self, item):
|
||||
instances = self.__class__.objects.filter(name=item)
|
||||
if len(instances) == 1:
|
||||
return instances[0].cleaned_value
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def cleaned_value(self):
|
||||
try:
|
||||
return json.loads(self.value)
|
||||
value = self.value
|
||||
if self.encrypted:
|
||||
value = signer.unsign(value)
|
||||
value = json.loads(value)
|
||||
return value
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
@ -45,6 +61,8 @@ class Setting(models.Model):
|
|||
def cleaned_value(self, item):
|
||||
try:
|
||||
v = json.dumps(item)
|
||||
if self.encrypted:
|
||||
v = signer.sign(v)
|
||||
self.value = v
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError("Json dump error: {}".format(str(e)))
|
||||
|
@ -59,11 +77,7 @@ class Setting(models.Model):
|
|||
pass
|
||||
|
||||
def refresh_setting(self):
|
||||
try:
|
||||
value = json.loads(self.value)
|
||||
except json.JSONDecodeError:
|
||||
return
|
||||
setattr(settings, self.name, value)
|
||||
setattr(settings, self.name, self.cleaned_value)
|
||||
|
||||
if self.name == "AUTH_LDAP":
|
||||
if self.cleaned_value and settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS:
|
||||
|
@ -81,3 +95,5 @@ class Setting(models.Model):
|
|||
class Meta:
|
||||
db_table = "settings"
|
||||
|
||||
|
||||
common_settings = Setting()
|
||||
|
|
|
@ -92,3 +92,9 @@ class AdminUserRequiredMixin(UserPassesTestMixin):
|
|||
return redirect('orgs:switch-a-org')
|
||||
return HttpResponseForbidden()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class SuperUserRequiredMixin(UserPassesTestMixin):
|
||||
def test_func(self):
|
||||
if self.request.user.is_authenticated and self.request.user.is_superuser:
|
||||
return True
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import post_save
|
||||
from django.db.models.signals import post_save, pre_save
|
||||
from django.conf import settings
|
||||
from django.db.utils import ProgrammingError, OperationalError
|
||||
|
||||
from jumpserver.utils import current_request
|
||||
from .models import Setting
|
||||
from .utils import get_logger
|
||||
from .signals import django_ready, ldap_auth_enable
|
||||
|
@ -42,3 +43,9 @@ def ldap_auth_on_changed(sender, enabled=True, **kwargs):
|
|||
if settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS:
|
||||
settings.AUTHENTICATION_BACKENDS.remove(settings.AUTH_LDAP_BACKEND)
|
||||
|
||||
|
||||
@receiver(pre_save, dispatch_uid="my_unique_identifier")
|
||||
def on_create_set_created_by(sender, instance=None, **kwargs):
|
||||
if hasattr(instance, 'created_by') and not instance.created_by:
|
||||
if current_request and current_request.user.is_authenticated:
|
||||
instance.created_by = current_request.user.name
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
|
||||
<h3>{% trans "User login settings" %}</h3>
|
||||
{% for field in form %}
|
||||
{% if forloop.counter == 4 %}
|
||||
{% if forloop.counter == 5 %}
|
||||
<div class="hr-line-dashed"></div>
|
||||
<h3>{% trans "Password check rule" %}</h3>
|
||||
{% endif %}
|
||||
|
|
|
@ -378,6 +378,15 @@ def get_signer():
|
|||
return signer
|
||||
|
||||
|
||||
def get_request_ip(request):
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')
|
||||
if x_forwarded_for and x_forwarded_for[0]:
|
||||
login_ip = x_forwarded_for[0]
|
||||
else:
|
||||
login_ip = request.META.get('REMOTE_ADDR', '')
|
||||
return login_ip
|
||||
|
||||
|
||||
class TeeObj:
|
||||
origin_stdout = sys.stdout
|
||||
|
||||
|
|
|
@ -6,11 +6,11 @@ from django.conf import settings
|
|||
|
||||
from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \
|
||||
TerminalSettingForm, SecuritySettingForm
|
||||
from common.permissions import AdminUserRequiredMixin
|
||||
from common.permissions import SuperUserRequiredMixin
|
||||
from .signals import ldap_auth_enable
|
||||
|
||||
|
||||
class BasicSettingView(AdminUserRequiredMixin, TemplateView):
|
||||
class BasicSettingView(SuperUserRequiredMixin, TemplateView):
|
||||
form_class = BasicSettingForm
|
||||
template_name = "common/basic_setting.html"
|
||||
|
||||
|
@ -36,7 +36,7 @@ class BasicSettingView(AdminUserRequiredMixin, TemplateView):
|
|||
return render(request, self.template_name, context)
|
||||
|
||||
|
||||
class EmailSettingView(AdminUserRequiredMixin, TemplateView):
|
||||
class EmailSettingView(SuperUserRequiredMixin, TemplateView):
|
||||
form_class = EmailSettingForm
|
||||
template_name = "common/email_setting.html"
|
||||
|
||||
|
@ -62,7 +62,7 @@ class EmailSettingView(AdminUserRequiredMixin, TemplateView):
|
|||
return render(request, self.template_name, context)
|
||||
|
||||
|
||||
class LDAPSettingView(AdminUserRequiredMixin, TemplateView):
|
||||
class LDAPSettingView(SuperUserRequiredMixin, TemplateView):
|
||||
form_class = LDAPSettingForm
|
||||
template_name = "common/ldap_setting.html"
|
||||
|
||||
|
@ -90,7 +90,7 @@ class LDAPSettingView(AdminUserRequiredMixin, TemplateView):
|
|||
return render(request, self.template_name, context)
|
||||
|
||||
|
||||
class TerminalSettingView(AdminUserRequiredMixin, TemplateView):
|
||||
class TerminalSettingView(SuperUserRequiredMixin, TemplateView):
|
||||
form_class = TerminalSettingForm
|
||||
template_name = "common/terminal_setting.html"
|
||||
|
||||
|
@ -120,7 +120,7 @@ class TerminalSettingView(AdminUserRequiredMixin, TemplateView):
|
|||
return render(request, self.template_name, context)
|
||||
|
||||
|
||||
class SecuritySettingView(AdminUserRequiredMixin, TemplateView):
|
||||
class SecuritySettingView(SuperUserRequiredMixin, TemplateView):
|
||||
form_class = SecuritySettingForm
|
||||
template_name = "common/security_setting.html"
|
||||
|
||||
|
|
Binary file not shown.
|
@ -6,6 +6,8 @@ import pytz
|
|||
from django.utils import timezone
|
||||
from django.shortcuts import HttpResponse
|
||||
|
||||
from .utils import set_current_request
|
||||
|
||||
|
||||
class TimezoneMiddleware:
|
||||
def __init__(self, get_response):
|
||||
|
@ -45,3 +47,13 @@ class DemoMiddleware:
|
|||
else:
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
|
||||
|
||||
class RequestMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
set_current_request(request)
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
|
|
|
@ -95,6 +95,7 @@ MIDDLEWARE = [
|
|||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'jumpserver.middleware.TimezoneMiddleware',
|
||||
'jumpserver.middleware.DemoMiddleware',
|
||||
'jumpserver.middleware.RequestMiddleware',
|
||||
'orgs.middleware.OrgMiddleware',
|
||||
]
|
||||
|
||||
|
@ -127,7 +128,6 @@ TEMPLATES = [
|
|||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'jumpserver.context_processor.jumpserver_processor',
|
||||
'django.template.context_processors.i18n',
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
|
@ -136,6 +136,7 @@ TEMPLATES = [
|
|||
'django.template.context_processors.static',
|
||||
'django.template.context_processors.request',
|
||||
'django.template.context_processors.media',
|
||||
'jumpserver.context_processor.jumpserver_processor',
|
||||
'orgs.context_processor.org_processor',
|
||||
*get_xpack_context_processor(),
|
||||
],
|
||||
|
@ -214,7 +215,10 @@ LOGGING = {
|
|||
},
|
||||
'file': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.FileHandler',
|
||||
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||
'when': "D",
|
||||
'interval': 1,
|
||||
"backupCount": 7,
|
||||
'formatter': 'main',
|
||||
'filename': os.path.join(PROJECT_DIR, 'logs', 'jumpserver.log')
|
||||
},
|
||||
|
@ -270,7 +274,8 @@ LOGGING = {
|
|||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.10/topics/i18n/
|
||||
LANGUAGE_CODE = 'en'
|
||||
# LANGUAGE_CODE = 'en'
|
||||
LANGUAGE_CODE = 'zh'
|
||||
|
||||
TIME_ZONE = 'Asia/Shanghai'
|
||||
|
||||
|
@ -281,7 +286,9 @@ USE_L10N = True
|
|||
USE_TZ = True
|
||||
|
||||
# I18N translation
|
||||
LOCALE_PATHS = [os.path.join(BASE_DIR, 'i18n'), ]
|
||||
LOCALE_PATHS = [
|
||||
os.path.join(BASE_DIR, 'locale'),
|
||||
]
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.10/howto/static-files/
|
||||
|
@ -441,7 +448,8 @@ TERMINAL_REPLAY_STORAGE = {
|
|||
|
||||
DEFAULT_PASSWORD_MIN_LENGTH = 6
|
||||
DEFAULT_LOGIN_LIMIT_COUNT = 7
|
||||
DEFAULT_LOGIN_LIMIT_TIME = 30
|
||||
DEFAULT_LOGIN_LIMIT_TIME = 30 # Unit: minute
|
||||
DEFAULT_SECURITY_MAX_IDLE_TIME = 30 # Unit: minute
|
||||
|
||||
# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
|
||||
BOOTSTRAP3 = {
|
||||
|
@ -465,4 +473,4 @@ SWAGGER_SETTINGS = {
|
|||
'type': 'basic'
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import os
|
|||
from django.urls import path, include, re_path
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.conf.urls.i18n import i18n_patterns
|
||||
from django.views.i18n import JavaScriptCatalog
|
||||
from rest_framework.response import Response
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.http import HttpResponse
|
||||
|
@ -14,7 +16,7 @@ from rest_framework import permissions
|
|||
from drf_yasg.views import get_schema_view
|
||||
from drf_yasg import openapi
|
||||
|
||||
from .views import IndexView, LunaView
|
||||
from .views import IndexView, LunaView, I18NView
|
||||
|
||||
schema_view = get_schema_view(
|
||||
openapi.Info(
|
||||
|
@ -78,10 +80,14 @@ app_view_patterns = [
|
|||
if settings.XPACK_ENABLED:
|
||||
app_view_patterns.append(path('xpack/', include('xpack.urls', namespace='xpack')))
|
||||
|
||||
js_i18n_patterns = i18n_patterns(
|
||||
path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('', IndexView.as_view(), name='index'),
|
||||
path('luna/', LunaView.as_view(), name='luna-error'),
|
||||
path('i18n/<str:lang>/', I18NView.as_view(), name='i18n-switch'),
|
||||
path('settings/', include('common.urls.view_urls', namespace='settings')),
|
||||
path('common/', include('common.urls.view_urls', namespace='common')),
|
||||
path('api/v1/', redirect_format_api),
|
||||
|
@ -93,6 +99,7 @@ urlpatterns = [
|
|||
urlpatterns += app_view_patterns
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \
|
||||
+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
urlpatterns += js_i18n_patterns
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from functools import partial
|
||||
|
||||
from common.utils import LocalProxy
|
||||
|
||||
try:
|
||||
from threading import local
|
||||
except ImportError:
|
||||
from django.utils._threading_local import local
|
||||
|
||||
_thread_locals = local()
|
||||
|
||||
|
||||
def set_current_request(request):
|
||||
setattr(_thread_locals, 'current_request', request)
|
||||
|
||||
|
||||
def _find(attr):
|
||||
return getattr(_thread_locals, attr, None)
|
||||
|
||||
|
||||
def get_current_request():
|
||||
return _find('current_request')
|
||||
|
||||
|
||||
current_request = LocalProxy(partial(_find, 'current_request'))
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import datetime
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.conf import settings
|
||||
from django.views.generic import TemplateView, View
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import redirect
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
|
@ -85,7 +87,7 @@ class IndexView(LoginRequiredMixin, TemplateView):
|
|||
return self.session_month.values('user').distinct().count()
|
||||
|
||||
def get_month_inactive_user_total(self):
|
||||
return User.objects.all().count() - self.get_month_active_user_total()
|
||||
return current_org.get_org_users().count() - self.get_month_active_user_total()
|
||||
|
||||
def get_month_active_asset_total(self):
|
||||
return self.session_month.values('asset').distinct().count()
|
||||
|
@ -95,7 +97,7 @@ class IndexView(LoginRequiredMixin, TemplateView):
|
|||
|
||||
@staticmethod
|
||||
def get_user_disabled_total():
|
||||
return User.objects.filter(is_active=False).count()
|
||||
return current_org.get_org_users().filter(is_active=False).count()
|
||||
|
||||
@staticmethod
|
||||
def get_asset_disabled_total():
|
||||
|
@ -173,11 +175,14 @@ class IndexView(LoginRequiredMixin, TemplateView):
|
|||
|
||||
class LunaView(View):
|
||||
def get(self, request):
|
||||
msg = """
|
||||
Luna是单独部署的一个程序,你需要部署luna,coco,配置nginx做url分发,
|
||||
如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运
|
||||
"""
|
||||
msg = _("<div>Luna is a separately deployed program, you need to deploy Luna, coco, configure nginx for url distribution,</div> "
|
||||
"</div>If you see this page, prove that you are not accessing the nginx listening port. Good luck.</div>")
|
||||
return HttpResponse(msg)
|
||||
|
||||
|
||||
|
||||
class I18NView(View):
|
||||
def get(self, request, lang):
|
||||
referer_url = request.META.get('HTTP_REFERER', '/')
|
||||
response = HttpResponseRedirect(referer_url)
|
||||
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang)
|
||||
return response
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
@ -0,0 +1,107 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-08-08 14:48+0800\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: static/js/jumpserver.js:158
|
||||
msgid "Update is successful!"
|
||||
msgstr "更新成功"
|
||||
|
||||
#: static/js/jumpserver.js:160
|
||||
msgid "An unknown error occurred while updating.."
|
||||
msgstr "更新时发生未知错误"
|
||||
|
||||
#: static/js/jumpserver.js:205 static/js/jumpserver.js:247
|
||||
#: static/js/jumpserver.js:252
|
||||
msgid "Error"
|
||||
msgstr "错误"
|
||||
|
||||
#: static/js/jumpserver.js:205
|
||||
msgid "Being used by the asset, please unbind the asset first."
|
||||
msgstr "正在被资产使用中,请先解除资产绑定"
|
||||
|
||||
#: static/js/jumpserver.js:212 static/js/jumpserver.js:260
|
||||
msgid "Delete the success"
|
||||
msgstr "删除成功"
|
||||
|
||||
#: static/js/jumpserver.js:219
|
||||
msgid "Are you sure about deleting it?"
|
||||
msgstr "你确定删除吗 ?"
|
||||
|
||||
#: static/js/jumpserver.js:224 static/js/jumpserver.js:273
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
#: static/js/jumpserver.js:227 static/js/jumpserver.js:276
|
||||
msgid "Confirm"
|
||||
msgstr "确认"
|
||||
|
||||
#: static/js/jumpserver.js:247
|
||||
msgid ""
|
||||
"The organization contains undeleted information. Please try again after "
|
||||
"deleting"
|
||||
msgstr "组织中包含未删除信息,请删除后重试"
|
||||
|
||||
#: static/js/jumpserver.js:252
|
||||
msgid ""
|
||||
"Do not perform this operation under this organization. Try again after "
|
||||
"switching to another organization"
|
||||
msgstr "请勿在此组织下执行此操作,切换到其他组织后重试"
|
||||
|
||||
#: static/js/jumpserver.js:267
|
||||
msgid ""
|
||||
"Please ensure that the following information in the organization has been "
|
||||
"deleted"
|
||||
msgstr "请确保组织内的以下信息已删除"
|
||||
|
||||
#: static/js/jumpserver.js:269
|
||||
msgid ""
|
||||
"User list、User group、Asset list、Domain list、Admin user、System user、"
|
||||
"Labels、Asset permission"
|
||||
msgstr ""
|
||||
"用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权"
|
||||
"规则"
|
||||
|
||||
#: static/js/jumpserver.js:311
|
||||
msgid "Loading ..."
|
||||
msgstr "加载中 ..."
|
||||
|
||||
#: static/js/jumpserver.js:313
|
||||
msgid "Search"
|
||||
msgstr "搜索"
|
||||
|
||||
#: static/js/jumpserver.js:317
|
||||
#, javascript-format
|
||||
msgid "Selected item %d"
|
||||
msgstr "选中 %d 项"
|
||||
|
||||
#: static/js/jumpserver.js:322
|
||||
msgid "Per page _MENU_"
|
||||
msgstr "每页 _MENU_"
|
||||
|
||||
#: static/js/jumpserver.js:324
|
||||
msgid ""
|
||||
"Displays the results of items _START_ to _END_; A total of _TOTAL_ entries"
|
||||
msgstr "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项"
|
||||
|
||||
#: static/js/jumpserver.js:328
|
||||
msgid "No match"
|
||||
msgstr "没有匹配项"
|
||||
|
||||
#: static/js/jumpserver.js:330
|
||||
msgid "No record"
|
||||
msgstr "没有记录"
|
|
@ -207,6 +207,7 @@ class AdHoc(models.Model):
|
|||
return {}
|
||||
|
||||
def run(self, record=True):
|
||||
set_to_root_org()
|
||||
if record:
|
||||
return self._run_and_record()
|
||||
else:
|
||||
|
|
|
@ -6,10 +6,10 @@ from django.views.generic import ListView, DetailView, TemplateView
|
|||
|
||||
from common.mixins import DatetimeSearchMixin
|
||||
from .models import Task, AdHoc, AdHocRunHistory, CeleryTask
|
||||
from common.permissions import AdminUserRequiredMixin
|
||||
from common.permissions import SuperUserRequiredMixin
|
||||
|
||||
|
||||
class TaskListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
|
||||
class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView):
|
||||
paginate_by = settings.DISPLAY_PER_PAGE
|
||||
model = Task
|
||||
ordering = ('-date_created',)
|
||||
|
@ -43,7 +43,7 @@ class TaskListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class TaskDetailView(AdminUserRequiredMixin, DetailView):
|
||||
class TaskDetailView(SuperUserRequiredMixin, DetailView):
|
||||
model = Task
|
||||
template_name = 'ops/task_detail.html'
|
||||
|
||||
|
@ -56,7 +56,7 @@ class TaskDetailView(AdminUserRequiredMixin, DetailView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class TaskAdhocView(AdminUserRequiredMixin, DetailView):
|
||||
class TaskAdhocView(SuperUserRequiredMixin, DetailView):
|
||||
model = Task
|
||||
template_name = 'ops/task_adhoc.html'
|
||||
|
||||
|
@ -69,7 +69,7 @@ class TaskAdhocView(AdminUserRequiredMixin, DetailView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class TaskHistoryView(AdminUserRequiredMixin, DetailView):
|
||||
class TaskHistoryView(SuperUserRequiredMixin, DetailView):
|
||||
model = Task
|
||||
template_name = 'ops/task_history.html'
|
||||
|
||||
|
@ -82,7 +82,7 @@ class TaskHistoryView(AdminUserRequiredMixin, DetailView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdHocDetailView(AdminUserRequiredMixin, DetailView):
|
||||
class AdHocDetailView(SuperUserRequiredMixin, DetailView):
|
||||
model = AdHoc
|
||||
template_name = 'ops/adhoc_detail.html'
|
||||
|
||||
|
@ -95,7 +95,7 @@ class AdHocDetailView(AdminUserRequiredMixin, DetailView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdHocHistoryView(AdminUserRequiredMixin, DetailView):
|
||||
class AdHocHistoryView(SuperUserRequiredMixin, DetailView):
|
||||
model = AdHoc
|
||||
template_name = 'ops/adhoc_history.html'
|
||||
|
||||
|
@ -108,7 +108,7 @@ class AdHocHistoryView(AdminUserRequiredMixin, DetailView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdHocHistoryDetailView(AdminUserRequiredMixin, DetailView):
|
||||
class AdHocHistoryDetailView(SuperUserRequiredMixin, DetailView):
|
||||
model = AdHocRunHistory
|
||||
template_name = 'ops/adhoc_history_detail.html'
|
||||
|
||||
|
@ -121,6 +121,6 @@ class AdHocHistoryDetailView(AdminUserRequiredMixin, DetailView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class CeleryTaskLogView(AdminUserRequiredMixin, DetailView):
|
||||
class CeleryTaskLogView(SuperUserRequiredMixin, DetailView):
|
||||
template_name = 'ops/celery_task_log.html'
|
||||
model = CeleryTask
|
||||
|
|
|
@ -20,6 +20,9 @@ class Organization(models.Model):
|
|||
ROOT_ID_NAME = 'ROOT'
|
||||
DEFAULT_ID_NAME = 'DEFAULT'
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Organization")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
@ -63,15 +66,18 @@ class Organization(models.Model):
|
|||
org = cls.default() if default else None
|
||||
return org
|
||||
|
||||
def get_org_users(self):
|
||||
def get_org_users(self, include_app=False):
|
||||
from users.models import User
|
||||
if self.is_default():
|
||||
users = User.objects.filter(orgs__isnull=True)
|
||||
elif not self.is_real():
|
||||
users = User.objects.all()
|
||||
elif self.is_root():
|
||||
users = User.objects.all()
|
||||
else:
|
||||
users = self.users.all()
|
||||
users = users.exclude(role=User.ROLE_APP)
|
||||
if not include_app:
|
||||
users = users.exclude(role=User.ROLE_APP)
|
||||
return users
|
||||
|
||||
def get_org_admins(self):
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.db.models.signals import m2m_changed
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .models import Organization
|
||||
from .hands import set_current_org, current_org, Node
|
||||
from perms.models import AssetPermission
|
||||
from users.models import UserGroup
|
||||
|
||||
|
||||
@receiver(post_save, sender=Organization)
|
||||
|
@ -21,3 +24,20 @@ def on_org_create_or_update(sender, instance=None, created=False, **kwargs):
|
|||
|
||||
if instance and not created:
|
||||
instance.expire_cache()
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Organization.users.through)
|
||||
def on_org_user_changed(sender, instance=None, **kwargs):
|
||||
if isinstance(instance, Organization):
|
||||
old_org = current_org
|
||||
set_current_org(instance)
|
||||
if kwargs['action'] == 'pre_remove':
|
||||
users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
|
||||
for user in users:
|
||||
perms = AssetPermission.objects.filter(users=user)
|
||||
user_groups = UserGroup.objects.filter(users=user)
|
||||
for perm in perms:
|
||||
perm.users.remove(user)
|
||||
for user_group in user_groups:
|
||||
user_group.users.remove(user)
|
||||
set_current_org(old_org)
|
||||
|
|
|
@ -40,5 +40,3 @@ def get_current_org():
|
|||
|
||||
|
||||
current_org = LocalProxy(partial(_find, 'current_org'))
|
||||
current_user = LocalProxy(partial(_find, 'current_user'))
|
||||
current_request = LocalProxy(partial(_find, 'current_request'))
|
||||
|
|
|
@ -6,23 +6,10 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
from orgs.mixins import OrgModelForm
|
||||
from orgs.utils import current_org
|
||||
from .hands import User
|
||||
from .models import AssetPermission
|
||||
|
||||
|
||||
class AssetPermissionForm(OrgModelForm):
|
||||
users = forms.ModelMultipleChoiceField(
|
||||
queryset=User.objects.exclude(role=User.ROLE_APP),
|
||||
label=_("User"),
|
||||
widget=forms.SelectMultiple(
|
||||
attrs={
|
||||
'class': 'select2',
|
||||
'data-placeholder': _('Select users')
|
||||
}
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if 'initial' not in kwargs:
|
||||
|
|
|
@ -42,6 +42,7 @@ class AssetPermission(OrgModelMixin):
|
|||
|
||||
class Meta:
|
||||
unique_together = [('org_id', 'name')]
|
||||
verbose_name = _("Asset permission")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
|
@ -154,8 +154,8 @@ function activeNav() {
|
|||
function APIUpdateAttr(props) {
|
||||
// props = {url: .., body: , success: , error: , method: ,}
|
||||
props = props || {};
|
||||
var success_message = props.success_message || '更新成功!';
|
||||
var fail_message = props.fail_message || '更新时发生未知错误.';
|
||||
var success_message = props.success_message || gettext('Update is successful!');
|
||||
var fail_message = props.fail_message || gettext('An unknown error occurred while updating..');
|
||||
var flash_message = props.flash_message || true;
|
||||
if (props.flash_message === false){
|
||||
flash_message = false;
|
||||
|
@ -199,25 +199,25 @@ function objectDelete(obj, name, url, redirectTo) {
|
|||
};
|
||||
var fail = function() {
|
||||
// swal("错误", "删除"+"[ "+name+" ]"+"遇到错误", "error");
|
||||
swal("错误", "[ "+name+" ]"+"正在被资产使用中,请先解除资产绑定", "error");
|
||||
swal(gettext('Error'), "[ "+name+" ]" + gettext("Being used by the asset, please unbind the asset first."), "error");
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: url,
|
||||
body: JSON.stringify(body),
|
||||
method: 'DELETE',
|
||||
success_message: "删除成功",
|
||||
success_message: gettext("Delete the success"),
|
||||
success: success,
|
||||
error: fail
|
||||
});
|
||||
}
|
||||
swal({
|
||||
title: '你确定删除吗 ?',
|
||||
title: gettext('Are you sure about deleting it?'),
|
||||
text: " [" + name + "] ",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonText: '取消',
|
||||
cancelButtonText: gettext('Cancel'),
|
||||
confirmButtonColor: "#ed5565",
|
||||
confirmButtonText: '确认',
|
||||
confirmButtonText: gettext('Confirm'),
|
||||
closeOnConfirm: true,
|
||||
}, function () {
|
||||
doDelete()
|
||||
|
@ -236,29 +236,29 @@ function orgDelete(obj, name, url, redirectTo){
|
|||
};
|
||||
var fail = function(responseText, status) {
|
||||
if (status === 400){
|
||||
swal("错误", "[ " + name + " ] 组织中包含未删除信息,请删除后重试", "error");
|
||||
swal(gettext("Error"), "[ " + name + " ] " + gettext("The organization contains undeleted information. Please try again after deleting"), "error");
|
||||
}
|
||||
else if (status === 405){
|
||||
swal("错误", "请勿在组织 [ "+ name + " ] 下执行此操作,切换到其他组织后重试", "error");
|
||||
swal(gettext("Error"), " [ "+ name + " ] " + gettext("Do not perform this operation under this organization. Try again after switching to another organization"), "error");
|
||||
}
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: url,
|
||||
body: JSON.stringify(body),
|
||||
method: 'DELETE',
|
||||
success_message: "删除成功",
|
||||
success_message: gettext("Delete the success"),
|
||||
success: success,
|
||||
error: fail
|
||||
});
|
||||
}
|
||||
swal({
|
||||
title: "请确保组织内的以下信息已删除",
|
||||
text: "用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权规则",
|
||||
title: gettext("Please ensure that the following information in the organization has been deleted"),
|
||||
text: gettext("User list、User group、Asset list、Domain list、Admin user、System user、Labels、Asset permission"),
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonText: '取消',
|
||||
cancelButtonText: gettext('Cancel'),
|
||||
confirmButtonColor: "#ed5565",
|
||||
confirmButtonText: '确认',
|
||||
confirmButtonText: gettext('Confirm'),
|
||||
closeOnConfirm: true
|
||||
}, function () {
|
||||
doDelete();
|
||||
|
@ -292,20 +292,20 @@ var jumpserver = {};
|
|||
jumpserver.checked = false;
|
||||
jumpserver.selected = {};
|
||||
jumpserver.language = {
|
||||
processing: "加载中",
|
||||
search: "搜索",
|
||||
processing: gettext('Loading ...'),
|
||||
search: gettext('Search'),
|
||||
select: {
|
||||
rows: {
|
||||
_: "选中 %d 项",
|
||||
_: gettext("Selected item %d"),
|
||||
0: ""
|
||||
}
|
||||
},
|
||||
lengthMenu: "每页 _MENU_",
|
||||
info: "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项",
|
||||
lengthMenu: gettext("Per page _MENU_"),
|
||||
info: gettext('Displays the results of items _START_ to _END_; A total of _TOTAL_ entries'),
|
||||
infoFiltered: "",
|
||||
infoEmpty: "",
|
||||
zeroRecords: "没有匹配项",
|
||||
emptyTable: "没有记录",
|
||||
zeroRecords: gettext("No match"),
|
||||
emptyTable: gettext('No record'),
|
||||
paginate: {
|
||||
first: "«",
|
||||
previous: "‹",
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
<strong>Copyright</strong> 北京堆栈科技有限公司 © 2014-2018
|
||||
{% load i18n %}
|
||||
<strong>Copyright</strong> {% trans ' Beijing Duizhan Tech, Inc. ' %} © 2014-2018
|
|
@ -6,6 +6,7 @@
|
|||
<!-- Custom and plugin javascript -->
|
||||
<script src="{% static "js/plugins/toastr/toastr.min.js" %}"></script>
|
||||
<script src="{% static "js/inspinia.js" %}"></script>
|
||||
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
|
||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
||||
<script>
|
||||
activeNav();
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
{% load i18n %}
|
||||
<div class="footer fixed">
|
||||
<div class="pull-right">
|
||||
Version <strong>1.4.0-{% include '_build.html' %}</strong> GPLv2.
|
||||
<!--<img style="display: none" src="http://www.jumpserver.org/img/evaluate_avatar1.jpg">-->
|
||||
</div>
|
||||
<div>
|
||||
<strong>Copyright</strong> 北京堆栈科技有限公司 © 2014-2018
|
||||
<strong>Copyright</strong> {% trans ' Beijing Duizhan Tech, Inc. ' %}© 2014-2018
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -13,16 +13,68 @@
|
|||
{# <li>#}
|
||||
{# <span class="m-r-sm text-muted welcome-message">{% trans 'Welcome to use Jumpserver system' %}</span>#}
|
||||
{# </li>#}
|
||||
|
||||
{# <li class="dropdown">#}
|
||||
{# <a class="count-info" href="https://market.aliyun.com/products/53690006/cmgj026011.html?spm=5176.730005.0.0.cY2io1" target="_blank">#}
|
||||
{# <span class="m-r-sm text-muted welcome-message">{% trans 'Supports' %}</span>#}
|
||||
{# </a>#}
|
||||
{# </li>#}
|
||||
{# <li class="dropdown">#}
|
||||
{# <a class="count-info" href="http://docs.jumpserver.org/" target="_blank">#}
|
||||
{# <span class="m-r-sm text-muted welcome-message">{% trans 'Docs' %}</span>#}
|
||||
{# </a>#}
|
||||
{# </li>#}
|
||||
|
||||
<li class="dropdown">
|
||||
<a class="count-info" href="https://market.aliyun.com/products/53690006/cmgj026011.html?spm=5176.730005.0.0.cY2io1" target="_blank">
|
||||
<span class="m-r-sm text-muted welcome-message">{% trans 'Supports' %}</span>
|
||||
<a class="count-info dropdown-toggle" data-toggle="dropdown" href="#" target="_blank">
|
||||
<i class="fa fa-handshake-o"></i>
|
||||
<span class="m-r-sm text-muted welcome-message">{% trans 'Help' %} <b class="caret"></b></span>
|
||||
</a>
|
||||
|
||||
<ul class="dropdown-menu animated fadeInRight m-t-xs profile-dropdown">
|
||||
<li>
|
||||
<a class="count-info" href="http://docs.jumpserver.org/" target="_blank">
|
||||
<i class="fa fa-file-text"></i>
|
||||
<span class="m-r-sm text-muted welcome-message">{% trans 'Docs' %}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="count-info" href="https://market.aliyun.com/products/53690006/cmgj026011.html?spm=5176.730005.0.0.cY2io1" target="_blank">
|
||||
<i class="fa fa-suitcase"></i>
|
||||
<span class="m-r-sm text-muted welcome-message">{% trans 'Commercial support' %}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="dropdown">
|
||||
<a class="count-info" href="http://docs.jumpserver.org/" target="_blank">
|
||||
<span class="m-r-sm text-muted welcome-message">{% trans 'Docs' %}</span>
|
||||
<a class="count-info dropdown-toggle" data-toggle="dropdown" href="#" target="_blank">
|
||||
<i class="fa fa-globe"></i>
|
||||
{% ifequal request.COOKIES.django_language 'en' %}
|
||||
<span class="m-r-sm text-muted welcome-message">English<b class="caret"></b></span>
|
||||
{% else %}
|
||||
<span class="m-r-sm text-muted welcome-message">中文<b class="caret"></b></span>
|
||||
{% endifequal %}
|
||||
</a>
|
||||
|
||||
<ul class="dropdown-menu animated fadeInRight m-t-xs profile-dropdown">
|
||||
<li>
|
||||
<a id="switch_cn" href="{% url 'i18n-switch' lang='zh-hans' %}">
|
||||
<i class="fa fa-flag"></i>
|
||||
<span> 中文</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a id="switch_en" href="{% url 'i18n-switch' lang='en' %}">
|
||||
<i class="fa fa-flag-checkered"></i>
|
||||
<span> English</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
||||
<li class="dropdown">
|
||||
{% if request.user.is_authenticated %}
|
||||
<a data-toggle="dropdown" class="dropdown-toggle" href="#">
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
<ul class="nav nav-second-level active">
|
||||
<li id="user"><a href="{% url 'users:user-list' %}">{% trans 'User list' %}</a></li>
|
||||
<li id="user-group"><a href="{% url 'users:user-group-list' %}">{% trans 'User group' %}</a></li>
|
||||
<li id="login-log"><a href="{% url 'users:login-log-list' %}">{% trans 'Login logs' %}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li id="assets">
|
||||
|
@ -68,7 +67,10 @@
|
|||
<i class="fa fa-history" style="width: 14px"></i> <span class="nav-label">{% trans 'Audits' %}</span><span class="fa arrow"></span>
|
||||
</a>
|
||||
<ul class="nav nav-second-level">
|
||||
<li id="login-log"><a href="{% url 'audits:login-log-list' %}">{% trans 'Login log' %}</a></li>
|
||||
<li id="ftp-log"><a href="{% url 'audits:ftp-log-list' %}">{% trans 'FTP log' %}</a></li>
|
||||
<li id="operate-log"><a href="{% url 'audits:operate-log-list' %}">{% trans 'Operate log' %}</a></li>
|
||||
<li id="password-change-log"><a href="{% url 'audits:password-change-log-list' %}">{% trans 'Password change log' %}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{#<li id="">#}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
{% load i18n %}
|
||||
{% load common_tags %}
|
||||
{% if is_paginated %}
|
||||
<div class="col-sm-4">
|
||||
<div class="dataTables_info text-center" id="editable_info" role="status" aria-live="polite">
|
||||
显示第 {{ page_obj.start_index }} 至 {{ page_obj.end_index }} 项结果,共 {{ paginator.count }} 项
|
||||
{# 显示第 {{ page_obj.start_index }} 至 {{ page_obj.end_index }} 项结果,共 {{ paginator.count }} 项#}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
|
@ -10,7 +11,7 @@
|
|||
<ul class="pagination" style="margin-top: 0; float: right">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="paginate_button previous" aria-controls="editable" tabindex="0" id="previous">
|
||||
<a data-page="next" href="?page={{ page_obj.previous_page_number}}">‹</a>
|
||||
<a data-page="next" class="page" href="?page={{ page_obj.previous_page_number}}">‹</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
|
@ -26,7 +27,7 @@
|
|||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="paginate_button next" aria-controls="editable" tabindex="0" id="next">
|
||||
<a data-page="next" href="?page={{ page_obj.next_page_number }}">›</a>
|
||||
<a data-page="next" class="page" href="?page={{ page_obj.next_page_number }}">›</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
@ -40,7 +41,7 @@
|
|||
var old_href = $(this).attr('href').replace('?', '');
|
||||
var searchArray = searchStr.split('&');
|
||||
|
||||
if (searchStr == '') {
|
||||
if (searchStr === '') {
|
||||
searchStr = '?page=1'
|
||||
}
|
||||
|
||||
|
@ -53,6 +54,13 @@
|
|||
$(this).attr('href', searchArray.join('&'));
|
||||
}
|
||||
})
|
||||
|
||||
$('#editable_info').html(
|
||||
"{% trans 'Displays the results of items _START_ to _END_; A total of _TOTAL_ entries' %}"
|
||||
.replace('_START_', {{ page_obj.start_index }})
|
||||
.replace('_END_', {{ page_obj.end_index }})
|
||||
.replace('_TOTAL_', {{ paginator.count }})
|
||||
)
|
||||
});
|
||||
|
||||
</script>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{% load i18n %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
@ -10,6 +11,7 @@
|
|||
|
||||
{% include '_head_css_js.html' %}
|
||||
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
|
||||
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
|
||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
||||
|
||||
</head>
|
||||
|
@ -40,7 +42,7 @@
|
|||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<a href="{{ redirect_url }}" class="btn btn-primary block full-width m-b">返回</a>
|
||||
<a href="{{ redirect_url }}" class="btn btn-primary block full-width m-b">{% trans 'Return' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content">
|
||||
|
@ -7,7 +8,7 @@
|
|||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label label-success pull-right">Users</span>
|
||||
<h5>用户总数</h5>
|
||||
<h5>{% trans 'Total users' %}</h5>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<h1 class="no-margins"><a href="{% url 'users:user-list' %}">{{ users_count }}</a></h1>
|
||||
|
@ -19,7 +20,7 @@
|
|||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label label-info pull-right">Hosts</span>
|
||||
<h5>主机总数</h5>
|
||||
<h5>{% trans 'Total hosts' %}</h5>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<h1 class="no-margins"><a href="{% url 'assets:asset-list' %}">{{ assets_count }}</a></h1>
|
||||
|
@ -32,7 +33,7 @@
|
|||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label label-primary pull-right">Online</span>
|
||||
<h5>在线用户</h5>
|
||||
<h5>{% trans 'Online users' %}</h5>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<h1 class="no-margins"><a href="{% url 'terminal:session-online-list' %}"> <span id="online_users"></span>{{ online_user_count }}</a></h1>
|
||||
|
@ -45,7 +46,8 @@
|
|||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label label-danger pull-right">Connected</span>
|
||||
<h5>在线会话</h5>
|
||||
<h5>{% trans 'Online sessions' %}</h5>
|
||||
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<h1 class="no-margins"><a href="{% url 'terminal:session-online-list' %}"> <span id="online_hosts"></span>{{ online_asset_count }}</a></h1>
|
||||
|
@ -56,13 +58,13 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-2 border-bottom white-bg dashboard-header" style="margin-left:15px;height: 346px">
|
||||
<h2>活跃用户TOP5</h2>
|
||||
<small>过去一周共有<span class="text-info">{{ user_visit_count_weekly }}</span>位用户登录<span class="text-success">{{ asset_visit_count_weekly }}</span>次资产.</small>
|
||||
<h2>{% trans ' Top 5 Active user' %}</h2>
|
||||
<small>{% trans 'In the past week, a total of ' %}<span class="text-info">{{ user_visit_count_weekly }}</span>{% trans ' users have logged in ' %}<span class="text-success">{{ asset_visit_count_weekly }}</span>{% trans ' times asset.' %}</small>
|
||||
<ul class="list-group clear-list m-t">
|
||||
{% for data in user_visit_count_top_five %}
|
||||
<li class="list-group-item fist-item">
|
||||
<span class="pull-right">
|
||||
{{ data.total }}次/周
|
||||
{{ data.total }}{% trans ' times/week' %}
|
||||
</span>
|
||||
<span class="label ">{{ forloop.counter }}</span> {{ data.user }}
|
||||
</li>
|
||||
|
@ -73,21 +75,20 @@
|
|||
<div class="col-sm-3 white-bg" id="top1" style="margin-left: -15px;height: 346px">
|
||||
<div class="statistic-box">
|
||||
<h4>
|
||||
活跃用户资产占比
|
||||
{% trans 'Active user asset ratio' %}
|
||||
</h4>
|
||||
<p>
|
||||
以下图形分别描述一个月活跃用户和资产占所有用户主机的百分比
|
||||
{% trans 'The following graphs describe the percentage of active users per month and assets per user host per month, respectively.' %}
|
||||
</p>
|
||||
<div class="row text-center">
|
||||
<div class="col-sm-6">
|
||||
<div id="activeUser" style="width: 140px; height: 140px;">
|
||||
|
||||
</div>
|
||||
<h5>用户</h5>
|
||||
<h5>{% trans 'User' %}</h5>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div id="activeAsset" style="width: 140px; height: 140px;"></div>
|
||||
<h5>主机</h5>
|
||||
<h5>{% trans 'Host' %}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-t">
|
||||
|
@ -102,7 +103,7 @@
|
|||
<div class="col-sm-4">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>一周Top10资产</h5>
|
||||
<h5>{% trans 'Top 10 assets in a week' %}</h5>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
|
@ -117,8 +118,8 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="ibox-content ibox-heading">
|
||||
<h3><i class="fa fa-inbox"></i> 一周Top10资产 </h3>
|
||||
<small><i class="fa fa-map-marker"></i> 登录次数及最近一次登录记录. </small>
|
||||
<h3><i class="fa fa-inbox"></i>{% trans 'Top 10 assets in a week'%}</h3>
|
||||
<small><i class="fa fa-map-marker"></i>{% trans 'Login frequency and last login record.' %}</small>
|
||||
</div>
|
||||
<div class="ibox-content inspinia-timeline">
|
||||
{% if week_asset_hot_ten %}
|
||||
|
@ -129,18 +130,18 @@
|
|||
<i class="fa fa-info-circle"></i>
|
||||
<strong data-toggle="tooltip" title="{{ data.asset }}">{{ data.asset }}</strong>
|
||||
<br/>
|
||||
<small class="text-navy">{{ data.total }}次</small>
|
||||
<small class="text-navy">{{ data.total }}{% trans ' times' %}</small>
|
||||
</div>
|
||||
<div class="col-xs-7 content no-top-border">
|
||||
<p class="m-b-xs">最近一次登录用户</p>
|
||||
<p class="m-b-xs">{% trans 'The last time a user logged in' %}</p>
|
||||
<p>{{ data.last.user }}</p>
|
||||
<p>于{{ data.last.date_start |date:"Y-m-d H:i:s" }}</p>
|
||||
<p>{% trans 'At ' %}{{ data.last.date_start |date:"Y-m-d H:i:s" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-center">(暂无)</p>
|
||||
<p class="text-center">{% trans '(No)' %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -148,14 +149,14 @@
|
|||
<div class="col-sm-4">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>最近十次登录</h5>
|
||||
<h5>{% trans 'Last 10 login' %}</h5>
|
||||
<div class="ibox-tools">
|
||||
<span class="label label-info-light">10 Messages</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content ibox-heading">
|
||||
<h3><i class="fa fa-paper-plane-o"></i> 登录记录 </h3>
|
||||
<small><i class="fa fa-map-marker"></i> 最近十次登录记录. </small>
|
||||
<h3><i class="fa fa-paper-plane-o"></i> {% trans 'Login record' %}</h3>
|
||||
<small><i class="fa fa-map-marker"></i>{% trans 'Last 10 login records.' %}</small>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<div>
|
||||
|
@ -168,18 +169,18 @@
|
|||
</a>
|
||||
<div class="media-body ">
|
||||
{% ifequal login.is_finished 0 %}
|
||||
<small class="pull-right text-navy">{{ login.date_start|timesince }} 前</small>
|
||||
<small class="pull-right text-navy">{{ login.date_start|timesince }} {% trans 'Before' %}</small>
|
||||
{% else %}
|
||||
<small class="pull-right">{{ login.date_start|timesince }} 前</small>
|
||||
<small class="pull-right">{{ login.date_start|timesince }} {% trans 'Before' %}</small>
|
||||
{% endifequal %}
|
||||
<strong>{{ login.user }}</strong> 登录了{{ login.asset }} <br>
|
||||
<strong>{{ login.user }}</strong> {% trans 'Login in ' %}{{ login.asset }} <br>
|
||||
<small class="text-muted">{{ login.date_start }}</small>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-center">(暂无)</p>
|
||||
<p class="text-center">{% trans '(No)' %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -190,7 +191,7 @@
|
|||
<div class="col-sm-4">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>一周Top10用户</h5>
|
||||
<h5>{% trans 'Top 10 users in a week' %}</h5>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
|
@ -205,8 +206,8 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="ibox-content ibox-heading">
|
||||
<h3><i class="fa fa-user"></i> 一周Top10用户 </h3>
|
||||
<small><i class="fa fa-map-marker"></i> 用户登录次数及最近一次登录记录. </small>
|
||||
<h3><i class="fa fa-user"></i>{% trans 'Top 10 users in a week' %}</h3>
|
||||
<small><i class="fa fa-map-marker"></i>{% trans 'User login frequency and last login record.' %}</small>
|
||||
</div>
|
||||
<div class="ibox-content inspinia-timeline">
|
||||
{% if week_user_hot_ten %}
|
||||
|
@ -217,18 +218,18 @@
|
|||
<i class="fa fa-info-circle"></i>
|
||||
<strong data-toggle="tooltip" title="{{ data.user }}">{{ data.user }}</strong>
|
||||
<br/>
|
||||
<small class="text-navy">{{ data.total }}次</small>
|
||||
<small class="text-navy">{{ data.total }}{% trans ' times' %}</small>
|
||||
</div>
|
||||
<div class="col-xs-7 content no-top-border">
|
||||
<p class="m-b-xs">最近一次登录主机</p>
|
||||
<p class="m-b-xs">{% trans 'The last time logged on to the host' %}</p>
|
||||
<p>{{ data.last.asset }}</p>
|
||||
<p>于{{ data.last.date_start |date:"Y-m-d H:i:s" }}</p>
|
||||
<p>{% trans 'At ' %}{{ data.last.date_start |date:"Y-m-d H:i:s" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-center">(暂无)</p>
|
||||
<p class="text-center">{% trans '(No)' %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -264,8 +265,8 @@ $(document).ready(function(){
|
|||
var top10Chart = ec.init(document.getElementById('top10'));
|
||||
var option = {
|
||||
title : {
|
||||
text: '月数据总览',
|
||||
subtext: '一个月内历史汇总',
|
||||
text: "{% trans 'Monthly data overview' %}",
|
||||
subtext: "{% trans 'History summary in one month' %}",
|
||||
x: 'center'
|
||||
},
|
||||
tooltip : {
|
||||
|
@ -273,7 +274,7 @@ $(document).ready(function(){
|
|||
},
|
||||
backgroundColor: '#fff',
|
||||
legend: {
|
||||
data:['登陆次数', '活跃用户','活跃资产'],
|
||||
data:["{% trans 'Login count' %}", "{% trans 'Active users' %}", "{% trans 'Active assets' %}"],
|
||||
y: 'bottom'
|
||||
},
|
||||
toolbox: {
|
||||
|
@ -297,21 +298,21 @@ $(document).ready(function(){
|
|||
],
|
||||
series : [
|
||||
{
|
||||
name:'登陆次数',
|
||||
name:"{% trans 'Login count' %}",
|
||||
type:'line',
|
||||
smooth:true,
|
||||
itemStyle: {normal: {areaStyle: {type: 'default'}}},
|
||||
data: {{ month_total_visit_count|safe}}
|
||||
},
|
||||
{
|
||||
name:'活跃用户',
|
||||
name:"{% trans 'Active users' %}",
|
||||
type:'line',
|
||||
smooth:true,
|
||||
itemStyle: {normal: {areaStyle: {type: 'default'}}},
|
||||
data: {{ month_user|safe }}
|
||||
},
|
||||
{
|
||||
name:'活跃资产',
|
||||
name:"{% trans 'Active assets' %}",
|
||||
type:'line',
|
||||
smooth:true,
|
||||
itemStyle: {normal: {areaStyle: {type: 'default'}}},
|
||||
|
@ -338,7 +339,7 @@ $(document).ready(function(){
|
|||
show: false,
|
||||
orient : 'vertical',
|
||||
x : 'left',
|
||||
data:['月活跃用户','禁用用户','月未登陆用户']
|
||||
data:["{% trans 'Monthly active users' %}", "{% trans 'Disable user' %}", "{% trans 'Month not logged in user' %}"]
|
||||
},
|
||||
toolbox: {
|
||||
show : false,
|
||||
|
@ -364,7 +365,7 @@ $(document).ready(function(){
|
|||
calculable : true,
|
||||
series : [
|
||||
{
|
||||
name:'访问来源',
|
||||
name:"{% trans 'Access to the source' %}",
|
||||
type:'pie',
|
||||
radius : ['50%', '70%'],
|
||||
itemStyle : {
|
||||
|
@ -388,9 +389,9 @@ $(document).ready(function(){
|
|||
}
|
||||
},
|
||||
data:[
|
||||
{value:{{ month_user_active }}, name:'月活跃用户'},
|
||||
{value:{{ month_user_disabled }}, name:'禁用用户'},
|
||||
{value:{{ month_user_inactive }}, name:'月未登陆用户'}
|
||||
{value:{{ month_user_active }}, name:"{% trans 'Monthly active users' %}"},
|
||||
{value:{{ month_user_disabled }}, name:"{% trans 'Disable user' %}"},
|
||||
{value:{{ month_user_inactive }}, name:"{% trans 'Month not logged in user' %}"}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -414,7 +415,7 @@ $(document).ready(function(){
|
|||
show: false,
|
||||
orient : 'vertical',
|
||||
x : 'left',
|
||||
data:['月被登陆主机','禁用主机','月未登陆主机']
|
||||
data:["{% trans 'Month is logged into the host' %}", "{% trans 'Disable host' %}", "{% trans 'Month not logged on host' %}"]
|
||||
},
|
||||
toolbox: {
|
||||
show : false,
|
||||
|
@ -440,7 +441,7 @@ $(document).ready(function(){
|
|||
calculable : true,
|
||||
series : [
|
||||
{
|
||||
name:'访问来源',
|
||||
name:"{% trans 'Access to the source' %}",
|
||||
type:'pie',
|
||||
radius : ['50%', '70%'],
|
||||
itemStyle : {
|
||||
|
@ -464,9 +465,9 @@ $(document).ready(function(){
|
|||
}
|
||||
},
|
||||
data:[
|
||||
{value:{{ month_asset_active }}, name:'月被登陆主机'},
|
||||
{value:{{ month_asset_disabled }}, name:'禁用主机'},
|
||||
{value:{{ month_asset_inactive }}, name:'月未登陆主机'}
|
||||
{value:{{ month_asset_active }}, name:"{% trans 'Month is logged into the host' %}"},
|
||||
{value:{{ month_asset_disabled }}, name:"{% trans 'Disable host' %}"},
|
||||
{value:{{ month_asset_inactive }}, name:"{% trans 'Month not logged on host' %}"}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -9,6 +9,7 @@ from django.conf import settings
|
|||
|
||||
from users.models import User
|
||||
from orgs.mixins import OrgModelMixin
|
||||
from common.models import common_settings
|
||||
from .backends.command.models import AbstractSessionCommand
|
||||
|
||||
|
||||
|
@ -62,6 +63,10 @@ class Terminal(models.Model):
|
|||
configs[k] = getattr(settings, k)
|
||||
configs.update(self.get_common_storage())
|
||||
configs.update(self.get_replay_storage())
|
||||
configs.update({
|
||||
'SECURITY_MAX_IDLE_TIME': common_settings.SECURITY_MAX_IDLE_TIME or
|
||||
settings.DEFAULT_SECURITY_MAX_IDLE_TIME,
|
||||
})
|
||||
return configs
|
||||
|
||||
def create_app_user(self):
|
||||
|
|
|
@ -10,7 +10,7 @@ from django.urls import reverse_lazy, reverse
|
|||
from common.mixins import JSONResponseMixin
|
||||
from ..models import Terminal
|
||||
from ..forms import TerminalForm
|
||||
from common.permissions import AdminUserRequiredMixin
|
||||
from common.permissions import SuperUserRequiredMixin
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
@ -20,7 +20,7 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
class TerminalListView(AdminUserRequiredMixin, ListView):
|
||||
class TerminalListView(SuperUserRequiredMixin, ListView):
|
||||
model = Terminal
|
||||
template_name = 'terminal/terminal_list.html'
|
||||
form_class = TerminalForm
|
||||
|
@ -35,7 +35,7 @@ class TerminalListView(AdminUserRequiredMixin, ListView):
|
|||
return context
|
||||
|
||||
|
||||
class TerminalUpdateView(AdminUserRequiredMixin, UpdateView):
|
||||
class TerminalUpdateView(SuperUserRequiredMixin, UpdateView):
|
||||
model = Terminal
|
||||
form_class = TerminalForm
|
||||
template_name = 'terminal/terminal_update.html'
|
||||
|
@ -47,7 +47,7 @@ class TerminalUpdateView(AdminUserRequiredMixin, UpdateView):
|
|||
return context
|
||||
|
||||
|
||||
class TerminalDetailView(LoginRequiredMixin, DetailView):
|
||||
class TerminalDetailView(LoginRequiredMixin, SuperUserRequiredMixin, DetailView):
|
||||
model = Terminal
|
||||
template_name = 'terminal/terminal_detail.html'
|
||||
context_object_name = 'terminal'
|
||||
|
@ -61,13 +61,13 @@ class TerminalDetailView(LoginRequiredMixin, DetailView):
|
|||
return context
|
||||
|
||||
|
||||
class TerminalDeleteView(AdminUserRequiredMixin, DeleteView):
|
||||
class TerminalDeleteView(SuperUserRequiredMixin, DeleteView):
|
||||
model = Terminal
|
||||
template_name = 'delete_confirm.html'
|
||||
success_url = reverse_lazy('terminal:terminal-list')
|
||||
|
||||
|
||||
class TerminalAcceptView(AdminUserRequiredMixin, JSONResponseMixin, UpdateView):
|
||||
class TerminalAcceptView(SuperUserRequiredMixin, JSONResponseMixin, UpdateView):
|
||||
model = Terminal
|
||||
form_class = TerminalForm
|
||||
template_name = 'terminal/terminal_modal_accept.html'
|
||||
|
@ -92,7 +92,7 @@ class TerminalAcceptView(AdminUserRequiredMixin, JSONResponseMixin, UpdateView):
|
|||
return self.render_json_response(data)
|
||||
|
||||
|
||||
class TerminalConnectView(LoginRequiredMixin, DetailView):
|
||||
class TerminalConnectView(LoginRequiredMixin, SuperUserRequiredMixin, DetailView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
model = Terminal
|
||||
|
||||
|
@ -118,6 +118,6 @@ class TerminalConnectView(LoginRequiredMixin, DetailView):
|
|||
return super(TerminalConnectView, self).get_context_data(**kwargs)
|
||||
|
||||
|
||||
class WebTerminalView(LoginRequiredMixin, View):
|
||||
class WebTerminalView(LoginRequiredMixin, SuperUserRequiredMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
return redirect('/luna/?' + request.GET.urlencode())
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .user import *
|
||||
from .auth import *
|
||||
from .group import *
|
|
@ -1,4 +1,5 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import uuid
|
||||
|
||||
from django.core.cache import cache
|
||||
|
@ -6,233 +7,36 @@ from django.urls import reverse
|
|||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from rest_framework import generics
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
|
||||
from .serializers import UserSerializer, UserGroupSerializer, \
|
||||
UserGroupUpdateMemeberSerializer, UserPKUpdateSerializer, \
|
||||
UserUpdateGroupSerializer, ChangeUserPasswordSerializer
|
||||
from .tasks import write_login_log_async
|
||||
from .models import User, UserGroup, LoginLog
|
||||
from .utils import check_user_valid, generate_token, get_login_ip, \
|
||||
check_otp_code, set_user_login_failed_count_to_cache, is_block_login
|
||||
from .hands import Asset, SystemUser
|
||||
from orgs.utils import current_org
|
||||
from common.permissions import IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser
|
||||
from .hands import Asset, SystemUser
|
||||
from common.mixins import IDInFilterMixin
|
||||
from common.utils import get_logger
|
||||
from common.utils import get_logger, get_request_ip
|
||||
from ..serializers import UserSerializer
|
||||
from ..tasks import write_login_log_async
|
||||
from ..models import User, LoginLog
|
||||
from ..utils import check_user_valid, generate_token, \
|
||||
check_otp_code, increase_login_failed_count, is_block_login, clean_failed_count
|
||||
from common.permissions import IsOrgAdminOrAppUser
|
||||
from ..hands import Asset, SystemUser
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class UserViewSet(IDInFilterMixin, BulkModelViewSet):
|
||||
queryset = User.objects.exclude(role="App")
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filter_fields = ('username', 'email', 'name', 'id')
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
org_users = current_org.get_org_users().values_list('id', flat=True)
|
||||
queryset = queryset.filter(id__in=org_users)
|
||||
return queryset
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action == "retrieve":
|
||||
self.permission_classes = (IsOrgAdminOrAppUser,)
|
||||
return super().get_permissions()
|
||||
|
||||
|
||||
class ChangeUserPasswordApi(generics.RetrieveUpdateAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
queryset = User.objects.all()
|
||||
serializer_class = ChangeUserPasswordSerializer
|
||||
|
||||
def perform_update(self, serializer):
|
||||
user = self.get_object()
|
||||
user.password_raw = serializer.validated_data["password"]
|
||||
user.save()
|
||||
|
||||
|
||||
class UserUpdateGroupApi(generics.RetrieveUpdateAPIView):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserUpdateGroupSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
|
||||
class UserResetPasswordApi(generics.UpdateAPIView):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
# Note: we are not updating the user object here.
|
||||
# We just do the reset-password stuff.
|
||||
from .utils import send_reset_password_mail
|
||||
user = self.get_object()
|
||||
user.password_raw = str(uuid.uuid4())
|
||||
user.save()
|
||||
send_reset_password_mail(user)
|
||||
|
||||
|
||||
class UserResetPKApi(generics.UpdateAPIView):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
from .utils import send_reset_ssh_key_mail
|
||||
user = self.get_object()
|
||||
user.is_public_key_valid = False
|
||||
user.save()
|
||||
send_reset_ssh_key_mail(user)
|
||||
|
||||
|
||||
class UserUpdatePKApi(generics.UpdateAPIView):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserPKUpdateSerializer
|
||||
permission_classes = (IsCurrentUserOrReadOnly,)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
user = self.get_object()
|
||||
user.public_key = serializer.validated_data['_public_key']
|
||||
user.save()
|
||||
|
||||
|
||||
class UserUnblockPKApi(generics.UpdateAPIView):
|
||||
queryset = User.objects.all()
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = UserSerializer
|
||||
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
|
||||
key_prefix_block = "_LOGIN_BLOCK_{}"
|
||||
|
||||
def perform_update(self, serializer):
|
||||
user = self.get_object()
|
||||
username = user.username if user else ''
|
||||
key_limit = self.key_prefix_limit.format(username, '*')
|
||||
key_block = self.key_prefix_block.format(username)
|
||||
cache.delete_pattern(key_limit)
|
||||
cache.delete(key_block)
|
||||
|
||||
|
||||
class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet):
|
||||
queryset = UserGroup.objects.all()
|
||||
serializer_class = UserGroupSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
|
||||
class UserGroupUpdateUserApi(generics.RetrieveUpdateAPIView):
|
||||
queryset = UserGroup.objects.all()
|
||||
serializer_class = UserGroupUpdateMemeberSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
|
||||
class UserToken(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def post(self, request):
|
||||
if not request.user.is_authenticated:
|
||||
username = request.data.get('username', '')
|
||||
email = request.data.get('email', '')
|
||||
password = request.data.get('password', '')
|
||||
public_key = request.data.get('public_key', '')
|
||||
|
||||
user, msg = check_user_valid(
|
||||
username=username, email=email,
|
||||
password=password, public_key=public_key)
|
||||
else:
|
||||
user = request.user
|
||||
msg = None
|
||||
if user:
|
||||
token = generate_token(request, user)
|
||||
return Response({'Token': token, 'Keyword': 'Bearer'}, status=200)
|
||||
else:
|
||||
return Response({'error': msg}, status=406)
|
||||
|
||||
|
||||
class UserProfile(generics.RetrieveAPIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
serializer_class = UserSerializer
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
|
||||
class UserOtpAuthApi(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
serializer_class = UserSerializer
|
||||
|
||||
def post(self, request):
|
||||
otp_code = request.data.get('otp_code', '')
|
||||
seed = request.data.get('seed', '')
|
||||
|
||||
user = cache.get(seed, None)
|
||||
if not user:
|
||||
return Response({'msg': '请先进行用户名和密码验证'}, status=401)
|
||||
|
||||
if not check_otp_code(user.otp_secret_key, otp_code):
|
||||
data = {
|
||||
'username': user.username,
|
||||
'mfa': int(user.otp_enabled),
|
||||
'reason': LoginLog.REASON_MFA,
|
||||
'status': False
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
return Response({'msg': 'MFA认证失败'}, status=401)
|
||||
|
||||
data = {
|
||||
'username': user.username,
|
||||
'mfa': int(user.otp_enabled),
|
||||
'reason': LoginLog.REASON_NOTHING,
|
||||
'status': True
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
token = generate_token(request, user)
|
||||
return Response(
|
||||
{
|
||||
'token': token,
|
||||
'user': self.serializer_class(user).data
|
||||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def write_login_log(request, data):
|
||||
login_ip = request.data.get('remote_addr', None)
|
||||
login_type = request.data.get('login_type', '')
|
||||
user_agent = request.data.get('HTTP_USER_AGENT', '')
|
||||
|
||||
if not login_ip:
|
||||
login_ip = get_login_ip(request)
|
||||
|
||||
tmp_data = {
|
||||
'ip': login_ip,
|
||||
'type': login_type,
|
||||
'user_agent': user_agent
|
||||
}
|
||||
data.update(tmp_data)
|
||||
write_login_log_async.delay(**data)
|
||||
|
||||
|
||||
class UserAuthApi(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
serializer_class = UserSerializer
|
||||
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
|
||||
key_prefix_block = "_LOGIN_BLOCK_{}"
|
||||
|
||||
def post(self, request):
|
||||
# limit login
|
||||
username = request.data.get('username')
|
||||
ip = request.data.get('remote_addr', None)
|
||||
ip = ip if ip else get_login_ip(request)
|
||||
key_limit = self.key_prefix_limit.format(username, ip)
|
||||
key_block = self.key_prefix_block.format(username)
|
||||
if is_block_login(key_limit):
|
||||
ip = ip or get_request_ip(request)
|
||||
|
||||
if is_block_login(username, ip):
|
||||
msg = _("Log in frequently and try again later")
|
||||
logger.warn(msg + ': ' + username + ':' + ip)
|
||||
return Response({'msg': msg}, status=401)
|
||||
|
||||
user, msg = self.check_user_valid(request)
|
||||
|
@ -244,8 +48,7 @@ class UserAuthApi(APIView):
|
|||
'status': False
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
|
||||
set_user_login_failed_count_to_cache(key_limit, key_block)
|
||||
increase_login_failed_count(username, ip)
|
||||
return Response({'msg': msg}, status=401)
|
||||
|
||||
if not user.otp_enabled:
|
||||
|
@ -256,6 +59,8 @@ class UserAuthApi(APIView):
|
|||
'status': True
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
# 登陆成功,清除原来的缓存计数
|
||||
clean_failed_count(username, ip)
|
||||
token = generate_token(request, user)
|
||||
return Response(
|
||||
{
|
||||
|
@ -269,7 +74,8 @@ class UserAuthApi(APIView):
|
|||
return Response(
|
||||
{
|
||||
'code': 101,
|
||||
'msg': '请携带seed值,进行MFA二次认证',
|
||||
'msg': _('Please carry seed value and '
|
||||
'conduct MFA secondary certification'),
|
||||
'otp_url': reverse('api-users:user-otp-auth'),
|
||||
'seed': seed,
|
||||
'user': self.serializer_class(user).data
|
||||
|
@ -294,7 +100,7 @@ class UserAuthApi(APIView):
|
|||
user_agent = request.data.get('HTTP_USER_AGENT', '')
|
||||
|
||||
if not login_ip:
|
||||
login_ip = get_login_ip(request)
|
||||
login_ip = get_request_ip(request)
|
||||
|
||||
tmp_data = {
|
||||
'ip': login_ip,
|
||||
|
@ -345,3 +151,84 @@ class UserConnectionTokenApi(APIView):
|
|||
if self.request.query_params.get('user-only', None):
|
||||
self.permission_classes = (AllowAny,)
|
||||
return super().get_permissions()
|
||||
|
||||
|
||||
class UserToken(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def post(self, request):
|
||||
if not request.user.is_authenticated:
|
||||
username = request.data.get('username', '')
|
||||
email = request.data.get('email', '')
|
||||
password = request.data.get('password', '')
|
||||
public_key = request.data.get('public_key', '')
|
||||
|
||||
user, msg = check_user_valid(
|
||||
username=username, email=email,
|
||||
password=password, public_key=public_key)
|
||||
else:
|
||||
user = request.user
|
||||
msg = None
|
||||
if user:
|
||||
token = generate_token(request, user)
|
||||
return Response({'Token': token, 'Keyword': 'Bearer'}, status=200)
|
||||
else:
|
||||
return Response({'error': msg}, status=406)
|
||||
|
||||
|
||||
class UserOtpAuthApi(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
serializer_class = UserSerializer
|
||||
|
||||
def post(self, request):
|
||||
otp_code = request.data.get('otp_code', '')
|
||||
seed = request.data.get('seed', '')
|
||||
|
||||
user = cache.get(seed, None)
|
||||
if not user:
|
||||
return Response(
|
||||
{'msg': _('Please verify the user name and password first')},
|
||||
status=401
|
||||
)
|
||||
|
||||
if not check_otp_code(user.otp_secret_key, otp_code):
|
||||
data = {
|
||||
'username': user.username,
|
||||
'mfa': int(user.otp_enabled),
|
||||
'reason': LoginLog.REASON_MFA,
|
||||
'status': False
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
return Response({'msg': _('MFA certification failed')}, status=401)
|
||||
|
||||
data = {
|
||||
'username': user.username,
|
||||
'mfa': int(user.otp_enabled),
|
||||
'reason': LoginLog.REASON_NOTHING,
|
||||
'status': True
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
token = generate_token(request, user)
|
||||
return Response(
|
||||
{
|
||||
'token': token,
|
||||
'user': self.serializer_class(user).data
|
||||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def write_login_log(request, data):
|
||||
login_ip = request.data.get('remote_addr', None)
|
||||
login_type = request.data.get('login_type', '')
|
||||
user_agent = request.data.get('HTTP_USER_AGENT', '')
|
||||
|
||||
if not login_ip:
|
||||
login_ip = get_request_ip(request)
|
||||
|
||||
tmp_data = {
|
||||
'ip': login_ip,
|
||||
'type': login_type,
|
||||
'user_agent': user_agent
|
||||
}
|
||||
data.update(tmp_data)
|
||||
write_login_log_async.delay(**data)
|
|
@ -0,0 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from rest_framework import generics
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
|
||||
from ..serializers import UserGroupSerializer, \
|
||||
UserGroupUpdateMemeberSerializer
|
||||
from ..models import UserGroup
|
||||
from common.permissions import IsOrgAdmin
|
||||
from common.mixins import IDInFilterMixin
|
||||
|
||||
|
||||
__all__ = ['UserGroupViewSet', 'UserGroupUpdateUserApi']
|
||||
|
||||
|
||||
class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet):
|
||||
queryset = UserGroup.objects.all()
|
||||
serializer_class = UserGroupSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
|
||||
class UserGroupUpdateUserApi(generics.RetrieveUpdateAPIView):
|
||||
queryset = UserGroup.objects.all()
|
||||
serializer_class = UserGroupUpdateMemeberSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
|
@ -0,0 +1,140 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
import uuid
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.contrib.auth import logout
|
||||
|
||||
from rest_framework import generics
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
|
||||
from ..serializers import UserSerializer, UserPKUpdateSerializer, \
|
||||
UserUpdateGroupSerializer, ChangeUserPasswordSerializer
|
||||
from ..models import User
|
||||
from orgs.utils import current_org
|
||||
from common.permissions import IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser
|
||||
from common.mixins import IDInFilterMixin
|
||||
from common.utils import get_logger
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
__all__ = [
|
||||
'UserViewSet', 'UserChangePasswordApi', 'UserUpdateGroupApi',
|
||||
'UserResetPasswordApi', 'UserResetPKApi', 'UserUpdatePKApi',
|
||||
'UserUnblockPKApi', 'UserProfileApi', 'UserResetOTPApi',
|
||||
]
|
||||
|
||||
|
||||
class UserViewSet(IDInFilterMixin, BulkModelViewSet):
|
||||
queryset = User.objects.exclude(role="App")
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filter_fields = ('username', 'email', 'name', 'id')
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
org_users = current_org.get_org_users()
|
||||
queryset = queryset.filter(id__in=org_users)
|
||||
return queryset
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action == "retrieve":
|
||||
self.permission_classes = (IsOrgAdminOrAppUser,)
|
||||
return super().get_permissions()
|
||||
|
||||
|
||||
class UserChangePasswordApi(generics.RetrieveUpdateAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
queryset = User.objects.all()
|
||||
serializer_class = ChangeUserPasswordSerializer
|
||||
|
||||
def perform_update(self, serializer):
|
||||
user = self.get_object()
|
||||
user.password_raw = serializer.validated_data["password"]
|
||||
user.save()
|
||||
|
||||
|
||||
class UserUpdateGroupApi(generics.RetrieveUpdateAPIView):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserUpdateGroupSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
|
||||
class UserResetPasswordApi(generics.UpdateAPIView):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
# Note: we are not updating the user object here.
|
||||
# We just do the reset-password stuff.
|
||||
from ..utils import send_reset_password_mail
|
||||
user = self.get_object()
|
||||
user.password_raw = str(uuid.uuid4())
|
||||
user.save()
|
||||
send_reset_password_mail(user)
|
||||
|
||||
|
||||
class UserResetPKApi(generics.UpdateAPIView):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
from ..utils import send_reset_ssh_key_mail
|
||||
user = self.get_object()
|
||||
user.is_public_key_valid = False
|
||||
user.save()
|
||||
send_reset_ssh_key_mail(user)
|
||||
|
||||
|
||||
class UserUpdatePKApi(generics.UpdateAPIView):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserPKUpdateSerializer
|
||||
permission_classes = (IsCurrentUserOrReadOnly,)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
user = self.get_object()
|
||||
user.public_key = serializer.validated_data['_public_key']
|
||||
user.save()
|
||||
|
||||
|
||||
class UserUnblockPKApi(generics.UpdateAPIView):
|
||||
queryset = User.objects.all()
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = UserSerializer
|
||||
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
|
||||
key_prefix_block = "_LOGIN_BLOCK_{}"
|
||||
|
||||
def perform_update(self, serializer):
|
||||
user = self.get_object()
|
||||
username = user.username if user else ''
|
||||
key_limit = self.key_prefix_limit.format(username, '*')
|
||||
key_block = self.key_prefix_block.format(username)
|
||||
cache.delete_pattern(key_limit)
|
||||
cache.delete(key_block)
|
||||
|
||||
|
||||
class UserProfileApi(generics.RetrieveAPIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
serializer_class = UserSerializer
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
|
||||
class UserResetOTPApi(generics.RetrieveAPIView):
|
||||
queryset = User.objects.all()
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
user = self.get_object() if kwargs.get('pk') else request.user
|
||||
if user == request.user:
|
||||
msg = _("Could not reset self otp, use profile reset instead")
|
||||
return Response({"msg": msg}, status=401)
|
||||
if user.otp_enabled and user.otp_secret_key:
|
||||
user.otp_secret_key = ''
|
||||
user.save()
|
||||
logout(request)
|
||||
return Response({"msg": "success"})
|
|
@ -308,7 +308,7 @@ def user_limit_to():
|
|||
|
||||
class UserGroupForm(OrgModelForm):
|
||||
users = forms.ModelMultipleChoiceField(
|
||||
queryset=User.objects.exclude(role=User.ROLE_APP),
|
||||
queryset=User.objects.all(),
|
||||
label=_("User"),
|
||||
widget=forms.SelectMultiple(
|
||||
attrs={
|
||||
|
@ -349,12 +349,5 @@ class UserGroupForm(OrgModelForm):
|
|||
}
|
||||
|
||||
|
||||
class OrgUserField(forms.ModelMultipleChoiceField):
|
||||
|
||||
def get_limit_choices_to(self):
|
||||
|
||||
return {"orgs"}
|
||||
|
||||
|
||||
class FileForm(forms.Form):
|
||||
file = forms.FileField()
|
||||
|
|
|
@ -14,7 +14,7 @@ from django.utils import timezone
|
|||
from django.shortcuts import reverse
|
||||
|
||||
from common.utils import get_signer, date_expired_default
|
||||
from common.models import Setting
|
||||
from common.models import common_settings
|
||||
from orgs.mixins import OrgManager
|
||||
from orgs.utils import current_org
|
||||
|
||||
|
@ -112,6 +112,10 @@ class User(AbstractUser):
|
|||
def password_raw(self, password_raw_):
|
||||
self.set_password(password_raw_)
|
||||
|
||||
def set_password(self, raw_password):
|
||||
self._set_password = True
|
||||
return super().set_password(raw_password)
|
||||
|
||||
@property
|
||||
def otp_secret_key(self):
|
||||
return signer.unsign(self._otp_secret_key)
|
||||
|
@ -278,8 +282,7 @@ class User(AbstractUser):
|
|||
|
||||
@property
|
||||
def otp_force_enabled(self):
|
||||
mfa_setting = Setting.objects.filter(name='SECURITY_MFA_AUTH').first()
|
||||
if mfa_setting and mfa_setting.cleaned_value:
|
||||
if common_settings.SECURITY_MFA_AUTH:
|
||||
return True
|
||||
return self.otp_level == 2
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<link rel="stylesheet" href="{% static 'css/otp.css' %}" />
|
||||
<script src="{% static 'js/jquery-2.1.1.js' %}"></script>
|
||||
<script src="{% static "js/plugins/qrcode/qrcode.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -23,9 +24,9 @@
|
|||
<a href="{% url 'index' %}">Jumpserver</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'index' %}">首页</a>
|
||||
<a href="{% url 'index' %}">{% trans 'Home page' %}</a>
|
||||
<b>丨</b>
|
||||
<a href="http://docs.jumpserver.org/zh/docs/">文档</a>
|
||||
<a href="http://docs.jumpserver.org/zh/docs/">{% trans 'Docs' %}</a>
|
||||
<b>丨</b>
|
||||
<a href="https://www.github.com/jumpserver/">GitHub</a>
|
||||
</div>
|
||||
|
@ -33,39 +34,14 @@
|
|||
|
||||
<!--内容-->
|
||||
<article>
|
||||
<div class="clearfix">
|
||||
<ul class="change-color">
|
||||
<li>
|
||||
<div>
|
||||
<i class="iconfont icon-step active"></i>
|
||||
<span></span>
|
||||
</div>
|
||||
<div class="back">验证身份</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
<i class="iconfont icon-step2"></i>
|
||||
<span></span>
|
||||
</div>
|
||||
<div class="back">安装应用</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
<i class="iconfont icon-step1"></i>
|
||||
<span></span>
|
||||
</div>
|
||||
<div class="back">绑定MFA</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
<i class="iconfont icon-duigou"></i>
|
||||
</div>
|
||||
<div>完成</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="" style="text-align: center; margin-bottom: 50px">
|
||||
<h2>
|
||||
{% block small_title %}
|
||||
{% endblock %}
|
||||
</h2>
|
||||
</div>
|
||||
<div >
|
||||
<div class="verify">安全令牌验证 账户 <span>{{ user.username }}</span> 请按照以下步骤完成绑定操作</div>
|
||||
<div class="verify">{% trans 'Security token validation' %} {% trans 'Account' %} <span>{{ user.username }}</span> {% trans 'Follow these steps to complete the binding operation' %}</div>
|
||||
<div class="line"></div>
|
||||
|
||||
{% block content %}
|
||||
|
|
|
@ -8,10 +8,11 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
|
||||
<title>忘记密码</title>
|
||||
<title>{% trans 'Forgot password' %}</title>
|
||||
|
||||
{% include '_head_css_js.html' %}
|
||||
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
|
||||
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
|
||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
||||
</head>
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
|
||||
{% include '_head_css_js.html' %}
|
||||
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
|
||||
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
|
||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
||||
<style>
|
||||
.captcha {
|
||||
|
@ -22,18 +23,19 @@
|
|||
<div class="loginColumns animated fadeInDown">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2 class="font-bold">欢迎使用Jumpserver开源堡垒机</h2>
|
||||
|
||||
<h2 class="font-bold">{% trans 'Welcome to the Jumpserver open source fortress' %}</h2>
|
||||
<p>
|
||||
全球首款完全开源的堡垒机,使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计系统。
|
||||
{% trans "The world's first fully open source fortress, using the GNU GPL v2.0 open source protocol, is a professional operation and maintenance audit system in compliance with 4A." %}
|
||||
</p>
|
||||
<p>
|
||||
使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。
|
||||
{% trans "Developed using Python/Django, following the Web 2.0 specification and equipped with industry-leading Web Terminal solutions, with beautiful interactive interface and good user experience." %}
|
||||
</p>
|
||||
<p>
|
||||
采纳分布式架构,支持多机房跨区域部署,中心节点提供 API,各机房部署登录节点,可横向扩展、无并发访问限制。
|
||||
{% trans 'Distributed architecture is adopted to support multi-machine room deployment across regions, central node provides API, and each machine room deploys login node, which can be extended horizontally and without concurrent access restrictions.' %}
|
||||
</p>
|
||||
<p>
|
||||
改变世界,从一点点开始。
|
||||
{% trans "Changes the world, starting with a little bit." %}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
|
||||
{% include '_head_css_js.html' %}
|
||||
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
|
||||
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
|
||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
||||
<script src="{% static "js/plugins/qrcode/qrcode.min.js" %}"></script>
|
||||
<style>
|
||||
|
@ -23,18 +24,18 @@
|
|||
<div class="loginColumns animated fadeInDown">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2 class="font-bold">欢迎使用Jumpserver开源堡垒机</h2>
|
||||
<h2 class="font-bold">{% trans 'Welcome to the Jumpserver open source fortress' %}</h2>
|
||||
<p>
|
||||
全球首款完全开源的堡垒机,使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计系统。
|
||||
{% trans "The world's first fully open source fortress, using the GNU GPL v2.0 open source protocol, is a professional operation and maintenance audit system in compliance with 4A." %}
|
||||
</p>
|
||||
<p>
|
||||
使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。
|
||||
{% trans "Developed using Python/Django, following the Web 2.0 specification and equipped with industry-leading Web Terminal solutions, with beautiful interactive interface and good user experience." %}
|
||||
</p>
|
||||
<p>
|
||||
采纳分布式架构,支持多机房跨区域部署,中心节点提供 API,各机房部署登录节点,可横向扩展、无并发访问限制。
|
||||
{% trans 'Distributed architecture is adopted to support multi-machine room deployment across regions, central node provides API, and each machine room deploys login node, which can be extended horizontally and without concurrent access restrictions.' %}
|
||||
</p>
|
||||
<p>
|
||||
改变世界,从一点点开始。
|
||||
{% trans "Changes the world, starting with a little bit." %}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
@ -47,11 +48,11 @@
|
|||
<div class="m-t">
|
||||
|
||||
<div class="form-group">
|
||||
<p style="margin:30px auto;" class="text-center"><strong style="color:#000000">账号保护已开启,请根据提示完成以下操作</strong></p>
|
||||
<p style="margin:30px auto;" class="text-center"><strong style="color:#000000">{% trans 'The account protection has been opened, please complete the following operations according to the prompts' %}</strong></p>
|
||||
<div class="text-center">
|
||||
<img src="{% static 'img/otp_auth.png' %}" alt="" width="72px" height="117">
|
||||
</div>
|
||||
<p style="margin: 30px auto"> 请打开手机Google Authenticator应用,输入6位动态码</p>
|
||||
<p style="margin: 30px auto"> {% trans 'Open Authenticator and enter the 6-bit dynamic code' %}</p>
|
||||
</div>
|
||||
|
||||
<form class="m-t" role="form" method="post" action="">
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
|
||||
{% include '_head_css_js.html' %}
|
||||
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
|
||||
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
|
||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/pwstrength-bootstrap.js' %}"></script>
|
||||
|
||||
|
@ -21,23 +22,22 @@
|
|||
<div class="row">
|
||||
|
||||
<div class="col-md-6">
|
||||
|
||||
<h2 class="font-bold">欢迎使用Jumpserver开源跳板机</h2>
|
||||
<h2 class="font-bold">{% trans 'Welcome to the Jumpserver open source fortress' %}</h2>
|
||||
|
||||
<p>
|
||||
Jumpserver是一款使用Python, Django开发的开源跳板机系统, 助力互联网企业高效 用户、资产、权限、审计 管理
|
||||
{% trans 'Jumpserver is an open source desktop system developed using Python and Django that helps Internet businesses with efficient users, assets, permissions, and audit management' %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
我们自五湖四海,我们对开源精神无比敬仰和崇拜,我们对完美、整洁、优雅 无止境的追求
|
||||
{% trans 'We are from all over the world, we have great admiration and worship for the spirit of open source, we have endless pursuit for perfection, neatness and elegance' %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
专注自动化运维,努力打造 易用、稳定、安全、自动化 的跳板机, 这是我们的不懈的追求和动力
|
||||
{% trans 'We focus on automatic operation and maintenance, and strive to build an easy-to-use, stable, safe and automatic board hopping machine, which is our unremitting pursuit and power' %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<small>永远年轻,永远热泪盈眶 stay foolish stay hungry</small>
|
||||
<small>{% trans 'Always young, always with tears in my eyes. Stay foolish Stay hungry' %}</small>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
|
||||
{% block form %}
|
||||
<div class="ydxbd" id="formlists" style="display: block;">
|
||||
<p id="tags_p" class="mgl-5 c02">选择需要修改属性</p>
|
||||
<p id="tags_p" class="mgl-5 c02">{% trans 'Select properties that need to be modified' %}</p>
|
||||
<div class="tagBtnList">
|
||||
<a class="label label-primary" id="change_all" value="1">全选</a>
|
||||
<a class="label label-primary" id="change_all" value="1">{% trans 'Select all' %}</a>
|
||||
{% for field in form %}
|
||||
{% if field.name != 'users' %}
|
||||
<a data-id="{{ field.id_for_label }}" class="label label-default label-primary field-tag" value="1">{{ field.label }}</a>
|
||||
|
|
|
@ -149,7 +149,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</span></td>
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Force enabled MFA' %}:</td>
|
||||
|
@ -166,6 +165,14 @@
|
|||
</div>
|
||||
</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Reset MFA' %}:</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
<button type="button" class="btn btn-primary btn-xs" id="btn-reset-mfa" style="width: 54px">{% trans 'Reset' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Send reset password mail' %}:</td>
|
||||
<td>
|
||||
|
@ -370,6 +377,7 @@ $(document).ready(function() {
|
|||
text: "{% trans "This will reset the user password and send a reset mail"%}",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonText: "{% trans 'Cancel' %}",
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: false
|
||||
|
@ -395,6 +403,7 @@ $(document).ready(function() {
|
|||
text: "{% trans 'This will reset the user public key and send a reset mail' %}",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonText: "{% trans 'Cancel' %}",
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: false
|
||||
|
@ -462,12 +471,19 @@ $(document).ready(function() {
|
|||
text: "{% trans "After unlocking the user, the user can log in normally."%}",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonText: "{% trans 'Cancel' %}",
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: false
|
||||
}, function() {
|
||||
doReset();
|
||||
});
|
||||
}).on('click', '#btn-reset-mfa', function () {
|
||||
APIUpdateAttr({
|
||||
url: "{% url 'api-users:user-reset-otp' pk=user_object.id %}",
|
||||
method: "GET",
|
||||
success_message: "{% trans 'Reset user MFA success' %}"
|
||||
})
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -82,6 +82,7 @@ $(document).ready(function() {
|
|||
text: "{% trans 'This will delete the selected groups !!!' %}",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonText: "{% trans 'Cancel' %}",
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: false
|
||||
|
|
|
@ -196,6 +196,7 @@ $(document).ready(function(){
|
|||
text: "{% trans 'This will delete the selected users !!!' %}",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonText: "{% trans 'Cancel' %}",
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: false
|
||||
|
|
|
@ -2,11 +2,15 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block small_title %}
|
||||
{% trans 'Authenticate' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="verify">
|
||||
<p style="margin: 20px auto;"><strong style="color: #000000">账号保护已开启,请根据提示完成以下操作</strong></p>
|
||||
<p style="margin: 20px auto;"><strong style="color: #000000">{% trans 'The account protection has been opened, please complete the following operations according to the prompts' %}</strong></p>
|
||||
<img src="{% static 'img/otp_auth.png' %}" alt="" width="72px" height="117">
|
||||
<p style="margin: 20px auto;">请在手机中打开Google Authenticator应用,输入6位动态码</p>
|
||||
<p style="margin: 20px auto;">{% trans 'Open Authenticator and enter the 6-bit dynamic code' %}</p>
|
||||
</div>
|
||||
|
||||
<form class="" role="form" method="post" action="">
|
||||
|
@ -22,14 +26,11 @@
|
|||
<button type="submit" class="next">{% trans 'Next' %}</button>
|
||||
</form>
|
||||
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
$('.change-color li').eq(2).remove();
|
||||
$('.change-color li:eq(1) div').eq(1).html('解绑MFA')
|
||||
})
|
||||
|
||||
|
||||
$('.change-color li:eq(1) div').eq(1).html("{% trans 'Unbind' %}")
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -2,10 +2,14 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block small_title %}
|
||||
{% trans 'Bind' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="verify">
|
||||
<p style="margin:20px auto;"><strong style="color: #000000">使用手机 Google Authenticator 应用扫描以下二维码,获取6位验证码</strong></p>
|
||||
<p style="margin:20px auto;"><strong style="color: #000000">{% trans 'Use the mobile Google Authenticator application to scan the following qr code for a 6-bit verification code' %}</strong></p>
|
||||
|
||||
<div id="qr_code"></div>
|
||||
|
||||
|
|
|
@ -2,21 +2,25 @@
|
|||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block small_title %}
|
||||
{% trans 'Install' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="verify">
|
||||
<p style="margin: 20px auto;"><strong style="color: #000000">请在手机端下载并安装 Google Authenticator 应用</strong></p>
|
||||
<p style="margin: 20px auto;"><strong style="color: #000000">{% trans 'Download and install the Google Authenticator application on your phone' %}</strong></p>
|
||||
<div>
|
||||
<img src="{% static 'img/authenticator_android.png' %}" width="128" height="128" alt="">
|
||||
<p>Android手机下载</p>
|
||||
<p>{% trans 'Android downloads' %}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<img src="{% static 'img/authenticator_iphone.png' %}" width="128" height="128" alt="">
|
||||
<p>iPhone手机下载</p>
|
||||
<p>{% trans 'iPhone downloads' %}</p>
|
||||
</div>
|
||||
|
||||
<p style="margin: 20px auto;"></p>
|
||||
<p style="margin: 20px auto;"><strong style="color: #000000">安装完成后点击下一步进入绑定页面(如已安装,直接进入下一步)</strong></p>
|
||||
<p style="margin: 20px auto;"><strong style="color: #000000">{% trans 'After installation, click the next step to enter the binding page (if installed, go to the next step directly).' %}</strong></p>
|
||||
</div>
|
||||
|
||||
<a href="{% url 'users:user-otp-enable-bind' %}" class="next">{% trans 'Next' %}</a>
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block small_title %}
|
||||
{% trans 'Authenticate' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form class="" role="form" method="post" action="">
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
<link href="{% static "css/plugins/sweetalert/sweetalert.css" %}" rel="stylesheet">
|
||||
<script src="{% static "js/plugins/sweetalert/sweetalert.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/pwstrength-bootstrap.js' %}"></script>
|
||||
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
|
||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -153,7 +153,7 @@
|
|||
</td>
|
||||
</tr>
|
||||
<tr class="no-borders-tr">
|
||||
<td>{% trans 'Update MFA settings' %}:</td>
|
||||
<td>{% trans 'Set MFA' %}:</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
<a type="button" class="btn btn-primary btn-xs" style="width: 54px" id=""
|
||||
|
@ -173,6 +173,16 @@
|
|||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% if request.user.otp_enabled and request.user.otp_secret_key %}
|
||||
<tr>
|
||||
<td>{% trans 'Update MFA' %}:</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
<a type="button" class="btn btn-primary btn-xs" style="width: 54px" href="{% url 'users:user-otp-update' %}">{% trans 'Update' %}</a>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>{% trans 'Update SSH public key' %}:</td>
|
||||
<td>
|
||||
|
|
|
@ -18,10 +18,12 @@ urlpatterns = [
|
|||
# path(r'', api.UserListView.as_view()),
|
||||
path('token/', api.UserToken.as_view(), name='user-token'),
|
||||
path('connection-token/', api.UserConnectionTokenApi.as_view(), name='connection-token'),
|
||||
path('profile/', api.UserProfile.as_view(), name='user-profile'),
|
||||
path('profile/', api.UserProfileApi.as_view(), name='user-profile'),
|
||||
path('auth/', api.UserAuthApi.as_view(), name='user-auth'),
|
||||
path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
|
||||
path('users/<uuid:pk>/password/', api.ChangeUserPasswordApi.as_view(), name='change-user-password'),
|
||||
path('otp/reset/', api.UserResetOTPApi.as_view(), name='my-otp-reset'),
|
||||
path('users/<uuid:pk>/otp/reset/', api.UserResetOTPApi.as_view(), name='user-reset-otp'),
|
||||
path('users/<uuid:pk>/password/', api.UserChangePasswordApi.as_view(), name='change-user-password'),
|
||||
path('users/<uuid:pk>/password/reset/', api.UserResetPasswordApi.as_view(), name='user-reset-password'),
|
||||
path('users/<uuid:pk>/pubkey/reset/', api.UserResetPKApi.as_view(), name='user-public-key-reset'),
|
||||
path('users/<uuid:pk>/pubkey/update/', api.UserUpdatePKApi.as_view(), name='user-public-key-update'),
|
||||
|
|
|
@ -26,6 +26,7 @@ urlpatterns = [
|
|||
path('profile/otp/enable/install-app/', views.UserOtpEnableInstallAppView.as_view(), name='user-otp-enable-install-app'),
|
||||
path('profile/otp/enable/bind/', views.UserOtpEnableBindView.as_view(), name='user-otp-enable-bind'),
|
||||
path('profile/otp/disable/authentication/', views.UserOtpDisableAuthenticationView.as_view(), name='user-otp-disable-authentication'),
|
||||
path('profile/otp/update/', views.UserOtpUpdateView.as_view(), name='user-otp-update'),
|
||||
path('profile/otp/settings-success/', views.UserOtpSettingsSuccessView.as_view(), name='user-otp-settings-success'),
|
||||
|
||||
# User view
|
||||
|
@ -48,5 +49,6 @@ urlpatterns = [
|
|||
path('user-group/<uuid:pk>/assets/', views.UserGroupGrantedAssetView.as_view(), name='user-group-granted-asset'),
|
||||
|
||||
# Login log
|
||||
# Abandon
|
||||
path('login-log/', views.LoginLogListView.as_view(), name='login-log-list'),
|
||||
]
|
||||
|
|
|
@ -19,7 +19,7 @@ from django.core.cache import cache
|
|||
|
||||
from common.tasks import send_mail_async
|
||||
from common.utils import reverse, get_object_or_none
|
||||
from common.models import Setting
|
||||
from common.models import common_settings, Setting
|
||||
from common.forms import SecuritySettingForm
|
||||
from .models import User, LoginLog
|
||||
|
||||
|
@ -190,16 +190,6 @@ def validate_ip(ip):
|
|||
return False
|
||||
|
||||
|
||||
def get_login_ip(request):
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')
|
||||
if x_forwarded_for and x_forwarded_for[0]:
|
||||
login_ip = x_forwarded_for[0]
|
||||
else:
|
||||
login_ip = request.META.get('REMOTE_ADDR', '')
|
||||
|
||||
return login_ip
|
||||
|
||||
|
||||
def write_login_log(*args, **kwargs):
|
||||
ip = kwargs.get('ip', '')
|
||||
if not (ip and validate_ip(ip)):
|
||||
|
@ -225,7 +215,12 @@ def get_ip_city(ip, timeout=10):
|
|||
try:
|
||||
data = r.json()
|
||||
if not isinstance(data, int) and data['code'] == 0:
|
||||
city = data['data']['country'] + ' ' + data['data']['city']
|
||||
country = data['data']['country']
|
||||
_city = data['data']['city']
|
||||
if country == 'XX':
|
||||
city = _city
|
||||
else:
|
||||
city = ' '.join([country, _city])
|
||||
except ValueError:
|
||||
pass
|
||||
return city
|
||||
|
@ -272,6 +267,8 @@ def generate_otp_uri(request, issuer="Jumpserver"):
|
|||
|
||||
|
||||
def check_otp_code(otp_secret_key, otp_code):
|
||||
if not otp_secret_key or not otp_code:
|
||||
return False
|
||||
totp = pyotp.TOTP(otp_secret_key)
|
||||
return totp.verify(otp_code)
|
||||
|
||||
|
@ -310,8 +307,8 @@ def check_password_rules(password):
|
|||
lower_field_name = 'SECURITY_PASSWORD_LOWER_CASE'
|
||||
number_field_name = 'SECURITY_PASSWORD_NUMBER'
|
||||
special_field_name = 'SECURITY_PASSWORD_SPECIAL_CHAR'
|
||||
min_length_setting = Setting.objects.filter(name=min_field_name).first()
|
||||
min_length = min_length_setting.value if min_length_setting else settings.DEFAULT_PASSWORD_MIN_LENGTH
|
||||
min_length = getattr(common_settings, min_field_name) or \
|
||||
settings.DEFAULT_PASSWORD_MIN_LENGTH
|
||||
|
||||
password_setting = Setting.objects.filter(name__startswith='SECURITY_PASSWORD')
|
||||
if not password_setting:
|
||||
|
@ -333,37 +330,40 @@ def check_password_rules(password):
|
|||
return bool(match_obj)
|
||||
|
||||
|
||||
def set_user_login_failed_count_to_cache(key_limit, key_block):
|
||||
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
|
||||
key_prefix_block = "_LOGIN_BLOCK_{}"
|
||||
|
||||
|
||||
# def increase_login_failed_count(key_limit, key_block):
|
||||
def increase_login_failed_count(username, ip):
|
||||
key_limit = key_prefix_limit.format(username, ip)
|
||||
count = cache.get(key_limit)
|
||||
count = count + 1 if count else 1
|
||||
|
||||
setting_limit_time = Setting.objects.filter(
|
||||
name='SECURITY_LOGIN_LIMIT_TIME'
|
||||
).first()
|
||||
limit_time = setting_limit_time.cleaned_value if setting_limit_time \
|
||||
else settings.DEFAULT_LOGIN_LIMIT_TIME
|
||||
|
||||
setting_limit_count = Setting.objects.filter(
|
||||
name='SECURITY_LOGIN_LIMIT_COUNT'
|
||||
).first()
|
||||
limit_count = setting_limit_count.cleaned_value if setting_limit_count \
|
||||
else settings.DEFAULT_LOGIN_LIMIT_COUNT
|
||||
|
||||
if count >= limit_count:
|
||||
cache.set(key_block, 1, int(limit_time)*60)
|
||||
|
||||
limit_time = common_settings.SECURITY_LOGIN_LIMIT_TIME or \
|
||||
settings.DEFAULT_LOGIN_LIMIT_TIME
|
||||
cache.set(key_limit, count, int(limit_time)*60)
|
||||
|
||||
|
||||
def is_block_login(key_limit):
|
||||
count = cache.get(key_limit)
|
||||
def clean_failed_count(username, ip):
|
||||
key_limit = key_prefix_limit.format(username, ip)
|
||||
key_block = key_prefix_block.format(username)
|
||||
cache.delete(key_limit)
|
||||
cache.delete(key_block)
|
||||
|
||||
setting_limit_count = Setting.objects.filter(
|
||||
name='SECURITY_LOGIN_LIMIT_COUNT'
|
||||
).first()
|
||||
limit_count = setting_limit_count.cleaned_value if setting_limit_count \
|
||||
else settings.DEFAULT_LOGIN_LIMIT_COUNT
|
||||
|
||||
def is_block_login(username, ip):
|
||||
key_limit = key_prefix_limit.format(username, ip)
|
||||
key_block = key_prefix_block.format(username)
|
||||
count = cache.get(key_limit, 0)
|
||||
|
||||
limit_count = common_settings.SECURITY_LOGIN_LIMIT_COUNT or \
|
||||
settings.DEFAULT_LOGIN_LIMIT_COUNT
|
||||
limit_time = common_settings.SECURITY_LOGIN_LIMIT_TIME or \
|
||||
settings.DEFAULT_LOGIN_LIMIT_TIME
|
||||
|
||||
if count >= limit_count:
|
||||
cache.set(key_block, 1, int(limit_time)*60)
|
||||
if count and count >= limit_count:
|
||||
return True
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ from django.contrib.messages.views import SuccessMessageMixin
|
|||
from common.utils import get_logger
|
||||
from common.const import create_success_msg, update_success_msg
|
||||
from common.permissions import AdminUserRequiredMixin
|
||||
from orgs.utils import current_org
|
||||
from ..models import User, UserGroup
|
||||
from .. import forms
|
||||
|
||||
|
@ -55,13 +56,10 @@ class UserGroupUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateVie
|
|||
success_message = update_success_msg
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
users = User.objects.all()
|
||||
group_users = [user.id for user in self.object.users.all()]
|
||||
context = {
|
||||
'app': _('Users'),
|
||||
'action': _('Update user group'),
|
||||
'users': users,
|
||||
'group_users': group_users
|
||||
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
@ -73,7 +71,7 @@ class UserGroupDetailView(AdminUserRequiredMixin, DetailView):
|
|||
template_name = 'users/user_group_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
users = User.objects.exclude(id__in=self.object.users.all()).exclude(role=User.ROLE_APP)
|
||||
users = current_org.get_org_users().exclude(id__in=self.object.users.all())
|
||||
context = {
|
||||
'app': _('Users'),
|
||||
'action': _('User group detail'),
|
||||
|
|
|
@ -8,7 +8,6 @@ from django.contrib.auth import login as auth_login, logout as auth_logout
|
|||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views.generic import ListView
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
from django.shortcuts import reverse, redirect
|
||||
from django.utils.decorators import method_decorator
|
||||
|
@ -21,15 +20,12 @@ from django.views.generic.edit import FormView
|
|||
from formtools.wizard.views import SessionWizardView
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_object_or_none
|
||||
from common.mixins import DatetimeSearchMixin
|
||||
from common.permissions import AdminUserRequiredMixin
|
||||
from orgs.utils import current_org
|
||||
from common.utils import get_object_or_none, get_request_ip
|
||||
from ..models import User, LoginLog
|
||||
from ..utils import send_reset_password_mail, check_otp_code, get_login_ip, \
|
||||
from ..utils import send_reset_password_mail, check_otp_code, \
|
||||
redirect_user_first_login_or_index, get_user_or_tmp_user, \
|
||||
set_tmp_user_to_cache, get_password_check_rules, check_password_rules, \
|
||||
is_block_login, set_user_login_failed_count_to_cache
|
||||
is_block_login, increase_login_failed_count, clean_failed_count
|
||||
from ..tasks import write_login_log_async
|
||||
from .. import forms
|
||||
|
||||
|
@ -51,8 +47,6 @@ class UserLoginView(FormView):
|
|||
form_class_captcha = forms.UserLoginCaptchaForm
|
||||
redirect_field_name = 'next'
|
||||
key_prefix_captcha = "_LOGIN_INVALID_{}"
|
||||
key_prefix_limit = "_LOGIN_LIMIT_{}_{}"
|
||||
key_prefix_block = "_LOGIN_BLOCK_{}"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if request.user.is_staff:
|
||||
|
@ -64,12 +58,10 @@ class UserLoginView(FormView):
|
|||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# limit login authentication
|
||||
ip = get_login_ip(request)
|
||||
ip = get_request_ip(request)
|
||||
username = self.request.POST.get('username')
|
||||
key_limit = self.key_prefix_limit.format(username, ip)
|
||||
if is_block_login(key_limit):
|
||||
if is_block_login(username, ip):
|
||||
return self.render_to_response(self.get_context_data(block_login=True))
|
||||
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
|
@ -77,6 +69,10 @@ class UserLoginView(FormView):
|
|||
return HttpResponse(_("Please enable cookies and try again."))
|
||||
|
||||
set_tmp_user_to_cache(self.request, form.get_user())
|
||||
username = form.cleaned_data.get('username')
|
||||
ip = get_request_ip(self.request)
|
||||
# 登陆成功,清除缓存计数
|
||||
clean_failed_count(username, ip)
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def form_invalid(self, form):
|
||||
|
@ -91,10 +87,8 @@ class UserLoginView(FormView):
|
|||
self.write_login_log(data)
|
||||
|
||||
# limit user login failed count
|
||||
ip = get_login_ip(self.request)
|
||||
key_limit = self.key_prefix_limit.format(username, ip)
|
||||
key_block = self.key_prefix_block.format(username)
|
||||
set_user_login_failed_count_to_cache(key_limit, key_block)
|
||||
ip = get_request_ip(self.request)
|
||||
increase_login_failed_count(username, ip)
|
||||
|
||||
# show captcha
|
||||
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
|
||||
|
@ -104,7 +98,7 @@ class UserLoginView(FormView):
|
|||
return super().form_invalid(form)
|
||||
|
||||
def get_form_class(self):
|
||||
ip = get_login_ip(self.request)
|
||||
ip = get_request_ip(self.request)
|
||||
if cache.get(self.key_prefix_captcha.format(ip)):
|
||||
return self.form_class_captcha
|
||||
else:
|
||||
|
@ -139,7 +133,7 @@ class UserLoginView(FormView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
def write_login_log(self, data):
|
||||
login_ip = get_login_ip(self.request)
|
||||
login_ip = get_request_ip(self.request)
|
||||
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
|
||||
tmp_data = {
|
||||
'ip': login_ip,
|
||||
|
@ -178,14 +172,14 @@ class UserLoginOtpView(FormView):
|
|||
'status': False
|
||||
}
|
||||
self.write_login_log(data)
|
||||
form.add_error('otp_code', _('MFA code invalid'))
|
||||
form.add_error('otp_code', _('MFA code invalid, or ntp sync server time'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
|
||||
|
||||
def write_login_log(self, data):
|
||||
login_ip = get_login_ip(self.request)
|
||||
login_ip = get_request_ip(self.request)
|
||||
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
|
||||
tmp_data = {
|
||||
'ip': login_ip,
|
||||
|
@ -361,46 +355,6 @@ class UserFirstLoginView(LoginRequiredMixin, SessionWizardView):
|
|||
return form
|
||||
|
||||
|
||||
class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
|
||||
template_name = 'users/login_log_list.html'
|
||||
model = LoginLog
|
||||
paginate_by = settings.DISPLAY_PER_PAGE
|
||||
user = keyword = ""
|
||||
date_to = date_from = None
|
||||
|
||||
@staticmethod
|
||||
def get_org_users():
|
||||
users = current_org.get_org_users().values_list('username', flat=True)
|
||||
return users
|
||||
|
||||
def get_queryset(self):
|
||||
users = self.get_org_users()
|
||||
queryset = super().get_queryset().filter(username__in=users)
|
||||
self.user = self.request.GET.get('user', '')
|
||||
self.keyword = self.request.GET.get("keyword", '')
|
||||
|
||||
queryset = queryset.filter(
|
||||
datetime__gt=self.date_from, datetime__lt=self.date_to
|
||||
)
|
||||
if self.user:
|
||||
queryset = queryset.filter(username=self.user)
|
||||
if self.keyword:
|
||||
queryset = queryset.filter(
|
||||
Q(ip__contains=self.keyword) |
|
||||
Q(city__contains=self.keyword) |
|
||||
Q(username__contains=self.keyword)
|
||||
)
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Users'),
|
||||
'action': _('Login log list'),
|
||||
'date_from': self.date_from,
|
||||
'date_to': self.date_to,
|
||||
'user': self.user,
|
||||
'keyword': self.keyword,
|
||||
'user_list': self.get_org_users(),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
class LoginLogListView(ListView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
return redirect(reverse('audits:login-log-list'))
|
||||
|
|
|
@ -33,8 +33,9 @@ from django.contrib.auth import logout as auth_logout
|
|||
from common.const import create_success_msg, update_success_msg
|
||||
from common.mixins import JSONResponseMixin
|
||||
from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen
|
||||
from common.models import Setting
|
||||
from common.models import Setting, common_settings
|
||||
from common.permissions import AdminUserRequiredMixin
|
||||
from orgs.utils import current_org
|
||||
from .. import forms
|
||||
from ..models import User, UserGroup
|
||||
from ..utils import generate_otp_uri, check_otp_code, \
|
||||
|
@ -52,7 +53,7 @@ __all__ = [
|
|||
'UserPublicKeyGenerateView',
|
||||
'UserOtpEnableAuthenticationView', 'UserOtpEnableInstallAppView',
|
||||
'UserOtpEnableBindView', 'UserOtpSettingsSuccessView',
|
||||
'UserOtpDisableAuthenticationView',
|
||||
'UserOtpDisableAuthenticationView', 'UserOtpUpdateView'
|
||||
]
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
@ -197,6 +198,12 @@ class UserDetailView(AdminUserRequiredMixin, DetailView):
|
|||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
org_users = current_org.get_org_users().values_list('id', flat=True)
|
||||
queryset = queryset.filter(id__in=org_users)
|
||||
return queryset
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class UserExportView(View):
|
||||
|
@ -355,10 +362,10 @@ class UserProfileView(LoginRequiredMixin, TemplateView):
|
|||
template_name = 'users/user_profile.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
mfa_setting = Setting.objects.filter(name='SECURITY_MFA_AUTH').first()
|
||||
mfa_setting = common_settings.SECURITY_MFA_AUTH
|
||||
context = {
|
||||
'action': _('Profile'),
|
||||
'mfa_setting': mfa_setting.cleaned_value if mfa_setting else False,
|
||||
'mfa_setting': mfa_setting if mfa_setting is not None else False,
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
@ -514,7 +521,7 @@ class UserOtpEnableBindView(TemplateView, FormView):
|
|||
return super().form_valid(form)
|
||||
|
||||
else:
|
||||
form.add_error("otp_code", _("MFA code invalid"))
|
||||
form.add_error("otp_code", _("MFA code invalid, or ntp sync server time"))
|
||||
return self.form_invalid(form)
|
||||
|
||||
def save_otp(self, otp_secret_key):
|
||||
|
@ -539,10 +546,14 @@ class UserOtpDisableAuthenticationView(FormView):
|
|||
user.save()
|
||||
return super().form_valid(form)
|
||||
else:
|
||||
form.add_error('otp_code', _('MFA code invalid'))
|
||||
form.add_error('otp_code', _('MFA code invalid, or ntp sync server time'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class UserOtpUpdateView(UserOtpDisableAuthenticationView):
|
||||
success_url = reverse_lazy('users:user-otp-enable-bind')
|
||||
|
||||
|
||||
class UserOtpSettingsSuccessView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
|
|
Loading…
Reference in New Issue