From 1fd2e782f8ba574058dfcc80c7f87639a49f269e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Thu, 12 Mar 2020 16:24:38 +0800 Subject: [PATCH] 1.5.7 Merge to dev (#3766) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 暂存,优化解决不了问题 * [Update] 待续(小白) * [Update] 修改asset user * [Update] 计划再次更改 * [Update] 修改asset user * [Update] 暂存与喜爱 * [Update] Add id in * [Update] 阶段性完成ops task该做 * [Update] 修改asset user api * [Update] 修改asset user 任务,查看认证等 * [Update] 基本完成asset user改造 * [Update] dynamic user only allow 1 * [Update] 修改asset user task * [Update] 修改node admin user task api * [Update] remove file header license * [Update] 添加sftp root * [Update] 暂存 * [Update] 暂存 * [Update] 修改翻译 * [Update] 修改系统用户改为同名后,用户名改为空 * [Update] 基本完成CAS调研 * [Update] 支持cas server * [Update] 支持cas server * [Update] 添加requirements * [Update] 为方便调试添加mysql ipython到包中 * [Update] 添加huaweiyun翻译 * [Update] 增加下载session 录像 * [Update] 只有第一次通知replay离线的使用方法 * [Update] 暂存一下 * [Bugfix] 获取系统用户信息报错 * [Bugfix] 修改system user info * [Update] 改成清理10天git status * [Update] 修改celery日志保留时间 * [Update]修复部分pip包依赖的版本不兼容问题 (#3672) * [Update] 修复用户更新页面会清空用户public_key的问题 * Fix broken dependencies Co-authored-by: BaiJiangJie <32935519+BaiJiangJie@users.noreply.github.com> * [Update] 修改获取系统用户auth info * [Update] Remove log * [Bugfix] 修复sftp home设置的bug * [Update] 授权的系统用户添加sftp root * [Update] 修改系统用户关联的用户 * [Update] 修改placeholder * [Update] 优化获取授权的系统用户 * [Update] 修改tasks * [Update] tree service update * [Update] 暂存 * [Update] 基本完成用户授权树和资产树改造 * [Update] Dashbaord perf * [update] Add huawei cloud sdk requirements * [Updte] 优化dashboard页面 * [Update] system user auth info 添加id * [Update] 修改系统用户serializer * [Update] 优化api * [Update] LDAP Test Util (#3720) * [Update] LDAPTestUtil 1 * [Update] LDAPTestUtil 2 * [Update] LDAPTestUtil 3 * [Update] LDAPTestUtil 4 * [Update] LDAPTestUtil 5 * [Update] LDAPTestUtil 6 * [Update] LDAPTestUtil 7 * [Update] session 已添加is success,并且添加display serializer * [Bugfix] 修复无法删除空节点的bug * [Update] 命令记录分组织显示 * [Update] Session is_success 添加迁移文件 * [Update] 批量命令添加org_id * [Update] 修复一些文案,修改不绑定MFA,不能ssh登录 * [Update] 修改replay api, 返回session信息 * [Update] 解决无效es导致访问命令记录页面失败的问题 * [Update] 拆分profile view * [Update] 修改一个翻译 * [Update] 修改aysnc api框架 * [Update] 命令列表添加risk level * [Update] 完成录像打包下载 * [Update] 更改登陆otp页面 * [Update] 修改command 存储redis_level * [Update] 修改翻译 * [Update] 修改系统用户的用户列表字段 * [Update] 使用新logo和统一Jumpserver为JumpServer * [Update] 优化cloud task * [Update] 统一period task * [Update] 统一period form serializer字段 * [Update] 修改period task * [Update] 修改资产网关信息 * [Update] 用户授权资产树资产信息添加domain * [Update] 修改翻译 * [Update] 测试可连接性 * 1.5.7 bai (#3764) * [Update] 修复index页面Bug;修复测试资产用户可连接性问题; * [Update] 修改测试资产用户可连接 * [Bugfix] 修复backends问题 * [Update] 修改marksafe依赖版本 * [Update] 修改测试资产用户可连接性 * [Update] 修改检测服务器性能时获取percent值 * [Update] 更新依赖boto3=1.12.14 Co-authored-by: Yanzhe Lee Co-authored-by: BaiJiangJie <32935519+BaiJiangJie@users.noreply.github.com> Co-authored-by: Bai --- apps/applications/hands.py | 2 +- apps/assets/api/asset.py | 68 +- apps/assets/api/asset_user.py | 194 +- apps/assets/api/node.py | 94 +- apps/assets/api/system_user.py | 145 +- apps/assets/api/system_user_relation.py | 39 +- apps/assets/backends/admin_user.py | 10 - apps/assets/backends/asset_user.py | 58 - apps/assets/backends/base.py | 116 +- apps/assets/backends/db.py | 334 ++- apps/assets/backends/manager.py | 224 +- apps/assets/backends/system_user.py | 30 - apps/assets/backends/utils.py | 9 - apps/assets/backends/vault.py | 7 - apps/assets/forms/asset.py | 22 +- apps/assets/forms/user.py | 10 +- apps/assets/hands.py | 2 +- apps/assets/migrations/0047_assetuser.py | 24 + .../migrations/0048_auto_20191230_1512.py | 35 + .../migrations/0049_systemuser_sftp_root.py | 18 + apps/assets/models/__init__.py | 2 + apps/assets/models/asset.py | 25 +- apps/assets/models/asset_user.py | 14 + apps/assets/models/authbook.py | 8 +- apps/assets/models/base.py | 266 ++- apps/assets/models/domain.py | 4 +- apps/assets/models/node.py | 159 +- apps/assets/models/user.py | 25 +- apps/assets/serializers/admin_user.py | 8 + apps/assets/serializers/asset.py | 45 +- apps/assets/serializers/asset_user.py | 61 +- apps/assets/serializers/base.py | 16 +- apps/assets/serializers/node.py | 11 +- apps/assets/serializers/system_user.py | 107 +- apps/assets/signals_handler.py | 21 +- apps/assets/tasks/admin_user_connectivity.py | 18 +- apps/assets/tasks/asset_connectivity.py | 54 +- apps/assets/tasks/asset_user_connectivity.py | 109 +- apps/assets/tasks/const.py | 43 +- .../tasks/gather_asset_hardware_info.py | 26 +- apps/assets/tasks/gather_asset_users.py | 9 +- apps/assets/tasks/push_system_user.py | 167 +- apps/assets/tasks/system_user_connectivity.py | 92 +- apps/assets/tasks/utils.py | 51 +- .../assets/_asset_user_auth_update_modal.html | 8 +- .../assets/_asset_user_auth_view_modal.html | 22 +- .../templates/assets/_asset_user_list.html | 129 +- apps/assets/templates/assets/_node_tree.html | 27 +- .../assets/templates/assets/_system_user.html | 17 +- .../templates/assets/admin_user_assets.html | 6 +- .../templates/assets/admin_user_list.html | 2 +- .../assets/asset_asset_user_list.html | 13 +- .../assets/templates/assets/asset_detail.html | 13 +- apps/assets/templates/assets/asset_list.html | 10 +- .../templates/assets/platform_detail.html | 18 +- .../templates/assets/system_user_assets.html | 117 +- .../templates/assets/system_user_detail.html | 62 +- .../templates/assets/system_user_list.html | 13 +- .../templates/assets/system_user_users.html | 212 ++ apps/assets/tests.py | 3 - apps/assets/tests/__init__.py | 2 + apps/assets/tests/test_system_user.py | 2 + apps/assets/urls/api_urls.py | 88 +- apps/assets/urls/views_urls.py | 1 + apps/assets/utils.py | 123 +- apps/assets/views/admin_user.py | 2 +- apps/assets/views/platform.py | 3 + apps/assets/views/system_user.py | 18 +- apps/authentication/backends/cas/__init__.py | 4 + apps/authentication/backends/cas/backends.py | 11 + apps/authentication/backends/cas/callback.py | 16 + apps/authentication/backends/cas/urls.py | 11 + apps/authentication/backends/ldap.py | 25 +- apps/authentication/backends/pubkey.py | 2 +- apps/authentication/errors.py | 15 +- apps/authentication/mixins.py | 7 +- .../authentication/_mfa_confirm_modal.html | 2 +- .../templates/authentication/login.html | 161 +- .../templates/authentication/login_otp.html | 110 +- apps/authentication/urls/view_urls.py | 9 +- apps/authentication/views/login.py | 42 +- apps/common/README.md | 2 +- apps/common/mixins/api.py | 128 +- apps/common/mixins/models.py | 3 +- apps/common/struct.py | 81 + apps/common/utils/common.py | 8 +- apps/common/utils/random.py | 39 + apps/jumpserver/conf.py | 16 +- apps/jumpserver/context_processor.py | 2 +- apps/jumpserver/settings/auth.py | 14 + apps/jumpserver/settings/base.py | 4 +- apps/jumpserver/settings/custom.py | 1 + apps/jumpserver/settings/libs.py | 1 + apps/jumpserver/settings/logging.py | 12 +- apps/jumpserver/views/index.py | 304 ++- apps/jumpserver/views/swagger.py | 19 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 84170 -> 88260 bytes apps/locale/zh/LC_MESSAGES/django.po | 2100 ++++++++++------- apps/locale/zh/LC_MESSAGES/djangojs.mo | Bin 2741 -> 2782 bytes apps/ops/ansible/runner.py | 2 +- apps/ops/api/adhoc.py | 14 +- apps/ops/api/command.py | 4 +- apps/ops/inventory.py | 8 +- .../ops/migrations/0011_auto_20200106_1534.py | 19 + .../ops/migrations/0012_auto_20200108_1659.py | 22 + .../ops/migrations/0013_auto_20200108_1706.py | 28 + .../ops/migrations/0014_auto_20200108_1749.py | 17 + .../ops/migrations/0015_auto_20200108_1809.py | 33 + .../0016_commandexecution_org_id.py | 18 + .../ops/migrations/0017_auto_20200306_1747.py | 23 + apps/ops/mixin.py | 181 ++ apps/ops/models/adhoc.py | 157 +- apps/ops/models/command.py | 4 +- apps/ops/serializers/adhoc.py | 22 +- apps/ops/tasks.py | 22 +- apps/ops/templates/ops/adhoc_detail.html | 24 +- apps/ops/templates/ops/adhoc_history.html | 12 +- .../templates/ops/adhoc_history_detail.html | 4 +- apps/ops/templates/ops/task_adhoc.html | 10 +- apps/ops/templates/ops/task_detail.html | 22 +- apps/ops/templates/ops/task_history.html | 20 +- apps/ops/templates/ops/task_list.html | 8 +- apps/ops/urls/api_urls.py | 2 +- apps/ops/urls/view_urls.py | 10 +- apps/ops/utils.py | 17 +- apps/ops/views/adhoc.py | 27 +- apps/ops/views/command.py | 4 +- apps/orgs/mixins/api.py | 8 +- apps/orgs/mixins/models.py | 12 + apps/orgs/models.py | 8 +- apps/orgs/utils.py | 31 +- apps/perms/api/mixin.py | 16 +- apps/perms/api/user_permission/common.py | 26 +- apps/perms/api/user_permission/mixin.py | 4 +- .../user_permission/user_permission_assets.py | 7 +- apps/perms/mixins.py | 62 +- apps/perms/serializers/user_permission.py | 1 + apps/perms/signals_handler.py | 77 +- .../perms/asset_permission_list.html | 2 +- apps/perms/urls/asset_permission.py | 2 +- apps/perms/utils/asset_permission.py | 133 +- apps/settings/api.py | 54 +- apps/settings/forms/security.py | 6 +- apps/settings/serializers/ldap.py | 12 +- .../settings/_ldap_test_user_login_modal.html | 58 + .../templates/settings/ldap_setting.html | 15 +- apps/settings/urls/api_urls.py | 3 +- apps/settings/utils/ldap.py | 251 +- apps/static/img/logo_text.png | Bin 21179 -> 11827 bytes apps/static/js/jumpserver.js | 68 +- apps/templates/_base_only_msg_content.html | 67 + apps/templates/_csv_update_modal.html | 2 +- apps/templates/_filter_dropdown.html | 4 +- apps/templates/_nav.html | 2 +- apps/templates/index.html | 22 +- apps/terminal/api/command.py | 20 +- apps/terminal/api/session.py | 83 +- apps/terminal/backends/command/db.py | 17 +- apps/terminal/backends/command/es.py | 37 +- apps/terminal/backends/command/models.py | 7 + apps/terminal/backends/command/serializers.py | 1 + .../migrations/0022_session_is_success.py | 18 + .../migrations/0023_command_risk_level.py | 18 + apps/terminal/models.py | 43 + apps/terminal/serializers/session.py | 23 +- .../templates/terminal/command_list.html | 63 +- .../templates/terminal/session_commands.html | 96 + .../terminal/session_commands_list_modal.html | 58 - .../templates/terminal/session_detail.html | 177 +- .../templates/terminal/session_list.html | 23 +- .../templates/terminal/terminal_detail.html | 54 +- apps/terminal/urls/views_urls.py | 2 + apps/terminal/utils.py | 57 +- apps/terminal/views/session.py | 71 +- apps/users/forms/profile.py | 8 +- apps/users/hands.py | 2 +- .../migrations/0025_auto_20200206_1216.py | 18 + apps/users/models/user.py | 27 +- apps/users/signals_handler.py | 22 +- apps/users/templates/users/_base_otp.html | 2 +- .../templates/users/_granted_assets.html | 8 +- apps/users/templates/users/first_login.html | 11 - .../templates/users/forgot_password.html | 5 - .../users/user_asset_permission.html | 2 +- apps/users/templates/users/user_detail.html | 2 +- .../templates/users/user_disable_mfa.html | 2 +- ...heck.html => user_otp_check_password.html} | 6 +- .../templates/users/user_otp_enable_bind.html | 9 +- .../users/user_otp_enable_install_app.html | 9 +- .../templates/users/user_password_verify.html | 24 + apps/users/templates/users/user_profile.html | 4 +- apps/users/urls/views_urls.py | 4 +- apps/users/utils.py | 41 +- apps/users/views/profile.py | 272 --- apps/users/views/profile/__init__.py | 7 + apps/users/views/profile/base.py | 50 + apps/users/views/profile/mfa.py | 2 + apps/users/views/profile/otp.py | 136 ++ apps/users/views/profile/password.py | 94 + apps/users/views/profile/pubkey.py | 54 + jms | 8 + requirements/requirements.txt | 24 +- requirements/rpm_requirements.txt | 2 +- utils/create_test_data.py | 3 + 204 files changed, 6429 insertions(+), 3826 deletions(-) delete mode 100644 apps/assets/backends/admin_user.py delete mode 100644 apps/assets/backends/asset_user.py delete mode 100644 apps/assets/backends/system_user.py create mode 100644 apps/assets/migrations/0047_assetuser.py create mode 100644 apps/assets/migrations/0048_auto_20191230_1512.py create mode 100644 apps/assets/migrations/0049_systemuser_sftp_root.py create mode 100644 apps/assets/models/asset_user.py create mode 100644 apps/assets/templates/assets/system_user_users.html delete mode 100644 apps/assets/tests.py create mode 100644 apps/assets/tests/__init__.py create mode 100644 apps/assets/tests/test_system_user.py create mode 100644 apps/authentication/backends/cas/__init__.py create mode 100644 apps/authentication/backends/cas/backends.py create mode 100644 apps/authentication/backends/cas/callback.py create mode 100644 apps/authentication/backends/cas/urls.py create mode 100644 apps/common/utils/random.py create mode 100644 apps/ops/migrations/0011_auto_20200106_1534.py create mode 100644 apps/ops/migrations/0012_auto_20200108_1659.py create mode 100644 apps/ops/migrations/0013_auto_20200108_1706.py create mode 100644 apps/ops/migrations/0014_auto_20200108_1749.py create mode 100644 apps/ops/migrations/0015_auto_20200108_1809.py create mode 100644 apps/ops/migrations/0016_commandexecution_org_id.py create mode 100644 apps/ops/migrations/0017_auto_20200306_1747.py create mode 100644 apps/ops/mixin.py create mode 100644 apps/settings/templates/settings/_ldap_test_user_login_modal.html create mode 100644 apps/templates/_base_only_msg_content.html create mode 100644 apps/terminal/migrations/0022_session_is_success.py create mode 100644 apps/terminal/migrations/0023_command_risk_level.py create mode 100644 apps/terminal/templates/terminal/session_commands.html delete mode 100644 apps/terminal/templates/terminal/session_commands_list_modal.html create mode 100644 apps/users/migrations/0025_auto_20200206_1216.py rename apps/users/templates/users/{user_password_check.html => user_otp_check_password.html} (96%) create mode 100644 apps/users/templates/users/user_password_verify.html delete mode 100644 apps/users/views/profile.py create mode 100644 apps/users/views/profile/__init__.py create mode 100644 apps/users/views/profile/base.py create mode 100644 apps/users/views/profile/mfa.py create mode 100644 apps/users/views/profile/otp.py create mode 100644 apps/users/views/profile/password.py create mode 100644 apps/users/views/profile/pubkey.py create mode 100644 utils/create_test_data.py diff --git a/apps/applications/hands.py b/apps/applications/hands.py index 7c83e1332..ee13e589e 100644 --- a/apps/applications/hands.py +++ b/apps/applications/hands.py @@ -6,7 +6,7 @@ Other module of this app shouldn't connect with other app. - :copyright: (c) 2014-2018 by Jumpserver Team. + :copyright: (c) 2014-2018 by JumpServer Team. :license: GPL v2, see LICENSE for more details. """ diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index 0b63522ea..6eb8ebeaf 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -23,8 +23,8 @@ from ..filters import AssetByNodeFilterBackend, LabelFilterBackend logger = get_logger(__file__) __all__ = [ 'AssetViewSet', 'AssetPlatformRetrieveApi', - 'AssetRefreshHardwareApi', 'AssetAdminUserTestApi', - 'AssetGatewayApi', 'AssetPlatformViewSet', + 'AssetGatewayListApi', 'AssetPlatformViewSet', + 'AssetTaskCreateApi', ] @@ -36,7 +36,10 @@ class AssetViewSet(OrgBulkModelViewSet): filter_fields = ("hostname", "ip", "systemuser__id", "admin_user__id") search_fields = ("hostname", "ip") ordering_fields = ("hostname", "ip", "port", "cpu_cores") - serializer_class = serializers.AssetSerializer + serializer_classes = { + 'default': serializers.AssetSerializer, + 'display': serializers.AssetDisplaySerializer, + } permission_classes = (IsOrgAdminOrAppUser,) extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend] @@ -80,53 +83,40 @@ class AssetPlatformViewSet(ModelViewSet): self.permission_denied( request, message={"detail": "Internal platform"} ) - return super().check_object_permissions(request, obj) -class AssetRefreshHardwareApi(generics.RetrieveAPIView): - """ - Refresh asset hardware info - """ +class AssetTaskCreateApi(generics.CreateAPIView): model = Asset - serializer_class = serializers.AssetSerializer + serializer_class = serializers.AssetTaskSerializer permission_classes = (IsOrgAdmin,) - def retrieve(self, request, *args, **kwargs): - asset_id = kwargs.get('pk') - asset = get_object_or_404(Asset, pk=asset_id) - task = update_asset_hardware_info_manual.delay(asset) - return Response({"task": task.id}) - - -class AssetAdminUserTestApi(generics.RetrieveAPIView): - """ - Test asset admin user assets_connectivity - """ - model = Asset - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.TaskIDSerializer + def get_object(self): + pk = self.kwargs.get("pk") + instance = get_object_or_404(Asset, pk=pk) + return instance - def retrieve(self, request, *args, **kwargs): - asset_id = kwargs.get('pk') - asset = get_object_or_404(Asset, pk=asset_id) - task = test_asset_connectivity_manual.delay(asset) - return Response({"task": task.id}) + def perform_create(self, serializer): + asset = self.get_object() + action = serializer.validated_data["action"] + if action == "refresh": + task = update_asset_hardware_info_manual.delay(asset) + else: + task = test_asset_connectivity_manual.delay(asset) + data = getattr(serializer, '_data', {}) + data["task"] = task.id + setattr(serializer, '_data', data) -class AssetGatewayApi(generics.RetrieveAPIView): +class AssetGatewayListApi(generics.ListAPIView): permission_classes = (IsOrgAdminOrAppUser,) serializer_class = serializers.GatewayWithAuthSerializer model = Asset - def retrieve(self, request, *args, **kwargs): - asset_id = kwargs.get('pk') + def get_queryset(self): + asset_id = self.kwargs.get('pk') asset = get_object_or_404(Asset, pk=asset_id) - - if asset.domain and \ - asset.domain.gateways.filter(protocol='ssh').exists(): - gateway = random.choice(asset.domain.gateways.filter(protocol='ssh')) - serializer = serializers.GatewayWithAuthSerializer(instance=gateway) - return Response(serializer.data) - else: - return Response({"msg": "Not have gateway"}, status=404) + if not asset.domain: + return [] + queryset = asset.domain.gateways.filter(protocol='ssh') + return queryset diff --git a/apps/assets/api/asset_user.py b/apps/assets/api/asset_user.py index a3b9d8906..ce5930a26 100644 --- a/apps/assets/api/asset_user.py +++ b/apps/assets/api/asset_user.py @@ -1,26 +1,23 @@ # -*- coding: utf-8 -*- # - +from django.conf import settings from rest_framework.response import Response -from rest_framework import generics -from rest_framework import filters +from rest_framework import generics, filters from rest_framework_bulk import BulkModelViewSet -from django.shortcuts import get_object_or_404 -from django.http import Http404 -from django.conf import settings from common.permissions import IsOrgAdminOrAppUser, NeedMFAVerify from common.utils import get_object_or_none, get_logger from common.mixins import CommonApiMixin from ..backends import AssetUserManager -from ..models import Asset, Node, SystemUser, AdminUser +from ..models import Asset, Node, SystemUser from .. import serializers -from ..tasks import test_asset_users_connectivity_manual +from ..tasks import ( + test_asset_users_connectivity_manual, push_system_user_a_asset_manual +) __all__ = [ - 'AssetUserViewSet', 'AssetUserAuthInfoApi', 'AssetUserTestConnectiveApi', - 'AssetUserExportViewSet', + 'AssetUserViewSet', 'AssetUserAuthInfoViewSet', 'AssetUserTaskCreateAPI', ] @@ -34,10 +31,17 @@ class AssetUserFilterBackend(filters.BaseFilterBackend): value = request.GET.get(field) if not value: continue - if field in ("node_id", "system_user_id", "admin_user_id"): + if field == "node_id": + value = get_object_or_none(Node, pk=value) + kwargs["node"] = value continue + elif field == "asset_id": + field = "asset" kwargs[field] = value - return queryset.filter(**kwargs) + if kwargs: + queryset = queryset.filter(**kwargs) + logger.debug("Filter {}".format(kwargs)) + return queryset class AssetUserSearchBackend(filters.BaseFilterBackend): @@ -45,71 +49,62 @@ class AssetUserSearchBackend(filters.BaseFilterBackend): value = request.GET.get('search') if not value: return queryset - _queryset = AssetUserManager.none() - for field in view.search_fields: - if field in ("node_id", "system_user_id", "admin_user_id"): - continue - _queryset |= queryset.filter(**{field: value}) - return _queryset.distinct() + queryset = queryset.search(value) + return queryset + + +class AssetUserLatestFilterBackend(filters.BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + latest = request.GET.get('latest') == '1' + if latest: + queryset = queryset.distinct() + return queryset class AssetUserViewSet(CommonApiMixin, BulkModelViewSet): - serializer_class = serializers.AssetUserSerializer + serializer_classes = { + 'default': serializers.AssetUserWriteSerializer, + 'list': serializers.AssetUserReadSerializer, + 'retrieve': serializers.AssetUserReadSerializer, + } permission_classes = [IsOrgAdminOrAppUser] - http_method_names = ['get', 'post'] filter_fields = [ - "id", "ip", "hostname", "username", "asset_id", "node_id", - "system_user_id", "admin_user_id" + "id", "ip", "hostname", "username", + "asset_id", "node_id", + "prefer", "prefer_id", ] - search_fields = filter_fields - filter_backends = ( - filters.OrderingFilter, + search_fields = ["ip", "hostname", "username"] + filter_backends = [ AssetUserFilterBackend, AssetUserSearchBackend, - ) + AssetUserLatestFilterBackend, + ] def allow_bulk_destroy(self, qs, filtered): return False - def get_queryset(self): - # 尽可能先返回更少的数据 - username = self.request.GET.get('username') - asset_id = self.request.GET.get('asset_id') - node_id = self.request.GET.get('node_id') - admin_user_id = self.request.GET.get("admin_user_id") - system_user_id = self.request.GET.get("system_user_id") + def get_object(self): + pk = self.kwargs.get("pk") + queryset = self.get_queryset() + obj = queryset.get(id=pk) + return obj - kwargs = {} - assets = None + def get_exception_handler(self): + def handler(e, context): + return Response({"error": str(e)}, status=400) + return handler + + def perform_destroy(self, instance): + manager = AssetUserManager() + manager.delete(instance) + def get_queryset(self): manager = AssetUserManager() - if system_user_id: - system_user = get_object_or_404(SystemUser, id=system_user_id) - assets = system_user.get_all_assets() - username = system_user.username - elif admin_user_id: - admin_user = get_object_or_404(AdminUser, id=admin_user_id) - assets = admin_user.assets.all() - username = admin_user.username - manager.prefer('admin_user') - - if asset_id: - asset = get_object_or_404(Asset, id=asset_id) - assets = [asset] - elif node_id: - node = get_object_or_404(Node, id=node_id) - assets = node.get_all_assets() - - if username: - kwargs['username'] = username - if assets is not None: - kwargs['assets'] = assets - - queryset = manager.filter(**kwargs) + queryset = manager.all() return queryset -class AssetUserExportViewSet(AssetUserViewSet): - serializer_class = serializers.AssetUserExportSerializer +class AssetUserAuthInfoViewSet(AssetUserViewSet): + serializer_classes = {"default": serializers.AssetUserAuthInfoSerializer} http_method_names = ['get'] permission_classes = [IsOrgAdminOrAppUser] @@ -119,66 +114,31 @@ class AssetUserExportViewSet(AssetUserViewSet): return super().get_permissions() -class AssetUserAuthInfoApi(generics.RetrieveAPIView): - serializer_class = serializers.AssetUserAuthInfoSerializer - permission_classes = [IsOrgAdminOrAppUser] - - def get_permissions(self): - if settings.SECURITY_VIEW_AUTH_NEED_MFA: - self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify] - return super().get_permissions() - - def get_object(self): - query_params = self.request.query_params - username = query_params.get('username') - asset_id = query_params.get('asset_id') - prefer = query_params.get("prefer") - asset = get_object_or_none(Asset, pk=asset_id) - try: - manger = AssetUserManager() - instance = manger.get(username, asset, prefer=prefer) - except Exception as e: - raise Http404("Not found") - else: - return instance - - -class AssetUserTestConnectiveApi(generics.RetrieveAPIView): - """ - Test asset users connective - """ +class AssetUserTaskCreateAPI(generics.CreateAPIView): permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = serializers.TaskIDSerializer + serializer_class = serializers.AssetUserTaskSerializer + filter_backends = AssetUserViewSet.filter_backends + filter_fields = AssetUserViewSet.filter_fields def get_asset_users(self): - username = self.request.GET.get('username') - asset_id = self.request.GET.get('asset_id') - prefer = self.request.GET.get("prefer") - asset = get_object_or_none(Asset, pk=asset_id) manager = AssetUserManager() - asset_users = manager.filter(username=username, assets=[asset], prefer=prefer) - return asset_users + queryset = manager.all() + for cls in self.filter_backends: + queryset = cls().filter_queryset(self.request, queryset, self) + return list(queryset) - def retrieve(self, request, *args, **kwargs): + def perform_create(self, serializer): asset_users = self.get_asset_users() - prefer = self.request.GET.get("prefer") - kwargs = {} - if prefer == "admin_user": - kwargs["run_as_admin"] = True - task = test_asset_users_connectivity_manual.delay(asset_users, **kwargs) - return Response({"task": task.id}) - - -class AssetUserPushApi(generics.CreateAPIView): - """ - Test asset users connective - """ - serializer_class = serializers.AssetUserPushSerializer - permission_classes = (IsOrgAdminOrAppUser,) - - def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - asset = serializer.validated_data["asset"] - username = serializer.validated_data["username"] - pass + # action = serializer.validated_data["action"] + # only this + # if action == "test": + task = test_asset_users_connectivity_manual.delay(asset_users) + data = getattr(serializer, '_data', {}) + data["task"] = task.id + setattr(serializer, '_data', data) + return task + + def get_exception_handler(self): + def handler(e, context): + return Response({"error": str(e)}, status=400) + return handler diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 24661bad6..2d959e9ea 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -1,24 +1,11 @@ # ~*~ coding: utf-8 ~*~ -# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved. -# -# Licensed under the GNU General Public License v2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.gnu.org/licenses/gpl-2.0.html -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +from collections import namedtuple from rest_framework import status from rest_framework.serializers import ValidationError -from rest_framework.views import APIView from rest_framework.response import Response from django.utils.translation import ugettext_lazy as _ -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, Http404 from common.utils import get_logger, get_object_or_none from common.tree import TreeNodeSerializer @@ -27,7 +14,8 @@ from orgs.mixins import generics from ..hands import IsOrgAdmin from ..models import Node from ..tasks import ( - update_assets_hardware_info_util, test_asset_connectivity_util + update_node_assets_hardware_info_manual, + test_node_assets_connectivity_manual, ) from .. import serializers @@ -36,9 +24,9 @@ logger = get_logger(__file__) __all__ = [ 'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi', 'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi', - 'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi', - 'TestNodeConnectiveApi', 'NodeListAsTreeApi', - 'NodeChildrenAsTreeApi', 'RefreshNodesCacheApi', + 'NodeAddChildrenApi', 'NodeListAsTreeApi', + 'NodeChildrenAsTreeApi', + 'NodeTaskCreateApi', ] @@ -64,9 +52,9 @@ class NodeViewSet(OrgModelViewSet): def destroy(self, request, *args, **kwargs): node = self.get_object() - if node.has_children_or_contains_assets(): - msg = _("Deletion failed and the node contains children or assets") - return Response(data={'msg': msg}, status=status.HTTP_403_FORBIDDEN) + if node.has_children_or_has_assets(): + error = _("Deletion failed and the node contains children or assets") + return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN) return super().destroy(request, *args, **kwargs) @@ -261,41 +249,41 @@ class NodeReplaceAssetsApi(generics.UpdateAPIView): asset.nodes.set([instance]) -class RefreshNodeHardwareInfoApi(APIView): +class NodeTaskCreateApi(generics.CreateAPIView): model = Node + serializer_class = serializers.NodeTaskSerializer permission_classes = (IsOrgAdmin,) - def get(self, request, *args, **kwargs): - node_id = kwargs.get('pk') - node = get_object_or_404(self.model, id=node_id) - assets = node.get_all_assets() - # 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}) - - -class TestNodeConnectiveApi(APIView): - permission_classes = (IsOrgAdmin,) - model = Node - - def get(self, request, *args, **kwargs): - node_id = kwargs.get('pk') - node = get_object_or_404(self.model, id=node_id) - assets = node.get_all_assets() - # task_name = _("测试节点下资产是否可连接: {}".format(node.name)) - task_name = _("Test if the assets under the node are connectable: {}".format(node.name)) - task = test_asset_connectivity_util.delay(assets, task_name=task_name) - return Response({"task": task.id}) - + def get_object(self): + node_id = self.kwargs.get('pk') + node = get_object_or_none(self.model, id=node_id) + return node -class RefreshNodesCacheApi(APIView): - permission_classes = (IsOrgAdmin,) + @staticmethod + def set_serializer_data(s, task): + data = getattr(s, '_data', {}) + data["task"] = task.id + setattr(s, '_data', data) - def get(self, request, *args, **kwargs): + @staticmethod + def refresh_nodes_cache(): Node.refresh_nodes() - return Response("Ok") + Task = namedtuple('Task', ['id']) + task = Task(id="0") + return task + + def perform_create(self, serializer): + action = serializer.validated_data["action"] + node = self.get_object() + if action == "refresh_cache" and node is None: + task = self.refresh_nodes_cache() + self.set_serializer_data(serializer, task) + return + if node is None: + raise Http404() + if action == "refresh": + task = update_node_assets_hardware_info_manual.delay(node) + else: + task = test_node_assets_connectivity_manual.delay(node) + self.set_serializer_data(serializer, task) - def delete(self, *args, **kwargs): - self.get(*args, **kwargs) - return Response(status=204) diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index e3dddd6f5..dc134ee97 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -1,42 +1,25 @@ # ~*~ coding: utf-8 ~*~ -# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved. -# -# Licensed under the GNU General Public License v2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.gnu.org/licenses/gpl-2.0.html -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# 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.response import Response -from django.db.models import Count -from common.serializers import CeleryTaskSerializer from common.utils import get_logger from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsAppUser from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics +from orgs.utils import tmp_to_org from ..models import SystemUser, Asset from .. import serializers +from ..serializers import SystemUserWithAuthInfoSerializer from ..tasks import ( push_system_user_to_assets_manual, test_system_user_connectivity_manual, - push_system_user_a_asset_manual, test_system_user_connectivity_a_asset, + push_system_user_a_asset_manual, ) logger = get_logger(__file__) __all__ = [ 'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi', - 'SystemUserPushApi', 'SystemUserTestConnectiveApi', - 'SystemUserAssetsListView', 'SystemUserPushToAssetApi', - 'SystemUserTestAssetConnectivityApi', 'SystemUserCommandFilterRuleListApi', - + 'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi', ] @@ -48,13 +31,12 @@ class SystemUserViewSet(OrgBulkModelViewSet): filter_fields = ("name", "username") search_fields = filter_fields serializer_class = serializers.SystemUserSerializer + serializer_classes = { + 'default': serializers.SystemUserSerializer, + 'list': serializers.SystemUserListSerializer, + } permission_classes = (IsOrgAdminOrAppUser,) - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate(_assets_amount=Count('assets')) - return queryset - class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): """ @@ -62,7 +44,7 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): """ model = SystemUser permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = serializers.SystemUserAuthSerializer + serializer_class = SystemUserWithAuthInfoSerializer def destroy(self, request, *args, **kwargs): instance = self.get_object() @@ -75,88 +57,61 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView): Get system user with asset auth info """ model = SystemUser - permission_classes = (IsAppUser,) - serializer_class = serializers.SystemUserAuthSerializer + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = SystemUserWithAuthInfoSerializer + + def get_exception_handler(self): + def handler(e, context): + return Response({"error": str(e)}, status=400) + return handler def get_object(self): instance = super().get_object() - aid = self.kwargs.get('aid') - asset = get_object_or_404(Asset, pk=aid) - instance.load_specific_asset_auth(asset) - return instance - - -class SystemUserPushApi(generics.RetrieveAPIView): - """ - Push system user to cluster assets api - """ - model = SystemUser - permission_classes = (IsOrgAdmin,) - serializer_class = CeleryTaskSerializer + username = instance.username + if instance.username_same_with_user: + username = self.request.query_params.get("username") + asset_id = self.kwargs.get('aid') + asset = get_object_or_404(Asset, pk=asset_id) - def retrieve(self, request, *args, **kwargs): - system_user = self.get_object() - nodes = system_user.nodes.all() - for node in nodes: - system_user.assets.add(*tuple(node.get_all_assets())) - task = push_system_user_to_assets_manual.delay(system_user) - return Response({"task": task.id}) + with tmp_to_org(asset.org_id): + instance.load_asset_special_auth(asset=asset, username=username) + return instance -class SystemUserTestConnectiveApi(generics.RetrieveAPIView): - """ - Push system user to cluster assets api - """ - model = SystemUser +class SystemUserTaskApi(generics.CreateAPIView): permission_classes = (IsOrgAdmin,) - serializer_class = CeleryTaskSerializer - - def retrieve(self, request, *args, **kwargs): - system_user = self.get_object() + serializer_class = serializers.SystemUserTaskSerializer + + def do_push(self, system_user, asset=None): + if asset is None: + task = push_system_user_to_assets_manual.delay(system_user) + else: + username = self.request.query_params.get('username') + task = push_system_user_a_asset_manual.delay( + system_user, asset, username=username + ) + return task + + @staticmethod + def do_test(system_user, asset=None): task = test_system_user_connectivity_manual.delay(system_user) - return Response({"task": task.id}) - - -class SystemUserAssetsListView(generics.ListAPIView): - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.AssetSimpleSerializer - filter_fields = ("hostname", "ip") - http_method_names = ['get'] - search_fields = filter_fields + return task def get_object(self): pk = self.kwargs.get('pk') return get_object_or_404(SystemUser, pk=pk) - def get_queryset(self): + def perform_create(self, serializer): + action = serializer.validated_data["action"] + asset = serializer.validated_data.get('asset') system_user = self.get_object() - return system_user.assets.all() - - -class SystemUserPushToAssetApi(generics.RetrieveAPIView): - model = SystemUser - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.TaskIDSerializer - - 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 SystemUserTestAssetConnectivityApi(generics.RetrieveAPIView): - model = SystemUser - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.TaskIDSerializer - - 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_connectivity_a_asset.delay(system_user, asset) - return Response({"task": task.id}) + if action == 'push': + task = self.do_push(system_user, asset) + else: + task = self.do_test(system_user, asset) + data = getattr(serializer, '_data', {}) + data["task"] = task.id + setattr(serializer, '_data', data) class SystemUserCommandFilterRuleListApi(generics.ListAPIView): diff --git a/apps/assets/api/system_user_relation.py b/apps/assets/api/system_user_relation.py index de88cfe38..02dd887a1 100644 --- a/apps/assets/api/system_user_relation.py +++ b/apps/assets/api/system_user_relation.py @@ -8,10 +8,13 @@ from orgs.mixins.api import OrgBulkModelViewSet from orgs.utils import current_org from .. import models, serializers -__all__ = ['SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet'] +__all__ = [ + 'SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet', + 'SystemUserUserRelationViewSet', +] -class RelationMixin(OrgBulkModelViewSet): +class RelationMixin: def get_queryset(self): queryset = self.model.objects.all() org_id = current_org.org_id() @@ -24,7 +27,11 @@ class RelationMixin(OrgBulkModelViewSet): return queryset -class SystemUserAssetRelationViewSet(RelationMixin): +class BaseRelationViewSet(RelationMixin, OrgBulkModelViewSet): + pass + + +class SystemUserAssetRelationViewSet(BaseRelationViewSet): serializer_class = serializers.SystemUserAssetRelationSerializer model = models.SystemUser.assets.through permission_classes = (IsOrgAdmin,) @@ -47,7 +54,7 @@ class SystemUserAssetRelationViewSet(RelationMixin): return queryset -class SystemUserNodeRelationViewSet(RelationMixin): +class SystemUserNodeRelationViewSet(BaseRelationViewSet): serializer_class = serializers.SystemUserNodeRelationSerializer model = models.SystemUser.nodes.through permission_classes = (IsOrgAdmin,) @@ -63,3 +70,27 @@ class SystemUserNodeRelationViewSet(RelationMixin): queryset = queryset \ .annotate(node_key=F('node__key')) return queryset + + +class SystemUserUserRelationViewSet(BaseRelationViewSet): + serializer_class = serializers.SystemUserUserRelationSerializer + model = models.SystemUser.users.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'user', 'systemuser', + ] + search_fields = [ + "user__username", "user__name", + "systemuser__name", "systemuser__username", + ] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.annotate( + user_display=Concat( + F('user__name'), Value('('), + F('user__username'), Value(')') + ) + ) + return queryset + diff --git a/apps/assets/backends/admin_user.py b/apps/assets/backends/admin_user.py deleted file mode 100644 index 8f3644b1b..000000000 --- a/apps/assets/backends/admin_user.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from ..models import AdminUser -from .asset_user import AssetUserBackend - - -class AdminUserBackend(AssetUserBackend): - model = AdminUser - backend = 'AdminUser' diff --git a/apps/assets/backends/asset_user.py b/apps/assets/backends/asset_user.py deleted file mode 100644 index e76baf14b..000000000 --- a/apps/assets/backends/asset_user.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- -# -from collections import defaultdict -from .base import BaseBackend - - -class AssetUserBackend(BaseBackend): - model = None - backend = "AssetUser" - - @classmethod - def filter_queryset_more(cls, queryset): - return queryset - - @classmethod - def filter(cls, username=None, assets=None, **kwargs): - queryset = cls.model.objects.all() - prefer_id = kwargs.get('prefer_id') - if prefer_id: - queryset = queryset.filter(id=prefer_id) - instances = cls.construct_authbook_objects(queryset, assets) - return instances - if username: - queryset = queryset.filter(username=username) - if assets: - queryset = queryset.filter(assets__in=assets).distinct() - - queryset = cls.filter_queryset_more(queryset) - instances = cls.construct_authbook_objects(queryset, assets) - return instances - - @classmethod - def construct_authbook_objects(cls, asset_users, assets): - instances = [] - assets_user_assets_map = defaultdict(set) - if isinstance(asset_users, list): - assets_user_assets_map = { - asset_user.id: asset_user.assets.values_list('id', flat=True) - for asset_user in asset_users - } - else: - assets_user_assets = asset_users.values_list('id', 'assets') - for i, asset_id in assets_user_assets: - assets_user_assets_map[i].add(asset_id) - - for asset_user in asset_users: - if not assets: - related_assets = asset_user.assets.all() - else: - assets_map = {a.id: a for a in assets} - related_assets = [ - assets_map.get(i) for i in assets_user_assets_map.get(asset_user.id) if i in assets_map - ] - for asset in related_assets: - instance = asset_user.construct_to_authbook(asset) - instance.backend = cls.backend - instances.append(instance) - return instances diff --git a/apps/assets/backends/base.py b/apps/assets/backends/base.py index 9c9f7ca1e..d5ce1f903 100644 --- a/apps/assets/backends/base.py +++ b/apps/assets/backends/base.py @@ -1,94 +1,48 @@ # -*- coding: utf-8 -*- # -import uuid from abc import abstractmethod +from ..models import Asset + class BaseBackend: - @classmethod @abstractmethod - def filter(cls, username=None, assets=None, latest=True, prefer=None, prefer_id=None): - """ - :param username: 用户名 - :param assets: 对象 - :param latest: 是否是最新记录 - :param prefer: 优先使用 - :param prefer_id: 使用id - :return: 元素为的可迭代对象( or ) - """ + def all(self): pass + @abstractmethod + def filter(self, username=None, hostname=None, ip=None, assets=None, + node=None, prefer_id=None, **kwargs): + pass -class AssetUserQuerySet(list): - def order_by(self, *ordering): - _ordering = [] - reverse = False - for i in ordering: - if i[0] == '-': - reverse = True - i = i[1:] - _ordering.append(i) - self.sort(key=lambda obj: [getattr(obj, j) for j in _ordering], reverse=reverse) - return self - - def filter_in(self, kwargs): - in_kwargs = {} - queryset = [] - for k, v in kwargs.items(): - if len(v) == 0: - return self - if k.find("__in") >= 0: - _k = k.split('__')[0] - in_kwargs[_k] = v - else: - in_kwargs[k] = v - for k in in_kwargs: - kwargs.pop(k, None) - - if len(in_kwargs) == 0: - return self - for i in self: - matched = False - for k, v in in_kwargs.items(): - attr = getattr(i, k, None) - # 如果属性或者value中是uuid,则转换成string - if isinstance(v[0], uuid.UUID): - v = [str(i) for i in v] - if isinstance(attr, uuid.UUID): - attr = str(attr) - if attr in v: - matched = True - if matched: - queryset.append(i) - return AssetUserQuerySet(queryset) - - def filter_equal(self, kwargs): - def filter_it(obj): - wanted = [] - real = [] - for k, v in kwargs.items(): - wanted.append(v) - value = getattr(obj, k, None) - if isinstance(value, uuid.UUID): - value = str(value) - real.append(value) - return wanted == real - kwargs = {k: v for k, v in kwargs.items() if k.find('__in') == -1} - if len(kwargs) > 0: - queryset = AssetUserQuerySet([i for i in self if filter_it(i)]) - else: - queryset = self - return queryset + @abstractmethod + def search(self, item): + pass - def filter(self, **kwargs): - queryset = self.filter_in(kwargs).filter_equal(kwargs) - return queryset + @abstractmethod + def get_queryset(self): + pass - def distinct(self): - items = list(set(self)) - self[:] = items - return self + @abstractmethod + def delete(self, union_id): + pass - def __or__(self, other): - self.extend(other) - return self + @staticmethod + def qs_to_values(qs): + values = qs.values( + 'hostname', 'ip', "asset_id", + 'username', 'password', 'private_key', 'public_key', + 'score', 'version', + "asset_username", "union_id", + 'date_created', 'date_updated', + 'org_id', 'backend', + ) + return values + + @staticmethod + def make_assets_as_id(assets): + if not assets: + return [] + if isinstance(assets[0], Asset): + assets = [a.id for a in assets] + return assets diff --git a/apps/assets/backends/db.py b/apps/assets/backends/db.py index 40fa41444..7cdba2754 100644 --- a/apps/assets/backends/db.py +++ b/apps/assets/backends/db.py @@ -1,29 +1,317 @@ # -*- coding: utf-8 -*- # +from functools import reduce +from django.db.models import F, CharField, Value, IntegerField, Q, Count +from django.db.models.functions import Concat -from ..models import AuthBook +from common.utils import get_object_or_none +from orgs.utils import current_org +from ..models import AuthBook, SystemUser, Asset, AdminUser from .base import BaseBackend -class AuthBookBackend(BaseBackend): - @classmethod - def filter(cls, username=None, assets=None, latest=True, **kwargs): - queryset = AuthBook.objects.all() - if username is not None: - queryset = queryset.filter(username=username) - if assets: - queryset = queryset.filter(asset__in=assets) - if latest: - queryset = queryset.latest_version() - return queryset - - @classmethod - def create(cls, **kwargs): - auth_info = { - 'password': kwargs.pop('password', ''), - 'public_key': kwargs.pop('public_key', ''), - 'private_key': kwargs.pop('private_key', '') - } - obj = AuthBook.objects.create(**kwargs) - obj.set_auth(**auth_info) - return obj +class DBBackend(BaseBackend): + union_id_length = 2 + + def __init__(self, queryset=None): + if queryset is None: + queryset = self.all() + self.queryset = queryset + + def _clone(self): + return self.__class__(self.queryset) + + def all(self): + return AuthBook.objects.none() + + def count(self): + return self.queryset.count() + + def get_queryset(self): + return self.queryset + + def delete(self, union_id): + cleaned_union_id = union_id.split('_') + # 如果union_id通不过本检查,代表可能不是本backend, 应该返回空 + if not self._check_union_id(union_id, cleaned_union_id): + return + return self._perform_delete_by_union_id(cleaned_union_id) + + def _perform_delete_by_union_id(self, union_id_cleaned): + pass + + def filter(self, assets=None, node=None, prefer=None, prefer_id=None, + union_id=None, id__in=None, **kwargs): + clone = self._clone() + clone._filter_union_id(union_id) + clone._filter_prefer(prefer, prefer_id) + clone._filter_node(node) + clone._filter_assets(assets) + clone._filter_other(kwargs) + clone._filter_id_in(id__in) + return clone + + def _filter_union_id(self, union_id): + if not union_id: + return + cleaned_union_id = union_id.split('_') + # 如果union_id通不过本检查,代表可能不是本backend, 应该返回空 + if not self._check_union_id(union_id, cleaned_union_id): + self.queryset = self.queryset.none() + return + return self._perform_filter_union_id(union_id, cleaned_union_id) + + def _check_union_id(self, union_id, cleaned_union_id): + return union_id and len(cleaned_union_id) == self.union_id_length + + def _perform_filter_union_id(self, union_id, union_id_cleaned): + self.queryset = self.queryset.filter(union_id=union_id) + + def _filter_assets(self, assets): + assets_id = self.make_assets_as_id(assets) + if assets_id: + self.queryset = self.queryset.filter(asset_id__in=assets_id) + + def _filter_node(self, node): + pass + + def _filter_id_in(self, ids): + if ids and isinstance(ids, list): + self.queryset = self.queryset.filter(union_id__in=ids) + + @staticmethod + def clean_kwargs(kwargs): + return {k: v for k, v in kwargs.items() if v} + + def _filter_other(self, kwargs): + kwargs = self.clean_kwargs(kwargs) + if kwargs: + self.queryset = self.queryset.filter(**kwargs) + + def _filter_prefer(self, prefer, prefer_id): + pass + + def search(self, item): + qs = [] + for i in ['hostname', 'ip', 'username']: + kwargs = {i + '__startswith': item} + qs.append(Q(**kwargs)) + q = reduce(lambda x, y: x | y, qs) + clone = self._clone() + clone.queryset = clone.queryset.filter(q).distinct() + return clone + + +class SystemUserBackend(DBBackend): + model = SystemUser.assets.through + backend = 'system_user' + prefer = backend + base_score = 0 + union_id_length = 2 + + def _filter_prefer(self, prefer, prefer_id): + if prefer and prefer != self.prefer: + self.queryset = self.queryset.none() + + if prefer_id: + self.queryset = self.queryset.filter(systemuser__id=prefer_id) + + def _perform_filter_union_id(self, union_id, union_id_cleaned): + system_user_id, asset_id = union_id_cleaned + self.queryset = self.queryset.filter( + asset_id=asset_id, systemuser__id=system_user_id, + ) + + def _perform_delete_by_union_id(self, union_id_cleaned): + system_user_id, asset_id = union_id_cleaned + system_user = get_object_or_none(SystemUser, pk=system_user_id) + asset = get_object_or_none(Asset, pk=asset_id) + if all((system_user, asset)): + system_user.assets.remove(asset) + + def _filter_node(self, node): + if node: + self.queryset = self.queryset.filter(asset__nodes__id=node.id) + + def get_annotate(self): + kwargs = dict( + hostname=F("asset__hostname"), + ip=F("asset__ip"), + username=F("systemuser__username"), + password=F("systemuser__password"), + private_key=F("systemuser__private_key"), + public_key=F("systemuser__public_key"), + score=F("systemuser__priority") + self.base_score, + version=Value(0, IntegerField()), + date_created=F("systemuser__date_created"), + date_updated=F("systemuser__date_updated"), + asset_username=Concat(F("asset__id"), Value("_"), + F("systemuser__username"), + output_field=CharField()), + union_id=Concat(F("systemuser_id"), Value("_"), F("asset_id"), + output_field=CharField()), + org_id=F("asset__org_id"), + backend=Value(self.backend, CharField()) + ) + return kwargs + + def get_filter(self): + return dict( + systemuser__username_same_with_user=False, + ) + + def all(self): + kwargs = self.get_annotate() + filters = self.get_filter() + qs = self.model.objects.all().annotate(**kwargs) + if current_org.org_id() is not None: + filters['org_id'] = current_org.org_id() + qs = qs.filter(**filters) + qs = self.qs_to_values(qs) + return qs + + +class DynamicSystemUserBackend(SystemUserBackend): + backend = 'system_user_dynamic' + prefer = 'system_user' + union_id_length = 3 + + def get_annotate(self): + kwargs = super().get_annotate() + kwargs.update(dict( + username=F("systemuser__users__username"), + asset_username=Concat( + F("asset__id"), Value("_"), + F("systemuser__users__username"), + output_field=CharField() + ), + union_id=Concat( + F("systemuser_id"), Value("_"), F("asset_id"), + Value("_"), F("systemuser__users__id"), + output_field=CharField() + ), + users_count=Count('systemuser__users'), + )) + return kwargs + + def _perform_filter_union_id(self, union_id, union_id_cleaned): + system_user_id, asset_id, user_id = union_id_cleaned + self.queryset = self.queryset.filter( + asset_id=asset_id, systemuser_id=system_user_id, + union_id=union_id, + ) + + def _perform_delete_by_union_id(self, union_id_cleaned): + system_user_id, asset_id, user_id = union_id_cleaned + system_user = get_object_or_none(SystemUser, pk=system_user_id) + if not system_user: + return + system_user.users.remove(user_id) + if system_user.users.count() == 0: + system_user.assets.remove(asset_id) + + def get_filter(self): + return dict( + users_count__gt=0, + systemuser__username_same_with_user=True + ) + + +class AdminUserBackend(DBBackend): + model = Asset + backend = 'admin_user' + prefer = backend + base_score = 200 + + def _filter_prefer(self, prefer, prefer_id): + if prefer and prefer != self.backend: + self.queryset = self.queryset.none() + if prefer_id: + self.queryset = self.queryset.filter(admin_user__id=prefer_id) + + def _filter_node(self, node): + if node: + self.queryset = self.queryset.filter(nodes__id=node.id) + + def _perform_filter_union_id(self, union_id, union_id_cleaned): + admin_user_id, asset_id = union_id_cleaned + self.queryset = self.queryset.filter( + id=asset_id, admin_user_id=admin_user_id, + ) + + def _perform_delete_by_union_id(self, union_id_cleaned): + raise PermissionError("Could remove asset admin user") + + def all(self): + qs = self.model.objects.all().annotate( + asset_id=F("id"), + username=F("admin_user__username"), + password=F("admin_user__password"), + private_key=F("admin_user__private_key"), + public_key=F("admin_user__public_key"), + score=Value(self.base_score, IntegerField()), + version=Value(0, IntegerField()), + date_updated=F("admin_user__date_updated"), + asset_username=Concat(F("id"), Value("_"), F("admin_user__username"), output_field=CharField()), + union_id=Concat(F("admin_user_id"), Value("_"), F("id"), output_field=CharField()), + backend=Value(self.backend, CharField()), + ) + qs = self.qs_to_values(qs) + return qs + + +class AuthbookBackend(DBBackend): + model = AuthBook + backend = 'db' + prefer = backend + base_score = 400 + + def _filter_node(self, node): + if node: + self.queryset = self.queryset.filter(asset__nodes__id=node.id) + + def _filter_prefer(self, prefer, prefer_id): + if not prefer or not prefer_id: + return + if prefer.lower() == "admin_user": + model = AdminUser + elif prefer.lower() == "system_user": + model = SystemUser + else: + self.queryset = self.queryset.none() + return + obj = get_object_or_none(model, pk=prefer_id) + if obj is None: + self.queryset = self.queryset.none() + return + username = obj.get_username() + if isinstance(username, str): + self.queryset = self.queryset.filter(username=username) + # dynamic system user return more username + else: + self.queryset = self.queryset.filter(username__in=username) + + def _perform_filter_union_id(self, union_id, union_id_cleaned): + authbook_id, asset_id = union_id_cleaned + self.queryset = self.queryset.filter( + id=authbook_id, asset_id=asset_id, + ) + + def _perform_delete_by_union_id(self, union_id_cleaned): + authbook_id, asset_id = union_id_cleaned + authbook = get_object_or_none(AuthBook, pk=authbook_id) + if authbook.is_latest: + raise PermissionError("Latest version could be delete") + AuthBook.objects.filter(id=authbook_id).delete() + + def all(self): + qs = self.model.objects.all().annotate( + hostname=F("asset__hostname"), + ip=F("asset__ip"), + score=F('version') + self.base_score, + asset_username=Concat(F("asset__id"), Value("_"), F("username"), output_field=CharField()), + union_id=Concat(F("id"), Value("_"), F("asset_id"), output_field=CharField()), + backend=Value(self.backend, CharField()), + ) + qs = self.qs_to_values(qs) + return qs diff --git a/apps/assets/backends/manager.py b/apps/assets/backends/manager.py index 75b9c38b8..d686cb922 100644 --- a/apps/assets/backends/manager.py +++ b/apps/assets/backends/manager.py @@ -1,110 +1,162 @@ # -*- coding: utf-8 -*- # +from itertools import chain, groupby from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist -from .base import AssetUserQuerySet -from .db import AuthBookBackend -from .system_user import SystemUserBackend -from .admin_user import AdminUserBackend +from orgs.utils import current_org +from common.utils import get_logger, lazyproperty +from common.struct import QuerySetChain + +from ..models import AssetUser, AuthBook +from .db import ( + AuthbookBackend, SystemUserBackend, AdminUserBackend, + DynamicSystemUserBackend +) + +logger = get_logger(__name__) class NotSupportError(Exception): pass -class AssetUserManager: - """ - 资产用户管理器 - """ +class AssetUserQueryset: ObjectDoesNotExist = ObjectDoesNotExist MultipleObjectsReturned = MultipleObjectsReturned - NotSupportError = NotSupportError - MSG_NOT_EXIST = '{} Object matching query does not exist' - MSG_MULTIPLE = '{} get() returned more than one object ' \ - '-- it returned {}!' - backends = ( - ('db', AuthBookBackend), - ('system_user', SystemUserBackend), - ('admin_user', AdminUserBackend), - ) + def __init__(self, backends=()): + self.backends = backends + self._distinct_queryset = None + + def backends_queryset(self): + return [b.get_queryset() for b in self.backends] + + @lazyproperty + def backends_counts(self): + return [b.count() for b in self.backends] + + def filter(self, hostname=None, ip=None, username=None, + assets=None, asset=None, node=None, + id=None, prefer_id=None, prefer=None, id__in=None): + if not assets and asset: + assets = [asset] + + kwargs = dict( + hostname=hostname, ip=ip, username=username, + assets=assets, node=node, prefer=prefer, prefer_id=prefer_id, + id__in=id__in, union_id=id, + ) + logger.debug("Filter: {}".format(kwargs)) + backends = [] + for backend in self.backends: + clone = backend.filter(**kwargs) + backends.append(clone) + return self._clone(backends) + + def _clone(self, backends=None): + if backends is None: + backends = self.backends + return self.__class__(backends) + + def search(self, item): + backends = [] + for backend in self.backends: + new = backend.search(item) + backends.append(new) + return self._clone(backends) + + def distinct(self): + logger.debug("Distinct asset user queryset") + queryset_chain = chain(*(backend.get_queryset() for backend in self.backends)) + queryset_sorted = sorted( + queryset_chain, + key=lambda item: (item["asset_username"], item["score"]), + reverse=True, + ) + results = groupby(queryset_sorted, key=lambda item: item["asset_username"]) + final = [next(result[1]) for result in results] + self._distinct_queryset = final + return self - _prefer = "system_user" - - def filter(self, username=None, assets=None, latest=True, prefer=None, prefer_id=None): - if assets is not None and not assets: - return AssetUserQuerySet([]) - - if prefer: - self._prefer = prefer - - instances_map = {} - instances = [] - for name, backend in self.backends: - # if name != "db": - # continue - _instances = backend.filter( - username=username, assets=assets, latest=latest, - prefer=self._prefer, prefer_id=prefer_id, - ) - instances_map[name] = _instances - - # 如果不是获取最新版本,就不再merge - if not latest: - for _instances in instances_map.values(): - instances.extend(_instances) - return AssetUserQuerySet(instances) - - # merge的顺序 - ordering = ["db"] - if self._prefer == "system_user": - ordering.extend(["system_user", "admin_user"]) + def get(self, latest=False, **kwargs): + queryset = self.filter(**kwargs) + if latest: + queryset = queryset.distinct() + queryset = list(queryset) + count = len(queryset) + if count == 1: + data = queryset[0] + return data + elif count > 1: + msg = 'Should return 1 record, but get {}'.format(count) + raise MultipleObjectsReturned(msg) else: - ordering.extend(["admin_user", "system_user"]) - # 根据prefer决定优先使用系统用户或管理用户谁的 - ordering_instances = [instances_map.get(i, []) for i in ordering] - instances = self._merge_instances(*ordering_instances) - return AssetUserQuerySet(instances) - - def get(self, username, asset, **kwargs): - instances = self.filter(username, assets=[asset], **kwargs) - if len(instances) == 1: - return instances[0] - elif len(instances) == 0: - self.raise_does_not_exist(self.__class__.__name__) - else: - self.raise_multiple_return(self.__class__.__name__, len(instances)) - - def raise_does_not_exist(self, name): - raise self.ObjectDoesNotExist(self.MSG_NOT_EXIST.format(name)) + msg = 'No record found(org is {})'.format(current_org.name) + raise ObjectDoesNotExist(msg) - def raise_multiple_return(self, name, length): - raise self.MultipleObjectsReturned(self.MSG_MULTIPLE.format(name, length)) + def get_latest(self, **kwargs): + return self.get(latest=True, **kwargs) @staticmethod - def create(**kwargs): - instance = AuthBookBackend.create(**kwargs) - return instance + def to_asset_user(data): + obj = AssetUser() + for k, v in data.items(): + setattr(obj, k, v) + return obj + + @property + def queryset(self): + if self._distinct_queryset is not None: + return self._distinct_queryset + return QuerySetChain(self.backends_queryset()) + + def count(self): + if self._distinct_queryset is not None: + return len(self._distinct_queryset) + else: + return sum(self.backends_counts) - def all(self): - return self.filter() + def __getitem__(self, ndx): + return self.queryset.__getitem__(ndx) - def prefer(self, s): - self._prefer = s + def __iter__(self): + self._data = iter(self.queryset) return self - @staticmethod - def none(): - return AssetUserQuerySet() + def __next__(self): + return self.to_asset_user(next(self._data)) + + +class AssetUserManager: + support_backends = ( + ('db', AuthbookBackend), + ('system_user', SystemUserBackend), + ('admin_user', AdminUserBackend), + ('system_user_dynamic', DynamicSystemUserBackend), + ) + + def __init__(self): + self.backends = [backend() for name, backend in self.support_backends] + self._queryset = AssetUserQueryset(self.backends) + + def all(self): + return self._queryset + + def delete(self, obj): + name_backends_map = dict(self.support_backends) + backend_name = obj.backend + backend_cls = name_backends_map.get(backend_name) + union_id = obj.union_id + if backend_cls: + backend_cls().delete(union_id) + else: + raise ObjectDoesNotExist("Not backend found") @staticmethod - def _merge_instances(*args): - instances = list(args[0]) - keywords = [obj.keyword for obj in instances] - - for _instances in args[1:]: - need_merge_instances = [obj for obj in _instances if obj.keyword not in keywords] - need_merge_keywords = [obj.keyword for obj in need_merge_instances] - instances.extend(need_merge_instances) - keywords.extend(need_merge_keywords) - return instances + def create(**kwargs): + authbook = AuthBook(**kwargs) + authbook.save() + return authbook + + def __getattr__(self, item): + return getattr(self._queryset, item) diff --git a/apps/assets/backends/system_user.py b/apps/assets/backends/system_user.py deleted file mode 100644 index 8dda67d2a..000000000 --- a/apps/assets/backends/system_user.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -# - -import itertools - -from assets.models import SystemUser -from .asset_user import AssetUserBackend - - -class SystemUserBackend(AssetUserBackend): - model = SystemUser - backend = 'SystemUser' - - @classmethod - def filter_queryset_more(cls, queryset): - queryset = cls._distinct_system_users_by_username(queryset) - return queryset - - @classmethod - def _distinct_system_users_by_username(cls, system_users): - system_users = sorted( - system_users, - key=lambda su: (su.username, su.priority, su.date_updated), - reverse=True, - ) - results = itertools.groupby(system_users, key=lambda su: su.username) - system_users = [next(result[1]) for result in results] - return system_users - - diff --git a/apps/assets/backends/utils.py b/apps/assets/backends/utils.py index 62be16c1d..fbe190ba3 100644 --- a/apps/assets/backends/utils.py +++ b/apps/assets/backends/utils.py @@ -3,14 +3,5 @@ # from django.conf import settings -from .db import AuthBookBackend # from .vault import VaultBackend - -def get_backend(): - default_backend = AuthBookBackend - - # if settings.BACKEND_ASSET_USER_AUTH_VAULT: - # return VaultBackend - - return default_backend diff --git a/apps/assets/backends/vault.py b/apps/assets/backends/vault.py index d21245247..f19a64d9a 100644 --- a/apps/assets/backends/vault.py +++ b/apps/assets/backends/vault.py @@ -1,11 +1,4 @@ # -*- coding: utf-8 -*- # -from .base import BaseBackend - -class VaultBackend(BaseBackend): - - @classmethod - def filter(cls, username=None, asset=None, latest=True): - pass diff --git a/apps/assets/forms/asset.py b/apps/assets/forms/asset.py index 3a7a0f220..54d97f036 100644 --- a/apps/assets/forms/asset.py +++ b/apps/assets/forms/asset.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- # +from itertools import groupby from django import forms from django.utils.translation import gettext_lazy as _ from common.utils import get_logger from orgs.mixins.forms import OrgModelForm -from ..models import Asset +from ..models import Asset, Platform logger = get_logger(__file__) @@ -42,9 +43,26 @@ class AssetCreateUpdateForm(OrgModelForm): ] nodes_field.choices = nodes_choices + @staticmethod + def sorted_platform(platform): + if platform['base'] == 'Other': + return 'zz' + return platform['base'] + def set_platform_to_name(self): + choices = [] + platforms = Platform.objects.all().values('name', 'base') + platforms_sorted = sorted(platforms, key=self.sorted_platform) + platforms_grouped = groupby(platforms_sorted, key=lambda x: x['base']) + for i in platforms_grouped: + base = i[0] + grouped = sorted(i[1], key=lambda x: x['name']) + grouped = [(j['name'], j['name']) for j in grouped] + choices.append( + (base, grouped) + ) platform_field = self.fields['platform'] - platform_field.to_field_name = 'name' + platform_field.choices = choices if self.instance: self.initial['platform'] = self.instance.platform.name diff --git a/apps/assets/forms/user.py b/apps/assets/forms/user.py index 3d67b434b..ff8d27dfd 100644 --- a/apps/assets/forms/user.py +++ b/apps/assets/forms/user.py @@ -88,7 +88,9 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm): fields = [ 'name', 'username', 'protocol', 'auto_generate_key', 'password', 'private_key', 'auto_push', 'sudo', + 'username_same_with_user', 'comment', 'shell', 'priority', 'login_mode', 'cmd_filters', + 'sftp_root', ] widgets = { 'name': forms.TextInput(attrs={'placeholder': _('Name')}), @@ -97,11 +99,17 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm): 'class': 'select2', 'data-placeholder': _('Command filter') }), } + labels = { + 'username_same_with_user': _("Username same with user"), + } help_texts = { 'auto_push': _('Auto push system user to asset'), 'priority': _('1-100, High level will be using login asset as default, ' 'if user was granted more than 2 system user'), 'login_mode': _('If you choose manual login mode, you do not ' 'need to fill in the username and password.'), - 'sudo': _("Use comma split multi command, ex: /bin/whoami,/bin/ifconfig") + 'sudo': _("Use comma split multi command, ex: /bin/whoami,/bin/ifconfig"), + 'sftp_root': _("SFTP root dir, tmp, home or custom"), + 'username_same_with_user': _("Username is dynamic, When connect asset, using current user's username"), + # 'username_same_with_user': _("用户名是动态的,登录资产时使用当前用户的用户名登录"), } diff --git a/apps/assets/hands.py b/apps/assets/hands.py index 7c83e1332..ee13e589e 100644 --- a/apps/assets/hands.py +++ b/apps/assets/hands.py @@ -6,7 +6,7 @@ Other module of this app shouldn't connect with other app. - :copyright: (c) 2014-2018 by Jumpserver Team. + :copyright: (c) 2014-2018 by JumpServer Team. :license: GPL v2, see LICENSE for more details. """ diff --git a/apps/assets/migrations/0047_assetuser.py b/apps/assets/migrations/0047_assetuser.py new file mode 100644 index 000000000..6c309a0f7 --- /dev/null +++ b/apps/assets/migrations/0047_assetuser.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.7 on 2020-01-06 07:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0046_auto_20191218_1705'), + ] + + operations = [ + migrations.CreateModel( + name='AssetUser', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('assets.authbook',), + ), + ] diff --git a/apps/assets/migrations/0048_auto_20191230_1512.py b/apps/assets/migrations/0048_auto_20191230_1512.py new file mode 100644 index 000000000..b4bf9f9bc --- /dev/null +++ b/apps/assets/migrations/0048_auto_20191230_1512.py @@ -0,0 +1,35 @@ +# Generated by Django 2.2.7 on 2019-12-30 07:12 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('assets', '0047_assetuser'), + ] + + operations = [ + migrations.RemoveField( + model_name='authbook', + name='is_active', + ), + migrations.AddField( + model_name='systemuser', + name='username_same_with_user', + field=models.BooleanField(default=False, verbose_name='Username same with user'), + ), + migrations.AddField( + model_name='systemuser', + name='users', + field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Users'), + ), + migrations.AddField( + model_name='systemuser', + name='groups', + field=models.ManyToManyField(blank=True, to='users.UserGroup', + verbose_name='User groups'), + ), + ] diff --git a/apps/assets/migrations/0049_systemuser_sftp_root.py b/apps/assets/migrations/0049_systemuser_sftp_root.py new file mode 100644 index 000000000..d8e992e25 --- /dev/null +++ b/apps/assets/migrations/0049_systemuser_sftp_root.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2020-01-19 07:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0048_auto_20191230_1512'), + ] + + operations = [ + migrations.AddField( + model_name='systemuser', + name='sftp_root', + field=models.CharField(default='tmp', max_length=128, verbose_name='SFTP Root'), + ), + ] diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index db9c54aed..1bed74a16 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -1,6 +1,8 @@ +from .base import * from .asset import * from .label import Label from .user import * +from .asset_user import * from .cluster import * from .group import * from .domain import * diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index 70e2abb2a..39ea8420f 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -14,6 +14,7 @@ from django.utils.translation import ugettext_lazy as _ from common.fields.model import JsonDictTextField from common.utils import lazyproperty from orgs.mixins.models import OrgModelMixin, OrgManager +from .base import ConnectivityMixin from .utils import Connectivity __all__ = ['Asset', 'ProtocolsMixin', 'Platform'] @@ -40,10 +41,11 @@ def default_node(): class AssetManager(OrgManager): - def get_queryset(self): - return super().get_queryset().annotate( - platform_base=models.F('platform__base') - ) + # def get_queryset(self): + # return super().get_queryset().annotate( + # platform_base=models.F('platform__base') + # ) + pass class AssetQuerySet(models.QuerySet): @@ -243,6 +245,13 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): def platform_base(self): return self.platform.base + @lazyproperty + def admin_user_username(self): + """求可连接性时,直接用用户名去取,避免再查一次admin user + serializer 中直接通过annotate方式返回了这个 + """ + return self.admin_user.username + def is_windows(self): return self.platform.is_windows() @@ -275,9 +284,11 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): def connectivity(self): if self._connectivity: return self._connectivity - if not self.admin_user: + if not self.admin_user_username: return Connectivity.unknown() - connectivity = self.admin_user.get_asset_connectivity(self) + connectivity = ConnectivityMixin.get_asset_username_connectivity( + self, self.admin_user_username + ) return connectivity @connectivity.setter @@ -290,7 +301,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): if not self.admin_user: return {} - self.admin_user.load_specific_asset_auth(self) + self.admin_user.load_asset_special_auth(self) info = { 'username': self.admin_user.username, 'password': self.admin_user.password, diff --git a/apps/assets/models/asset_user.py b/apps/assets/models/asset_user.py new file mode 100644 index 000000000..118d4549b --- /dev/null +++ b/apps/assets/models/asset_user.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# +from .authbook import AuthBook + + +class AssetUser(AuthBook): + hostname = "" + ip = "" + backend = "" + union_id = "" + asset_username = "" + + class Meta: + proxy = True diff --git a/apps/assets/models/authbook.py b/apps/assets/models/authbook.py index 0243b43fb..0a0154100 100644 --- a/apps/assets/models/authbook.py +++ b/apps/assets/models/authbook.py @@ -5,26 +5,24 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from orgs.mixins.models import OrgManager -from .base import AssetUser +from .base import BaseUser __all__ = ['AuthBook'] class AuthBookQuerySet(models.QuerySet): - def latest_version(self): - return self.filter(is_latest=True).filter(is_active=True) + return self.filter(is_latest=True) class AuthBookManager(OrgManager): pass -class AuthBook(AssetUser): +class AuthBook(BaseUser): asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset')) is_latest = models.BooleanField(default=False, verbose_name=_('Latest version')) version = models.IntegerField(default=1, verbose_name=_('Version')) - is_active = models.BooleanField(default=True, verbose_name=_("Is active")) objects = AuthBookManager.from_queryset(AuthBookQuerySet)() backend = "db" diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 5d0f3cef1..3c9a481a5 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -12,98 +12,29 @@ from django.utils.translation import ugettext_lazy as _ from django.conf import settings from common.utils import ( - signer, ssh_key_string_to_obj, ssh_key_gen, get_logger + ssh_key_string_to_obj, ssh_key_gen, get_logger, lazyproperty ) from common.validators import alphanumeric from common import fields from orgs.mixins.models import OrgModelMixin -from .utils import private_key_validator, Connectivity +from .utils import Connectivity logger = get_logger(__file__) -class AssetUser(OrgModelMixin): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - name = models.CharField(max_length=128, verbose_name=_('Name')) - username = models.CharField(max_length=32, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True) - password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password')) - private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key')) - public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key')) - comment = models.TextField(blank=True, verbose_name=_('Comment')) - date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created")) - date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated")) - created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by')) - +class ConnectivityMixin: CONNECTIVITY_ASSET_CACHE_KEY = "ASSET_USER_{}_{}_ASSET_CONNECTIVITY" CONNECTIVITY_AMOUNT_CACHE_KEY = "ASSET_USER_{}_{}_CONNECTIVITY_AMOUNT" - ASSETS_AMOUNT_CACHE_KEY = "ASSET_USER_{}_ASSETS_AMOUNT" ASSET_USER_CACHE_TIME = 3600 * 24 - - _prefer = "system_user" - _assets_amount = None - - @property - def private_key_obj(self): - if self.private_key: - return ssh_key_string_to_obj(self.private_key, password=self.password) - else: - return None - - @property - def private_key_file(self): - if not self.private_key_obj: - return None - project_dir = settings.PROJECT_DIR - tmp_dir = os.path.join(project_dir, 'tmp') - key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest() - key_path = os.path.join(tmp_dir, key_name) - if not os.path.exists(key_path): - self.private_key_obj.write_private_key_file(key_path) - os.chmod(key_path, 0o400) - return key_path - - @property - def public_key_obj(self): - if self.public_key: - try: - return sshpubkeys.SSHKey(self.public_key) - except TabError: - pass - return None + id = '' + username = '' @property def part_id(self): i = '-'.join(str(self.id).split('-')[:3]) return i - def get_private_key(self): - if not self.private_key_obj: - return None - string_io = io.StringIO() - self.private_key_obj.write_private_key(string_io) - private_key = string_io.getvalue() - return private_key - - def get_related_assets(self): - assets = self.assets.all() - return assets - - def set_auth(self, password=None, private_key=None, public_key=None): - update_fields = [] - if password: - self.password = password - update_fields.append('password') - if private_key: - self.private_key = private_key - update_fields.append('private_key') - if public_key: - self.public_key = public_key - update_fields.append('public_key') - - if update_fields: - self.save(update_fields=update_fields) - def set_connectivity(self, summary): unreachable = summary.get('dark', {}).keys() reachable = summary.get('contacted', {}).keys() @@ -150,20 +81,10 @@ class AssetUser(OrgModelMixin): cache.set(cache_key, amount, self.ASSET_USER_CACHE_TIME) return amount - @property - def assets_amount(self): - if self._assets_amount is not None: - return self._assets_amount - cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id) - cached = cache.get(cache_key) - if not cached: - cached = self.get_related_assets().count() - cache.set(cache_key, cached, self.ASSET_USER_CACHE_TIME) - return cached - - def expire_assets_amount(self): - cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id) - cache.delete(cache_key) + @classmethod + def get_asset_username_connectivity(cls, asset, username): + key = cls.CONNECTIVITY_ASSET_CACHE_KEY.format(username, asset.id) + return Connectivity.get(key) def get_asset_connectivity(self, asset): key = self.get_asset_connectivity_key(asset) @@ -176,28 +97,103 @@ class AssetUser(OrgModelMixin): key = self.get_asset_connectivity_key(asset) Connectivity.set(key, c) - def get_asset_user(self, asset): + +class AuthMixin: + private_key = '' + password = '' + public_key = '' + username = '' + _prefer = 'system_user' + + @property + def private_key_obj(self): + if self.private_key: + key_obj = ssh_key_string_to_obj(self.private_key, password=self.password) + return key_obj + else: + return None + + @property + def private_key_file(self): + if not self.private_key_obj: + return None + project_dir = settings.PROJECT_DIR + tmp_dir = os.path.join(project_dir, 'tmp') + key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest() + key_path = os.path.join(tmp_dir, key_name) + if not os.path.exists(key_path): + self.private_key_obj.write_private_key_file(key_path) + os.chmod(key_path, 0o400) + return key_path + + def get_private_key(self): + if not self.private_key_obj: + return None + string_io = io.StringIO() + self.private_key_obj.write_private_key(string_io) + private_key = string_io.getvalue() + return private_key + + @property + def public_key_obj(self): + if self.public_key: + try: + return sshpubkeys.SSHKey(self.public_key) + except TabError: + pass + return None + + def set_auth(self, password=None, private_key=None, public_key=None): + update_fields = [] + if password: + self.password = password + update_fields.append('password') + if private_key: + self.private_key = private_key + update_fields.append('private_key') + if public_key: + self.public_key = public_key + update_fields.append('public_key') + + if update_fields: + self.save(update_fields=update_fields) + + def has_special_auth(self, asset=None): + from .authbook import AuthBook + queryset = AuthBook.objects.filter(username=self.username) + if asset: + queryset = queryset.filter(asset=asset) + return queryset.exists() + + def get_asset_user(self, asset, username=None): from ..backends import AssetUserManager + if username is None: + username = self.username try: - manager = AssetUserManager().prefer(self._prefer) - other = manager.get(username=self.username, asset=asset, prefer_id=self.id) + manager = AssetUserManager() + other = manager.get_latest( + username=username, asset=asset, + prefer_id=self.id, prefer=self._prefer, + ) return other except Exception as e: logger.error(e, exc_info=True) return None - def load_specific_asset_auth(self, asset): - instance = self.get_asset_user(asset) + def load_asset_special_auth(self, asset=None, username=None): + if not asset: + return self + + instance = self.get_asset_user(asset, username=username) if instance: self._merge_auth(instance) def _merge_auth(self, other): if other.password: self.password = other.password - if other.public_key: - self.public_key = other.public_key - if other.private_key: + if other.public_key or other.private_key: self.private_key = other.private_key + self.public_key = other.public_key def clear_auth(self): self.password = '' @@ -216,19 +212,57 @@ class AssetUser(OrgModelMixin): ) return private_key, public_key - def auto_gen_auth(self): - password = str(uuid.uuid4()) - private_key, public_key = ssh_key_gen( - username=self.username - ) + def auto_gen_auth(self, password=True, key=True): + _password = None + _private_key = None + _public_key = None + + if password: + _password = self.gen_password() + if key: + _private_key, _public_key = self.gen_key(self.username) self.set_auth( - password=password, private_key=private_key, - public_key=public_key + password=_password, private_key=_private_key, + public_key=_public_key ) - def auto_gen_auth_password(self): - password = str(uuid.uuid4()) - self.set_auth(password=password) + +class BaseUser(OrgModelMixin, AuthMixin, ConnectivityMixin): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + name = models.CharField(max_length=128, verbose_name=_('Name')) + username = models.CharField(max_length=32, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True) + password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password')) + private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key')) + public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key')) + comment = models.TextField(blank=True, verbose_name=_('Comment')) + date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created")) + date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated")) + created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by')) + + ASSETS_AMOUNT_CACHE_KEY = "ASSET_USER_{}_ASSETS_AMOUNT" + ASSET_USER_CACHE_TIME = 600 + + _prefer = "system_user" + + def get_related_assets(self): + assets = self.assets.filter(org_id=self.org_id) + return assets + + def get_username(self): + return self.username + + @lazyproperty + def assets_amount(self): + cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id) + cached = cache.get(cache_key) + if not cached: + cached = self.get_related_assets().count() + cache.set(cache_key, cached, self.ASSET_USER_CACHE_TIME) + return cached + + def expire_assets_amount(self): + cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id) + cache.delete(cache_key) def _to_secret_json(self): """Push system user use it""" @@ -240,26 +274,6 @@ class AssetUser(OrgModelMixin): 'private_key': self.private_key_file, } - def generate_id_with_asset(self, asset): - user_id = [self.part_id] - asset_id = str(asset.id).split('-')[3:] - ids = user_id + asset_id - return '-'.join(ids) - - def construct_to_authbook(self, asset): - from . import AuthBook - fields = [ - 'name', 'username', 'comment', 'org_id', - 'password', 'private_key', 'public_key', - 'date_created', 'date_updated', 'created_by' - ] - i = self.generate_id_with_asset(asset) - obj = AuthBook(id=i, asset=asset, version=0, is_latest=True) - for field in fields: - value = getattr(self, field) - setattr(obj, field, value) - return obj - class Meta: abstract = True diff --git a/apps/assets/models/domain.py b/apps/assets/models/domain.py index b89c3b128..2f4cc82b3 100644 --- a/apps/assets/models/domain.py +++ b/apps/assets/models/domain.py @@ -10,7 +10,7 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from orgs.mixins.models import OrgModelMixin -from .base import AssetUser +from .base import BaseUser __all__ = ['Domain', 'Gateway'] @@ -39,7 +39,7 @@ class Domain(OrgModelMixin): return random.choice(self.gateways) -class Gateway(AssetUser): +class Gateway(BaseUser): PROTOCOL_SSH = 'ssh' PROTOCOL_RDP = 'rdp' PROTOCOL_CHOICES = ( diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index f349eb9f6..2a884a6a1 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -11,9 +11,9 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext from django.core.cache import cache -from common.utils import get_logger, timeit, lazyproperty +from common.utils import get_logger, lazyproperty from orgs.mixins.models import OrgModelMixin, OrgManager -from orgs.utils import set_current_org, get_current_org, tmp_to_org +from orgs.utils import get_current_org, tmp_to_org, current_org from orgs.models import Organization @@ -26,63 +26,108 @@ class NodeQuerySet(models.QuerySet): raise PermissionError("Bulk delete node deny") -class TreeMixin: - tree_created_time = None - tree_updated_time_cache_key = 'NODE_TREE_UPDATED_AT' - tree_cache_time = 3600 - tree_assets_cache_key = 'NODE_TREE_ASSETS_UPDATED_AT' - tree_assets_created_time = None - _tree_service = None +class TreeCache: + updated_time_cache_key = 'NODE_TREE_UPDATED_AT_{}' + cache_time = 3600 + assets_updated_time_cache_key = 'NODE_TREE_ASSETS_UPDATED_AT_{}' - @classmethod - def tree(cls): - from ..utils import TreeService - tree_updated_time = cache.get(cls.tree_updated_time_cache_key, 0) + def __init__(self, tree, org_id): now = time.time() - # 什么时候重新初始化 _tree_service - if not cls.tree_created_time or \ - tree_updated_time > cls.tree_created_time: - logger.debug("Create node tree") - tree = TreeService.new() - cls.tree_created_time = now - cls.tree_assets_created_time = now - cls._tree_service = tree - return tree - # 是否要重新初始化节点资产 - node_assets_updated_time = cache.get(cls.tree_assets_cache_key, 0) - if not cls.tree_assets_created_time or \ - node_assets_updated_time > cls.tree_assets_created_time: - cls._tree_service.init_assets() - cls.tree_assets_created_time = now - logger.debug("Refresh node tree assets") - return cls._tree_service + self.created_time = now + self.assets_created_time = now + self.tree = tree + self.org_id = org_id + + def _has_changed(self, tp="tree"): + if tp == "assets": + key = self.assets_updated_time_cache_key.format(self.org_id) + else: + key = self.updated_time_cache_key.format(self.org_id) + updated_time = cache.get(key, 0) + if updated_time > self.created_time: + return True + else: + return False @classmethod - def refresh_tree(cls, t=None): - logger.debug("Refresh node tree") - key = cls.tree_updated_time_cache_key - ttl = cls.tree_cache_time + def set_changed(cls, tp="tree", t=None, org_id=None): + if org_id is None: + org_id = current_org.id + if tp == "assets": + key = cls.assets_updated_time_cache_key.format(org_id) + else: + key = cls.updated_time_cache_key.format(org_id) + ttl = cls.cache_time if not t: t = time.time() cache.set(key, t, ttl) + def tree_has_changed(self): + return self._has_changed("tree") + + def set_tree_changed(self, t=None): + logger.debug("Set tree tree changed") + self.__class__.set_changed(t=t, tp="tree") + + def assets_has_changed(self): + return self._has_changed("assets") + + def set_tree_assets_changed(self, t=None): + logger.debug("Set tree assets changed") + self.__class__.set_changed(t=t, tp="assets") + + def get(self): + if self.tree_has_changed(): + self.renew() + return self.tree + if self.assets_has_changed(): + self.tree.init_assets() + return self.tree + + def renew(self): + new_obj = self.__class__.new(self.org_id) + self.tree = new_obj.tree + self.created_time = new_obj.created_time + self.assets_created_time = new_obj.assets_created_time + @classmethod - def refresh_node_assets(cls, t=None): - logger.debug("Refresh node assets") - key = cls.tree_assets_cache_key - ttl = cls.tree_cache_time - if not t: - t = time.time() - cache.set(key, t, ttl) + def new(cls, org_id=None): + from ..utils import TreeService + logger.debug("Create node tree") + if not org_id: + org_id = current_org.id + with tmp_to_org(org_id): + tree = TreeService.new() + obj = cls(tree, org_id) + obj.tree = tree + return obj - @staticmethod - def refresh_user_tree_cache(): - """ - 当节点-节点关系,节点-资产关系发生变化时,应该刷新用户授权树缓存 - :return: - """ - from perms.utils.asset_permission import AssetPermissionUtilV2 - AssetPermissionUtilV2.expire_all_user_tree_cache() + +class TreeMixin: + _org_tree_map = {} + + @classmethod + def tree(cls): + org_id = current_org.org_id() + t = cls.get_local_tree_cache(org_id) + + if t is None: + t = TreeCache.new() + cls._org_tree_map[org_id] = t + return t.get() + + @classmethod + def get_local_tree_cache(cls, org_id=None): + t = cls._org_tree_map.get(org_id) + return t + + @classmethod + def refresh_tree(cls, t=None): + TreeCache.set_changed(tp="tree", t=t, org_id=current_org.id) + + @classmethod + def refresh_node_assets(cls, t=None): + TreeCache.set_changed(tp="assets", t=t, org_id=current_org.id) class FamilyMixin: @@ -376,15 +421,6 @@ class SomeNodesMixin: ) return obj - @classmethod - def empty_node(cls): - with tmp_to_org(Organization.system()): - defaults = {'value': cls.empty_value} - obj, created = cls.objects.get_or_create( - defaults=defaults, key=cls.empty_key - ) - return obj - @classmethod def default_node(cls): with tmp_to_org(Organization.default()): @@ -413,7 +449,6 @@ class SomeNodesMixin: @classmethod def initial_some_nodes(cls): cls.default_node() - cls.empty_node() cls.ungrouped_node() cls.favorite_node() @@ -523,13 +558,13 @@ class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin tree_node = TreeNode(**data) return tree_node - def has_children_or_contains_assets(self): - if self.children or self.get_assets(): + def has_children_or_has_assets(self): + if self.children or self.get_assets().exists(): return True return False def delete(self, using=None, keep_parents=False): - if self.has_children_or_contains_assets(): + if self.has_children_or_has_assets(): return return super().delete(using=using, keep_parents=keep_parents) diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 053ca0f77..40f2b8583 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -4,14 +4,12 @@ import logging -from functools import reduce from django.db import models -from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator, MaxValueValidator from common.utils import signer -from .base import AssetUser +from .base import BaseUser from .asset import Asset @@ -19,7 +17,7 @@ __all__ = ['AdminUser', 'SystemUser'] logger = logging.getLogger(__name__) -class AdminUser(AssetUser): +class AdminUser(BaseUser): """ A privileged user that ansible can use it to push system user and so on """ @@ -87,7 +85,7 @@ class AdminUser(AssetUser): continue -class SystemUser(AssetUser): +class SystemUser(BaseUser): PROTOCOL_SSH = 'ssh' PROTOCOL_RDP = 'rdp' PROTOCOL_TELNET = 'telnet' @@ -107,9 +105,11 @@ class SystemUser(AssetUser): (LOGIN_AUTO, _('Automatic login')), (LOGIN_MANUAL, _('Manually login')) ) - + username_same_with_user = models.BooleanField(default=False, verbose_name=_("Username same with user")) nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes")) assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets")) + users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users")) + groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User groups")) priority = models.IntegerField(default=20, verbose_name=_("Priority"), validators=[MinValueValidator(1), MaxValueValidator(100)]) protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol')) auto_push = models.BooleanField(default=True, verbose_name=_('Auto push')) @@ -117,9 +117,20 @@ class SystemUser(AssetUser): shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell')) login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode')) cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True) + sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root")) + _prefer = 'system_user' def __str__(self): - return '{0.name}({0.username})'.format(self) + username = self.username + if self.username_same_with_user: + username = 'dynamic' + return '{0.name}({1})'.format(self, username) + + def get_username(self): + if self.username_same_with_user: + return list(self.users.values_list('username', flat=True)) + else: + return self.username @property def nodes_amount(self): diff --git a/apps/assets/serializers/admin_user.py b/apps/assets/serializers/admin_user.py index 63aac8cc0..5e27b0c64 100644 --- a/apps/assets/serializers/admin_user.py +++ b/apps/assets/serializers/admin_user.py @@ -55,3 +55,11 @@ class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer): class TaskIDSerializer(serializers.Serializer): task = serializers.CharField(read_only=True) + + +class AssetUserTaskSerializer(serializers.Serializer): + ACTION_CHOICES = ( + ('test', 'test'), + ) + action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True) + task = serializers.CharField(read_only=True) diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index c34fad77b..82d6964a4 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -import re from rest_framework import serializers -from django.db.models import Prefetch +from django.db.models import Prefetch, F + from django.utils.translation import ugettext_lazy as _ from orgs.mixins.serializers import BulkOrgResourceModelSerializer @@ -12,8 +12,9 @@ from .base import ConnectivitySerializer __all__ = [ 'AssetSerializer', 'AssetSimpleSerializer', + 'AssetDisplaySerializer', 'ProtocolsField', 'PlatformSerializer', - 'AssetDetailSerializer', + 'AssetDetailSerializer', 'AssetTaskSerializer', ] @@ -66,8 +67,6 @@ class AssetSerializer(BulkOrgResourceModelSerializer): slug_field='name', queryset=Platform.objects.all(), label=_("Platform") ) protocols = ProtocolsField(label=_('Protocols'), required=False) - connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity")) - """ 资产的数据结构 """ @@ -81,7 +80,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer): 'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info', 'os', 'os_version', 'os_arch', 'hostname_raw', 'comment', 'created_by', 'date_created', - 'hardware_info', 'connectivity', + 'hardware_info', ] read_only_fields = ( 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count', @@ -102,7 +101,8 @@ class AssetSerializer(BulkOrgResourceModelSerializer): queryset = queryset.prefetch_related( Prefetch('nodes', queryset=Node.objects.all().only('id')), Prefetch('labels', queryset=Label.objects.all().only('id')), - ).select_related('admin_user', 'domain', 'platform') + ).select_related('admin_user', 'domain', 'platform') \ + .annotate(platform_base=F('platform__base')) return queryset def compatible_with_old_protocol(self, validated_data): @@ -130,6 +130,28 @@ class AssetSerializer(BulkOrgResourceModelSerializer): return super().update(instance, validated_data) +class AssetDisplaySerializer(AssetSerializer): + connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity")) + + class Meta(AssetSerializer.Meta): + fields = [ + 'id', 'ip', 'hostname', 'protocol', 'port', + 'protocols', 'is_active', 'public_ip', + 'number', 'vendor', 'model', 'sn', + 'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory', + 'disk_total', 'disk_info', 'os', 'os_version', 'os_arch', + 'hostname_raw', 'comment', 'created_by', 'date_created', + 'hardware_info', 'connectivity', + ] + + @classmethod + def setup_eager_loading(cls, queryset): + """ Perform necessary eager loading of data. """ + queryset = queryset\ + .annotate(admin_user_username=F('admin_user__username')) + return queryset + + class PlatformSerializer(serializers.ModelSerializer): meta = serializers.DictField(required=False, allow_null=True) @@ -151,3 +173,12 @@ class AssetSimpleSerializer(serializers.ModelSerializer): class Meta: model = Asset fields = ['id', 'hostname', 'ip', 'connectivity', 'port'] + + +class AssetTaskSerializer(serializers.Serializer): + ACTION_CHOICES = ( + ('refresh', 'refresh'), + ('test', 'test'), + ) + task = serializers.CharField(read_only=True) + action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True) diff --git a/apps/assets/serializers/asset_user.py b/apps/assets/serializers/asset_user.py index 1e05b8f5b..896ad9bef 100644 --- a/apps/assets/serializers/asset_user.py +++ b/apps/assets/serializers/asset_user.py @@ -8,39 +8,23 @@ from common.serializers import AdaptedBulkListSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import AuthBook, Asset from ..backends import AssetUserManager + from .base import ConnectivitySerializer, AuthSerializerMixin __all__ = [ - 'AssetUserSerializer', 'AssetUserAuthInfoSerializer', - 'AssetUserExportSerializer', 'AssetUserPushSerializer', + 'AssetUserWriteSerializer', 'AssetUserReadSerializer', + 'AssetUserAuthInfoSerializer', 'AssetUserPushSerializer', ] -class BasicAssetSerializer(serializers.ModelSerializer): - class Meta: - model = Asset - fields = ['hostname', 'ip'] - - -class AssetUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): - hostname = serializers.CharField(read_only=True, label=_("Hostname")) - ip = serializers.CharField(read_only=True, label=_("IP")) - connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity")) - - backend = serializers.CharField(read_only=True, label=_("Backend")) - +class AssetUserWriteSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class Meta: model = AuthBook list_serializer_class = AdaptedBulkListSerializer - read_only_fields = ( - 'date_created', 'date_updated', 'created_by', - 'is_latest', 'version', 'connectivity', - ) fields = [ - "id", "hostname", "ip", "username", "password", "asset", "version", - "is_latest", "connectivity", "backend", - "date_created", "date_updated", "private_key", "public_key", + 'id', 'username', 'password', 'private_key', "public_key", + 'asset', 'comment', ] extra_kwargs = { 'username': {'required': True}, @@ -57,7 +41,32 @@ class AssetUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): return instance -class AssetUserExportSerializer(AssetUserSerializer): +class AssetUserReadSerializer(AssetUserWriteSerializer): + id = serializers.CharField(read_only=True, source='union_id', label=_("ID")) + hostname = serializers.CharField(read_only=True, label=_("Hostname")) + ip = serializers.CharField(read_only=True, label=_("IP")) + asset = serializers.CharField(source='asset_id', label=_('Asset')) + backend = serializers.CharField(read_only=True, label=_("Backend")) + + class Meta(AssetUserWriteSerializer.Meta): + read_only_fields = ( + 'date_created', 'date_updated', + 'created_by', 'version', + ) + fields = [ + 'id', 'username', 'password', 'private_key', "public_key", + 'asset', 'hostname', 'ip', 'backend', 'version', + 'date_created', "date_updated", 'comment', + ] + extra_kwargs = { + 'username': {'required': True}, + 'password': {'write_only': True}, + 'private_key': {'write_only': True}, + 'public_key': {'write_only': True}, + } + + +class AssetUserAuthInfoSerializer(AssetUserReadSerializer): password = serializers.CharField( max_length=256, allow_blank=True, allow_null=True, required=False, label=_('Password') @@ -72,12 +81,6 @@ class AssetUserExportSerializer(AssetUserSerializer): ) -class AssetUserAuthInfoSerializer(serializers.ModelSerializer): - class Meta: - model = AuthBook - fields = ['password', 'private_key', 'public_key'] - - class AssetUserPushSerializer(serializers.Serializer): asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, label=_("Asset")) username = serializers.CharField(max_length=1024) diff --git a/apps/assets/serializers/base.py b/apps/assets/serializers/base.py index 39e33ffe1..c64b767e9 100644 --- a/apps/assets/serializers/base.py +++ b/apps/assets/serializers/base.py @@ -5,6 +5,7 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers from common.utils import ssh_pubkey_gen, validate_ssh_private_key +from ..models import AssetUser class AuthSerializer(serializers.ModelSerializer): @@ -60,9 +61,6 @@ class AuthSerializerMixin: if not value: validated_data.pop(field, None) - # print(validated_data) - # raise serializers.ValidationError(">>>>>>") - def create(self, validated_data): self.clean_auth_fields(validated_data) return super().create(validated_data) @@ -70,3 +68,15 @@ class AuthSerializerMixin: def update(self, instance, validated_data): self.clean_auth_fields(validated_data) return super().update(instance, validated_data) + + +class AuthInfoSerializer(serializers.ModelSerializer): + private_key = serializers.ReadOnlyField(source='get_private_key') + + class Meta: + model = AssetUser + fields = [ + 'username', 'password', + 'private_key', 'public_key', + 'date_updated', + ] diff --git a/apps/assets/serializers/node.py b/apps/assets/serializers/node.py index 79e07df13..063c91e0f 100644 --- a/apps/assets/serializers/node.py +++ b/apps/assets/serializers/node.py @@ -8,7 +8,7 @@ from ..models import Asset, Node __all__ = [ 'NodeSerializer', "NodeAddChildrenSerializer", - "NodeAssetsSerializer", + "NodeAssetsSerializer", "NodeTaskSerializer", ] @@ -51,3 +51,12 @@ class NodeAssetsSerializer(BulkOrgResourceModelSerializer): class NodeAddChildrenSerializer(serializers.Serializer): nodes = serializers.ListField() + +class NodeTaskSerializer(serializers.Serializer): + ACTION_CHOICES = ( + ('refresh', 'refresh'), + ('test', 'test'), + ('refresh_cache', 'refresh_cache'), + ) + task = serializers.CharField(read_only=True) + action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True) diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index e16a7a271..a7e413018 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -1,20 +1,21 @@ -import re from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ +from django.db.models import Count from common.serializers import AdaptedBulkListSerializer from common.mixins.serializers import BulkSerializerMixin from common.utils import ssh_pubkey_gen from orgs.mixins.serializers import BulkOrgResourceModelSerializer from assets.models import Node -from ..models import SystemUser -from .base import AuthSerializer, AuthSerializerMixin +from ..models import SystemUser, Asset +from .base import AuthSerializerMixin __all__ = [ - 'SystemUserSerializer', 'SystemUserAuthSerializer', + 'SystemUserSerializer', 'SystemUserListSerializer', 'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer', - 'SystemUserNodeRelationSerializer', + 'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer', + 'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer', ] @@ -28,10 +29,13 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): model = SystemUser list_serializer_class = AdaptedBulkListSerializer fields = [ - 'id', 'name', 'username', 'password', 'public_key', 'private_key', - 'login_mode', 'login_mode_display', 'priority', 'protocol', + 'id', 'name', 'username', 'protocol', + 'password', 'public_key', 'private_key', + 'login_mode', 'login_mode_display', + 'priority', 'username_same_with_user', 'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment', - 'assets_amount', 'nodes_amount', 'auto_generate_key' + 'auto_generate_key', 'sftp_root', + 'assets_amount', ] extra_kwargs = { 'password': {"write_only": True}, @@ -67,17 +71,43 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): value = False return value + def validate_username_same_with_user(self, username_same_with_user): + if not username_same_with_user: + return username_same_with_user + protocol = self.initial_data.get("protocol", "ssh") + queryset = SystemUser.objects.filter( + protocol=protocol, username_same_with_user=True + ) + if self.instance: + queryset = queryset.exclude(id=self.instance.id) + exists = queryset.exists() + if not exists: + return username_same_with_user + error = _("Username same with user with protocol {} only allow 1").format(protocol) + raise serializers.ValidationError(error) + def validate_username(self, username): if username: return username login_mode = self.initial_data.get("login_mode") protocol = self.initial_data.get("protocol") + username_same_with_user = self.initial_data.get("username_same_with_user") + if username_same_with_user: + return '' if login_mode == SystemUser.LOGIN_AUTO and \ protocol != SystemUser.PROTOCOL_VNC: msg = _('* Automatic login mode must fill in the username.') raise serializers.ValidationError(msg) return username + def validate_sftp_root(self, value): + if value in ['home', 'tmp']: + return value + if not value.startswith('/'): + error = _("Path should starts with /") + raise serializers.ValidationError(error) + return value + def validate_password(self, password): super().validate_password(password) auto_gen_key = self.initial_data.get("auto_generate_key", False) @@ -112,29 +142,34 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): attrs["public_key"] = public_key return attrs + +class SystemUserListSerializer(SystemUserSerializer): + class Meta(SystemUserSerializer.Meta): + fields = [ + 'id', 'name', 'username', 'protocol', + 'login_mode', 'login_mode_display', + 'priority', "username_same_with_user", + 'auto_push', 'sudo', 'shell', 'comment', + "assets_amount", + 'auto_generate_key', + 'sftp_root', + ] + @classmethod def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ - queryset = queryset.prefetch_related('cmd_filters', 'nodes') + queryset = queryset.annotate(assets_amount=Count("assets")) return queryset -class SystemUserAuthSerializer(AuthSerializer): - """ - 系统用户认证信息 - """ - private_key = serializers.SerializerMethodField() - - class Meta: - model = SystemUser - fields = [ - "id", "name", "username", "protocol", - "login_mode", "password", "private_key", - ] - - @staticmethod - def get_private_key(obj): - return obj.get_private_key() +class SystemUserWithAuthInfoSerializer(SystemUserSerializer): + class Meta(SystemUserSerializer.Meta): + extra_kwargs = { + 'nodes_amount': {'label': _('Node')}, + 'assets_amount': {'label': _('Asset')}, + 'login_mode_display': {'label': _('Login mode display')}, + 'created_by': {'read_only': True}, + } class SystemUserSimpleSerializer(serializers.ModelSerializer): @@ -186,3 +221,25 @@ class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerialize return self.tree.get_node_full_tag(obj.node_key) else: return obj.node.full_value + + +class SystemUserUserRelationSerializer(RelationMixin, serializers.ModelSerializer): + user_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = SystemUser.users.through + fields = [ + 'id', "user", "user_display", + ] + + +class SystemUserTaskSerializer(serializers.Serializer): + ACTION_CHOICES = ( + ("test", "test"), + ("push", "push"), + ) + action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True) + asset = serializers.PrimaryKeyRelatedField( + queryset=Asset.objects, allow_null=True, required=False, write_only=True + ) + task = serializers.CharField(read_only=True) diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index 4f171d36f..696732fc8 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -7,8 +7,9 @@ from django.db.models.signals import ( from django.db.models.aggregates import Count from django.dispatch import receiver -from common.utils import get_logger, timeit +from common.utils import get_logger from common.decorator import on_transaction_commit +from orgs.utils import tmp_to_root_org from .models import Asset, SystemUser, Node, AuthBook from .utils import TreeService from .tasks import ( @@ -113,6 +114,20 @@ def on_system_user_nodes_change(sender, instance=None, action=None, model=None, add_nodes_assets_to_system_users.delay(nodes_keys, system_users) +@receiver(m2m_changed, sender=SystemUser.groups.through) +def on_system_user_groups_change(sender, instance=None, action=None, model=None, + pk_set=None, reverse=False, **kwargs): + """ + 当系统用户和用户组关系发生变化时,应该将组下用户关联到新的系统用户上 + """ + if action != "post_add" or reverse: + return + logger.info("System user groups update signal recv: {}".format(instance)) + groups = model.objects.filter(pk__in=pk_set).annotate(users_count=Count("users")) + users = groups.filter(users_count__gt=0).values_list('users', flat=True) + instance.users.add(*tuple(users)) + + @receiver(m2m_changed, sender=Asset.nodes.through) def on_asset_nodes_change(sender, instance=None, action='', **kwargs): """ @@ -121,6 +136,8 @@ def on_asset_nodes_change(sender, instance=None, action='', **kwargs): if action.startswith('post'): logger.debug("Asset nodes change signal recv: {}".format(instance)) Node.refresh_assets() + with tmp_to_root_org(): + Node.refresh_assets() @receiver(m2m_changed, sender=Asset.nodes.through) @@ -195,6 +212,8 @@ def on_asset_nodes_remove(sender, instance=None, action='', model=None, def on_node_update_or_created(sender, **kwargs): # 刷新节点 Node.refresh_nodes() + with tmp_to_root_org(): + Node.refresh_nodes() @receiver(post_save, sender=AuthBook) diff --git a/apps/assets/tasks/admin_user_connectivity.py b/apps/assets/tasks/admin_user_connectivity.py index ed089553e..1760c1f4d 100644 --- a/apps/assets/tasks/admin_user_connectivity.py +++ b/apps/assets/tasks/admin_user_connectivity.py @@ -4,11 +4,12 @@ from celery import shared_task from django.utils.translation import ugettext as _ from django.core.cache import cache +from orgs.utils import tmp_to_root_org, org_aware_func from common.utils import get_logger from ops.celery.decorator import register_as_period_task from ..models import AdminUser -from .utils import clean_hosts +from .utils import clean_ansible_task_hosts from .asset_connectivity import test_asset_connectivity_util from . import const @@ -20,7 +21,7 @@ __all__ = [ ] -@shared_task(queue="ansible") +@org_aware_func("admin_user") def test_admin_user_connectivity_util(admin_user, task_name): """ Test asset admin user can connect or not. Using ansible api do that @@ -29,7 +30,7 @@ def test_admin_user_connectivity_util(admin_user, task_name): :return: """ assets = admin_user.get_related_assets() - hosts = clean_hosts(assets) + hosts = clean_ansible_task_hosts(assets) if not hosts: return {} summary = test_asset_connectivity_util(hosts, task_name) @@ -51,10 +52,13 @@ def test_admin_user_connectivity_period(): logger.debug("Test admin user connectivity, less than 40 minutes, skip") return cache.set(key, 1, 60*40) - admin_users = AdminUser.objects.all() - for admin_user in admin_users: - task_name = _("Test admin user connectivity period: {}").format(admin_user.name) - test_admin_user_connectivity_util(admin_user, task_name) + with tmp_to_root_org(): + admin_users = AdminUser.objects.all() + for admin_user in admin_users: + task_name = _("Test admin user connectivity period: {}").format( + admin_user.name + ) + test_admin_user_connectivity_util(admin_user, task_name) cache.set(key, 1, 60*40) diff --git a/apps/assets/tasks/asset_connectivity.py b/apps/assets/tasks/asset_connectivity.py index b777dde7c..8c02d0db1 100644 --- a/apps/assets/tasks/asset_connectivity.py +++ b/apps/assets/tasks/asset_connectivity.py @@ -1,55 +1,55 @@ # ~*~ coding: utf-8 ~*~ +from itertools import groupby from collections import defaultdict from celery import shared_task from django.utils.translation import ugettext as _ from common.utils import get_logger +from orgs.utils import org_aware_func from ..models.utils import Connectivity from . import const -from .utils import clean_hosts +from .utils import clean_ansible_task_hosts, group_asset_by_platform logger = get_logger(__file__) -__all__ = ['test_asset_connectivity_util', 'test_asset_connectivity_manual'] +__all__ = [ + 'test_asset_connectivity_util', 'test_asset_connectivity_manual', + 'test_node_assets_connectivity_manual', +] @shared_task(queue="ansible") +@org_aware_func("assets") def test_asset_connectivity_util(assets, task_name=None): from ops.utils import update_or_create_ansible_task if task_name is None: task_name = _("Test assets connectivity") - hosts = clean_hosts(assets) + hosts = clean_ansible_task_hosts(assets) if not hosts: return {} + platform_hosts_map = {} + hosts_sorted = sorted(hosts, key=group_asset_by_platform) + platform_hosts = groupby(hosts_sorted, key=group_asset_by_platform) + for i in platform_hosts: + platform_hosts_map[i[0]] = list(i[1]) - hosts_category = { - 'linux': { - 'hosts': [], - 'tasks': const.TEST_ADMIN_USER_CONN_TASKS - }, - 'windows': { - 'hosts': [], - 'tasks': const.TEST_WINDOWS_ADMIN_USER_CONN_TASKS - } + platform_tasks_map = { + "unixlike": const.PING_UNIXLIKE_TASKS, + "windows": const.PING_WINDOWS_TASKS } - for host in hosts: - hosts_list = hosts_category['windows']['hosts'] if host.is_windows() \ - else hosts_category['linux']['hosts'] - hosts_list.append(host) - results_summary = dict( contacted=defaultdict(dict), dark=defaultdict(dict), success=True ) - created_by = assets[0].org_id - for k, value in hosts_category.items(): - if not value['hosts']: + for platform, _hosts in platform_hosts_map.items(): + if not _hosts: continue + logger.debug("System user not has special auth") + tasks = platform_tasks_map.get(platform) task, created = update_or_create_ansible_task( - task_name=task_name, hosts=value['hosts'], tasks=value['tasks'], + task_name=task_name, hosts=_hosts, tasks=tasks, pattern='all', options=const.TASK_OPTIONS, run_as_admin=True, - created_by=created_by, ) raw, summary = task.run() success = summary.get('success', False) @@ -59,6 +59,7 @@ def test_asset_connectivity_util(assets, task_name=None): results_summary['success'] &= success results_summary['contacted'].update(contacted) results_summary['dark'].update(dark) + continue for asset in assets: if asset.hostname in results_summary.get('dark', {}).keys(): @@ -79,3 +80,12 @@ def test_asset_connectivity_manual(asset): return False, summary['dark'] else: return True, "" + + +@shared_task(queue="ansible") +def test_node_assets_connectivity_manual(node): + task_name = _("Test if the assets under the node are connectable: {}".format(node.name)) + assets = node.get_all_assets() + result = test_asset_connectivity_util(assets, task_name=task_name) + return result + diff --git a/apps/assets/tasks/asset_user_connectivity.py b/apps/assets/tasks/asset_user_connectivity.py index 5a4bbcc43..6d979d6b1 100644 --- a/apps/assets/tasks/asset_user_connectivity.py +++ b/apps/assets/tasks/asset_user_connectivity.py @@ -3,7 +3,9 @@ from celery import shared_task from django.utils.translation import ugettext as _ -from common.utils import get_logger +from common.utils import get_logger, get_object_or_none +from orgs.utils import org_aware_func +from ..models import Asset from . import const from .utils import check_asset_can_run_ansible @@ -13,15 +15,16 @@ logger = get_logger(__file__) __all__ = [ 'test_asset_user_connectivity_util', 'test_asset_users_connectivity_manual', - 'get_test_asset_user_connectivity_tasks', + 'get_test_asset_user_connectivity_tasks', 'test_user_connectivity', + 'run_adhoc', ] def get_test_asset_user_connectivity_tasks(asset): if asset.is_unixlike(): - tasks = const.TEST_ASSET_USER_CONN_TASKS + tasks = const.PING_UNIXLIKE_TASKS elif asset.is_windows(): - tasks = const.TEST_WINDOWS_ASSET_USER_CONN_TASKS + tasks = const.PING_WINDOWS_TASKS else: msg = _( "The asset {} system platform {} does not " @@ -32,46 +35,98 @@ def get_test_asset_user_connectivity_tasks(asset): return tasks -@shared_task(queue="ansible") -def test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=False): +def run_adhoc(task_name, tasks, inventory): + """ + :param task_name + :param tasks + :param inventory + """ + from ops.ansible.runner import AdHocRunner + runner = AdHocRunner(inventory, options=const.TASK_OPTIONS) + result = runner.run(tasks, 'all', task_name) + return result.results_raw, result.results_summary + + +def test_user_connectivity(task_name, asset, username, password=None, private_key=None): + """ + :param task_name + :param asset + :param username + :param password + :param private_key + """ + from ops.inventory import JMSCustomInventory + + tasks = get_test_asset_user_connectivity_tasks(asset) + if not tasks: + logger.debug("No tasks ") + return {}, {} + inventory = JMSCustomInventory( + assets=[asset], username=username, password=password, + private_key=private_key + ) + raw, summary = run_adhoc( + task_name=task_name, tasks=tasks, inventory=inventory + ) + return raw, summary + + +@org_aware_func("asset_user") +def test_asset_user_connectivity_util(asset_user, task_name): """ :param asset_user: 对象 :param task_name: - :param run_as_admin: :return: """ - from ops.utils import update_or_create_ansible_task - if not check_asset_can_run_ansible(asset_user.asset): return - tasks = get_test_asset_user_connectivity_tasks(asset_user.asset) - if not tasks: - logger.debug("No tasks ") + try: + raw, summary = test_user_connectivity( + task_name=task_name, asset=asset_user.asset, + username=asset_user.username, password=asset_user.password, + private_key=asset_user.private_key + ) + except Exception as e: + logger.warn("Failed run adhoc {}, {}".format(task_name, e)) return - - args = (task_name,) - kwargs = { - 'hosts': [asset_user.asset], 'tasks': tasks, - 'pattern': 'all', 'options': const.TASK_OPTIONS, - 'created_by': asset_user.org_id, - } - if run_as_admin: - kwargs["run_as_admin"] = True - else: - kwargs["run_as"] = asset_user.username - task, created = update_or_create_ansible_task(*args, **kwargs) - raw, summary = task.run() asset_user.set_connectivity(summary) @shared_task(queue="ansible") -def test_asset_users_connectivity_manual(asset_users, run_as_admin=False): +def test_asset_users_connectivity_manual(asset_users): """ :param asset_users: 对象 """ for asset_user in asset_users: task_name = _("Test asset user connectivity: {}").format(asset_user) - test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=run_as_admin) + test_asset_user_connectivity_util(asset_user, task_name) + + +@shared_task(queue="ansible") +def push_asset_user_util(asset_user): + """ + :param asset_user: 对象 + """ + from .push_system_user import push_system_user_util + if not asset_user.backend.startswith('system_user'): + logger.error("Asset user is not from system user") + return + union_id = asset_user.union_id + union_id_list = union_id.split('_') + if len(union_id_list) < 2: + logger.error("Asset user union id length less than 2") + return + system_user_id = union_id_list[0] + asset_id = union_id_list[1] + asset = get_object_or_none(Asset, pk=asset_id) + system_user = None + if not asset: + return + hosts = check_asset_can_run_ansible([asset]) + if asset.is_unixlike: + pass + + diff --git a/apps/assets/tasks/const.py b/apps/assets/tasks/const.py index 5b7db13cd..ce9ad9074 100644 --- a/apps/assets/tasks/const.py +++ b/apps/assets/tasks/const.py @@ -18,27 +18,10 @@ UPDATE_ASSETS_HARDWARE_TASKS = [ } ] -TEST_ADMIN_USER_CONN_TASKS = [ - { - "name": "ping", - "action": { - "module": "ping", - } - } -] -TEST_WINDOWS_ADMIN_USER_CONN_TASKS = [ - { - "name": "ping", - "action": { - "module": "win_ping", - } - } -] - ASSET_ADMIN_CONN_CACHE_KEY = "ASSET_ADMIN_USER_CONN_{}" SYSTEM_USER_CONN_CACHE_KEY = "SYSTEM_USER_CONN_{}" -TEST_SYSTEM_USER_CONN_TASKS = [ +PING_UNIXLIKE_TASKS = [ { "name": "ping", "action": { @@ -46,7 +29,7 @@ TEST_SYSTEM_USER_CONN_TASKS = [ } } ] -TEST_WINDOWS_SYSTEM_USER_CONN_TASKS = [ +PING_WINDOWS_TASKS = [ { "name": "ping", "action": { @@ -55,24 +38,6 @@ TEST_WINDOWS_SYSTEM_USER_CONN_TASKS = [ } ] -TEST_ASSET_USER_CONN_TASKS = [ - { - "name": "ping", - "action": { - "module": "ping", - } - } -] -TEST_WINDOWS_ASSET_USER_CONN_TASKS = [ - { - "name": "ping", - "action": { - "module": "win_ping", - } - } -] - - TASK_OPTIONS = { 'timeout': 10, 'forks': 10, @@ -98,7 +63,9 @@ GATHER_ASSET_USERS_TASKS = [ "name": "get last login", "action": { "module": "shell", - "args": "users=$(getent passwd | grep -v 'nologin' | grep -v 'shudown' | awk -F: '{ print $1 }');for i in $users;do last -F $i -1 | head -1 | grep -v '^$' | awk '{ print $1\"@\"$3\"@\"$5,$6,$7,$8 }';done" + "args": "users=$(getent passwd | grep -v 'nologin' | " + "grep -v 'shudown' | awk -F: '{ print $1 }');for i in $users;do last -F $i -1 | " + "head -1 | grep -v '^$' | awk '{ print $1\"@\"$3\"@\"$5,$6,$7,$8 }';done" } } ] diff --git a/apps/assets/tasks/gather_asset_hardware_info.py b/apps/assets/tasks/gather_asset_hardware_info.py index 2c0d75e99..e473366a5 100644 --- a/apps/assets/tasks/gather_asset_hardware_info.py +++ b/apps/assets/tasks/gather_asset_hardware_info.py @@ -9,15 +9,16 @@ from django.utils.translation import ugettext as _ from common.utils import ( capacity_convert, sum_capacity, get_logger ) +from orgs.utils import org_aware_func from . import const -from .utils import clean_hosts +from .utils import clean_ansible_task_hosts logger = get_logger(__file__) disk_pattern = re.compile(r'^hd|sd|xvd|vd|nv') __all__ = [ 'update_assets_hardware_info_util', 'update_asset_hardware_info_manual', - 'update_assets_hardware_info_period', + 'update_assets_hardware_info_period', 'update_node_assets_hardware_info_manual', ] @@ -82,6 +83,7 @@ def set_assets_hardware_info(assets, result, **kwargs): @shared_task +@org_aware_func("assets") def update_assets_hardware_info_util(assets, task_name=None): """ Using ansible api to update asset hardware info @@ -93,13 +95,13 @@ def update_assets_hardware_info_util(assets, task_name=None): if task_name is None: task_name = _("Update some assets hardware info") tasks = const.UPDATE_ASSETS_HARDWARE_TASKS - hosts = clean_hosts(assets) + hosts = clean_ansible_task_hosts(assets) if not hosts: return {} - created_by = str(assets[0].org_id) task, created = update_or_create_ansible_task( - task_name, hosts=hosts, tasks=tasks, created_by=created_by, - pattern='all', options=const.TASK_OPTIONS, run_as_admin=True, + task_name, hosts=hosts, tasks=tasks, + pattern='all', options=const.TASK_OPTIONS, + run_as_admin=True, ) result = task.run() set_assets_hardware_info(assets, result) @@ -109,9 +111,7 @@ def update_assets_hardware_info_util(assets, task_name=None): @shared_task(queue="ansible") def update_asset_hardware_info_manual(asset): task_name = _("Update asset hardware info: {}").format(asset.hostname) - update_assets_hardware_info_util( - [asset], task_name=task_name - ) + update_assets_hardware_info_util([asset], task_name=task_name) @shared_task(queue="ansible") @@ -123,3 +123,11 @@ def update_assets_hardware_info_period(): if not const.PERIOD_TASK_ENABLED: logger.debug("Period task disabled, update assets hardware info pass") return + + +@shared_task(queue="ansible") +def update_node_assets_hardware_info_manual(node): + task_name = _("Update node asset hardware information: {}").format(node.name) + assets = node.get_all_assets() + result = update_assets_hardware_info_util.delay(assets, task_name=task_name) + return result diff --git a/apps/assets/tasks/gather_asset_users.py b/apps/assets/tasks/gather_asset_users.py index 7dfe0fb01..7914c9720 100644 --- a/apps/assets/tasks/gather_asset_users.py +++ b/apps/assets/tasks/gather_asset_users.py @@ -7,10 +7,10 @@ from celery import shared_task from django.utils.translation import ugettext as _ from django.utils import timezone -from orgs.utils import tmp_to_org +from orgs.utils import tmp_to_org, org_aware_func from common.utils import get_logger from ..models import GatheredUser, Node -from .utils import clean_hosts +from .utils import clean_ansible_task_hosts from . import const __all__ = ['gather_asset_users', 'gather_nodes_asset_users'] @@ -101,11 +101,12 @@ def add_asset_users(assets, results): @shared_task(queue="ansible") +@org_aware_func("assets") def gather_asset_users(assets, task_name=None): from ops.utils import update_or_create_ansible_task if task_name is None: task_name = _("Gather assets users") - assets = clean_hosts(assets) + assets = clean_ansible_task_hosts(assets) if not assets: return hosts_category = { @@ -131,7 +132,7 @@ def gather_asset_users(assets, task_name=None): task, created = update_or_create_ansible_task( task_name=_task_name, hosts=value['hosts'], tasks=value['tasks'], pattern='all', options=const.TASK_OPTIONS, - run_as_admin=True, created_by=value['hosts'][0].org_id, + run_as_admin=True, ) raw, summary = task.run() results[k].update(raw['ok']) diff --git a/apps/assets/tasks/push_system_user.py b/apps/assets/tasks/push_system_user.py index 01348a0fe..961f24a01 100644 --- a/apps/assets/tasks/push_system_user.py +++ b/apps/assets/tasks/push_system_user.py @@ -1,11 +1,13 @@ # ~*~ coding: utf-8 ~*~ +from itertools import groupby from celery import shared_task from django.utils.translation import ugettext as _ from common.utils import encrypt_password, get_logger +from orgs.utils import tmp_to_org, org_aware_func from . import const -from .utils import clean_hosts_by_protocol, clean_hosts +from .utils import clean_ansible_task_hosts, group_asset_by_platform logger = get_logger(__file__) @@ -15,31 +17,34 @@ __all__ = [ ] -def get_push_linux_system_user_tasks(system_user): +def get_push_unixlike_system_user_tasks(system_user, username=None): + if username is None: + username = system_user.username + password = system_user.password + public_key = system_user.public_key + tasks = [ { - 'name': 'Add user {}'.format(system_user.username), + 'name': 'Add user {}'.format(username), 'action': { 'module': 'user', 'args': 'name={} shell={} state=present'.format( - system_user.username, system_user.shell, + username, system_user.shell or '/bin/bash', ), } }, { - 'name': 'Add group {}'.format(system_user.username), + 'name': 'Add group {}'.format(username), 'action': { 'module': 'group', - 'args': 'name={} state=present'.format( - system_user.username, - ), + 'args': 'name={} state=present'.format(username), } }, { 'name': 'Check home dir exists', 'action': { 'module': 'stat', - 'args': 'path=/home/{}'.format(system_user.username) + 'args': 'path=/home/{}'.format(username) }, 'register': 'home_existed' }, @@ -47,29 +52,29 @@ def get_push_linux_system_user_tasks(system_user): 'name': "Set home dir permission", 'action': { 'module': 'file', - 'args': "path=/home/{0} owner={0} group={0} mode=700".format(system_user.username) + 'args': "path=/home/{0} owner={0} group={0} mode=700".format(username) }, 'when': 'home_existed.stat.exists == true' } ] - if system_user.password: + if password: tasks.append({ - 'name': 'Set {} password'.format(system_user.username), + 'name': 'Set {} password'.format(username), 'action': { 'module': 'user', 'args': 'name={} shell={} state=present password={}'.format( - system_user.username, system_user.shell, - encrypt_password(system_user.password, salt="K3mIlKK"), + username, system_user.shell, + encrypt_password(password, salt="K3mIlKK"), ), } }) - if system_user.public_key: + if public_key: tasks.append({ - 'name': 'Set {} authorized key'.format(system_user.username), + 'name': 'Set {} authorized key'.format(username), 'action': { 'module': 'authorized_key', 'args': "user={} state=present key='{}'".format( - system_user.username, system_user.public_key + username, public_key ) } }) @@ -81,26 +86,27 @@ def get_push_linux_system_user_tasks(system_user): sudo_tmp.append(s.strip(',')) sudo = ','.join(sudo_tmp) tasks.append({ - 'name': 'Set {} sudo setting'.format(system_user.username), + 'name': 'Set {} sudo setting'.format(username), 'action': { 'module': 'lineinfile', 'args': "dest=/etc/sudoers state=present regexp='^{0} ALL=' " "line='{0} ALL=(ALL) NOPASSWD: {1}' " - "validate='visudo -cf %s'".format( - system_user.username, sudo, - ) + "validate='visudo -cf %s'".format(username, sudo) } }) return tasks -def get_push_windows_system_user_tasks(system_user): +def get_push_windows_system_user_tasks(system_user, username=None): + if username is None: + username = system_user.username + password = system_user.password tasks = [] - if not system_user.password: + if not password: return tasks - tasks.append({ - 'name': 'Add user {}'.format(system_user.username), + task = { + 'name': 'Add user {}'.format(username), 'action': { 'module': 'win_user', 'args': 'fullname={} ' @@ -112,78 +118,95 @@ def get_push_windows_system_user_tasks(system_user): 'password_never_expires=yes ' 'groups="Users,Remote Desktop Users" ' 'groups_action=add ' - ''.format(system_user.name, - system_user.username, - system_user.password), + ''.format(username, username, password), } - }) + } + print(task) + tasks.append(task) return tasks -def get_push_system_user_tasks(host, system_user): - if host.is_unixlike(): - tasks = get_push_linux_system_user_tasks(system_user) - elif host.is_windows(): - tasks = get_push_windows_system_user_tasks(system_user) - else: - msg = _( - "The asset {} system platform {} does not " - "support run Ansible tasks".format(host.hostname, host.platform) - ) - logger.info(msg) - tasks = [] +def get_push_system_user_tasks(system_user, platform="unixlike", username=None): + """ + :param system_user: + :param platform: + :param username: 当动态时,近推送某个 + :return: + """ + get_task_map = { + "unixlike": get_push_unixlike_system_user_tasks, + "windows": get_push_windows_system_user_tasks, + } + get_tasks = get_task_map.get(platform, get_push_unixlike_system_user_tasks) + if not system_user.username_same_with_user: + return get_tasks(system_user) + tasks = [] + # 仅推送这个username + if username is not None: + tasks.extend(get_tasks(system_user, username)) + return tasks + users = system_user.users.all().values_list('username', flat=True) + print(_("System user is dynamic: {}").format(list(users))) + for _username in users: + tasks.extend(get_tasks(system_user, _username)) return tasks -@shared_task(queue="ansible") -def push_system_user_util(system_user, assets, task_name): +@org_aware_func("system_user") +def push_system_user_util(system_user, assets, task_name, username=None): from ops.utils import update_or_create_ansible_task - if not system_user.is_need_push(): - msg = _("Push system user task skip, auto push not enable or " - "protocol is not ssh or rdp: {}").format(system_user.name) - logger.info(msg) - return {} - - # Set root as system user is dangerous - if system_user.username.lower() in ["root", "administrator"]: - msg = _("For security, do not push user {}".format(system_user.username)) - logger.info(msg) - return {} - - hosts = clean_hosts(assets) + hosts = clean_ansible_task_hosts(assets, system_user=system_user) if not hosts: return {} - hosts = clean_hosts_by_protocol(system_user, hosts) - if not hosts: - return {} + platform_hosts_map = {} + hosts_sorted = sorted(hosts, key=group_asset_by_platform) + platform_hosts = groupby(hosts_sorted, key=group_asset_by_platform) + for i in platform_hosts: + platform_hosts_map[i[0]] = list(i[1]) - for host in hosts: - system_user.load_specific_asset_auth(host) - tasks = get_push_system_user_tasks(host, system_user) - if not tasks: - continue + def run_task(_tasks, _hosts): + if not _tasks: + return task, created = update_or_create_ansible_task( - task_name=task_name, hosts=[host], tasks=tasks, pattern='all', + task_name=task_name, hosts=_hosts, tasks=_tasks, pattern='all', options=const.TASK_OPTIONS, run_as_admin=True, - created_by=system_user.org_id, ) task.run() + for platform, _hosts in platform_hosts_map.items(): + if not _hosts: + continue + print(_("Start push system user for platform: [{}]").format(platform)) + print(_("Hosts count: {}").format(len(_hosts))) + + if not system_user.has_special_auth(): + logger.debug("System user not has special auth") + tasks = get_push_system_user_tasks(system_user, platform, username=username) + run_task(tasks, _hosts) + continue + + for _host in _hosts: + system_user.load_asset_special_auth(_host) + tasks = get_push_system_user_tasks(system_user, platform, username=username) + run_task(tasks, [_host]) + @shared_task(queue="ansible") def push_system_user_to_assets_manual(system_user): - assets = system_user.get_all_assets() + assets = system_user.get_related_assets() 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(queue="ansible") -def push_system_user_a_asset_manual(system_user, asset): - task_name = _("Push system users to asset: {} => {}").format( - system_user.name, asset +def push_system_user_a_asset_manual(system_user, asset, username=None): + if username is None: + username = system_user.username + task_name = _("Push system users to asset: {}({}) => {}").format( + system_user.name, username, asset ) - return push_system_user_util(system_user, [asset], task_name=task_name) + return push_system_user_util(system_user, [asset], task_name=task_name, username=username) @shared_task(queue="ansible") @@ -199,4 +222,4 @@ def push_system_user_to_assets(system_user, assets): # @after_app_shutdown_clean_periodic # def push_system_user_period(): # for system_user in SystemUser.objects.all(): -# push_system_user_related_nodes(system_user) \ No newline at end of file +# push_system_user_related_nodes(system_user) diff --git a/apps/assets/tasks/system_user_connectivity.py b/apps/assets/tasks/system_user_connectivity.py index ffe618a59..4b3a80b2c 100644 --- a/apps/assets/tasks/system_user_connectivity.py +++ b/apps/assets/tasks/system_user_connectivity.py @@ -1,13 +1,17 @@ +from itertools import groupby from collections import defaultdict + from celery import shared_task from django.utils.translation import ugettext as _ from common.utils import get_logger - +from orgs.utils import tmp_to_org, org_aware_func from ..models import SystemUser from . import const -from .utils import clean_hosts, clean_hosts_by_protocol +from .utils import ( + clean_ansible_task_hosts, group_asset_by_platform +) logger = get_logger(__name__) __all__ = [ @@ -16,7 +20,7 @@ __all__ = [ ] -@shared_task(queue="ansible") +@org_aware_func("system_user") def test_system_user_connectivity_util(system_user, assets, task_name): """ Test system cant connect his assets or not. @@ -27,41 +31,34 @@ def test_system_user_connectivity_util(system_user, assets, task_name): """ from ops.utils import update_or_create_ansible_task - hosts = clean_hosts(assets) + hosts = clean_ansible_task_hosts(assets, system_user=system_user) if not hosts: return {} - - hosts = clean_hosts_by_protocol(system_user, hosts) - if not hosts: - return {} - - hosts_category = { - 'linux': { - 'hosts': [], - 'tasks': const.TEST_SYSTEM_USER_CONN_TASKS - }, - 'windows': { - 'hosts': [], - 'tasks': const.TEST_WINDOWS_SYSTEM_USER_CONN_TASKS - } + platform_hosts_map = {} + hosts_sorted = sorted(hosts, key=group_asset_by_platform) + platform_hosts = groupby(hosts_sorted, key=group_asset_by_platform) + for i in platform_hosts: + platform_hosts_map[i[0]] = list(i[1]) + + platform_tasks_map = { + "unixlike": const.PING_UNIXLIKE_TASKS, + "windows": const.PING_WINDOWS_TASKS } - for host in hosts: - hosts_list = hosts_category['windows']['hosts'] if host.is_windows() \ - else hosts_category['linux']['hosts'] - hosts_list.append(host) results_summary = dict( contacted=defaultdict(dict), dark=defaultdict(dict), success=True ) - for k, value in hosts_category.items(): - if not value['hosts']: - continue - task, created = update_or_create_ansible_task( - task_name=task_name, hosts=value['hosts'], tasks=value['tasks'], + + def run_task(_tasks, _hosts, _username): + old_name = "({})".format(system_user.username) + new_name = "({})".format(_username) + _task_name = task_name.replace(old_name, new_name) + _task, created = update_or_create_ansible_task( + task_name=_task_name, hosts=_hosts, tasks=_tasks, pattern='all', options=const.TASK_OPTIONS, - run_as=system_user.username, created_by=system_user.org_id, + run_as=_username, ) - raw, summary = task.run() + raw, summary = _task.run() success = summary.get('success', False) contacted = summary.get('contacted', {}) dark = summary.get('dark', {}) @@ -70,23 +67,45 @@ def test_system_user_connectivity_util(system_user, assets, task_name): results_summary['contacted'].update(contacted) results_summary['dark'].update(dark) + for platform, _hosts in platform_hosts_map.items(): + if not _hosts: + continue + if platform not in ["unixlike", "windows"]: + continue + + tasks = platform_tasks_map[platform] + print(_("Start test system user connectivity for platform: [{}]").format(platform)) + print(_("Hosts count: {}").format(len(_hosts))) + # 用户名不是动态的,用户名则是一个 + if not system_user.username_same_with_user: + logger.debug("System user not has special auth") + run_task(tasks, _hosts, system_user.username) + # 否则需要多个任务 + else: + users = system_user.users.all().values_list('username', flat=True) + print(_("System user is dynamic: {}").format(list(users))) + for username in users: + run_task(tasks, _hosts, username) + system_user.set_connectivity(results_summary) return results_summary @shared_task(queue="ansible") +@org_aware_func("system_user") def test_system_user_connectivity_manual(system_user): task_name = _("Test system user connectivity: {}").format(system_user) - assets = system_user.get_all_assets() - return test_system_user_connectivity_util(system_user, assets, task_name) + assets = system_user.get_related_assets() + test_system_user_connectivity_util(system_user, assets, task_name) @shared_task(queue="ansible") +@org_aware_func("system_user") def test_system_user_connectivity_a_asset(system_user, asset): task_name = _("Test system user connectivity: {} => {}").format( system_user, asset ) - return test_system_user_connectivity_util(system_user, [asset], task_name) + test_system_user_connectivity_util(system_user, [asset], task_name) @shared_task(queue="ansible") @@ -94,8 +113,9 @@ def test_system_user_connectivity_period(): if not const.PERIOD_TASK_ENABLED: logger.debug("Period task disabled, test system user connectivity pass") return - system_users = SystemUser.objects.all() - for system_user in system_users: + queryset_map = SystemUser.objects.all_group_by_org() + for org, system_user in queryset_map.items(): task_name = _("Test system user connectivity period: {}").format(system_user) - assets = system_user.get_all_assets() - test_system_user_connectivity_util(system_user, assets, task_name) + with tmp_to_org(org): + assets = system_user.get_related_assets() + test_system_user_connectivity_util(system_user, assets, task_name) diff --git a/apps/assets/tasks/utils.py b/apps/assets/tasks/utils.py index e5983143f..9956665ee 100644 --- a/apps/assets/tasks/utils.py +++ b/apps/assets/tasks/utils.py @@ -7,7 +7,8 @@ from common.utils import get_logger logger = get_logger(__file__) __all__ = [ - 'check_asset_can_run_ansible', 'clean_hosts', 'clean_hosts_by_protocol' + 'check_asset_can_run_ansible', 'clean_ansible_task_hosts', + 'group_asset_by_platform', ] @@ -23,23 +24,43 @@ def check_asset_can_run_ansible(asset): return True -def clean_hosts(assets): - clean_assets = [] +def check_system_user_can_run_ansible(system_user): + if not system_user.is_need_push(): + msg = _("Push system user task skip, auto push not enable or " + "protocol is not ssh or rdp: {}").format(system_user.name) + logger.info(msg) + return False + + # Push root as system user is dangerous + if system_user.username.lower() in ["root", "administrator"]: + msg = _("For security, do not push user {}".format(system_user.username)) + logger.info(msg) + return False + + # if system_user.protocol != "ssh": + # msg = _("System user protocol not ssh: {}".format(system_user)) + # logger.info(msg) + # return False + return True + + +def clean_ansible_task_hosts(assets, system_user=None): + if system_user and not check_system_user_can_run_ansible(system_user): + return [] + cleaned_assets = [] for asset in assets: if not check_asset_can_run_ansible(asset): continue - clean_assets.append(asset) - if not clean_assets: + cleaned_assets.append(asset) + if not cleaned_assets: logger.info(_("No assets matched, stop task")) - return clean_assets + return cleaned_assets -def clean_hosts_by_protocol(system_user, assets): - hosts = [ - asset for asset in assets - if asset.has_protocol(system_user.protocol) - ] - if not hosts: - msg = _("No assets matched related system user protocol, stop task") - logger.info(msg) - return hosts +def group_asset_by_platform(asset): + if asset.is_unixlike(): + return 'unixlike' + elif asset.is_windows(): + return 'windows' + else: + return 'other' diff --git a/apps/assets/templates/assets/_asset_user_auth_update_modal.html b/apps/assets/templates/assets/_asset_user_auth_update_modal.html index 28a1a956d..fe88b7426 100644 --- a/apps/assets/templates/assets/_asset_user_auth_update_modal.html +++ b/apps/assets/templates/assets/_asset_user_auth_update_modal.html @@ -6,25 +6,25 @@
{% csrf_token %}
- +

- +

- +
- +
diff --git a/apps/assets/templates/assets/_asset_user_auth_view_modal.html b/apps/assets/templates/assets/_asset_user_auth_view_modal.html index 417e1021d..8cc3a78de 100644 --- a/apps/assets/templates/assets/_asset_user_auth_view_modal.html +++ b/apps/assets/templates/assets/_asset_user_auth_view_modal.html @@ -12,19 +12,19 @@
- +

- +

- +
@@ -38,11 +38,11 @@ {% endblock %} diff --git a/apps/assets/templates/assets/_asset_user_list.html b/apps/assets/templates/assets/_asset_user_list.html index 3321e2578..d43cea10f 100644 --- a/apps/assets/templates/assets/_asset_user_list.html +++ b/apps/assets/templates/assets/_asset_user_list.html @@ -8,8 +8,8 @@ table.dataTable tbody tr.selected a { color: rgb(103, 106, 108);; } - + @@ -20,7 +20,7 @@ - +{# #} @@ -33,62 +33,60 @@ {% include 'authentication/_mfa_confirm_modal.html' %} diff --git a/apps/assets/templates/assets/_node_tree.html b/apps/assets/templates/assets/_node_tree.html index b4b5040b0..c500f55f8 100644 --- a/apps/assets/templates/assets/_node_tree.html +++ b/apps/assets/templates/assets/_node_tree.html @@ -113,10 +113,11 @@ function initNodeTree(options) { $.get(treeUrl, function (data, status) { zTree = $.fn.zTree.init($("#nodeTree"), setting, data); rootNodeAddDom(zTree, function () { - const url = '{% url 'api-assets:refresh-nodes-cache' %}'; + const url = '{% url 'api-assets:node-task-create' pk=DEFAULT_PK %}'; requestApi({ url: url, - method: 'GET', + method: 'POST', + data: {action: "refresh_cache"}, flash_message: false, success: function () { initNodeTree(options); @@ -173,20 +174,14 @@ function removeTreeNode() { if (!current_node){ return } - if (current_node.children && current_node.children.length > 0) { - toastr.error("{% trans 'Have child node, cancel' %}"); - } else if (current_node.meta.node.assets_amount !== 0) { - toastr.error("{% trans 'Have assets, cancel' %}"); - } else { - var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id); - $.ajax({ - url: url, - method: "DELETE", - success: function () { - zTree.removeNode(current_node); - } - }); - } + var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id); + requestApi({ + url: url, + method: "DELETE", + success: function () { + zTree.removeNode(current_node) + } + }) } function editTreeNode() { diff --git a/apps/assets/templates/assets/_system_user.html b/apps/assets/templates/assets/_system_user.html index d941bf20a..e05a9448b 100644 --- a/apps/assets/templates/assets/_system_user.html +++ b/apps/assets/templates/assets/_system_user.html @@ -34,6 +34,7 @@ {% bootstrap_field form.name layout="horizontal" %} {% bootstrap_field form.login_mode layout="horizontal" %} {% bootstrap_field form.username layout="horizontal" %} + {% bootstrap_field form.username_same_with_user layout="horizontal" %} {% bootstrap_field form.priority layout="horizontal" %} {% bootstrap_field form.protocol layout="horizontal" %} @@ -63,6 +64,7 @@ {% bootstrap_field form.cmd_filters layout="horizontal" %}

{% trans 'Other' %}

+ {% bootstrap_field form.sftp_root layout="horizontal" %} {% bootstrap_field form.sudo layout="horizontal" %} {% bootstrap_field form.shell layout="horizontal" %} {% bootstrap_field form.comment layout="horizontal" %} @@ -226,6 +228,10 @@ $(document).ready(function () { $('.select2').select2(); authFieldsDisplay(); fieldDisplay(); + var checked = $("#id_username_same_with_user").prop('checked'); + if (checked) { + $("#id_username").attr("disabled", true) + } }) .on('change', auto_generate_key, function(){ authFieldsDisplay(); @@ -246,7 +252,7 @@ $(document).ready(function () { var data = form.serializeObject(); objectAttrsIsList(data, ['cmd_filters']); - objectAttrsIsBool(data, ["auto_generate_key", "auto_push"]); + objectAttrsIsBool(data, ["auto_generate_key", "auto_push", "username_same_with_user"]); data["private_key"] = $("#id_private_key").data('file'); var props = { @@ -261,6 +267,15 @@ $(document).ready(function () { readFile($(this)).on("onload", function (evt, data) { $(this).data("file", data) }) +}).on("change", '#id_username_same_with_user', function () { + var checked = $(this).prop('checked'); + var usernameRef = $("#id_username"); + if (checked) { + usernameRef.val(''); + usernameRef.attr("disabled", true) + } else { + usernameRef.attr("disabled", false) + } }) diff --git a/apps/assets/templates/assets/admin_user_assets.html b/apps/assets/templates/assets/admin_user_assets.html index e77cbb689..a42c6032f 100644 --- a/apps/assets/templates/assets/admin_user_assets.html +++ b/apps/assets/templates/assets/admin_user_assets.html @@ -72,9 +72,9 @@ {% endblock %} diff --git a/apps/assets/templates/assets/system_user_assets.html b/apps/assets/templates/assets/system_user_assets.html index ec024c1a7..d48f48201 100644 --- a/apps/assets/templates/assets/system_user_assets.html +++ b/apps/assets/templates/assets/system_user_assets.html @@ -23,16 +23,23 @@
  • - {% trans 'Assets' %} + {% trans 'Asset list' %}
  • + {% if system_user.username_same_with_user %} +
  • + + {% trans 'User list' %} + +
  • + {% endif %}
    - {% trans 'Assets of ' %} {{ system_user.name }} {{ paginator.count }} + {{ system_user.name }} {{ paginator.count }}
    {% trans 'IP' %} {% trans 'Username' %} {% trans 'Version' %}{% trans 'Connectivity'%}{% trans 'Connectivity'%}{% trans 'Datetime' %} {% trans 'Action' %}
    +
    +
    + {% trans 'Assets' %} +
    +
    + + + + + + + + + + + +
    + +
    + +
    +
    +
    {% trans 'Nodes' %} @@ -92,7 +123,7 @@
    - @@ -114,6 +145,7 @@
    + {% include 'assets/_asset_list_modal.html' %} {% endblock %} {% block custom_foot_js %} diff --git a/apps/assets/templates/assets/system_user_detail.html b/apps/assets/templates/assets/system_user_detail.html index b9d6e5c0b..b26cfbf11 100644 --- a/apps/assets/templates/assets/system_user_detail.html +++ b/apps/assets/templates/assets/system_user_detail.html @@ -15,7 +15,14 @@ {% if system_user.can_perm_to_asset %}
  • - {% trans 'Assets' %} + {% trans 'Asset list' %} + +
  • + {% endif %} + {% if system_user.username_same_with_user %} +
  • + + {% trans 'User list' %}
  • {% endif %} @@ -57,7 +64,11 @@ {% trans 'Username' %}: - {{ system_user.username }} + {% if system_user.username_same_with_user %} + {% trans 'Username same with user' %} + {% else %} + {{ system_user.username }} + {% endif %} {% trans 'Login mode' %}: @@ -131,26 +142,6 @@ - {% if system_user.auto_push %} - - {% trans 'Push system user now' %}: - - - - - - - {% endif %} - {% if system_user.is_need_test_asset_connective %} - - {% trans 'Test assets connective' %}: - - - - - - - {% endif %}
    @@ -246,32 +237,7 @@ $(document).ready(function () { var redirect_url = "{% url 'assets:system-user-list' %}"; objectDelete($this, name, the_url, redirect_url); }) -.on('click', '.btn-push', function () { - var the_url = "{% url 'api-assets:system-user-push' pk=system_user.id %}"; - var success = function (data) { - var task_id = data.task; - showCeleryTaskLog(task_id); - }; - requestApi({ - url: the_url, - method: 'GET', - success: success, - flash_message: false - }); -}) -.on('click', '.btn-test-connective', function () { - var the_url = "{% url 'api-assets:system-user-connective' pk=system_user.id %}"; - var success = function (data) { - var task_id = data.task; - showCeleryTaskLog(task_id); - }; - requestApi({ - url: the_url, - method: 'GET', - success: success, - flash_message: false - }); -}).on('click', '#btn-binding-command-filters', function () { +.on('click', '#btn-binding-command-filters', function () { var new_selected_cmd_filters = $.map($('#command_filters_selected').select2('data'), function (i) { return i.id; }); diff --git a/apps/assets/templates/assets/system_user_list.html b/apps/assets/templates/assets/system_user_list.html index 0e88b6461..b3bd556ab 100644 --- a/apps/assets/templates/assets/system_user_list.html +++ b/apps/assets/templates/assets/system_user_list.html @@ -2,9 +2,9 @@ {% load i18n %} {% block help_message %} - {% 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) does not support ansible, please manually fill in the account password.' %} + {% 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) does not support ansible, please manually fill in the account password.' %} {% endblock %} {% block table_search %} @@ -26,9 +26,6 @@ {% trans 'Protocol' %} {% trans 'Login mode' %} {% trans 'Asset' %} -{# {% trans 'Reachable' %}#} -{# {% trans 'Unreachable' %}#} -{# {% trans 'Ratio' %}#} {% trans 'Comment' %} {% trans 'Action' %} @@ -40,6 +37,8 @@ {% block custom_foot_js %} +{% endblock %} diff --git a/apps/assets/tests.py b/apps/assets/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/apps/assets/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/apps/assets/tests/__init__.py b/apps/assets/tests/__init__.py new file mode 100644 index 000000000..ec51c5a2b --- /dev/null +++ b/apps/assets/tests/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# diff --git a/apps/assets/tests/test_system_user.py b/apps/assets/tests/test_system_user.py new file mode 100644 index 000000000..ec51c5a2b --- /dev/null +++ b/apps/assets/tests/test_system_user.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 653b71447..279bd26be 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -21,82 +21,46 @@ router.register(r'domains', api.DomainViewSet, 'domain') router.register(r'gateways', api.GatewayViewSet, 'gateway') router.register(r'cmd-filters', api.CommandFilterViewSet, 'cmd-filter') router.register(r'asset-users', api.AssetUserViewSet, 'asset-user') -router.register(r'asset-users-info', api.AssetUserExportViewSet, 'asset-user-info') +router.register(r'asset-user-auth-infos', api.AssetUserAuthInfoViewSet, 'asset-user-auth-info') router.register(r'gathered-users', api.GatheredUserViewSet, 'gathered-user') router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset') router.register(r'system-users-assets-relations', api.SystemUserAssetRelationViewSet, 'system-users-assets-relation') router.register(r'system-users-nodes-relations', api.SystemUserNodeRelationViewSet, 'system-users-nodes-relation') +router.register(r'system-users-users-relations', api.SystemUserUserRelationViewSet, 'system-users-users-relation') cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filters', lookup='filter') cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule') urlpatterns = [ - path('assets//refresh/', - api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'), - path('assets//alive/', - api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'), - path('assets//gateway/', - api.AssetGatewayApi.as_view(), name='asset-gateway'), - path('assets//platform/', - api.AssetPlatformRetrieveApi.as_view(), name='asset-platform-detail'), - - path('asset-users/auth-info/', - api.AssetUserAuthInfoApi.as_view(), name='asset-user-auth-info'), - path('asset-users/test-connective/', - api.AssetUserTestConnectiveApi.as_view(), name='asset-user-connective'), - - - path('admin-users//nodes/', - api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'), - path('admin-users//auth/', - api.AdminUserAuthApi.as_view(), name='admin-user-auth'), - path('admin-users//connective/', - api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'), - path('admin-users//assets/', - api.AdminUserAssetsListView.as_view(), name='admin-user-assets'), - - path('system-users//auth-info/', - api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'), - path('system-users//assets//auth-info/', - api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'), - path('system-users//assets/', - api.SystemUserAssetsListView.as_view(), name='system-user-assets'), - path('system-users//push/', - api.SystemUserPushApi.as_view(), name='system-user-push'), - path('system-users//assets//push/', - api.SystemUserPushToAssetApi.as_view(), name='system-user-push-to-asset'), - path('system-users//assets//test/', - api.SystemUserTestAssetConnectivityApi.as_view(), name='system-user-test-to-asset'), - path('system-users//connective/', - api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'), - path('system-users//cmd-filter-rules/', - api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'), + path('assets//gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'), + path('assets//platform/', api.AssetPlatformRetrieveApi.as_view(), name='asset-platform-detail'), + path('assets//tasks/', api.AssetTaskCreateApi.as_view(), name='asset-task-create'), + + path('asset-users/tasks/', api.AssetUserTaskCreateAPI.as_view(), name='asset-user-task-create'), + + path('admin-users//nodes/', api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'), + path('admin-users//auth/', api.AdminUserAuthApi.as_view(), name='admin-user-auth'), + path('admin-users//connective/', api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'), + path('admin-users//assets/', api.AdminUserAssetsListView.as_view(), name='admin-user-assets'), + + path('system-users//auth-info/', api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'), + path('system-users//assets//auth-info/', api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'), + path('system-users//tasks/', api.SystemUserTaskApi.as_view(), name='system-user-task-create'), + path('system-users//cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'), path('nodes/tree/', api.NodeListAsTreeApi.as_view(), name='node-tree'), path('nodes/children/tree/', api.NodeChildrenAsTreeApi.as_view(), name='node-children-tree'), - path('nodes//children/', - api.NodeChildrenApi.as_view(), name='node-children'), + path('nodes//children/', api.NodeChildrenApi.as_view(), name='node-children'), path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'), - path('nodes//children/add/', - api.NodeAddChildrenApi.as_view(), name='node-add-children'), - path('nodes//assets/', - api.NodeAssetsApi.as_view(), name='node-assets'), - path('nodes//assets/add/', - api.NodeAddAssetsApi.as_view(), name='node-add-assets'), - path('nodes//assets/replace/', - api.NodeReplaceAssetsApi.as_view(), name='node-replace-assets'), - path('nodes//assets/remove/', - api.NodeRemoveAssetsApi.as_view(), name='node-remove-assets'), - path('nodes//refresh-hardware-info/', - api.RefreshNodeHardwareInfoApi.as_view(), name='node-refresh-hardware-info'), - path('nodes//test-connective/', - api.TestNodeConnectiveApi.as_view(), name='node-test-connective'), - - path('nodes/cache/', api.RefreshNodesCacheApi.as_view(), name='refresh-nodes-cache'), - - path('gateways//test-connective/', - api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'), + path('nodes//children/add/', api.NodeAddChildrenApi.as_view(), name='node-add-children'), + path('nodes//assets/', api.NodeAssetsApi.as_view(), name='node-assets'), + path('nodes//assets/add/', api.NodeAddAssetsApi.as_view(), name='node-add-assets'), + path('nodes//assets/replace/', api.NodeReplaceAssetsApi.as_view(), name='node-replace-assets'), + path('nodes//assets/remove/', api.NodeRemoveAssetsApi.as_view(), name='node-remove-assets'), + path('nodes//tasks/', api.NodeTaskCreateApi.as_view(), name='node-task-create'), + + path('gateways//test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'), ] diff --git a/apps/assets/urls/views_urls.py b/apps/assets/urls/views_urls.py index eec9dcc0d..25b6deefd 100644 --- a/apps/assets/urls/views_urls.py +++ b/apps/assets/urls/views_urls.py @@ -39,6 +39,7 @@ urlpatterns = [ path('system-user//update/', views.SystemUserUpdateView.as_view(), name='system-user-update'), path('system-user//delete/', views.SystemUserDeleteView.as_view(), name='system-user-delete'), path('system-user//asset/', views.SystemUserAssetView.as_view(), name='system-user-asset'), + path('system-user//user/', views.SystemUserUserView.as_view(), name='system-user-user'), path('label/', views.LabelListView.as_view(), name='label-list'), path('label/create/', views.LabelCreateView.as_view(), name='label-create'), diff --git a/apps/assets/utils.py b/apps/assets/utils.py index eaf3d502a..25e8a46ea 100644 --- a/apps/assets/utils.py +++ b/apps/assets/utils.py @@ -5,68 +5,53 @@ from treelib.exceptions import NodeIDAbsentError from collections import defaultdict from copy import deepcopy -from common.utils import get_object_or_none, get_logger, timeit -from .models import SystemUser, Asset +from common.utils import get_logger, timeit, lazyproperty +from .models import Asset, Node logger = get_logger(__file__) -def get_system_user_by_name(name): - system_user = get_object_or_none(SystemUser, name=name) - return system_user - - -def get_system_user_by_id(id): - system_user = get_object_or_none(SystemUser, id=id) - return system_user - - class TreeService(Tree): tag_sep = ' / ' - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.nodes_assets_map = defaultdict(set) - self.all_nodes_assets_map = {} - self._invalid_assets = frozenset() + @staticmethod + @timeit + def get_nodes_assets_map(): + nodes_assets_map = defaultdict(set) + asset_node_list = Node.assets.through.objects.values_list( + 'asset', 'node__key' + ) + for asset_id, key in asset_node_list: + nodes_assets_map[key].add(asset_id) + return nodes_assets_map @classmethod @timeit def new(cls): from .models import Node - from orgs.utils import tmp_to_root_org - - with tmp_to_root_org(): - all_nodes = list(Node.objects.all().values("key", "value")) - all_nodes.sort(key=lambda x: len(x["key"].split(":"))) - tree = cls() - tree.create_node(tag='', identifier='') - for node in all_nodes: - key = node["key"] - value = node["value"] - parent_key = ":".join(key.split(":")[:-1]) - tree.safe_create_node( - tag=value, identifier=key, - parent=parent_key, - ) - tree.init_assets() + all_nodes = list(Node.objects.all().values("key", "value")) + all_nodes.sort(key=lambda x: len(x["key"].split(":"))) + tree = cls() + tree.create_node(tag='', identifier='', data={}) + for node in all_nodes: + key = node["key"] + value = node["value"] + parent_key = ":".join(key.split(":")[:-1]) + tree.safe_create_node( + tag=value, identifier=key, + parent=parent_key, + ) + tree.init_assets() return tree - @timeit def init_assets(self): - from orgs.utils import tmp_to_root_org - self.all_nodes_assets_map = {} - self.nodes_assets_map = defaultdict(set) - with tmp_to_root_org(): - queryset = Asset.objects.all().values_list('id', 'nodes__key') - invalid_assets = Asset.objects.filter(is_active=False)\ - .values_list('id', flat=True) - self._invalid_assets = frozenset(invalid_assets) - for asset_id, key in queryset: - if not key: - continue - self.nodes_assets_map[key].add(asset_id) + node_assets_map = self.get_nodes_assets_map() + for node in self.all_nodes_itr(): + key = node.identifier + assets = node_assets_map.get(key, set()) + data = {"assets": assets, "all_assets": None} + node.data = data def safe_create_node(self, **kwargs): parent = kwargs.get("parent") @@ -125,32 +110,43 @@ class TreeService(Tree): parent = self.copy_node(parent) return parent + @lazyproperty + def invalid_assets(self): + assets = Asset.objects.filter(is_active=False).values_list('id', flat=True) + return assets + def set_assets(self, nid, assets): - self.nodes_assets_map[nid] = set(assets) + node = self.get_node(nid) + if node.data is None: + node.data = {} + node.data["assets"] = assets def assets(self, nid): - assets = self.nodes_assets_map[nid] - return assets + node = self.get_node(nid) + return node.data.get("assets", set()) def valid_assets(self, nid): - return set(self.assets(nid)) - set(self._invalid_assets) + return set(self.assets(nid)) - set(self.invalid_assets) def all_assets(self, nid): - assets = self.all_nodes_assets_map.get(nid) - if assets: - return assets - assets = set(self.assets(nid)) + node = self.get_node(nid) + if node.data is None: + node.data = {} + all_assets = node.data.get("all_assets") + if all_assets is not None: + return all_assets + all_assets = set(self.assets(nid)) try: children = self.children(nid) except NodeIDAbsentError: children = [] for child in children: - assets.update(self.all_assets(child.identifier)) - self.all_nodes_assets_map[nid] = assets - return assets + all_assets.update(self.all_assets(child.identifier)) + node.data["all_assets"] = all_assets + return all_assets def all_valid_assets(self, nid): - return set(self.all_assets(nid)) - set(self._invalid_assets) + return set(self.all_assets(nid)) - set(self.invalid_assets) def assets_amount(self, nid): return len(self.all_assets(nid)) @@ -186,15 +182,12 @@ class TreeService(Tree): else: # logger.debug('Add node: {}'.format(node.identifier)) self.add_node(node, parent) - # # def __getstate__(self): # self.mutex = None + # self.all_nodes_assets_map = {} + # self.nodes_assets_map = {} # return self.__dict__ - # - def __setstate__(self, state): - self.__dict__ = state - if '_invalid_assets' not in state: - self._invalid_assets = frozenset() - # self.mutex = threading.Lock() + # def __setstate__(self, state): + # self.__dict__ = state diff --git a/apps/assets/views/admin_user.py b/apps/assets/views/admin_user.py index eaefffab7..611b03d7d 100644 --- a/apps/assets/views/admin_user.py +++ b/apps/assets/views/admin_user.py @@ -106,7 +106,7 @@ class AdminUserAssetsView(PermissionsMixin, SingleObjectMixin, ListView): def get_context_data(self, **kwargs): context = { 'app': _('Assets'), - 'action': _('Admin user detail'), + 'action': _('Admin user assets'), } kwargs.update(context) return super().get_context_data(**kwargs) diff --git a/apps/assets/views/platform.py b/apps/assets/views/platform.py index 2e3aff49c..8c74da138 100644 --- a/apps/assets/views/platform.py +++ b/apps/assets/views/platform.py @@ -48,6 +48,9 @@ class PlatformUpdateView(generic.UpdateView): model = Platform template_name = 'assets/platform_create_update.html' + def post(self, *args, **kwargs): + pass + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) meta_form = PlatformMetaForm(initial=self.object.meta) diff --git a/apps/assets/views/system_user.py b/apps/assets/views/system_user.py index 546df4430..5aa1e8431 100644 --- a/apps/assets/views/system_user.py +++ b/apps/assets/views/system_user.py @@ -17,6 +17,7 @@ __all__ = [ 'SystemUserCreateView', 'SystemUserUpdateView', 'SystemUserDetailView', 'SystemUserDeleteView', 'SystemUserAssetView', 'SystemUserListView', + 'SystemUserUserView', ] @@ -100,7 +101,22 @@ class SystemUserAssetView(PermissionsMixin, DetailView): def get_context_data(self, **kwargs): context = { 'app': _('assets'), - 'action': _('System user asset'), + 'action': _('System user assets'), + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class SystemUserUserView(PermissionsMixin, DetailView): + model = SystemUser + template_name = 'assets/system_user_users.html' + context_object_name = 'system_user' + permission_classes = [IsOrgAdmin] + + def get_context_data(self, **kwargs): + context = { + 'app': _('assets'), + 'action': _('System user users'), } kwargs.update(context) return super().get_context_data(**kwargs) diff --git a/apps/authentication/backends/cas/__init__.py b/apps/authentication/backends/cas/__init__.py new file mode 100644 index 000000000..bf0101c81 --- /dev/null +++ b/apps/authentication/backends/cas/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# +from .backends import * +from .callback import * diff --git a/apps/authentication/backends/cas/backends.py b/apps/authentication/backends/cas/backends.py new file mode 100644 index 000000000..ec56c6d4d --- /dev/null +++ b/apps/authentication/backends/cas/backends.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# +from django_cas_ng.backends import CASBackend as _CASBackend + + +__all__ = ['CASBackend'] + + +class CASBackend(_CASBackend): + def user_can_authenticate(self, user): + return True diff --git a/apps/authentication/backends/cas/callback.py b/apps/authentication/backends/cas/callback.py new file mode 100644 index 000000000..64201e607 --- /dev/null +++ b/apps/authentication/backends/cas/callback.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# +from django.contrib.auth import get_user_model + + +User = get_user_model() + + +def cas_callback(response): + username = response['username'] + user, user_created = User.objects.get_or_create(username=username) + profile, created = user.get_profile() + + profile.role = response['attributes']['role'] + profile.birth_date = response['attributes']['birth_date'] + profile.save() diff --git a/apps/authentication/backends/cas/urls.py b/apps/authentication/backends/cas/urls.py new file mode 100644 index 000000000..39a838b6a --- /dev/null +++ b/apps/authentication/backends/cas/urls.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# +from django.urls import path +import django_cas_ng.views + + +urlpatterns = [ + path('login/', django_cas_ng.views.LoginView.as_view(), name='cas-login'), + path('logout/', django_cas_ng.views.LogoutView.as_view(), name='cas-logout'), + path('callback/', django_cas_ng.views.CallbackView.as_view(), name='cas-proxy-callback'), +] diff --git a/apps/authentication/backends/ldap.py b/apps/authentication/backends/ldap.py index e01803707..ac3cfc254 100644 --- a/apps/authentication/backends/ldap.py +++ b/apps/authentication/backends/ldap.py @@ -29,26 +29,27 @@ class LDAPAuthorizationBackend(LDAPBackend): def pre_check(self, username, password): if not settings.AUTH_LDAP: - return False - logger.info('Authentication LDAP backend') + error = 'Not enabled auth ldap' + return False, error if not username: - logger.info('Authenticate failed: username is None') - return False + error = 'Username is None' + return False, error if not password: - logger.info('Authenticate failed: password is None') - return False + error = 'Password is None' + return False, error if settings.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS: user_model = self.get_user_model() exist = user_model.objects.filter(username=username).exists() if not exist: - msg = 'Authentication failed: user ({}) is not in the user list' - logger.info(msg.format(username)) - return False - return True + error = 'user ({}) is not in the user list'.format(username) + return False, error + return True, '' def authenticate(self, request=None, username=None, password=None, **kwargs): - match = self.pre_check(username, password) + logger.info('Authentication LDAP backend') + match, msg = self.pre_check(username, password) if not match: + logger.info('Authenticate failed: {}'.format(msg)) return None ldap_user = LDAPUser(self, username=username.strip(), request=request) user = self.authenticate_ldap_user(ldap_user, password) @@ -130,5 +131,5 @@ class LDAPUser(_LDAPUser): setattr(self._user, field, value) email = getattr(self._user, 'email', '') - email = construct_user_email(email, self._user.username) + email = construct_user_email(self._user.username, email) setattr(self._user, 'email', email) diff --git a/apps/authentication/backends/pubkey.py b/apps/authentication/backends/pubkey.py index db0ace648..3355eacaa 100644 --- a/apps/authentication/backends/pubkey.py +++ b/apps/authentication/backends/pubkey.py @@ -19,7 +19,7 @@ class PublicKeyAuthBackend: return None else: if user.check_public_key(public_key) and \ - self.user_can_authenticate(user): + self.user_can_authenticate(user): return user @staticmethod diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index b736ef606..183e69288 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -11,6 +11,7 @@ from users.utils import ( reason_password_failed = 'password_failed' reason_mfa_failed = 'mfa_failed' +reason_mfa_unset = 'mfa_unset' reason_user_not_exist = 'user_not_exist' reason_password_expired = 'password_expired' reason_user_invalid = 'user_invalid' @@ -18,7 +19,8 @@ reason_user_inactive = 'user_inactive' reason_choices = { reason_password_failed: _('Username/password check failed'), - reason_mfa_failed: _('MFA authentication failed'), + reason_mfa_failed: _('MFA failed'), + reason_mfa_unset: _('MFA unset'), reason_user_not_exist: _("Username does not exist"), reason_password_expired: _("Password expired"), reason_user_invalid: _('Disabled or expired'), @@ -46,6 +48,7 @@ block_login_msg = _( mfa_failed_msg = _("MFA code invalid, or ntp sync server time") mfa_required_msg = _("MFA required") +mfa_unset_msg = _("MFA not set, please set it first") login_confirm_required_msg = _("Login confirm required") login_confirm_wait_msg = _("Wait login confirm ticket for accept") login_confirm_error_msg = _("Login confirm ticket was {}") @@ -116,6 +119,16 @@ class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError): super().__init__(username=username, request=request) +class MFAUnsetError(AuthFailedNeedLogMixin, AuthFailedError): + error = reason_mfa_unset + msg = mfa_unset_msg + + def __init__(self, user, request, url): + super().__init__(username=user.username, request=request) + self.user = user + self.url = url + + class BlockLoginError(AuthFailedNeedBlockMixin, AuthFailedError): error = 'block_login' diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 003027b0d..1c4fb5aa1 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -6,7 +6,7 @@ from django.conf import settings from common.utils import get_object_or_none, get_request_ip, get_logger from users.models import User from users.utils import ( - is_block_login, clean_failed_count, increase_login_failed_count, + is_block_login, clean_failed_count ) from . import errors from .utils import check_user_valid @@ -91,8 +91,9 @@ class AuthMixin: return if not user.mfa_enabled: return - if not user.otp_secret_key and user.mfa_is_otp(): - return + unset, url = user.mfa_enabled_but_not_set() + if unset: + raise errors.MFAUnsetError(user, self.request, url) raise errors.MFARequiredError() def check_user_mfa(self, code): diff --git a/apps/authentication/templates/authentication/_mfa_confirm_modal.html b/apps/authentication/templates/authentication/_mfa_confirm_modal.html index 60512d7de..dfa554ebc 100644 --- a/apps/authentication/templates/authentication/_mfa_confirm_modal.html +++ b/apps/authentication/templates/authentication/_mfa_confirm_modal.html @@ -14,7 +14,7 @@
    - {% trans "Need otp auth for view auth" %} + {% trans "Need MFA for view auth" %}
    {% trans "Confirm" %} diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html index 5f1c68c5f..bff33eb17 100644 --- a/apps/authentication/templates/authentication/login.html +++ b/apps/authentication/templates/authentication/login.html @@ -1,116 +1,67 @@ +{% extends '_base_only_msg_content.html' %} {% load static %} {% load i18n %} - - - - - - Jumpserver - - {% include '_head_css_js.html' %} - - - - - +{% block content_title %} + {% trans 'Login' %} +{% endblock %} - -
    -
    -
    - -

    {% trans 'Welcome to the Jumpserver open source fortress' %}

    -

    - {% 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." %} -

    -

    - {% 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." %} -

    -

    - {% 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.' %} -

    -

    - {% trans "Changes the world, starting with a little bit." %} -

    +{% block content %} + + {% csrf_token %} + {% if form.non_field_errors %} +
    +

    {{ form.non_field_errors.as_text }}

    -
    -
    -
    - - {% trans 'Login' %} -
    - - {% csrf_token %} - {% if form.non_field_errors %} -
    -

    {{ form.non_field_errors.as_text }}

    -
    - {% elif form.errors.captcha %} -

    {% trans 'Captcha invalid' %}

    - {% endif %} - -
    - - {% if form.errors.username %} -
    -

    {{ form.errors.username.as_text }}

    -
    - {% endif %} -
    -
    - - {% if form.errors.password %} -
    -

    {{ form.errors.password.as_text }}

    -
    - {% endif %} -
    -
    - {{ form.captcha }} -
    - - - {% if demo_mode %} -

    - Demo账号: admin 密码: admin -

    - {% endif %} - - + {% elif form.errors.captcha %} +

    {% trans 'Captcha invalid' %}

    + {% endif %} - {% if AUTH_OPENID %} -
    -

    {% trans "More login options" %}

    -
    - -
    - {% endif %} +
    + + {% if form.errors.username %} +
    +

    {{ form.errors.username.as_text }}

    +
    + {% endif %} +
    +
    + + {% if form.errors.password %} +
    +

    {{ form.errors.password.as_text }}

    +
    + {% endif %} +
    +
    + {{ form.captcha }} +
    + - + {% if demo_mode %} +

    + Demo账号: admin 密码: admin +

    + {% endif %} -
    + -
    -
    -
    - {% include '_copyright.html' %} + + {% if AUTH_OPENID %} +
    +

    {% trans "More login options" %}

    +
    +
    -
    -
    - - + {% endif %} + + +{% endblock %} diff --git a/apps/authentication/templates/authentication/login_otp.html b/apps/authentication/templates/authentication/login_otp.html index 33ddb8e45..4b460ae66 100644 --- a/apps/authentication/templates/authentication/login_otp.html +++ b/apps/authentication/templates/authentication/login_otp.html @@ -1,88 +1,32 @@ +{% extends '_base_only_content.html' %} {% load static %} {% load i18n %} - - - - - - {{ JMS_TITLE }} - - {% include '_head_css_js.html' %} - - - - - - - - -
    -
    -
    -

    {% trans 'Welcome to the Jumpserver open source fortress' %}

    -

    - {% 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." %} -

    -

    - {% 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." %} -

    -

    - {% 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.' %} -

    -

    - {% trans "Changes the world, starting with a little bit." %} -

    - -
    -
    -
    -
    - - {% trans 'MFA certification' %} -
    -
    - -
    -

    {% trans 'The account protection has been opened, please complete the following operations according to the prompts' %}

    -
    - -
    -

     {% trans 'Open Authenticator and enter the 6-bit dynamic code' %}

    -
    - -
    - - {% csrf_token %} - {% if 'otp_code' in form.errors %} -

    {{ form.otp_code.errors.as_text }}

    - {% endif %} -
    - -
    - - - - {% trans "Can't provide security? Please contact the administrator!" %} - - -
    -
    -

    -

    -
    -
    +{% block title %} + {% trans 'MFA' %} +{% endblock %} + +{% block content %} +
    + {% csrf_token %} + {% if 'otp_code' in form.errors %} +

    {{ form.otp_code.errors.as_text }}

    + {% endif %} +
    +
    -
    -
    -
    - {% include '_copyright.html' %} -
    +
    + + + {% trans 'Open Google Authenticator and enter the 6-bit dynamic code' %} + +
    + + +
    + {% trans "Can't provide security? Please contact the administrator!" %}
    -
    - - +
    +{% endblock %} diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index 64d01ae34..b9f76e731 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -1,8 +1,6 @@ # coding:utf-8 # -from __future__ import absolute_import - from django.urls import path, include from .. import views @@ -10,13 +8,14 @@ from .. import views app_name = 'authentication' urlpatterns = [ - # openid - path('openid/', include(('authentication.backends.openid.urls', 'authentication'), namespace='openid')), - # login path('login/', views.UserLoginView.as_view(), name='login'), path('login/otp/', views.UserLoginOtpView.as_view(), name='login-otp'), path('login/wait-confirm/', views.UserLoginWaitConfirmView.as_view(), name='login-wait-confirm'), path('login/guard/', views.UserLoginGuardView.as_view(), name='login-guard'), path('logout/', views.UserLogoutView.as_view(), name='logout'), + + # openid + path('openid/', include(('authentication.backends.openid.urls', 'authentication'), namespace='openid')), + path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')), ] diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index de3e0dd31..8310b4c8b 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -20,7 +20,7 @@ from django.urls import reverse_lazy from common.utils import get_request_ip, get_object_or_none from users.utils import ( - redirect_user_first_login_or_index, set_tmp_user_to_cache + redirect_user_first_login_or_index ) from .. import forms, mixins, errors @@ -52,17 +52,29 @@ class UserLoginView(mixins.AuthMixin, FormView): template_name = 'authentication/xpack_login.html' return template_name + def get_redirect_url_if_need(self, request): + redirect_url = '' + # show jumpserver login page if request http://{JUMP-SERVER}/?admin=1 + if self.request.GET.get("admin", 0): + return None + if settings.AUTH_OPENID: + redirect_url = reverse("authentication:openid:openid-login") + elif settings.AUTH_CAS: + redirect_url = reverse(settings.CAS_LOGIN_URL_NAME) + + if redirect_url: + query_string = request.GET.urlencode() + redirect_url = "{}?{}".format(redirect_url, query_string) + return redirect_url + def get(self, request, *args, **kwargs): if request.user.is_staff: return redirect(redirect_user_first_login_or_index( request, self.redirect_field_name) ) - # show jumpserver login page if request http://{JUMP-SERVER}/?admin=1 - if settings.AUTH_OPENID and not self.request.GET.get('admin', 0): - query_string = request.GET.urlencode() - openid_login_url = reverse_lazy("authentication:openid:openid-login") - login_url = "{}?{}".format(openid_login_url, query_string) - return redirect(login_url) + redirect_url = self.get_redirect_url_if_need(request) + if redirect_url: + return redirect(redirect_url) request.session.set_test_cookie() return super().get(request, *args, **kwargs) @@ -127,12 +139,9 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView): return self.format_redirect_url(self.login_otp_url) except errors.LoginConfirmBaseError: return self.format_redirect_url(self.login_confirm_url) + except errors.MFAUnsetError as e: + return e.url else: - # 启用但是没有设置otp, 排除radius - if user.mfa_enabled_but_not_set(): - # 1,2,mfa_setting & F - set_tmp_user_to_cache(self.request, user) - return reverse('users:user-otp-enable-authentication') auth_login(self.request, user) self.send_auth_signal(success=True, user=user) self.clear_auth_mark() @@ -174,8 +183,17 @@ class UserLoginWaitConfirmView(TemplateView): class UserLogoutView(TemplateView): template_name = 'flash_message_standalone.html' + @staticmethod + def get_backend_logout_url(): + if settings.AUTH_CAS: + return settings.CAS_LOGOUT_URL_NAME + return None + def get(self, request, *args, **kwargs): auth_logout(request) + backend_logout_url = self.get_backend_logout_url() + if backend_logout_url: + return redirect(backend_logout_url) next_uri = request.COOKIES.get("next") if next_uri: return redirect(next_uri) diff --git a/apps/common/README.md b/apps/common/README.md index 400f19fc8..457fee410 100644 --- a/apps/common/README.md +++ b/apps/common/README.md @@ -16,7 +16,7 @@ provide this ability, not common, You should write it on your app utils. ## Celery usage -Jumpserver use celery to run task async. Using redis as the broker, so +JumpServer use celery to run task async. Using redis as the broker, so you should run a redis instance #### Run redis diff --git a/apps/common/mixins/api.py b/apps/common/mixins/api.py index 694c5606d..6440cdd10 100644 --- a/apps/common/mixins/api.py +++ b/apps/common/mixins/api.py @@ -1,13 +1,20 @@ # -*- coding: utf-8 -*- # +import time +from hashlib import md5 +from threading import Thread + +from django.core.cache import cache from django.http import JsonResponse +from rest_framework.response import Response from rest_framework.settings import api_settings from common.drf.filters import IDSpmFilter, CustomFilter +from ..utils import lazyproperty __all__ = [ "JSONResponseMixin", "CommonApiMixin", - "IDSpmFilterMixin", + "IDSpmFilterMixin", 'AsyncApiMixin', ] @@ -62,3 +69,122 @@ class ExtraFilterFieldsMixin: class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin): pass + + +class InterceptMixin: + def dispatch(self, request, *args, **kwargs): + self.args = args + self.kwargs = kwargs + request = self.initialize_request(request, *args, **kwargs) + self.request = request + self.headers = self.default_response_headers # deprecate? + + try: + self.initial(request, *args, **kwargs) + + # Get the appropriate handler method + if request.method.lower() in self.http_method_names: + handler = getattr(self, request.method.lower(), + self.http_method_not_allowed) + else: + handler = self.http_method_not_allowed + + response = self.do(handler, request, *args, **kwargs) + + except Exception as exc: + response = self.handle_exception(exc) + + self.response = self.finalize_response(request, response, *args, **kwargs) + return self.response + + +class AsyncApiMixin(InterceptMixin): + def get_request_user_id(self): + user = self.request.user + if hasattr(user, 'id'): + return str(user.id) + return '' + + @lazyproperty + def async_cache_key(self): + method = self.request.method + path = self.get_request_md5() + user = self.get_request_user_id() + key = '{}_{}_{}'.format(method, path, user) + return key + + def get_request_md5(self): + path = self.request.path + query = {k: v for k, v in self.request.GET.items()} + query.pop("_", None) + query.pop('refresh', None) + query = "&".join(["{}={}".format(k, v) for k, v in query.items()]) + full_path = "{}?{}".format(path, query) + return md5(full_path.encode()).hexdigest() + + @lazyproperty + def initial_data(self): + data = { + "status": "running", + "start_time": time.time(), + "key": self.async_cache_key, + } + return data + + def get_cache_data(self): + key = self.async_cache_key + if self.is_need_refresh(): + cache.delete(key) + return None + data = cache.get(key) + return data + + def do(self, handler, *args, **kwargs): + if not self.is_need_async(): + return handler(*args, **kwargs) + resp = self.do_async(handler, *args, **kwargs) + return resp + + def is_need_refresh(self): + if self.request.GET.get("refresh"): + return True + return False + + def is_need_async(self): + return False + + def do_async(self, handler, *args, **kwargs): + data = self.get_cache_data() + if not data: + t = Thread( + target=self.do_in_thread, + args=(handler, *args), + kwargs=kwargs + ) + t.start() + resp = Response(self.initial_data) + return resp + status = data.get("status") + resp = data.get("resp") + if status == "ok" and resp: + resp = Response(**resp) + else: + resp = Response(data) + return resp + + def do_in_thread(self, handler, *args, **kwargs): + key = self.async_cache_key + data = self.initial_data + cache.set(key, data, 600) + try: + response = handler(*args, **kwargs) + data["status"] = "ok" + data["resp"] = { + "data": response.data, + "status": response.status_code + } + cache.set(key, data, 600) + except Exception as e: + data["error"] = str(e) + data["status"] = "error" + cache.set(key, data, 600) diff --git a/apps/common/mixins/models.py b/apps/common/mixins/models.py index e373a6b08..e0d9b392a 100644 --- a/apps/common/mixins/models.py +++ b/apps/common/mixins/models.py @@ -5,7 +5,6 @@ from django.db import models from django.utils import timezone from django.utils.translation import ugettext_lazy as _ - __all__ = [ "NoDeleteManager", "NoDeleteModelMixin", "NoDeleteQuerySet", "CommonModelMixin" @@ -65,3 +64,5 @@ class DebugQueryManager(models.Manager): print("<<<<<<<<<<<<<<<<<<<<<<<<<<<<") queryset = super().get_queryset() return queryset + + diff --git a/apps/common/struct.py b/apps/common/struct.py index 88bace4a7..e9949de98 100644 --- a/apps/common/struct.py +++ b/apps/common/struct.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +from itertools import chain +from .utils import lazyproperty class Stack(list): @@ -23,3 +25,82 @@ class Stack(list): def push(self, item): self.append(item) + + +class QuerySetChain: + def __init__(self, querysets): + self.querysets = querysets + + @lazyproperty + def querysets_counts(self): + counts = [s.count() for s in self.querysets] + return counts + + def count(self): + return self.total_count + + @lazyproperty + def total_count(self): + return sum(self.querysets_counts) + + def __iter__(self): + self._chain = chain(*self.querysets) + return self + + def __next__(self): + return next(self._chain) + + def __getitem__(self, ndx): + querysets_count_zip = zip(self.querysets, self.querysets_counts) + length = 0 # 加上本数组后的大数组长度 + pre_length = 0 # 不包含本数组的大数组长度 + items = [] # 返回的值 + loop = 0 + + if isinstance(ndx, slice): + ndx_start = ndx.start or 0 + ndx_stop = ndx.stop or self.total_count + ndx_step = ndx.step or 1 + else: + ndx_start = ndx + ndx_stop, ndx_step = None, None + + for queryset, count in querysets_count_zip: + length += count + loop += 1 + # 取当前数组的start角标, 存在3中情况 + # 1. start角标在当前数组 + if length > ndx_start >= pre_length: + start = ndx_start - pre_length + # print("[loop {}] Start is: {}".format(loop, start)) + if ndx_step is None: + return queryset[start] + # 2. 不包含当前数组,因为起始已经超过了当前数组的长度 + elif ndx_start >= length: + pre_length += count + continue + # 3. 不在当前数组,但是应该从当前数组0开始计算 + else: + start = 0 + + # 可能取单个值, ndx_stop 为None, 不应该再找 + if ndx_stop is None: + pre_length += count + continue + + # 取当前数组的stop角标, 存在2中情况 + # 不存在第3中情况是因为找到了会提交结束循环 + # 1. 结束角标小于length代表 结束位在当前数组上 + if ndx_stop < length: + stop = ndx_stop - pre_length + # 2. 结束位置包含改数组到了最后 + else: + stop = count + # print("[loop {}] Slice: {} {} {}".format(loop, start, stop, ndx_step)) + items.extend(list(queryset[slice(start, stop, ndx_step)])) + pre_length += count + + # 如果结束再当前数组,则结束循环 + if ndx_stop < length: + break + return items diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index 07116ea8b..77a48dc60 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -199,11 +199,15 @@ logger = get_logger(__name__) def timeit(func): def wrapper(*args, **kwargs): - logger.debug("Start call: {}".format(func.__name__)) + if hasattr(func, '__name__'): + name = func.__name__ + else: + name = func + logger.debug("Start call: {}".format(name)) now = time.time() result = func(*args, **kwargs) using = (time.time() - now) * 1000 - msg = "End call {}, using: {:.1f}ms".format(func.__name__, using) + msg = "End call {}, using: {:.1f}ms".format(name, using) logger.debug(msg) return result return wrapper diff --git a/apps/common/utils/random.py b/apps/common/utils/random.py new file mode 100644 index 000000000..f32147b6d --- /dev/null +++ b/apps/common/utils/random.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# +import socket +import struct +import random + + +def random_datetime(date_start, date_end): + random_delta = (date_end - date_start) * random.random() + return date_start + random_delta + + +def random_ip(): + return socket.inet_ntoa(struct.pack('>I', random.randint(1, 0xffffffff))) + + + +# def strTimeProp(start, end, prop, fmt): +# time_start = time.mktime(time.strptime(start, fmt)) +# time_end = time.mktime(time.strptime(end, fmt)) +# ptime = time_start + prop * (time_end - time_start) +# return int(ptime) +# +# +# def randomTimestamp(start, end, fmt='%Y-%m-%d %H:%M:%S'): +# return strTimeProp(start, end, random.random(), fmt) +# +# +# def randomDate(start, end, frmt='%Y-%m-%d %H:%M:%S'): +# return time.strftime(frmt, time.localtime(strTimeProp(start, end, random.random(), frmt))) +# +# +# def randomTimestampList(start, end, n, frmt='%Y-%m-%d %H:%M:%S'): +# return [randomTimestamp(start, end, frmt) for _ in range(n)] +# +# +# def randomDateList(start, end, n, frmt='%Y-%m-%d %H:%M:%S'): +# return [randomDate(start, end, frmt) for _ in range(n)] + diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 8707690c2..bfed4ec9d 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -84,11 +84,10 @@ class Config(dict): :param defaults: an optional dictionary of default values """ defaults = { - # Django Config + # Django Config, Must set before start 'SECRET_KEY': '', 'BOOTSTRAP_TOKEN': '', 'DEBUG': True, - 'SITE_URL': 'http://localhost:8080', 'LOG_LEVEL': 'DEBUG', 'LOG_DIR': os.path.join(PROJECT_DIR, 'logs'), 'DB_ENGINE': 'mysql', @@ -100,10 +99,13 @@ class Config(dict): 'REDIS_HOST': '127.0.0.1', 'REDIS_PORT': 6379, 'REDIS_PASSWORD': '', + # Default value 'REDIS_DB_CELERY': 3, 'REDIS_DB_CACHE': 4, 'REDIS_DB_SESSION': 5, 'REDIS_DB_WS': 6, + + 'SITE_URL': 'http://localhost:8080', 'CAPTCHA_TEST_MODE': None, 'TOKEN_EXPIRATION': 3600 * 24, 'DISPLAY_PER_PAGE': 25, @@ -148,8 +150,13 @@ class Config(dict): 'RADIUS_ENCRYPT_PASSWORD': True, 'OTP_IN_RADIUS': False, + 'AUTH_CAS': False, + 'CAS_SERVER_URL': "http://host/cas/", + 'CAS_LOGOUT_COMPLETELY': False, + 'CAS_VERSION': 3, + 'OTP_VALID_WINDOW': 2, - 'OTP_ISSUER_NAME': 'Jumpserver', + 'OTP_ISSUER_NAME': 'JumpServer', 'EMAIL_SUFFIX': 'jumpserver.org', 'TERMINAL_PASSWORD_AUTH': True, @@ -179,6 +186,7 @@ class Config(dict): 'HTTP_LISTEN_PORT': 8080, 'WS_LISTEN_PORT': 8070, 'LOGIN_LOG_KEEP_DAYS': 90, + 'TASK_LOG_KEEP_DAYS': 10, 'ASSETS_PERM_CACHE_TIME': 3600 * 24, 'SECURITY_MFA_VERIFY_TTL': 3600, 'ASSETS_PERM_CACHE_ENABLE': False, @@ -284,6 +292,8 @@ class DynamicConfig: ] if self.get('AUTH_LDAP'): backends.insert(0, 'authentication.backends.ldap.LDAPAuthorizationBackend') + if self.static_config.get('AUTH_CAS'): + backends.insert(0, 'authentication.backends.cas.CASBackend') if self.static_config.get('AUTH_OPENID'): backends.insert(0, 'authentication.backends.openid.backends.OpenIDAuthorizationPasswordBackend') backends.insert(0, 'authentication.backends.openid.backends.OpenIDAuthorizationCodeBackend') diff --git a/apps/jumpserver/context_processor.py b/apps/jumpserver/context_processor.py index 0fbef047f..0a49b957d 100644 --- a/apps/jumpserver/context_processor.py +++ b/apps/jumpserver/context_processor.py @@ -13,7 +13,7 @@ def jumpserver_processor(request): 'LOGO_TEXT_URL': static('img/logo_text.png'), 'LOGIN_IMAGE_URL': static('img/login_image.png'), 'FAVICON_URL': static('img/facio.ico'), - 'JMS_TITLE': 'Jumpserver', + 'JMS_TITLE': 'JumpServer', 'VERSION': settings.VERSION, 'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2020', 'SECURITY_COMMAND_EXECUTION': settings.SECURITY_COMMAND_EXECUTION, diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 978faa751..e6245d02c 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -64,7 +64,21 @@ RADIUS_SERVER = CONFIG.RADIUS_SERVER RADIUS_PORT = CONFIG.RADIUS_PORT RADIUS_SECRET = CONFIG.RADIUS_SECRET +# CAS Auth +AUTH_CAS = CONFIG.AUTH_CAS +CAS_SERVER_URL = CONFIG.CAS_SERVER_URL +CAS_VERIFY_SSL_CERTIFICATE = False +CAS_LOGIN_URL_NAME = "authentication:cas:cas-login" +CAS_LOGOUT_URL_NAME = "authentication:cas:cas-logout" +CAS_LOGIN_MSG = None +CAS_LOGGED_MSG = None +CAS_LOGOUT_COMPLETELY = CONFIG.CAS_LOGOUT_COMPLETELY +CAS_VERSION = CONFIG.CAS_VERSION + +# Other setting TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS + +AUTHENTICATION_BACKENDS = DYNAMIC.AUTHENTICATION_BACKENDS diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index 49425b488..22ebd1775 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -51,6 +51,7 @@ INSTALLED_APPS = [ 'rest_framework', 'rest_framework_swagger', 'drf_yasg', + 'django_cas_ng', 'channels', 'django_filters', 'bootstrap3', @@ -75,6 +76,7 @@ MIDDLEWARE = [ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'authentication.backends.openid.middleware.OpenIDAuthenticationMiddleware', + 'django_cas_ng.middleware.CASMiddleware', 'jumpserver.middleware.TimezoneMiddleware', 'jumpserver.middleware.DemoMiddleware', 'jumpserver.middleware.RequestMiddleware', @@ -220,8 +222,6 @@ EMAIL_USE_SSL = DYNAMIC.EMAIL_USE_SSL EMAIL_USE_TLS = DYNAMIC.EMAIL_USE_TLS -AUTHENTICATION_BACKENDS = DYNAMIC.AUTHENTICATION_BACKENDS - # Custom User Auth model AUTH_USER_MODEL = 'users.User' diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 8a86ba284..a55693c55 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -82,5 +82,6 @@ USER_GUIDE_URL = DYNAMIC.USER_GUIDE_URL HTTP_LISTEN_PORT = CONFIG.HTTP_LISTEN_PORT WS_LISTEN_PORT = CONFIG.WS_LISTEN_PORT LOGIN_LOG_KEEP_DAYS = DYNAMIC.LOGIN_LOG_KEEP_DAYS +TASK_LOG_KEEP_DAYS = CONFIG.TASK_LOG_KEEP_DAYS ORG_CHANGE_TO_URL = CONFIG.ORG_CHANGE_TO_URL WINDOWS_SKIP_ALL_MANUAL_PASSWORD = CONFIG.WINDOWS_SKIP_ALL_MANUAL_PASSWORD diff --git a/apps/jumpserver/settings/libs.py b/apps/jumpserver/settings/libs.py index 69b580d66..706fb3647 100644 --- a/apps/jumpserver/settings/libs.py +++ b/apps/jumpserver/settings/libs.py @@ -53,6 +53,7 @@ SWAGGER_SETTINGS = { 'in': 'header' } }, + 'DEFAULT_INFO': 'jumpserver.views.swagger.api_info', } diff --git a/apps/jumpserver/settings/logging.py b/apps/jumpserver/settings/logging.py index 7de84ab7e..35eaee3b6 100644 --- a/apps/jumpserver/settings/logging.py +++ b/apps/jumpserver/settings/logging.py @@ -96,12 +96,16 @@ LOGGING = { 'handlers': ['syslog'], 'level': 'INFO' }, - # 'django.db': { - # 'handlers': ['console', 'file'], - # 'level': 'DEBUG' - # } + } } + +if os.environ.get("DEBUG_DB"): + LOGGING['loggers']['django.db'] = { + 'handlers': ['console', 'file'], + 'level': 'DEBUG' + } + SYSLOG_ENABLE = CONFIG.SYSLOG_ENABLE if CONFIG.SYSLOG_ADDR != '' and len(CONFIG.SYSLOG_ADDR.split(':')) == 2: diff --git a/apps/jumpserver/views/index.py b/apps/jumpserver/views/index.py index 2a3304a8c..5cd034a2d 100644 --- a/apps/jumpserver/views/index.py +++ b/apps/jumpserver/views/index.py @@ -1,182 +1,246 @@ -import datetime - +from django.core.cache import cache from django.views.generic import TemplateView from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from django.db.models import Count +from django.db.models import Count, Max from django.shortcuts import redirect - from users.models import User from assets.models import Asset from terminal.models import Session from orgs.utils import current_org from common.permissions import PermissionsMixin, IsValidUser +from common.utils import timeit, lazyproperty __all__ = ['IndexView'] -class IndexView(PermissionsMixin, TemplateView): - template_name = 'index.html' - permission_classes = [IsValidUser] - - session_week = None - session_month = None - session_month_dates = [] - session_month_dates_archive = [] - - def dispatch(self, request, *args, **kwargs): - if not request.user.is_authenticated: - return self.handle_no_permission() - if request.user.is_common_user: - return redirect('assets:user-asset-list') - return super(IndexView, self).dispatch(request, *args, **kwargs) - - @staticmethod - def get_user_count(): - return current_org.get_org_members().count() +class MonthLoginMetricMixin: + @lazyproperty + def session_month(self): + month_ago = timezone.now() - timezone.timedelta(days=30) + session_month = Session.objects.filter(date_start__gt=month_ago) + return session_month - @staticmethod - def get_asset_count(): - return Asset.objects.all().count() + @lazyproperty + def session_month_dates(self): + return self.session_month.dates('date_start', 'day') - @staticmethod - def get_online_user_count(): - return len(set(Session.objects.filter(is_finished=False).values_list('user', flat=True))) + def get_month_day_metrics(self): + month_str = [ + d.strftime('%m-%d') for d in self.session_month_dates + ] or ['0'] + return month_str @staticmethod - def get_online_session_count(): - return Session.objects.filter(is_finished=False).count() + def get_cache_key(date, tp): + date_str = date.strftime("%Y%m%d") + key = "SESSION_MONTH_{}_{}".format(tp, date_str) + return key + + def __get_data_from_cache(self, date, tp): + if date == timezone.now().date(): + return None + cache_key = self.get_cache_key(date, tp) + count = cache.get(cache_key) + return count - def get_top5_user_a_week(self): - return self.session_week.values('user').annotate(total=Count('user')).order_by('-total')[:5] + def __set_data_to_cache(self, date, tp, count): + cache_key = self.get_cache_key(date, tp) + cache.set(cache_key, count, 3600*24*7) - def get_week_login_user_count(self): - return self.session_week.values('user').distinct().count() + @lazyproperty + def user_disabled_total(self): + return current_org.get_org_members().filter(is_active=False).count() - def get_week_login_asset_count(self): - return self.session_week.count() + @lazyproperty + def asset_disabled_total(self): + return Asset.objects.filter(is_active=False).count() - def get_month_day_metrics(self): - month_str = [d.strftime('%m-%d') for d in self.session_month_dates] or ['0'] - return month_str + def get_date_login_count(self, date): + tp = "LOGIN" + count = self.__get_data_from_cache(date, tp) + if count is not None: + return count + count = Session.objects.filter(date_start__date=date).count() + self.__set_data_to_cache(date, tp, count) + return count def get_month_login_metrics(self): data = [] - time_min = datetime.datetime.min.time() - time_max = datetime.datetime.max.time() for d in self.session_month_dates: - ds = datetime.datetime.combine(d, time_min).replace(tzinfo=timezone.get_current_timezone()) - de = datetime.datetime.combine(d, time_max).replace(tzinfo=timezone.get_current_timezone()) - data.append(self.session_month.filter(date_start__range=(ds, de)).count()) + count = self.get_date_login_count(d) + data.append(count) + if len(data) == 0: + data = [0] return data + def get_date_user_count(self, date): + tp = "USER" + count = self.__get_data_from_cache(date, tp) + if count is not None: + return count + count = Session.objects.filter(date_start__date=date)\ + .values('user').distinct().count() + self.__set_data_to_cache(date, tp, count) + return count + def get_month_active_user_metrics(self): - if self.session_month_dates_archive: - return [q.values('user').distinct().count() - for q in self.session_month_dates_archive] - else: - return [0] + data = [] + for d in self.session_month_dates: + count = self.get_date_user_count(d) + data.append(count) + return data + + def get_date_asset_count(self, date): + tp = "ASSET" + count = self.__get_data_from_cache(date, tp) + if count is not None: + return count + count = Session.objects.filter(date_start__date=date) \ + .values('asset').distinct().count() + self.__set_data_to_cache(date, tp, count) + return count def get_month_active_asset_metrics(self): - if self.session_month_dates_archive: - return [q.values('asset').distinct().count() - for q in self.session_month_dates_archive] - else: - return [0] + data = [] + for d in self.session_month_dates: + count = self.get_date_asset_count(d) + data.append(count) + return data - def get_month_active_user_total(self): - return self.session_month.values('user').distinct().count() + @lazyproperty + def month_active_user_total(self): + count = self.session_month.values('user').distinct().count() + return count - def get_month_inactive_user_total(self): - count = current_org.get_org_members().count() - self.get_month_active_user_total() + @lazyproperty + def month_inactive_user_total(self): + total = current_org.get_org_members().count() + active = self.month_active_user_total + count = total - active if count < 0: count = 0 return count - def get_month_active_asset_total(self): + @lazyproperty + def month_active_asset_total(self): return self.session_month.values('asset').distinct().count() - def get_month_inactive_asset_total(self): - count = Asset.objects.all().count() - self.get_month_active_asset_total() + @lazyproperty + def month_inactive_asset_total(self): + total = Asset.objects.all().count() + active = self.month_active_asset_total + count = total - active if count < 0: count = 0 return count - @staticmethod - def get_user_disabled_total(): - return current_org.get_org_members().filter(is_active=False).count() + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + 'month_str': self.get_month_day_metrics(), + 'month_total_visit_count': self.get_month_login_metrics(), + 'month_user': self.get_month_active_user_metrics(), + 'mouth_asset': self.get_month_active_asset_metrics(), + 'month_user_active': self.month_active_user_total, + 'month_user_inactive': self.month_inactive_user_total, + 'month_user_disabled': self.user_disabled_total, + 'month_asset_active': self.month_active_asset_total, + 'month_asset_inactive': self.month_inactive_asset_total, + 'month_asset_disabled': self.asset_disabled_total, + }) + return context - @staticmethod - def get_asset_disabled_total(): - return Asset.objects.filter(is_active=False).count() - def get_week_top10_asset(self): - assets = list(self.session_week.values('asset').annotate(total=Count('asset')).order_by('-total')[:10]) - for asset in assets: - last_login = self.session_week.filter(asset=asset["asset"]).order_by('date_start').last() - asset['last'] = last_login +class WeekSessionMetricMixin: + session_week = None + + @lazyproperty + def session_week(self): + week_ago = timezone.now() - timezone.timedelta(weeks=1) + session_week = Session.objects.filter(date_start__gt=week_ago) + return session_week + + def get_top5_user_a_week(self): + users = self.session_week.values('user') \ + .annotate(total=Count('user')) \ + .order_by('-total')[:5] + return users + + def get_week_login_user_count(self): + return self.session_week.values('user').distinct().count() + + def get_week_login_asset_count(self): + return self.session_week.count() + + def get_week_top10_assets(self): + assets = self.session_week.values("asset")\ + .annotate(total=Count("asset"))\ + .annotate(last=Max("date_start")).order_by("-total")[:10] return assets - def get_week_top10_user(self): - users = list(self.session_week.values('user').annotate( - total=Count('asset')).order_by('-total')[:10]) - for user in users: - last_login = self.session_week.filter(user=user["user"]).order_by('date_start').last() - user['last'] = last_login + def get_week_top10_users(self): + users = self.session_week.values("user") \ + .annotate(total=Count("user")) \ + .annotate(last=Max("date_start")).order_by("-total")[:10] return users def get_last10_sessions(self): sessions = self.session_week.order_by('-date_start')[:10] for session in sessions: - try: - session.avatar_url = User.objects.get(username=session.user).avatar_url() - except User.DoesNotExist: - session.avatar_url = User.objects.first().avatar_url() + session.avatar_url = User.get_avatar_url("") return sessions def get_context_data(self, **kwargs): - week_ago = timezone.now() - timezone.timedelta(weeks=1) - month_ago = timezone.now() - timezone.timedelta(days=30) - self.session_week = Session.objects.filter(date_start__gt=week_ago) - self.session_month = Session.objects.filter(date_start__gt=month_ago) - self.session_month_dates = self.session_month.dates('date_start', 'day') + context = super().get_context_data(**kwargs) + context.update({ + 'user_visit_count_weekly': self.get_week_login_user_count(), + 'asset_visit_count_weekly': self.get_week_login_asset_count(), + 'user_visit_count_top_five': self.get_top5_user_a_week(), + 'last_login_ten': self.get_last10_sessions(), + 'week_asset_hot_ten': self.get_week_top10_assets(), + 'week_user_hot_ten': self.get_week_top10_users(), + }) + return context - self.session_month_dates_archive = [] - time_min = datetime.datetime.min.time() - time_max = datetime.datetime.max.time() - for d in self.session_month_dates: - ds = datetime.datetime.combine(d, time_min).replace( - tzinfo=timezone.get_current_timezone()) - de = datetime.datetime.combine(d, time_max).replace( - tzinfo=timezone.get_current_timezone()) - self.session_month_dates_archive.append( - self.session_month.filter(date_start__range=(ds, de))) - - context = { +class IndexView(PermissionsMixin, MonthLoginMetricMixin, WeekSessionMetricMixin, TemplateView): + template_name = 'index.html' + permission_classes = [IsValidUser] + + def dispatch(self, request, *args, **kwargs): + if not request.user.is_authenticated: + return self.handle_no_permission() + if request.user.is_common_user: + return redirect('assets:user-asset-list') + return super(IndexView, self).dispatch(request, *args, **kwargs) + + @staticmethod + def get_user_count(): + return current_org.get_org_members().count() + + @staticmethod + def get_asset_count(): + return Asset.objects.all().count() + + @staticmethod + def get_online_user_count(): + count = Session.objects.filter(is_finished=False)\ + .values_list('user', flat=True).distinct().count() + return count + + @staticmethod + def get_online_session_count(): + return Session.objects.filter(is_finished=False).count() + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ 'assets_count': self.get_asset_count(), 'users_count': self.get_user_count(), 'online_user_count': self.get_online_user_count(), 'online_asset_count': self.get_online_session_count(), - 'user_visit_count_weekly': self.get_week_login_user_count(), - 'asset_visit_count_weekly': self.get_week_login_asset_count(), - 'user_visit_count_top_five': self.get_top5_user_a_week(), - 'month_str': self.get_month_day_metrics(), - 'month_total_visit_count': self.get_month_login_metrics(), - 'month_user': self.get_month_active_user_metrics(), - 'mouth_asset': self.get_month_active_asset_metrics(), - 'month_user_active': self.get_month_active_user_total(), - 'month_user_inactive': self.get_month_inactive_user_total(), - 'month_user_disabled': self.get_user_disabled_total(), - 'month_asset_active': self.get_month_active_asset_total(), - 'month_asset_inactive': self.get_month_inactive_asset_total(), - 'month_asset_disabled': self.get_asset_disabled_total(), - 'week_asset_hot_ten': self.get_week_top10_asset(), - 'last_login_ten': self.get_last10_sessions(), - 'week_user_hot_ten': self.get_week_top10_user(), 'app': _("Dashboard"), - } - - kwargs.update(context) - return super(IndexView, self).get_context_data(**kwargs) + }) + return context diff --git a/apps/jumpserver/views/swagger.py b/apps/jumpserver/views/swagger.py index 4aed12663..f6f195bed 100644 --- a/apps/jumpserver/views/swagger.py +++ b/apps/jumpserver/views/swagger.py @@ -49,6 +49,16 @@ class CustomSwaggerAutoSchema(SwaggerAutoSchema): return fields +api_info = openapi.Info( + title="JumpServer API Docs", + default_version='v1', + description="JumpServer Restful api docs", + terms_of_service="https://www.jumpserver.org", + contact=openapi.Contact(email="support@fit2cloud.com"), + license=openapi.License(name="GPLv2 License"), +) + + def get_swagger_view(version='v1'): from ..urls import api_v1, api_v2 from django.urls import path, include @@ -65,14 +75,7 @@ def get_swagger_view(version='v1'): else: patterns = api_v1_patterns schema_view = get_schema_view( - openapi.Info( - title="Jumpserver API Docs", - default_version=version, - description="Jumpserver Restful api docs", - terms_of_service="https://www.jumpserver.org", - contact=openapi.Contact(email="support@fit2cloud.com"), - license=openapi.License(name="GPLv2 License"), - ), + api_info, public=True, patterns=patterns, permission_classes=(permissions.AllowAny,), diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 89354bcf1d09cbd9cb194210c3fb13c358df6e2a..f9e23c2815041175cc8508df4de51afe51ad7646 100644 GIT binary patch delta 30916 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 delta 27048 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\n" -"Language-Team: Jumpserver team\n" +"Language-Team: JumpServer team\n" "Language: zh_CN\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -26,9 +26,10 @@ msgstr "自定义" #: applications/templates/applications/remote_app_list.html:27 #: applications/templates/applications/user_remote_app_list.html:18 #: assets/forms/domain.py:15 assets/forms/label.py:13 -#: assets/models/asset.py:342 assets/models/authbook.py:24 +#: assets/models/asset.py:353 assets/models/authbook.py:23 #: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:32 -#: assets/serializers/asset_user.py:82 assets/serializers/system_user.py:41 +#: assets/serializers/asset_user.py:48 assets/serializers/asset_user.py:85 +#: assets/serializers/system_user.py:45 assets/serializers/system_user.py:169 #: assets/templates/assets/admin_user_list.html:23 #: assets/templates/assets/asset_list.html:170 #: assets/templates/assets/domain_detail.html:55 @@ -42,22 +43,23 @@ msgstr "自定义" #: perms/templates/perms/asset_permission_create_update.html:57 #: perms/templates/perms/asset_permission_list.html:35 #: perms/templates/perms/asset_permission_list.html:87 -#: terminal/backends/command/models.py:13 terminal/models.py:178 -#: terminal/templates/terminal/command_list.html:30 -#: terminal/templates/terminal/command_list.html:66 +#: terminal/backends/command/models.py:19 terminal/models.py:187 +#: terminal/templates/terminal/command_list.html:31 +#: terminal/templates/terminal/command_list.html:106 +#: terminal/templates/terminal/session_detail.html:52 #: terminal/templates/terminal/session_list.html:26 #: terminal/templates/terminal/session_list.html:70 #: users/templates/users/user_asset_permission.html:40 #: users/templates/users/user_asset_permission.html:70 #: users/templates/users/user_granted_remote_app.html:36 -#: xpack/plugins/change_auth_plan/forms.py:73 -#: xpack/plugins/change_auth_plan/models.py:418 +#: xpack/plugins/change_auth_plan/forms.py:74 +#: xpack/plugins/change_auth_plan/models.py:364 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:40 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:54 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:13 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:14 -#: xpack/plugins/cloud/models.py:306 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:60 +#: xpack/plugins/cloud/models.py:266 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:36 #: xpack/plugins/orgs/templates/orgs/org_list.html:17 #: xpack/plugins/vault/forms.py:13 xpack/plugins/vault/forms.py:15 msgid "Asset" @@ -108,8 +110,8 @@ msgstr "运行参数" #: applications/templates/applications/remote_app_list.html:25 #: applications/templates/applications/user_database_app_list.html:16 #: applications/templates/applications/user_remote_app_list.html:16 -#: assets/forms/asset.py:20 assets/forms/domain.py:77 assets/forms/user.py:74 -#: assets/forms/user.py:94 assets/models/asset.py:144 assets/models/base.py:28 +#: assets/forms/asset.py:21 assets/forms/domain.py:77 assets/forms/user.py:74 +#: assets/forms/user.py:96 assets/models/asset.py:146 assets/models/base.py:232 #: assets/models/cluster.py:18 assets/models/cmd_filter.py:21 #: assets/models/domain.py:20 assets/models/group.py:20 #: assets/models/label.py:18 assets/templates/assets/_node_detail_modal.html:27 @@ -121,10 +123,10 @@ msgstr "运行参数" #: assets/templates/assets/domain_gateway_list.html:62 #: assets/templates/assets/domain_list.html:21 #: assets/templates/assets/label_list.html:14 -#: assets/templates/assets/platform_detail.html:43 +#: assets/templates/assets/platform_detail.html:48 #: assets/templates/assets/platform_list.html:16 -#: assets/templates/assets/system_user_detail.html:55 -#: assets/templates/assets/system_user_list.html:24 ops/models/adhoc.py:40 +#: assets/templates/assets/system_user_detail.html:62 +#: assets/templates/assets/system_user_list.html:24 ops/mixin.py:24 #: ops/templates/ops/task_detail.html:58 ops/templates/ops/task_list.html:11 #: orgs/models.py:12 perms/models/base.py:48 #: perms/templates/perms/asset_permission_detail.html:57 @@ -140,11 +142,11 @@ msgstr "运行参数" #: perms/templates/perms/remote_app_permission_user.html:49 #: settings/models.py:26 #: settings/templates/settings/_ldap_list_users_modal.html:32 -#: terminal/models.py:26 terminal/models.py:282 terminal/models.py:314 -#: terminal/models.py:351 terminal/templates/terminal/base_storage_list.html:31 +#: terminal/models.py:26 terminal/models.py:334 terminal/models.py:366 +#: terminal/models.py:403 terminal/templates/terminal/base_storage_list.html:31 #: terminal/templates/terminal/terminal_detail.html:43 #: terminal/templates/terminal/terminal_list.html:30 users/forms/profile.py:20 -#: users/models/group.py:15 users/models/user.py:438 +#: users/models/group.py:15 users/models/user.py:440 #: users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 @@ -158,16 +160,14 @@ msgstr "运行参数" #: users/templates/users/user_profile.html:51 #: users/templates/users/user_pubkey_update.html:57 #: users/templates/users/user_remote_app_permission.html:36 -#: xpack/plugins/change_auth_plan/forms.py:56 -#: xpack/plugins/change_auth_plan/models.py:63 +#: xpack/plugins/change_auth_plan/forms.py:57 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:59 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:12 -#: xpack/plugins/cloud/models.py:58 xpack/plugins/cloud/models.py:143 +#: xpack/plugins/cloud/models.py:35 #: xpack/plugins/cloud/templates/cloud/account_detail.html:47 #: xpack/plugins/cloud/templates/cloud/account_list.html:12 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:53 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:12 -#: xpack/plugins/gathered_user/models.py:28 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:16 #: xpack/plugins/orgs/templates/orgs/org_detail.html:47 #: xpack/plugins/orgs/templates/orgs/org_list.html:12 @@ -182,7 +182,7 @@ msgstr "名称" #: assets/templates/assets/cmd_filter_rule_list.html:53 #: audits/templates/audits/login_log_list.html:58 #: perms/templates/perms/remote_app_permission_remote_app.html:50 -#: terminal/models.py:316 terminal/models.py:353 +#: terminal/models.py:368 terminal/models.py:405 #: terminal/templates/terminal/base_storage_list.html:32 #: tickets/models/ticket.py:43 tickets/templates/tickets/ticket_detail.html:33 #: tickets/templates/tickets/ticket_list.html:35 @@ -194,7 +194,7 @@ msgstr "类型" #: applications/templates/applications/database_app_detail.html:56 #: applications/templates/applications/database_app_list.html:25 #: applications/templates/applications/user_database_app_list.html:18 -#: ops/models/adhoc.py:185 templates/index.html:91 +#: ops/models/adhoc.py:146 templates/index.html:90 #: users/templates/users/user_granted_database_app.html:36 msgid "Host" msgstr "主机" @@ -202,7 +202,7 @@ msgstr "主机" #: applications/models/database_app.py:27 #: applications/templates/applications/database_app_detail.html:60 #: applications/templates/applications/database_app_list.html:26 -#: assets/forms/asset.py:24 assets/models/asset.py:190 +#: assets/forms/asset.py:25 assets/models/asset.py:192 #: assets/models/domain.py:50 #: assets/templates/assets/domain_gateway_list.html:64 msgid "Port" @@ -225,8 +225,8 @@ msgstr "数据库" #: applications/templates/applications/remote_app_list.html:28 #: applications/templates/applications/user_database_app_list.html:20 #: applications/templates/applications/user_remote_app_list.html:19 -#: assets/models/asset.py:149 assets/models/asset.py:225 -#: assets/models/base.py:33 assets/models/cluster.py:29 +#: assets/models/asset.py:151 assets/models/asset.py:227 +#: assets/models/base.py:237 assets/models/cluster.py:29 #: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:56 #: assets/models/domain.py:21 assets/models/domain.py:53 #: assets/models/group.py:23 assets/models/label.py:23 @@ -239,33 +239,33 @@ msgstr "数据库" #: assets/templates/assets/domain_detail.html:71 #: assets/templates/assets/domain_gateway_list.html:67 #: assets/templates/assets/domain_list.html:24 -#: assets/templates/assets/platform_detail.html:59 +#: assets/templates/assets/platform_detail.html:64 #: assets/templates/assets/platform_list.html:18 -#: assets/templates/assets/system_user_detail.html:101 -#: assets/templates/assets/system_user_list.html:32 ops/models/adhoc.py:46 +#: assets/templates/assets/system_user_detail.html:112 +#: assets/templates/assets/system_user_list.html:29 ops/models/adhoc.py:37 #: orgs/models.py:18 perms/models/base.py:56 #: perms/templates/perms/asset_permission_detail.html:97 #: perms/templates/perms/database_app_permission_detail.html:93 #: perms/templates/perms/remote_app_permission_detail.html:89 -#: settings/models.py:31 terminal/models.py:36 terminal/models.py:321 -#: terminal/models.py:358 terminal/templates/terminal/base_storage_list.html:33 +#: settings/models.py:31 terminal/models.py:36 terminal/models.py:373 +#: terminal/models.py:410 terminal/templates/terminal/base_storage_list.html:33 #: terminal/templates/terminal/terminal_detail.html:63 #: tickets/templates/tickets/ticket_detail.html:104 users/models/group.py:16 -#: users/models/user.py:471 users/templates/users/user_detail.html:115 +#: users/models/user.py:473 users/templates/users/user_detail.html:115 #: users/templates/users/user_granted_database_app.html:38 #: users/templates/users/user_granted_remote_app.html:37 #: users/templates/users/user_group_detail.html:62 #: users/templates/users/user_group_list.html:16 #: users/templates/users/user_profile.html:138 -#: xpack/plugins/change_auth_plan/models.py:104 +#: xpack/plugins/change_auth_plan/models.py:93 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:115 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:19 -#: xpack/plugins/cloud/models.py:76 xpack/plugins/cloud/models.py:172 +#: xpack/plugins/cloud/models.py:53 xpack/plugins/cloud/models.py:136 #: xpack/plugins/cloud/templates/cloud/account_detail.html:67 #: xpack/plugins/cloud/templates/cloud/account_list.html:15 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:102 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:18 -#: xpack/plugins/gathered_user/models.py:42 +#: xpack/plugins/gathered_user/models.py:26 #: xpack/plugins/orgs/templates/orgs/org_detail.html:59 #: xpack/plugins/orgs/templates/orgs/org_list.html:23 msgid "Comment" @@ -280,7 +280,8 @@ msgstr "备注" #: perms/templates/perms/database_app_permission_detail.html:22 #: perms/templates/perms/database_app_permission_list.html:17 #: perms/templates/perms/database_app_permission_user.html:23 -#: templates/_nav.html:66 templates/_nav.html:86 templates/_nav_user.html:22 +#: perms/utils/database_app_permission.py:77 templates/_nav.html:66 +#: templates/_nav.html:86 templates/_nav_user.html:22 #: users/templates/users/user_database_app_permission.html:39 #: users/templates/users/user_database_app_permission.html:64 msgid "DatabaseApp" @@ -306,25 +307,25 @@ msgstr "参数" #: applications/models/remote_app.py:39 #: applications/templates/applications/database_app_detail.html:72 #: applications/templates/applications/remote_app_detail.html:68 -#: assets/models/asset.py:223 assets/models/base.py:36 +#: assets/models/asset.py:225 assets/models/base.py:240 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:26 #: assets/models/cmd_filter.py:59 assets/models/group.py:21 #: assets/templates/assets/admin_user_detail.html:63 #: assets/templates/assets/asset_detail.html:120 #: assets/templates/assets/cmd_filter_detail.html:72 #: assets/templates/assets/domain_detail.html:67 -#: assets/templates/assets/system_user_detail.html:97 -#: common/mixins/models.py:50 ops/templates/ops/adhoc_detail.html:84 +#: assets/templates/assets/system_user_detail.html:108 +#: common/mixins/models.py:49 ops/templates/ops/adhoc_detail.html:84 #: orgs/models.py:16 perms/models/base.py:54 #: perms/templates/perms/asset_permission_detail.html:93 #: perms/templates/perms/database_app_permission_detail.html:89 #: perms/templates/perms/remote_app_permission_detail.html:85 -#: users/models/user.py:479 users/serializers/group.py:32 +#: users/models/user.py:481 users/serializers/group.py:32 #: users/templates/users/user_detail.html:97 -#: xpack/plugins/change_auth_plan/models.py:108 +#: xpack/plugins/change_auth_plan/models.py:97 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:111 -#: xpack/plugins/cloud/models.py:79 xpack/plugins/cloud/models.py:178 -#: xpack/plugins/gathered_user/models.py:46 +#: xpack/plugins/cloud/models.py:56 xpack/plugins/cloud/models.py:142 +#: xpack/plugins/gathered_user/models.py:30 msgid "Created by" msgstr "创建者" @@ -333,14 +334,14 @@ msgstr "创建者" #: applications/models/remote_app.py:42 #: applications/templates/applications/database_app_detail.html:68 #: applications/templates/applications/remote_app_detail.html:64 -#: assets/models/asset.py:224 assets/models/base.py:34 +#: assets/models/asset.py:226 assets/models/base.py:238 #: assets/models/cluster.py:26 assets/models/domain.py:23 #: assets/models/gathered_user.py:19 assets/models/group.py:22 #: assets/models/label.py:25 assets/templates/assets/admin_user_detail.html:59 #: assets/templates/assets/cmd_filter_detail.html:64 #: assets/templates/assets/domain_detail.html:63 -#: assets/templates/assets/system_user_detail.html:93 -#: common/mixins/models.py:51 ops/models/adhoc.py:48 +#: assets/templates/assets/system_user_detail.html:104 +#: common/mixins/models.py:50 ops/models/adhoc.py:38 #: ops/templates/ops/adhoc_detail.html:88 ops/templates/ops/task_detail.html:62 #: orgs/models.py:17 perms/models/base.py:55 #: perms/templates/perms/asset_permission_detail.html:89 @@ -350,7 +351,7 @@ msgstr "创建者" #: tickets/templates/tickets/ticket_detail.html:52 users/models/group.py:18 #: users/templates/users/user_group_detail.html:58 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:103 -#: xpack/plugins/cloud/models.py:82 xpack/plugins/cloud/models.py:181 +#: xpack/plugins/cloud/models.py:59 xpack/plugins/cloud/models.py:145 #: xpack/plugins/cloud/templates/cloud/account_detail.html:63 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:98 #: xpack/plugins/orgs/templates/orgs/org_detail.html:55 @@ -364,7 +365,8 @@ msgstr "创建日期" #: perms/templates/perms/remote_app_permission_list.html:17 #: perms/templates/perms/remote_app_permission_remote_app.html:22 #: perms/templates/perms/remote_app_permission_user.html:22 -#: templates/_nav.html:64 templates/_nav.html:82 templates/_nav_user.html:16 +#: perms/utils/remote_app_permission.py:76 templates/_nav.html:64 +#: templates/_nav.html:82 templates/_nav_user.html:16 #: users/templates/users/user_remote_app_permission.html:39 #: users/templates/users/user_remote_app_permission.html:64 msgid "RemoteApp" @@ -372,7 +374,7 @@ msgstr "远程应用" #: applications/templates/applications/database_app_create_update.html:12 #: applications/templates/applications/remote_app_create_update.html:12 -#: assets/templates/assets/_system_user.html:71 +#: assets/templates/assets/_system_user.html:73 #: assets/templates/assets/admin_user_create_update.html:41 #: assets/templates/assets/asset_bulk_update.html:23 #: assets/templates/assets/asset_create.html:81 @@ -414,7 +416,7 @@ msgstr "重置" #: applications/templates/applications/database_app_create_update.html:13 #: applications/templates/applications/remote_app_create_update.html:14 -#: assets/templates/assets/_system_user.html:72 +#: assets/templates/assets/_system_user.html:74 #: assets/templates/assets/admin_user_create_update.html:42 #: assets/templates/assets/asset_bulk_update.html:24 #: assets/templates/assets/asset_create.html:82 @@ -432,15 +434,15 @@ msgstr "重置" #: settings/templates/settings/basic_setting.html:46 #: settings/templates/settings/email_content_setting.html:36 #: settings/templates/settings/email_setting.html:47 -#: settings/templates/settings/ldap_setting.html:48 +#: settings/templates/settings/ldap_setting.html:49 #: settings/templates/settings/security_setting.html:55 #: settings/templates/settings/terminal_setting.html:55 #: terminal/templates/terminal/base_storage_create_update.html:13 -#: terminal/templates/terminal/command_list.html:47 +#: terminal/templates/terminal/command_list.html:48 #: terminal/templates/terminal/session_list.html:50 #: terminal/templates/terminal/terminal_update.html:44 #: users/templates/users/_user.html:52 -#: users/templates/users/forgot_password.html:29 +#: users/templates/users/forgot_password.html:24 #: users/templates/users/user_bulk_update.html:24 #: users/templates/users/user_list.html:40 #: users/templates/users/user_password_update.html:76 @@ -463,6 +465,7 @@ msgstr "提交" #: assets/templates/assets/platform_detail.html:13 #: assets/templates/assets/system_user_assets.html:22 #: assets/templates/assets/system_user_detail.html:13 +#: assets/templates/assets/system_user_users.html:21 #: ops/templates/ops/adhoc_history.html:128 #: ops/templates/ops/task_adhoc.html:114 #: ops/templates/ops/task_history.html:135 @@ -487,7 +490,7 @@ msgstr "详情" #: applications/templates/applications/database_app_list.html:53 #: applications/templates/applications/remote_app_detail.html:16 #: applications/templates/applications/remote_app_list.html:59 -#: assets/templates/assets/_asset_user_list.html:75 +#: assets/templates/assets/_asset_user_list.html:67 #: assets/templates/assets/admin_user_detail.html:19 #: assets/templates/assets/admin_user_list.html:46 #: assets/templates/assets/asset_detail.html:24 @@ -502,8 +505,8 @@ msgstr "详情" #: assets/templates/assets/label_list.html:39 #: assets/templates/assets/platform_detail.html:16 #: assets/templates/assets/platform_list.html:40 -#: assets/templates/assets/system_user_detail.html:23 -#: assets/templates/assets/system_user_list.html:56 audits/models.py:34 +#: assets/templates/assets/system_user_detail.html:30 +#: assets/templates/assets/system_user_list.html:55 audits/models.py:34 #: perms/templates/perms/asset_permission_detail.html:25 #: perms/templates/perms/asset_permission_list.html:144 #: perms/templates/perms/database_app_permission_detail.html:25 @@ -542,6 +545,7 @@ msgstr "更新" #: applications/templates/applications/database_app_list.html:54 #: applications/templates/applications/remote_app_detail.html:20 #: applications/templates/applications/remote_app_list.html:60 +#: assets/templates/assets/_asset_user_list.html:70 #: assets/templates/assets/admin_user_detail.html:23 #: assets/templates/assets/admin_user_list.html:47 #: assets/templates/assets/asset_detail.html:28 @@ -554,9 +558,10 @@ msgstr "更新" #: assets/templates/assets/domain_gateway_list.html:93 #: assets/templates/assets/domain_list.html:51 #: assets/templates/assets/label_list.html:40 +#: assets/templates/assets/platform_detail.html:20 #: assets/templates/assets/platform_list.html:41 -#: assets/templates/assets/system_user_detail.html:27 -#: assets/templates/assets/system_user_list.html:57 audits/models.py:35 +#: assets/templates/assets/system_user_detail.html:34 +#: assets/templates/assets/system_user_list.html:56 audits/models.py:35 #: authentication/templates/authentication/_access_key_modal.html:65 #: ops/templates/ops/task_list.html:74 #: perms/templates/perms/asset_permission_detail.html:29 @@ -607,7 +612,8 @@ msgstr "创建数据库应用" #: assets/templates/assets/domain_list.html:25 #: assets/templates/assets/label_list.html:17 #: assets/templates/assets/platform_list.html:19 -#: assets/templates/assets/system_user_list.html:33 audits/models.py:39 +#: assets/templates/assets/system_user_list.html:30 +#: assets/templates/assets/system_user_users.html:64 audits/models.py:39 #: audits/templates/audits/operate_log_list.html:45 #: audits/templates/audits/operate_log_list.html:71 #: authentication/templates/authentication/_access_key_modal.html:34 @@ -627,7 +633,7 @@ msgstr "创建数据库应用" #: terminal/templates/terminal/session_list.html:34 #: terminal/templates/terminal/terminal_list.html:37 #: tickets/templates/tickets/ticket_list.html:108 -#: users/templates/users/_granted_assets.html:34 +#: users/templates/users/_granted_assets.html:29 #: users/templates/users/user_asset_permission.html:44 #: users/templates/users/user_asset_permission.html:79 #: users/templates/users/user_database_app_permission.html:42 @@ -638,7 +644,7 @@ msgstr "创建数据库应用" #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:18 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:20 #: xpack/plugins/cloud/templates/cloud/account_list.html:16 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:67 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:42 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:19 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:20 #: xpack/plugins/orgs/templates/orgs/org_list.html:24 @@ -712,44 +718,36 @@ msgstr "我的远程应用" msgid "Deleted failed, There are related assets" msgstr "删除失败,存在关联资产" -#: assets/api/node.py:61 +#: assets/api/node.py:49 msgid "You can't update the root node name" msgstr "不能修改根节点名称" -#: assets/api/node.py:68 +#: assets/api/node.py:56 msgid "Deletion failed and the node contains children or assets" msgstr "删除失败,节点包含子节点或资产" -#: assets/api/node.py:273 -msgid "Update node asset hardware information: {}" -msgstr "更新节点资产硬件信息: {}" - -#: assets/api/node.py:287 -msgid "Test if the assets under the node are connectable: {}" -msgstr "测试节点下资产是否可连接: {}" - -#: assets/forms/asset.py:65 assets/models/asset.py:194 -#: assets/models/user.py:111 assets/templates/assets/asset_detail.html:186 +#: assets/forms/asset.py:83 assets/models/asset.py:196 +#: assets/models/user.py:109 assets/templates/assets/asset_detail.html:186 #: assets/templates/assets/asset_detail.html:194 -#: assets/templates/assets/system_user_assets.html:87 +#: assets/templates/assets/system_user_assets.html:118 #: perms/models/asset_permission.py:81 -#: xpack/plugins/change_auth_plan/models.py:74 -#: xpack/plugins/gathered_user/models.py:31 +#: xpack/plugins/change_auth_plan/models.py:72 +#: xpack/plugins/gathered_user/models.py:24 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:17 msgid "Nodes" msgstr "节点" -#: assets/forms/asset.py:68 assets/models/asset.py:198 -#: assets/models/cluster.py:19 assets/models/user.py:67 +#: assets/forms/asset.py:86 assets/models/asset.py:200 +#: assets/models/cluster.py:19 assets/models/user.py:65 #: assets/templates/assets/admin_user_list.html:62 #: assets/templates/assets/asset_detail.html:72 templates/_nav.html:44 -#: xpack/plugins/cloud/models.py:160 +#: xpack/plugins/cloud/models.py:133 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:65 #: xpack/plugins/orgs/templates/orgs/org_list.html:19 msgid "Admin user" msgstr "管理用户" -#: assets/forms/asset.py:71 assets/forms/asset.py:113 +#: assets/forms/asset.py:89 assets/forms/asset.py:131 #: assets/templates/assets/asset_create.html:48 #: assets/templates/assets/asset_create.html:50 #: assets/templates/assets/asset_list.html:13 @@ -757,7 +755,7 @@ msgstr "管理用户" msgid "Label" msgstr "标签" -#: assets/forms/asset.py:74 assets/models/asset.py:193 +#: assets/forms/asset.py:92 assets/models/asset.py:195 #: assets/models/domain.py:26 assets/models/domain.py:52 #: assets/templates/assets/asset_detail.html:76 #: assets/templates/assets/user_asset_list.html:80 @@ -765,15 +763,15 @@ msgstr "标签" msgid "Domain" msgstr "网域" -#: assets/forms/asset.py:77 assets/models/asset.py:168 -#: assets/models/asset.py:192 assets/serializers/asset.py:66 +#: assets/forms/asset.py:95 assets/models/asset.py:170 +#: assets/models/asset.py:194 assets/serializers/asset.py:67 #: assets/templates/assets/asset_detail.html:100 #: assets/templates/assets/user_asset_list.html:78 msgid "Platform" msgstr "系统平台" -#: assets/forms/asset.py:81 assets/forms/asset.py:116 assets/models/node.py:462 -#: assets/serializers/system_user.py:40 +#: assets/forms/asset.py:99 assets/forms/asset.py:134 assets/models/node.py:497 +#: assets/serializers/system_user.py:44 assets/serializers/system_user.py:168 #: assets/templates/assets/asset_create.html:42 #: perms/forms/asset_permission.py:92 perms/forms/asset_permission.py:99 #: perms/templates/perms/asset_permission_list.html:36 @@ -782,16 +780,15 @@ msgstr "系统平台" #: users/templates/users/user_asset_permission.html:41 #: users/templates/users/user_asset_permission.html:73 #: users/templates/users/user_asset_permission.html:158 -#: xpack/plugins/change_auth_plan/forms.py:74 +#: xpack/plugins/change_auth_plan/forms.py:75 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:55 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:15 -#: xpack/plugins/cloud/models.py:156 +#: xpack/plugins/cloud/models.py:129 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:61 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:61 msgid "Node" msgstr "节点" -#: assets/forms/asset.py:85 +#: assets/forms/asset.py:103 msgid "" "root or other NOPASSWD sudo privilege user existed in asset,If asset is " "windows or other set any one, more see admin user left menu" @@ -799,19 +796,20 @@ msgstr "" "root或其他拥有NOPASSWD: ALL权限的用户, 如果是windows或其它硬件可以随意设置一" "个, 更多信息查看左侧 `管理用户` 菜单" -#: assets/forms/asset.py:88 +#: assets/forms/asset.py:106 msgid "Windows 2016 RDP protocol is different, If is window 2016, set it" msgstr "Windows 2016的RDP协议与之前不同,如果是请设置" -#: assets/forms/asset.py:89 +#: assets/forms/asset.py:107 msgid "" "If your have some network not connect with each other, you can set domain" msgstr "如果有多个的互相隔离的网络,设置资产属于的网域,使用网域网关跳转登录" -#: assets/forms/asset.py:96 assets/forms/asset.py:100 assets/forms/domain.py:17 -#: assets/forms/label.py:15 +#: assets/forms/asset.py:114 assets/forms/asset.py:118 +#: assets/forms/domain.py:17 assets/forms/label.py:15 +#: assets/templates/assets/system_user_assets.html:102 #: perms/templates/perms/asset_permission_asset.html:74 -#: xpack/plugins/change_auth_plan/forms.py:64 +#: xpack/plugins/change_auth_plan/forms.py:65 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:70 msgid "Select assets" msgstr "选择资产" @@ -828,30 +826,31 @@ msgstr "不能包含特殊字符" msgid "SSH gateway support proxy SSH,RDP,VNC" msgstr "SSH网关,支持代理SSH,RDP和VNC" -#: assets/forms/domain.py:78 assets/forms/user.py:75 assets/forms/user.py:95 -#: assets/models/base.py:29 assets/models/gathered_user.py:15 +#: assets/forms/domain.py:78 assets/forms/user.py:75 assets/forms/user.py:97 +#: assets/models/base.py:233 assets/models/gathered_user.py:15 #: assets/templates/assets/_asset_user_auth_update_modal.html:15 #: assets/templates/assets/_asset_user_auth_view_modal.html:21 #: assets/templates/assets/_asset_user_list.html:21 #: assets/templates/assets/admin_user_detail.html:55 #: assets/templates/assets/admin_user_list.html:22 #: assets/templates/assets/domain_gateway_list.html:66 -#: assets/templates/assets/system_user_detail.html:59 +#: assets/templates/assets/system_user_detail.html:66 #: assets/templates/assets/system_user_list.html:25 audits/models.py:81 #: audits/templates/audits/login_log_list.html:57 authentication/forms.py:10 -#: authentication/templates/authentication/login.html:58 +#: authentication/templates/authentication/login.html:21 #: authentication/templates/authentication/xpack_login.html:93 -#: ops/models/adhoc.py:187 perms/templates/perms/asset_permission_list.html:185 +#: ops/models/adhoc.py:148 perms/templates/perms/asset_permission_list.html:185 #: perms/templates/perms/remote_app_permission_user.html:50 #: settings/templates/settings/_ldap_list_users_modal.html:31 -#: users/forms/profile.py:19 users/models/user.py:436 +#: settings/templates/settings/_ldap_test_user_login_modal.html:10 +#: users/forms/profile.py:19 users/models/user.py:438 #: users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:53 #: users/templates/users/user_list.html:15 #: users/templates/users/user_profile.html:47 -#: xpack/plugins/change_auth_plan/forms.py:58 -#: xpack/plugins/change_auth_plan/models.py:65 -#: xpack/plugins/change_auth_plan/models.py:414 +#: xpack/plugins/change_auth_plan/forms.py:59 +#: xpack/plugins/change_auth_plan/models.py:63 +#: xpack/plugins/change_auth_plan/models.py:360 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:63 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:53 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:12 @@ -882,7 +881,7 @@ msgstr "" msgid "RDP console" msgstr "" -#: assets/forms/platform.py:40 assets/templates/assets/platform_detail.html:47 +#: assets/forms/platform.py:40 assets/templates/assets/platform_detail.html:52 #: assets/templates/assets/platform_list.html:17 msgid "Base platform" msgstr "基础平台" @@ -891,27 +890,30 @@ msgstr "基础平台" msgid "Password or private key passphrase" msgstr "密码或密钥密码" -#: assets/forms/user.py:26 assets/models/base.py:30 -#: assets/serializers/asset_user.py:63 +#: assets/forms/user.py:26 assets/models/base.py:234 +#: assets/serializers/asset_user.py:72 #: assets/templates/assets/_asset_user_auth_update_modal.html:21 #: assets/templates/assets/_asset_user_auth_view_modal.html:27 #: authentication/forms.py:12 -#: authentication/templates/authentication/login.html:66 +#: authentication/templates/authentication/login.html:29 #: authentication/templates/authentication/xpack_login.html:101 -#: settings/forms/ldap.py:22 users/forms/user.py:22 users/forms/user.py:193 -#: users/templates/users/user_password_check.html:13 +#: settings/forms/ldap.py:22 +#: settings/templates/settings/_ldap_test_user_login_modal.html:16 +#: users/forms/user.py:22 users/forms/user.py:193 +#: users/templates/users/user_otp_check_password.html:13 #: users/templates/users/user_password_update.html:44 +#: users/templates/users/user_password_verify.html:18 #: users/templates/users/user_profile_update.html:41 #: users/templates/users/user_pubkey_update.html:41 #: users/templates/users/user_update.html:20 -#: xpack/plugins/change_auth_plan/models.py:95 -#: xpack/plugins/change_auth_plan/models.py:263 +#: xpack/plugins/change_auth_plan/models.py:84 +#: xpack/plugins/change_auth_plan/models.py:209 msgid "Password" msgstr "密码" -#: assets/forms/user.py:29 assets/serializers/asset_user.py:71 +#: assets/forms/user.py:29 assets/serializers/asset_user.py:80 #: assets/templates/assets/_asset_user_auth_update_modal.html:27 -#: users/models/user.py:465 +#: users/models/user.py:467 msgid "Private key" msgstr "ssh私钥" @@ -923,17 +925,22 @@ msgstr "不合法的密钥,仅支持RSA/DSA格式的密钥" msgid "Password and private key file must be input one" msgstr "密码和私钥, 必须输入一个" -#: assets/forms/user.py:97 assets/models/cmd_filter.py:32 -#: assets/models/user.py:119 assets/templates/assets/_system_user.html:62 -#: assets/templates/assets/system_user_detail.html:164 +#: assets/forms/user.py:99 assets/models/cmd_filter.py:32 +#: assets/models/user.py:119 assets/templates/assets/_system_user.html:63 +#: assets/templates/assets/system_user_detail.html:155 msgid "Command filter" msgstr "命令过滤器" -#: assets/forms/user.py:101 +#: assets/forms/user.py:103 assets/models/user.py:108 +#: assets/templates/assets/system_user_detail.html:68 +msgid "Username same with user" +msgstr "用户名与用户相同" + +#: assets/forms/user.py:106 msgid "Auto push system user to asset" msgstr "自动推送系统用户到资产" -#: assets/forms/user.py:102 +#: assets/forms/user.py:107 msgid "" "1-100, High level will be using login asset as default, if user was granted " "more than 2 system user" @@ -941,35 +948,43 @@ msgstr "" "1-100, 1最低优先级,100最高优先级。授权多个用户时,高优先级的系统用户将会作为" "默认登录用户" -#: assets/forms/user.py:104 +#: assets/forms/user.py:109 msgid "" "If you choose manual login mode, you do not need to fill in the username and " "password." msgstr "如果选择手动登录模式,用户名和密码可以不填写" -#: assets/forms/user.py:106 +#: assets/forms/user.py:111 msgid "Use comma split multi command, ex: /bin/whoami,/bin/ifconfig" msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig" -#: assets/models/asset.py:145 +#: assets/forms/user.py:112 +msgid "SFTP root dir, tmp, home or custom" +msgstr "SFTP的起始路径,tmp目录, 用户home目录或者自定义" + +#: assets/forms/user.py:113 +msgid "Username is dynamic, When connect asset, using current user's username" +msgstr "用户名是动态的,登录资产时使用当前用户的用户名登录" + +#: assets/models/asset.py:147 xpack/plugins/cloud/providers/base.py:16 msgid "Base" msgstr "基础" -#: assets/models/asset.py:146 assets/templates/assets/platform_detail.html:51 +#: assets/models/asset.py:148 assets/templates/assets/platform_detail.html:56 msgid "Charset" msgstr "编码" -#: assets/models/asset.py:147 assets/templates/assets/platform_detail.html:55 +#: assets/models/asset.py:149 assets/templates/assets/platform_detail.html:60 #: tickets/models/ticket.py:38 msgid "Meta" msgstr "元数据" -#: assets/models/asset.py:148 +#: assets/models/asset.py:150 msgid "Internal" msgstr "内部的" -#: assets/models/asset.py:185 assets/models/domain.py:49 -#: assets/serializers/asset_user.py:28 +#: assets/models/asset.py:187 assets/models/domain.py:49 +#: assets/serializers/asset_user.py:47 #: assets/templates/assets/_asset_list_modal.html:47 #: assets/templates/assets/_asset_user_list.html:20 #: assets/templates/assets/asset_detail.html:60 @@ -978,14 +993,14 @@ msgstr "内部的" #: assets/templates/assets/user_asset_list.html:76 #: audits/templates/audits/login_log_list.html:60 #: perms/templates/perms/asset_permission_list.html:187 -#: settings/forms/terminal.py:16 users/templates/users/_granted_assets.html:31 +#: settings/forms/terminal.py:16 users/templates/users/_granted_assets.html:26 #: users/templates/users/user_asset_permission.html:156 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:50 #: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:24 msgid "IP" msgstr "IP" -#: assets/models/asset.py:186 assets/serializers/asset_user.py:27 +#: assets/models/asset.py:188 assets/serializers/asset_user.py:46 #: assets/serializers/gathered_user.py:20 #: assets/templates/assets/_asset_list_modal.html:46 #: assets/templates/assets/_asset_user_auth_update_modal.html:9 @@ -995,112 +1010,112 @@ msgstr "IP" #: assets/templates/assets/asset_list.html:24 #: assets/templates/assets/user_asset_list.html:75 #: perms/templates/perms/asset_permission_list.html:188 -#: settings/forms/terminal.py:15 users/templates/users/_granted_assets.html:30 +#: settings/forms/terminal.py:15 users/templates/users/_granted_assets.html:25 #: users/templates/users/user_asset_permission.html:157 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:49 #: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:23 msgid "Hostname" msgstr "主机名" -#: assets/models/asset.py:189 assets/models/domain.py:51 +#: assets/models/asset.py:191 assets/models/domain.py:51 #: assets/models/user.py:114 assets/templates/assets/asset_detail.html:68 #: assets/templates/assets/domain_gateway_list.html:65 -#: assets/templates/assets/system_user_detail.html:67 +#: assets/templates/assets/system_user_detail.html:78 #: assets/templates/assets/system_user_list.html:26 #: terminal/forms/storage.py:152 +#: terminal/templates/terminal/session_detail.html:60 #: terminal/templates/terminal/session_list.html:29 #: terminal/templates/terminal/session_list.html:73 msgid "Protocol" msgstr "协议" -#: assets/models/asset.py:191 assets/serializers/asset.py:68 +#: assets/models/asset.py:193 assets/serializers/asset.py:69 #: assets/templates/assets/asset_create.html:24 #: assets/templates/assets/user_asset_list.html:77 -#: perms/serializers/user_permission.py:59 +#: perms/serializers/user_permission.py:60 msgid "Protocols" msgstr "协议组" -#: assets/models/asset.py:195 assets/models/authbook.py:27 -#: assets/models/cmd_filter.py:22 assets/models/domain.py:54 -#: assets/models/label.py:22 assets/templates/assets/asset_detail.html:108 -#: authentication/models.py:45 +#: assets/models/asset.py:197 assets/models/cmd_filter.py:22 +#: assets/models/domain.py:54 assets/models/label.py:22 +#: assets/templates/assets/asset_detail.html:108 authentication/models.py:45 msgid "Is active" msgstr "激活" -#: assets/models/asset.py:201 assets/templates/assets/asset_detail.html:64 +#: assets/models/asset.py:203 assets/templates/assets/asset_detail.html:64 msgid "Public IP" msgstr "公网IP" -#: assets/models/asset.py:202 assets/templates/assets/asset_detail.html:116 +#: assets/models/asset.py:204 assets/templates/assets/asset_detail.html:116 msgid "Asset number" msgstr "资产编号" -#: assets/models/asset.py:205 assets/templates/assets/asset_detail.html:80 +#: assets/models/asset.py:207 assets/templates/assets/asset_detail.html:80 msgid "Vendor" msgstr "制造商" -#: assets/models/asset.py:206 assets/templates/assets/asset_detail.html:84 +#: assets/models/asset.py:208 assets/templates/assets/asset_detail.html:84 msgid "Model" msgstr "型号" -#: assets/models/asset.py:207 assets/templates/assets/asset_detail.html:112 +#: assets/models/asset.py:209 assets/templates/assets/asset_detail.html:112 msgid "Serial number" msgstr "序列号" -#: assets/models/asset.py:209 +#: assets/models/asset.py:211 msgid "CPU model" msgstr "CPU型号" -#: assets/models/asset.py:210 +#: assets/models/asset.py:212 msgid "CPU count" msgstr "CPU数量" -#: assets/models/asset.py:211 +#: assets/models/asset.py:213 msgid "CPU cores" msgstr "CPU核数" -#: assets/models/asset.py:212 +#: assets/models/asset.py:214 msgid "CPU vcpus" msgstr "CPU总数" -#: assets/models/asset.py:213 assets/templates/assets/asset_detail.html:92 +#: assets/models/asset.py:215 assets/templates/assets/asset_detail.html:92 msgid "Memory" msgstr "内存" -#: assets/models/asset.py:214 +#: assets/models/asset.py:216 msgid "Disk total" msgstr "硬盘大小" -#: assets/models/asset.py:215 +#: assets/models/asset.py:217 msgid "Disk info" msgstr "硬盘信息" -#: assets/models/asset.py:217 assets/templates/assets/asset_detail.html:104 +#: assets/models/asset.py:219 assets/templates/assets/asset_detail.html:104 msgid "OS" msgstr "操作系统" -#: assets/models/asset.py:218 +#: assets/models/asset.py:220 msgid "OS version" msgstr "系统版本" -#: assets/models/asset.py:219 +#: assets/models/asset.py:221 msgid "OS arch" msgstr "系统架构" -#: assets/models/asset.py:220 +#: assets/models/asset.py:222 msgid "Hostname raw" msgstr "主机名原始" -#: assets/models/asset.py:222 assets/templates/assets/asset_create.html:46 +#: assets/models/asset.py:224 assets/templates/assets/asset_create.html:46 #: assets/templates/assets/asset_detail.html:220 templates/_nav.html:46 msgid "Labels" msgstr "标签管理" -#: assets/models/authbook.py:25 ops/templates/ops/task_detail.html:70 +#: assets/models/authbook.py:24 ops/templates/ops/task_detail.html:70 msgid "Latest version" msgstr "最新版本" -#: assets/models/authbook.py:26 +#: assets/models/authbook.py:25 #: assets/templates/assets/_asset_user_list.html:22 #: ops/templates/ops/adhoc_history.html:56 #: ops/templates/ops/adhoc_history_detail.html:55 @@ -1108,23 +1123,23 @@ msgstr "最新版本" msgid "Version" msgstr "版本" -#: assets/models/authbook.py:36 +#: assets/models/authbook.py:34 msgid "AuthBook" msgstr "" -#: assets/models/base.py:31 xpack/plugins/change_auth_plan/models.py:99 -#: xpack/plugins/change_auth_plan/models.py:270 +#: assets/models/base.py:235 xpack/plugins/change_auth_plan/models.py:88 +#: xpack/plugins/change_auth_plan/models.py:216 msgid "SSH private key" msgstr "ssh密钥" -#: assets/models/base.py:32 xpack/plugins/change_auth_plan/models.py:102 -#: xpack/plugins/change_auth_plan/models.py:266 +#: assets/models/base.py:236 xpack/plugins/change_auth_plan/models.py:91 +#: xpack/plugins/change_auth_plan/models.py:212 msgid "SSH public key" msgstr "ssh公钥" -#: assets/models/base.py:35 assets/models/gathered_user.py:20 -#: assets/templates/assets/cmd_filter_detail.html:68 common/mixins/models.py:52 -#: ops/models/adhoc.py:49 +#: assets/models/base.py:239 assets/models/gathered_user.py:20 +#: assets/templates/assets/cmd_filter_detail.html:68 common/mixins/models.py:51 +#: ops/models/adhoc.py:39 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:107 #: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:29 msgid "Date updated" @@ -1138,7 +1153,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:457 +#: assets/models/cluster.py:22 users/models/user.py:459 #: users/templates/users/user_detail.html:62 msgid "Phone" msgstr "手机" @@ -1164,7 +1179,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:580 +#: users/models/user.py:595 msgid "System" msgstr "系统" @@ -1192,11 +1207,11 @@ msgstr "BGP全网通" msgid "Regex" msgstr "正则表达式" -#: assets/models/cmd_filter.py:40 ops/models/command.py:22 -#: ops/templates/ops/command_execution_list.html:67 terminal/models.py:186 +#: assets/models/cmd_filter.py:40 ops/models/command.py:23 +#: ops/templates/ops/command_execution_list.html:67 terminal/models.py:196 #: terminal/templates/terminal/command_list.html:28 -#: terminal/templates/terminal/command_list.html:68 -#: terminal/templates/terminal/session_detail.html:48 +#: terminal/templates/terminal/command_list.html:108 +#: terminal/templates/terminal/session_commands.html:49 #: terminal/templates/terminal/session_list.html:31 msgid "Command" msgstr "命令" @@ -1270,8 +1285,9 @@ msgstr "资产组" msgid "Default asset group" msgstr "默认资产组" -#: assets/models/label.py:15 audits/models.py:18 audits/models.py:38 -#: audits/models.py:51 audits/templates/audits/ftp_log_list.html:37 +#: assets/models/label.py:15 assets/templates/assets/system_user_users.html:63 +#: audits/models.py:18 audits/models.py:38 audits/models.py:51 +#: audits/templates/audits/ftp_log_list.html:37 #: audits/templates/audits/ftp_log_list.html:74 #: audits/templates/audits/operate_log_list.html:37 #: audits/templates/audits/password_change_log_list.html:37 @@ -1287,15 +1303,16 @@ msgstr "默认资产组" #: perms/templates/perms/database_app_permission_list.html:15 #: perms/templates/perms/remote_app_permission_create_update.html:41 #: perms/templates/perms/remote_app_permission_list.html:15 -#: templates/index.html:87 terminal/backends/command/models.py:12 -#: terminal/models.py:176 terminal/templates/terminal/command_list.html:29 -#: terminal/templates/terminal/command_list.html:65 +#: templates/index.html:86 terminal/backends/command/models.py:18 +#: terminal/models.py:185 terminal/templates/terminal/command_list.html:30 +#: terminal/templates/terminal/command_list.html:105 +#: terminal/templates/terminal/session_detail.html:48 #: terminal/templates/terminal/session_list.html:25 #: terminal/templates/terminal/session_list.html:69 tickets/models/ticket.py:33 #: tickets/models/ticket.py:128 tickets/templates/tickets/ticket_detail.html:32 #: tickets/templates/tickets/ticket_list.html:34 #: tickets/templates/tickets/ticket_list.html:103 users/forms/group.py:15 -#: users/models/user.py:143 users/models/user.py:159 users/models/user.py:568 +#: users/models/user.py:143 users/models/user.py:159 users/models/user.py:583 #: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -1305,13 +1322,13 @@ msgstr "默认资产组" #: users/templates/users/user_group_list.html:15 #: users/templates/users/user_remote_app_permission.html:37 #: users/templates/users/user_remote_app_permission.html:58 -#: users/views/profile.py:70 xpack/plugins/orgs/forms.py:27 +#: users/views/profile/base.py:46 xpack/plugins/orgs/forms.py:27 #: xpack/plugins/orgs/templates/orgs/org_detail.html:108 #: xpack/plugins/orgs/templates/orgs/org_list.html:15 msgid "User" msgstr "用户" -#: assets/models/label.py:19 assets/models/node.py:453 +#: assets/models/label.py:19 assets/models/node.py:488 #: assets/templates/assets/label_list.html:15 settings/models.py:27 msgid "Value" msgstr "值" @@ -1320,38 +1337,37 @@ msgstr "值" msgid "Category" msgstr "分类" -#: assets/models/node.py:164 +#: assets/models/node.py:209 msgid "New node" msgstr "新节点" -#: assets/models/node.py:325 +#: assets/models/node.py:370 msgid "ungrouped" msgstr "未分组" -#: assets/models/node.py:327 +#: assets/models/node.py:372 users/templates/users/_granted_assets.html:130 msgid "empty" msgstr "空" -#: assets/models/node.py:329 +#: assets/models/node.py:374 msgid "favorite" msgstr "收藏夹" -#: assets/models/node.py:452 assets/templates/assets/_node_detail_modal.html:39 +#: assets/models/node.py:487 assets/templates/assets/_node_detail_modal.html:39 msgid "Key" msgstr "键" -#: assets/models/user.py:107 +#: assets/models/user.py:105 msgid "Automatic login" msgstr "自动登录" -#: assets/models/user.py:108 +#: assets/models/user.py:106 msgid "Manually login" msgstr "手动登录" -#: assets/models/user.py:112 +#: assets/models/user.py:110 #: assets/templates/assets/_asset_group_bulk_update_modal.html:11 -#: assets/templates/assets/system_user_assets.html:26 -#: assets/templates/assets/system_user_detail.html:18 +#: assets/templates/assets/system_user_assets.html:94 #: assets/views/admin_user.py:30 assets/views/admin_user.py:49 #: assets/views/admin_user.py:67 assets/views/admin_user.py:84 #: assets/views/admin_user.py:108 assets/views/asset.py:37 @@ -1366,33 +1382,54 @@ msgstr "手动登录" #: assets/views/domain.py:136 assets/views/domain.py:157 #: assets/views/label.py:27 assets/views/label.py:45 assets/views/label.py:73 #: assets/views/platform.py:22 assets/views/platform.py:38 -#: assets/views/platform.py:55 assets/views/platform.py:71 -#: assets/views/system_user.py:29 assets/views/system_user.py:46 -#: assets/views/system_user.py:63 assets/views/system_user.py:79 -#: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:70 +#: assets/views/platform.py:58 assets/views/platform.py:74 +#: assets/views/system_user.py:30 assets/views/system_user.py:47 +#: assets/views/system_user.py:64 assets/views/system_user.py:80 +#: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:68 msgid "Assets" msgstr "资产管理" -#: assets/models/user.py:115 assets/templates/assets/_system_user.html:55 -#: assets/templates/assets/system_user_detail.html:119 +#: assets/models/user.py:111 assets/templates/assets/system_user_users.html:76 +#: templates/_nav.html:17 users/views/group.py:28 users/views/group.py:45 +#: users/views/group.py:63 users/views/group.py:82 users/views/group.py:99 +#: users/views/login.py:158 users/views/profile/password.py:40 +#: users/views/profile/pubkey.py:36 users/views/user.py:50 +#: users/views/user.py:67 users/views/user.py:111 users/views/user.py:178 +#: users/views/user.py:206 users/views/user.py:220 users/views/user.py:234 +#: users/views/user.py:248 users/views/user.py:262 users/views/user.py:276 +msgid "Users" +msgstr "用户管理" + +#: assets/models/user.py:112 users/templates/users/user_group_list.html:90 +#: users/templates/users/user_list.html:135 +#: users/templates/users/user_profile.html:124 +msgid "User groups" +msgstr "用户组" + +#: assets/models/user.py:115 assets/templates/assets/_system_user.html:56 +#: assets/templates/assets/system_user_detail.html:130 #: assets/templates/assets/system_user_update.html:10 msgid "Auto push" msgstr "自动推送" -#: assets/models/user.py:116 assets/templates/assets/system_user_detail.html:71 +#: assets/models/user.py:116 assets/templates/assets/system_user_detail.html:82 msgid "Sudo" msgstr "Sudo" -#: assets/models/user.py:117 assets/templates/assets/system_user_detail.html:76 +#: assets/models/user.py:117 assets/templates/assets/system_user_detail.html:87 msgid "Shell" msgstr "Shell" -#: assets/models/user.py:118 assets/templates/assets/system_user_detail.html:63 +#: assets/models/user.py:118 assets/templates/assets/system_user_detail.html:74 #: assets/templates/assets/system_user_list.html:27 msgid "Login mode" msgstr "登录模式" -#: assets/models/user.py:179 assets/templates/assets/system_user_list.html:74 +#: assets/models/user.py:120 +msgid "SFTP Root" +msgstr "SFTP根路径" + +#: assets/models/user.py:190 assets/templates/assets/system_user_list.html:73 #: assets/templates/assets/user_asset_list.html:79 audits/models.py:21 #: audits/templates/audits/ftp_log_list.html:53 #: audits/templates/audits/ftp_log_list.html:76 @@ -1408,12 +1445,13 @@ msgstr "登录模式" #: perms/templates/perms/database_app_permission_list.html:18 #: perms/templates/perms/remote_app_permission_detail.html:126 #: perms/templates/perms/remote_app_permission_list.html:18 -#: templates/_nav.html:45 terminal/backends/command/models.py:14 -#: terminal/models.py:180 terminal/templates/terminal/command_list.html:31 -#: terminal/templates/terminal/command_list.html:67 +#: templates/_nav.html:45 terminal/backends/command/models.py:20 +#: terminal/models.py:189 terminal/templates/terminal/command_list.html:32 +#: terminal/templates/terminal/command_list.html:107 +#: terminal/templates/terminal/session_detail.html:56 #: terminal/templates/terminal/session_list.html:27 #: terminal/templates/terminal/session_list.html:71 -#: users/templates/users/_granted_assets.html:32 +#: users/templates/users/_granted_assets.html:27 #: users/templates/users/user_asset_permission.html:42 #: users/templates/users/user_asset_permission.html:76 #: users/templates/users/user_asset_permission.html:159 @@ -1430,47 +1468,58 @@ msgstr "系统用户" msgid "%(value)s is not an even number" msgstr "%(value)s is not an even number" -#: assets/models/utils.py:43 assets/tasks/const.py:84 +#: assets/models/utils.py:43 assets/tasks/const.py:49 msgid "Unreachable" msgstr "不可达" -#: assets/models/utils.py:44 assets/tasks/const.py:85 +#: assets/models/utils.py:44 assets/tasks/const.py:50 #: assets/templates/assets/asset_list.html:27 msgid "Reachable" msgstr "可连接" -#: assets/models/utils.py:45 assets/tasks/const.py:86 audits/utils.py:30 +#: assets/models/utils.py:45 assets/tasks/const.py:51 audits/utils.py:30 msgid "Unknown" msgstr "未知" -#: assets/serializers/asset.py:23 +#: assets/serializers/asset.py:24 msgid "Protocol format should {}/{}" msgstr "协议格式 {}/{}" -#: assets/serializers/asset.py:40 +#: assets/serializers/asset.py:41 msgid "Protocol duplicate: {}" msgstr "协议重复: {}" -#: assets/serializers/asset.py:69 assets/serializers/asset.py:149 -#: assets/serializers/asset_user.py:29 -#: assets/templates/assets/_asset_user_list.html:23 -msgid "Connectivity" -msgstr "连接" - -#: assets/serializers/asset.py:95 +#: assets/serializers/asset.py:94 msgid "Hardware info" msgstr "硬件信息" -#: assets/serializers/asset.py:96 orgs/mixins/serializers.py:27 +#: assets/serializers/asset.py:95 orgs/mixins/serializers.py:27 msgid "Org name" msgstr "组织名称" -#: assets/serializers/asset_user.py:31 +#: assets/serializers/asset.py:134 assets/serializers/asset.py:171 +msgid "Connectivity" +msgstr "连接" + +#: assets/serializers/asset_user.py:45 +#: assets/templates/assets/_node_detail_modal.html:18 +#: audits/templates/audits/login_log_list.html:56 +#: authentication/templates/authentication/_access_key_modal.html:30 +#: ops/templates/ops/adhoc_detail.html:47 +#: ops/templates/ops/adhoc_history_detail.html:47 +#: ops/templates/ops/task_detail.html:54 +#: terminal/templates/terminal/session_list.html:24 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:35 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:33 +msgid "ID" +msgstr "ID" + +#: assets/serializers/asset_user.py:49 msgid "Backend" msgstr "后端" -#: assets/serializers/asset_user.py:67 users/forms/profile.py:148 -#: users/models/user.py:468 users/templates/users/first_login.html:42 +#: assets/serializers/asset_user.py:76 users/forms/profile.py:148 +#: users/models/user.py:470 users/templates/users/first_login.html:42 #: users/templates/users/user_password_update.html:49 #: users/templates/users/user_profile.html:69 #: users/templates/users/user_profile_update.html:46 @@ -1478,12 +1527,12 @@ msgstr "后端" msgid "Public key" msgstr "ssh公钥" -#: assets/serializers/base.py:44 +#: assets/serializers/base.py:45 msgid "" "Not support openssh format key, using ssh-keygen -t rsa -m pem to generate" msgstr "暂不支持OPENSSH格式的密钥,使用 ssh-keygen -t rsa -m pem生成" -#: assets/serializers/base.py:50 +#: assets/serializers/base.py:51 msgid "private key invalid" msgstr "密钥不合法" @@ -1495,106 +1544,135 @@ msgstr "值" msgid "The same level node name cannot be the same" msgstr "同级别节点名字不能重复" -#: assets/serializers/system_user.py:42 +#: assets/serializers/system_user.py:46 assets/serializers/system_user.py:170 msgid "Login mode display" msgstr "登录模式显示" -#: assets/serializers/system_user.py:77 +#: assets/serializers/system_user.py:86 +msgid "Username same with user with protocol {} only allow 1" +msgstr "用户名和用户相同的一种协议只允许存在一个" + +#: assets/serializers/system_user.py:99 msgid "* Automatic login mode must fill in the username." msgstr "自动登录模式,必须填写用户名" -#: assets/serializers/system_user.py:88 +#: assets/serializers/system_user.py:107 +msgid "Path should starts with /" +msgstr "路径应该以 / 开头" + +#: assets/serializers/system_user.py:118 msgid "Password or private key required" msgstr "密码或密钥密码需要一个" -#: assets/tasks/admin_user_connectivity.py:56 +#: assets/tasks/admin_user_connectivity.py:58 msgid "Test admin user connectivity period: {}" msgstr "定期测试管理账号可连接性: {}" -#: assets/tasks/admin_user_connectivity.py:63 +#: assets/tasks/admin_user_connectivity.py:67 msgid "Test admin user connectivity: {}" msgstr "测试管理行号可连接性: {}" -#: assets/tasks/asset_connectivity.py:21 +#: assets/tasks/asset_connectivity.py:27 msgid "Test assets connectivity" msgstr "测试资产可连接性" -#: assets/tasks/asset_connectivity.py:75 +#: assets/tasks/asset_connectivity.py:76 msgid "Test assets connectivity: {}" msgstr "测试资产可连接性: {}" -#: assets/tasks/asset_user_connectivity.py:27 -#: assets/tasks/push_system_user.py:130 -#: xpack/plugins/change_auth_plan/models.py:527 +#: assets/tasks/asset_connectivity.py:87 +msgid "Test if the assets under the node are connectable: {}" +msgstr "测试节点下资产是否可连接: {}" + +#: assets/tasks/asset_user_connectivity.py:29 +#: xpack/plugins/change_auth_plan/models.py:473 msgid "The asset {} system platform {} does not support run Ansible tasks" msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务" -#: assets/tasks/asset_user_connectivity.py:74 +#: assets/tasks/asset_user_connectivity.py:75 msgid "Test asset user connectivity: {}" msgstr "测试资产用户可连接性: {}" -#: assets/tasks/gather_asset_hardware_info.py:44 +#: assets/tasks/gather_asset_hardware_info.py:45 msgid "Get asset info failed: {}" msgstr "获取资产信息失败:{}" -#: assets/tasks/gather_asset_hardware_info.py:94 +#: assets/tasks/gather_asset_hardware_info.py:96 msgid "Update some assets hardware info" msgstr "更新资产硬件信息" -#: assets/tasks/gather_asset_hardware_info.py:111 +#: assets/tasks/gather_asset_hardware_info.py:113 msgid "Update asset hardware info: {}" msgstr "更新资产硬件信息: {}" -#: assets/tasks/gather_asset_users.py:107 +#: assets/tasks/gather_asset_hardware_info.py:130 +msgid "Update node asset hardware information: {}" +msgstr "更新节点资产硬件信息: {}" + +#: assets/tasks/gather_asset_users.py:108 msgid "Gather assets users" msgstr "收集资产上的用户" -#: assets/tasks/push_system_user.py:142 -msgid "" -"Push system user task skip, auto push not enable or protocol is not ssh or " -"rdp: {}" -msgstr "推送系统用户任务跳过,自动推送没有打开,或协议不是ssh或rdp: {}" - #: assets/tasks/push_system_user.py:149 -msgid "For security, do not push user {}" -msgstr "为了安全,禁止推送用户 {}" +#: assets/tasks/system_user_connectivity.py:86 +msgid "System user is dynamic: {}" +msgstr "系统用户是动态的: {}" + +#: assets/tasks/push_system_user.py:180 +msgid "Start push system user for platform: [{}]" +msgstr "推送系统用户到平台: [{}]" + +#: assets/tasks/push_system_user.py:181 +#: assets/tasks/system_user_connectivity.py:78 +msgid "Hosts count: {}" +msgstr "主机数量: {}" -#: assets/tasks/push_system_user.py:177 assets/tasks/push_system_user.py:191 +#: assets/tasks/push_system_user.py:198 assets/tasks/push_system_user.py:214 msgid "Push system users to assets: {}" msgstr "推送系统用户到入资产: {}" -#: assets/tasks/push_system_user.py:183 -msgid "Push system users to asset: {} => {}" -msgstr "推送系统用户到入资产: {} => {}" +#: assets/tasks/push_system_user.py:206 +msgid "Push system users to asset: {}({}) => {}" +msgstr "推送系统用户到入资产: {}({}) => {}" -#: assets/tasks/system_user_connectivity.py:79 +#: assets/tasks/system_user_connectivity.py:77 +msgid "Start test system user connectivity for platform: [{}]" +msgstr "开始测试系统用户在该系统平台的可连接性: [{}]" + +#: assets/tasks/system_user_connectivity.py:97 msgid "Test system user connectivity: {}" msgstr "测试系统用户可连接性: {}" -#: assets/tasks/system_user_connectivity.py:86 +#: assets/tasks/system_user_connectivity.py:105 msgid "Test system user connectivity: {} => {}" msgstr "测试系统用户可连接性: {} => {}" -#: assets/tasks/system_user_connectivity.py:99 +#: assets/tasks/system_user_connectivity.py:118 msgid "Test system user connectivity period: {}" msgstr "定期测试系统用户可连接性: {}" -#: assets/tasks/utils.py:16 +#: assets/tasks/utils.py:17 msgid "Asset has been disabled, skipped: {}" msgstr "资产或许不支持ansible, 跳过: {}" -#: assets/tasks/utils.py:20 +#: assets/tasks/utils.py:21 msgid "Asset may not be support ansible, skipped: {}" msgstr "资产或许不支持ansible, 跳过: {}" -#: assets/tasks/utils.py:33 +#: assets/tasks/utils.py:29 +msgid "" +"Push system user task skip, auto push not enable or protocol is not ssh or " +"rdp: {}" +msgstr "推送系统用户任务跳过,自动推送没有打开,或协议不是ssh或rdp: {}" + +#: assets/tasks/utils.py:36 +msgid "For security, do not push user {}" +msgstr "为了安全,禁止推送用户 {}" + +#: assets/tasks/utils.py:56 msgid "No assets matched, stop task" msgstr "没有匹配到资产,结束任务" -#: assets/tasks/utils.py:43 -msgid "No assets matched related system user protocol, stop task" -msgstr "没有匹配到与系统用户协议相关的资产,结束任务" - #: assets/templates/assets/_asset_group_bulk_update_modal.html:5 msgid "Update asset group" msgstr "更新用户组" @@ -1620,9 +1698,12 @@ msgstr "选择系统用户" #: assets/templates/assets/_asset_group_bulk_update_modal.html:34 msgid "Enable-MFA" -msgstr "启用MFA" +msgstr "启用多因子认证" -#: assets/templates/assets/_asset_list_modal.html:7 assets/views/asset.py:38 +#: assets/templates/assets/_asset_list_modal.html:7 +#: assets/templates/assets/system_user_assets.html:26 +#: assets/templates/assets/system_user_detail.html:18 +#: assets/templates/assets/system_user_users.html:25 assets/views/asset.py:38 #: templates/_nav.html:42 xpack/plugins/change_auth_plan/views.py:118 msgid "Asset list" msgstr "资产列表" @@ -1642,7 +1723,8 @@ msgid "Update asset user auth" msgstr "更新资产用户认证信息" #: assets/templates/assets/_asset_user_auth_update_modal.html:23 -#: xpack/plugins/change_auth_plan/forms.py:60 +#: settings/templates/settings/_ldap_test_user_login_modal.html:18 +#: xpack/plugins/change_auth_plan/forms.py:61 msgid "Please input password" msgstr "请输入密码" @@ -1664,11 +1746,11 @@ msgstr "资产用户信息" msgid "Copy success" msgstr "复制成功" -#: assets/templates/assets/_asset_user_auth_view_modal.html:70 +#: assets/templates/assets/_asset_user_auth_view_modal.html:68 msgid "Get auth info error" msgstr "获取认证信息错误" -#: assets/templates/assets/_asset_user_auth_view_modal.html:97 +#: assets/templates/assets/_asset_user_auth_view_modal.html:101 #: assets/templates/assets/_node_detail_modal.html:67 #: assets/templates/assets/_user_asset_detail_modal.html:23 #: authentication/templates/authentication/_access_key_modal.html:142 @@ -1683,8 +1765,8 @@ msgstr "关闭" #: audits/templates/audits/operate_log_list.html:75 #: audits/templates/audits/password_change_log_list.html:57 #: ops/templates/ops/task_adhoc.html:61 -#: terminal/templates/terminal/command_list.html:33 -#: terminal/templates/terminal/session_detail.html:50 +#: terminal/templates/terminal/command_list.html:34 +#: terminal/templates/terminal/session_commands.html:51 #: tickets/templates/tickets/ticket_list.html:37 msgid "Datetime" msgstr "日期" @@ -1694,26 +1776,32 @@ msgstr "日期" msgid "Test datetime: " msgstr "测试日期: " -#: assets/templates/assets/_asset_user_list.html:74 +#: assets/templates/assets/_asset_user_list.html:44 +msgid "Only latest version" +msgstr "仅最新版本" + +#: assets/templates/assets/_asset_user_list.html:66 msgid "View" msgstr "查看" -#: assets/templates/assets/_asset_user_list.html:76 +#: assets/templates/assets/_asset_user_list.html:68 #: assets/templates/assets/admin_user_assets.html:56 #: assets/templates/assets/asset_asset_user_list.html:57 #: assets/templates/assets/asset_detail.html:174 -#: assets/templates/assets/system_user_assets.html:67 -#: assets/templates/assets/system_user_detail.html:149 +#: assets/templates/assets/system_user_assets.html:74 #: terminal/templates/terminal/base_storage_list.html:72 msgid "Test" msgstr "测试" -#: assets/templates/assets/_asset_user_list.html:77 -#: assets/templates/assets/system_user_assets.html:76 -#: assets/templates/assets/system_user_detail.html:139 +#: assets/templates/assets/_asset_user_list.html:69 +#: assets/templates/assets/system_user_assets.html:83 msgid "Push" msgstr "推送" +#: assets/templates/assets/_asset_user_list.html:167 +msgid "Delete success" +msgstr "删除成功" + #: assets/templates/assets/_gateway_test_modal.html:4 msgid "Test gateway test connection" msgstr "测试连接网关" @@ -1731,18 +1819,6 @@ msgstr "如果使用了nat端口映射,请设置为ssh真实监听的端口" msgid "Node detail" msgstr "节点详情" -#: assets/templates/assets/_node_detail_modal.html:18 -#: audits/templates/audits/login_log_list.html:56 -#: authentication/templates/authentication/_access_key_modal.html:30 -#: ops/templates/ops/adhoc_detail.html:47 -#: ops/templates/ops/adhoc_history_detail.html:47 -#: ops/templates/ops/task_detail.html:54 -#: terminal/templates/terminal/session_list.html:24 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:59 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:57 -msgid "ID" -msgstr "ID" - #: assets/templates/assets/_node_detail_modal.html:33 msgid "Full name" msgstr "全称" @@ -1759,19 +1835,11 @@ msgstr "重命名节点" msgid "Delete node" msgstr "删除节点" -#: assets/templates/assets/_node_tree.html:165 +#: assets/templates/assets/_node_tree.html:166 msgid "Create node failed" msgstr "创建节点失败" -#: assets/templates/assets/_node_tree.html:177 -msgid "Have child node, cancel" -msgstr "存在子节点,不能删除" - -#: assets/templates/assets/_node_tree.html:179 -msgid "Have assets, cancel" -msgstr "存在资产,不能删除" - -#: assets/templates/assets/_node_tree.html:255 +#: assets/templates/assets/_node_tree.html:250 msgid "Rename success" msgstr "重命名成功" @@ -1787,18 +1855,18 @@ msgstr "重命名成功" msgid "Basic" msgstr "基本" -#: assets/templates/assets/_system_user.html:40 +#: assets/templates/assets/_system_user.html:41 #: assets/templates/assets/asset_create.html:38 #: assets/templates/assets/gateway_create_update.html:41 #: users/templates/users/_user.html:21 msgid "Auth" msgstr "认证" -#: assets/templates/assets/_system_user.html:44 +#: assets/templates/assets/_system_user.html:45 msgid "Auto generate key" msgstr "自动生成密钥" -#: assets/templates/assets/_system_user.html:65 +#: assets/templates/assets/_system_user.html:66 #: assets/templates/assets/asset_create.html:74 #: assets/templates/assets/gateway_create_update.html:49 #: perms/templates/perms/asset_permission_create_update.html:97 @@ -1829,8 +1897,8 @@ msgid "Asset list of " msgstr "资产列表" #: assets/templates/assets/admin_user_assets.html:47 -#: assets/templates/assets/system_user_assets.html:58 -#: assets/templates/assets/system_user_detail.html:113 +#: assets/templates/assets/system_user_assets.html:65 +#: assets/templates/assets/system_user_detail.html:124 #: perms/templates/perms/asset_permission_detail.html:109 #: perms/templates/perms/database_app_permission_detail.html:105 #: perms/templates/perms/remote_app_permission_detail.html:101 @@ -1848,10 +1916,11 @@ msgid "Replace node assets admin user with this" msgstr "替换资产的管理员" #: assets/templates/assets/admin_user_detail.html:86 +#: assets/templates/assets/system_user_assets.html:126 #: perms/templates/perms/asset_permission_asset.html:99 -#: xpack/plugins/change_auth_plan/forms.py:68 +#: xpack/plugins/change_auth_plan/forms.py:69 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:95 -#: xpack/plugins/gathered_user/forms.py:36 +#: xpack/plugins/gathered_user/forms.py:33 msgid "Select nodes" msgstr "选择节点" @@ -1859,10 +1928,12 @@ msgstr "选择节点" #: assets/templates/assets/asset_detail.html:200 #: assets/templates/assets/asset_list.html:258 #: assets/templates/assets/cmd_filter_detail.html:101 -#: assets/templates/assets/system_user_assets.html:101 -#: assets/templates/assets/system_user_detail.html:181 +#: assets/templates/assets/system_user_assets.html:108 +#: assets/templates/assets/system_user_assets.html:132 +#: assets/templates/assets/system_user_detail.html:172 +#: assets/templates/assets/system_user_users.html:90 #: authentication/templates/authentication/_mfa_confirm_modal.html:20 -#: templates/_modal.html:23 terminal/templates/terminal/session_detail.html:112 +#: templates/_modal.html:23 terminal/templates/terminal/session_detail.html:122 #: users/templates/users/user_detail.html:264 #: users/templates/users/user_detail.html:417 #: users/templates/users/user_detail.html:443 @@ -1870,6 +1941,7 @@ msgstr "选择节点" #: users/templates/users/user_detail.html:511 #: users/templates/users/user_group_create_update.html:28 #: users/templates/users/user_list.html:184 +#: users/templates/users/user_password_verify.html:20 #: xpack/plugins/cloud/templates/cloud/account_create_update.html:30 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:50 #: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:41 @@ -1887,9 +1959,9 @@ msgstr "" #: assets/templates/assets/admin_user_list.html:5 msgid "" -"Jumpserver users of the system using the user to `push system user`, `get " +"JumpServer users of the system using the user to `push system user`, `get " "assets hardware information`, etc. " -msgstr "Jumpserver 使用该用户来 `推送系统用户`、`获取资产硬件信息` 等。" +msgstr "JumpServer 使用该用户来 `推送系统用户`、`获取资产硬件信息` 等。" #: assets/templates/assets/admin_user_list.html:13 #: assets/views/admin_user.py:50 @@ -1907,7 +1979,7 @@ msgstr "资产用户" #: assets/templates/assets/asset_asset_user_list.html:47 #: assets/templates/assets/asset_detail.html:140 -#: terminal/templates/terminal/session_detail.html:85 +#: terminal/templates/terminal/session_detail.html:87 #: users/templates/users/user_detail.html:126 #: users/templates/users/user_profile.html:150 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:126 @@ -2178,69 +2250,87 @@ msgstr "创建标签" msgid "Create platform" msgstr "创建系统平台" -#: assets/templates/assets/system_user_assets.html:35 -msgid "Assets of " -msgstr "资产" +#: assets/templates/assets/system_user_assets.html:32 +#: assets/templates/assets/system_user_detail.html:25 +#: assets/templates/assets/system_user_users.html:31 templates/_nav.html:20 +#: users/views/user.py:51 +msgid "User list" +msgstr "用户列表" -#: assets/templates/assets/system_user_assets.html:64 -#: assets/templates/assets/system_user_detail.html:146 +#: assets/templates/assets/system_user_assets.html:71 msgid "Test assets connective" msgstr "测试资产可连接性" -#: assets/templates/assets/system_user_assets.html:73 -#: assets/templates/assets/system_user_detail.html:136 +#: assets/templates/assets/system_user_assets.html:80 msgid "Push system user now" msgstr "立刻推送系统" -#: assets/templates/assets/system_user_assets.html:95 -msgid "Add to node" -msgstr "添加到节点" +#: assets/templates/assets/system_user_assets.html:279 +#: assets/templates/assets/system_user_users.html:205 +msgid "Have existed: " +msgstr "已经存在: " -#: assets/templates/assets/system_user_detail.html:82 +#: assets/templates/assets/system_user_detail.html:93 msgid "Home" msgstr "家目录" -#: assets/templates/assets/system_user_detail.html:88 +#: assets/templates/assets/system_user_detail.html:99 msgid "Uid" msgstr "Uid" -#: assets/templates/assets/system_user_detail.html:172 +#: assets/templates/assets/system_user_detail.html:163 msgid "Binding command filters" msgstr "绑定命令过滤器" #: assets/templates/assets/system_user_list.html:5 msgid "" -"System user is Jumpserver jump login assets used by the users, can be " +"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 `); " msgstr "" -"系统用户是 Jumpserver 跳转登录资产时使用的用户,可以理解为登录资产用户,如 " +"系统用户是 JumpServer 跳转登录资产时使用的用户,可以理解为登录资产用户,如 " "web,sa,dba(`ssh web@some-host`),而不是使用某个用户的用户名跳转登录服务器" "(`ssh xiaoming@some-host`);" #: assets/templates/assets/system_user_list.html:6 msgid "" -"In simple terms, users log into Jumpserver using their own username, and " -"Jumpserver uses system users to log into assets. " +"In simple terms, users log into JumpServer using their own username, and " +"JumpServer uses system users to log into assets. " msgstr "" -"简单来说是用户使用自己的用户名登录 Jumpserver,Jumpserver 使用系统用户登录资" +"简单来说是用户使用自己的用户名登录 JumpServer,JumpServer 使用系统用户登录资" "产。" #: assets/templates/assets/system_user_list.html:7 msgid "" -"When system users are created, if you choose auto push Jumpserver to use " +"When system users are created, if you choose auto push JumpServer to use " "Ansible push system users into the asset, if the asset (Switch) does not " "support ansible, please manually fill in the account password." msgstr "" -"系统用户创建时,如果选择了自动推送,Jumpserver 会使用 Ansible 自动推送系统用" +"系统用户创建时,如果选择了自动推送,JumpServer 会使用 Ansible 自动推送系统用" "户到资产中,如果资产(交换机)不支持 Ansible,请手动填写账号密码。" #: assets/templates/assets/system_user_list.html:16 -#: assets/views/system_user.py:47 +#: assets/views/system_user.py:48 msgid "Create system user" msgstr "创建系统用户" +#: assets/templates/assets/system_user_users.html:84 users/forms/group.py:19 +#: users/forms/user.py:143 users/forms/user.py:148 +#: xpack/plugins/orgs/forms.py:17 +msgid "Select users" +msgstr "选择用户" + +#: assets/templates/assets/system_user_users.html:118 +#: users/templates/users/user_list.html:106 +#: users/templates/users/user_list.html:110 +msgid "Remove" +msgstr "移除" + +#: assets/templates/assets/system_user_users.html:176 +msgid "Remove success" +msgstr "移除成功" + #: assets/views/admin_user.py:31 msgid "Admin user list" msgstr "管理用户列表" @@ -2249,10 +2339,14 @@ msgstr "管理用户列表" msgid "Update admin user" msgstr "更新管理用户" -#: assets/views/admin_user.py:85 assets/views/admin_user.py:109 +#: assets/views/admin_user.py:85 msgid "Admin user detail" msgstr "管理用户详情" +#: assets/views/admin_user.py:109 +msgid "Admin user assets" +msgstr "管理用户关联资产" + #: assets/views/asset.py:67 templates/_nav_user.html:4 msgid "My assets" msgstr "我的资产" @@ -2325,39 +2419,44 @@ msgstr "更新标签" msgid "Platform list" msgstr "平台列表" -#: assets/views/platform.py:56 +#: assets/views/platform.py:59 msgid "Update platform" msgstr "更新系统平台" -#: assets/views/platform.py:72 +#: assets/views/platform.py:75 msgid "Platform detail" msgstr "平台详情" -#: assets/views/system_user.py:30 +#: assets/views/system_user.py:31 msgid "System user list" msgstr "系统用户列表" -#: assets/views/system_user.py:64 +#: assets/views/system_user.py:65 msgid "Update system user" msgstr "更新系统用户" -#: assets/views/system_user.py:80 +#: assets/views/system_user.py:81 msgid "System user detail" msgstr "系统用户详情" -#: assets/views/system_user.py:102 +#: assets/views/system_user.py:103 assets/views/system_user.py:118 msgid "assets" msgstr "资产管理" -#: assets/views/system_user.py:103 -msgid "System user asset" -msgstr "系统用户资产" +#: assets/views/system_user.py:104 +msgid "System user assets" +msgstr "系统用户关联资产" + +#: assets/views/system_user.py:119 +msgid "System user users" +msgstr "系统用户关联用户" #: audits/models.py:19 audits/models.py:42 audits/models.py:53 #: audits/templates/audits/ftp_log_list.html:77 #: audits/templates/audits/operate_log_list.html:74 #: audits/templates/audits/password_change_log_list.html:56 -#: terminal/models.py:183 terminal/templates/terminal/session_list.html:28 +#: terminal/models.py:192 terminal/templates/terminal/session_detail.html:68 +#: terminal/templates/terminal/session_list.html:28 #: terminal/templates/terminal/session_list.html:72 #: terminal/templates/terminal/terminal_detail.html:47 msgid "Remote addr" @@ -2378,13 +2477,13 @@ msgstr "文件名" #: ops/templates/ops/task_list.html:14 #: users/templates/users/user_detail.html:487 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:14 -#: xpack/plugins/cloud/api.py:61 +#: xpack/plugins/cloud/api.py:85 msgid "Success" msgstr "成功" #: audits/models.py:33 #: authentication/templates/authentication/_access_key_modal.html:22 -#: xpack/plugins/vault/templates/vault/vault.html:8 +#: xpack/plugins/vault/templates/vault/vault.html:7 msgid "Create" msgstr "创建" @@ -2414,8 +2513,7 @@ msgstr "启用" msgid "-" msgstr "" -#: audits/models.py:78 xpack/plugins/cloud/models.py:263 -#: xpack/plugins/cloud/models.py:286 +#: audits/models.py:78 xpack/plugins/cloud/models.py:201 msgid "Failed" msgstr "失败" @@ -2437,16 +2535,18 @@ msgstr "Agent" #: audits/models.py:86 audits/templates/audits/login_log_list.html:62 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms/profile.py:52 users/models/user.py:460 -#: users/templates/users/first_login.html:45 +#: authentication/templates/authentication/login_otp.html:6 +#: settings/forms/security.py:16 users/forms/profile.py:52 +#: users/models/user.py:462 users/templates/users/first_login.html:45 +#: users/templates/users/user_detail.html:77 +#: users/templates/users/user_profile.html:87 msgid "MFA" -msgstr "MFA" +msgstr "多因子认证" #: audits/models.py:87 audits/templates/audits/login_log_list.html:63 -#: xpack/plugins/change_auth_plan/models.py:422 +#: xpack/plugins/change_auth_plan/models.py:368 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:15 -#: xpack/plugins/cloud/models.py:277 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:64 +#: xpack/plugins/cloud/models.py:214 msgid "Reason" msgstr "原因" @@ -2454,9 +2554,9 @@ msgstr "原因" #: tickets/templates/tickets/ticket_detail.html:34 #: tickets/templates/tickets/ticket_list.html:36 #: tickets/templates/tickets/ticket_list.html:104 -#: xpack/plugins/cloud/models.py:274 xpack/plugins/cloud/models.py:309 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:65 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:62 +#: xpack/plugins/cloud/models.py:211 xpack/plugins/cloud/models.py:269 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:40 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:37 msgid "Status" msgstr "状态" @@ -2472,12 +2572,13 @@ msgstr "登录日期" #: perms/templates/perms/asset_permission_detail.html:81 #: perms/templates/perms/database_app_permission_detail.html:77 #: perms/templates/perms/remote_app_permission_detail.html:73 -#: terminal/models.py:189 terminal/templates/terminal/session_list.html:32 -#: xpack/plugins/change_auth_plan/models.py:249 -#: xpack/plugins/change_auth_plan/models.py:425 +#: terminal/models.py:199 terminal/templates/terminal/session_detail.html:72 +#: terminal/templates/terminal/session_list.html:32 +#: xpack/plugins/change_auth_plan/models.py:195 +#: xpack/plugins/change_auth_plan/models.py:371 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:59 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:17 -#: xpack/plugins/gathered_user/models.py:140 +#: xpack/plugins/gathered_user/models.py:76 msgid "Date start" msgstr "开始日期" @@ -2495,8 +2596,6 @@ msgstr "选择用户" #: ops/templates/ops/command_execution_list.html:49 #: ops/templates/ops/command_execution_list.html:54 #: templates/_base_list.html:37 templates/_user_profile.html:23 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:47 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:45 msgid "Search" msgstr "搜索" @@ -2599,35 +2698,39 @@ msgstr "" msgid "Invalid token or cache refreshed." msgstr "" -#: authentication/errors.py:20 +#: authentication/errors.py:21 msgid "Username/password check failed" msgstr "用户名/密码 校验失败" -#: authentication/errors.py:21 -msgid "MFA authentication failed" -msgstr "MFA 认证失败" - #: authentication/errors.py:22 +msgid "MFA failed" +msgstr "多因子认证失败" + +#: authentication/errors.py:23 +msgid "MFA unset" +msgstr "多因子认证没有设定" + +#: authentication/errors.py:24 msgid "Username does not exist" msgstr "用户名不存在" -#: authentication/errors.py:23 +#: authentication/errors.py:25 msgid "Password expired" msgstr "密码已过期" -#: authentication/errors.py:24 +#: authentication/errors.py:26 msgid "Disabled or expired" msgstr "禁用或失效" -#: authentication/errors.py:25 +#: authentication/errors.py:27 msgid "This account is inactive." msgstr "此账户已禁用" -#: authentication/errors.py:35 +#: authentication/errors.py:37 msgid "No session found, check your cookie" msgstr "会话已变更,刷新页面" -#: authentication/errors.py:37 +#: authentication/errors.py:39 #, python-brace-format msgid "" "The username or password you entered is incorrect, please enter it again. " @@ -2637,36 +2740,40 @@ msgstr "" "您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次(账号将" "被临时 锁定 {block_time} 分钟)" -#: authentication/errors.py:43 +#: authentication/errors.py:45 msgid "" "The account has been locked (please contact admin to unlock it or try again " "after {} minutes)" msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)" -#: authentication/errors.py:46 users/views/profile.py:202 -#: users/views/profile.py:238 +#: authentication/errors.py:48 users/views/profile/otp.py:63 +#: users/views/profile/otp.py:102 msgid "MFA code invalid, or ntp sync server time" msgstr "MFA验证码不正确,或者服务器端时间不对" -#: authentication/errors.py:48 +#: authentication/errors.py:50 msgid "MFA required" -msgstr "" +msgstr "需要多因子认证" + +#: authentication/errors.py:51 +msgid "MFA not set, please set it first" +msgstr "多因子认证没有设置,请先完成设置" -#: authentication/errors.py:49 +#: authentication/errors.py:52 msgid "Login confirm required" msgstr "需要登录复核" -#: authentication/errors.py:50 +#: authentication/errors.py:53 msgid "Wait login confirm ticket for accept" msgstr "等待登录复核处理" -#: authentication/errors.py:51 +#: authentication/errors.py:54 msgid "Login confirm ticket was {}" msgstr "登录复核 {}" #: authentication/forms.py:29 users/forms/user.py:199 msgid "MFA code" -msgstr "MFA 验证码" +msgstr "多因子认证验证码" #: authentication/models.py:39 msgid "Private Token" @@ -2698,12 +2805,13 @@ msgid "Secret" msgstr "密文" #: authentication/templates/authentication/_access_key_modal.html:48 -#: users/templates/users/_granted_assets.html:80 +#: users/templates/users/_granted_assets.html:75 msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:360 users/templates/users/user_profile.html:94 +#: users/models/user.py:360 users/templates/users/user_disable_mfa.html:32 +#: users/templates/users/user_profile.html:94 #: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:166 msgid "Disable" @@ -2717,121 +2825,63 @@ msgstr "启用" #: authentication/templates/authentication/_mfa_confirm_modal.html:5 msgid "MFA confirm" -msgstr "MFA确认" +msgstr "多因子认证校验" #: authentication/templates/authentication/_mfa_confirm_modal.html:17 -msgid "Need otp auth for view auth" -msgstr "需要二次认证来查看账号信息" +msgid "Need MFA for view auth" +msgstr "需要多因子认证来查看账号信息" #: authentication/templates/authentication/_mfa_confirm_modal.html:25 msgid "Code error" msgstr "代码错误" -#: authentication/templates/authentication/login.html:27 -#: authentication/templates/authentication/login_otp.html:27 -#: xpack/plugins/interface/models.py:36 -msgid "Welcome to the Jumpserver open source fortress" -msgstr "欢迎使用Jumpserver开源堡垒机" - -#: authentication/templates/authentication/login.html:29 -#: authentication/templates/authentication/login_otp.html:29 -msgid "" -"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." -msgstr "" -"全球首款完全开源的堡垒机,使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计" -"系统。" - -#: authentication/templates/authentication/login.html:32 -#: authentication/templates/authentication/login_otp.html:32 -msgid "" -"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." -msgstr "" -"使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web " -"Terminal 解决方案,交互界面美观、用户体验好。" - -#: authentication/templates/authentication/login.html:35 -#: authentication/templates/authentication/login_otp.html:35 -msgid "" -"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." -msgstr "" -"采纳分布式架构,支持多机房跨区域部署,中心节点提供 API,各机房部署登录节点," -"可横向扩展、无并发访问限制。" - -#: authentication/templates/authentication/login.html:38 -#: authentication/templates/authentication/login_otp.html:38 -msgid "Changes the world, starting with a little bit." -msgstr "改变世界,从一点点开始。" - -#: authentication/templates/authentication/login.html:45 -#: authentication/templates/authentication/login.html:76 +#: authentication/templates/authentication/login.html:6 +#: authentication/templates/authentication/login.html:39 #: authentication/templates/authentication/xpack_login.html:112 -#: templates/_header_bar.html:83 +#: templates/_base_only_msg_content.html:52 templates/_header_bar.html:83 msgid "Login" msgstr "登录" -#: authentication/templates/authentication/login.html:54 +#: authentication/templates/authentication/login.html:17 #: authentication/templates/authentication/xpack_login.html:87 msgid "Captcha invalid" msgstr "验证码错误" -#: authentication/templates/authentication/login.html:87 +#: authentication/templates/authentication/login.html:50 #: authentication/templates/authentication/xpack_login.html:116 -#: users/templates/users/forgot_password.html:12 -#: users/templates/users/forgot_password.html:13 +#: users/templates/users/forgot_password.html:7 +#: users/templates/users/forgot_password.html:8 msgid "Forgot password" msgstr "忘记密码" -#: authentication/templates/authentication/login.html:94 +#: authentication/templates/authentication/login.html:57 msgid "More login options" msgstr "更多登录方式" -#: authentication/templates/authentication/login.html:98 +#: authentication/templates/authentication/login.html:61 msgid "Keycloak" msgstr "" -#: authentication/templates/authentication/login_otp.html:46 -#: users/templates/users/user_detail.html:77 -#: users/templates/users/user_profile.html:87 -msgid "MFA certification" -msgstr "MFA认证" - -#: authentication/templates/authentication/login_otp.html:51 -#: users/templates/users/user_disable_mfa.html:11 -msgid "" -"The account protection has been opened, please complete the following " -"operations according to the prompts" -msgstr "账号保护已开启,请根据提示完成以下操作" - -#: authentication/templates/authentication/login_otp.html:55 -#: users/templates/users/user_disable_mfa.html:13 -msgid "Open Authenticator and enter the 6-bit dynamic code" -msgstr "请打开手机Google Authenticator应用,输入6位动态码" +#: authentication/templates/authentication/login_otp.html:17 +msgid "One-time password" +msgstr "一次性密码" -#: authentication/templates/authentication/login_otp.html:65 -#: users/templates/users/user_disable_mfa.html:23 -#: users/templates/users/user_otp_enable_bind.html:25 -msgid "Six figures" -msgstr "6位数字" +#: authentication/templates/authentication/login_otp.html:23 +msgid "Open Google Authenticator and enter the 6-bit dynamic code" +msgstr "请打开 Google Authenticator,输入6位动态码" -#: authentication/templates/authentication/login_otp.html:67 -#: users/templates/users/first_login.html:108 +#: authentication/templates/authentication/login_otp.html:26 +#: users/templates/users/first_login.html:100 #: users/templates/users/user_disable_mfa.html:26 -#: users/templates/users/user_otp_enable_bind.html:28 -#: users/templates/users/user_otp_enable_install_app.html:25 -#: users/templates/users/user_password_check.html:16 +#: users/templates/users/user_otp_check_password.html:15 +#: users/templates/users/user_otp_enable_bind.html:24 +#: users/templates/users/user_otp_enable_install_app.html:29 msgid "Next" msgstr "下一步" -#: authentication/templates/authentication/login_otp.html:70 +#: authentication/templates/authentication/login_otp.html:29 msgid "Can't provide security? Please contact the administrator!" -msgstr "如果不能提供MFA验证码,请联系管理员!" +msgstr "如果不能提供多因子认证验证码,请联系管理员!" #: authentication/templates/authentication/login_wait_confirm.html:47 msgid "Copy link" @@ -2846,11 +2896,11 @@ msgstr "返回" msgid "Welcome back, please enter username and password to login" msgstr "欢迎回来,请输入用户名和密码登录" -#: authentication/views/login.py:71 +#: authentication/views/login.py:83 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:159 +#: authentication/views/login.py:168 msgid "" "Wait for {} confirm, You also can copy link to her/him
    \n" " Don't close this page" @@ -2858,15 +2908,15 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
    \n" " 不要关闭本页面" -#: authentication/views/login.py:164 +#: authentication/views/login.py:173 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:187 +#: authentication/views/login.py:205 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:188 +#: authentication/views/login.py:206 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" @@ -2916,11 +2966,11 @@ msgstr "" msgid "Encrypt field using Secret Key" msgstr "" -#: common/mixins/models.py:34 +#: common/mixins/models.py:33 msgid "is discard" msgstr "" -#: common/mixins/models.py:35 +#: common/mixins/models.py:34 msgid "discard time" msgstr "" @@ -2940,7 +2990,7 @@ msgstr "字段必须唯一" msgid "

    Flow service unavailable, check it

    " msgstr "" -#: jumpserver/views/index.py:178 templates/_nav.html:7 +#: jumpserver/views/index.py:242 templates/_nav.html:7 msgid "Dashboard" msgstr "仪表盘" @@ -2975,123 +3025,157 @@ msgstr "等待任务开始" msgid "Not has host {} permission" msgstr "没有该主机 {} 权限" -#: ops/models/adhoc.py:41 -msgid "Interval" -msgstr "间隔" +#: ops/mixin.py:29 ops/mixin.py:92 ops/mixin.py:162 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:98 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:88 +msgid "Cycle perform" +msgstr "周期执行" -#: ops/models/adhoc.py:41 settings/forms/terminal.py:34 -msgid "Units: seconds" -msgstr "单位: 秒" +#: ops/mixin.py:33 ops/mixin.py:90 ops/mixin.py:111 ops/mixin.py:150 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:90 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:80 +msgid "Regularly perform" +msgstr "定期执行" -#: ops/models/adhoc.py:42 -msgid "Crontab" -msgstr "Crontab" +#: ops/mixin.py:108 ops/mixin.py:147 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:54 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:79 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:17 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:37 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:69 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:16 +#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:28 +msgid "Periodic perform" +msgstr "定时执行" -#: ops/models/adhoc.py:42 -msgid "5 * * * *" -msgstr "5 * * * *" +#: ops/mixin.py:122 +msgid "* Please enter a valid crontab expression" +msgstr "* 请输入有效的 crontab 表达式" + +#: ops/mixin.py:129 +msgid "Range {} to {}" +msgstr "输入在 {} - {} 范围之间" + +#: ops/mixin.py:140 +msgid "Require periodic or regularly perform setting" +msgstr "需要周期或定期设置" + +#: ops/mixin.py:151 +msgid "" +"eg: Every Sunday 03:05 run <5 3 * * 0>
    Tips: Using 5 digits linux " +"crontab expressions (Online tools)
    Note: If both Regularly " +"perform and Cycle perform are set, give priority to Regularly perform" +msgstr "" +"eg:每周日 03:05 执行 <5 3 * * 0>
    提示: 使用5位 Linux crontab 表达式 <" +"分 时 日 月 星期> (在线工" +"具
    注意: 如果同时设置了定期执行和周期执行,优先使用定期执行" + +#: ops/mixin.py:162 +msgid "Tips: (Units: hour)" +msgstr "提示:(单位: 时)" -#: ops/models/adhoc.py:44 +#: ops/models/adhoc.py:35 msgid "Callback" msgstr "回调" -#: ops/models/adhoc.py:182 ops/templates/ops/adhoc_detail.html:112 +#: ops/models/adhoc.py:143 ops/templates/ops/adhoc_detail.html:112 msgid "Tasks" msgstr "任务" -#: ops/models/adhoc.py:183 ops/templates/ops/adhoc_detail.html:55 +#: ops/models/adhoc.py:144 ops/templates/ops/adhoc_detail.html:55 #: ops/templates/ops/task_adhoc.html:58 msgid "Pattern" msgstr "模式" -#: ops/models/adhoc.py:184 ops/templates/ops/adhoc_detail.html:59 +#: ops/models/adhoc.py:145 ops/templates/ops/adhoc_detail.html:59 msgid "Options" msgstr "选项" -#: ops/models/adhoc.py:186 +#: ops/models/adhoc.py:147 msgid "Run as admin" msgstr "再次执行" -#: ops/models/adhoc.py:188 ops/templates/ops/adhoc_detail.html:80 +#: ops/models/adhoc.py:149 ops/templates/ops/adhoc_detail.html:80 #: ops/templates/ops/task_adhoc.html:60 msgid "Become" msgstr "Become" -#: ops/models/adhoc.py:189 users/templates/users/user_group_detail.html:54 +#: ops/models/adhoc.py:150 users/templates/users/user_group_detail.html:54 #: xpack/plugins/cloud/templates/cloud/account_detail.html:59 #: xpack/plugins/orgs/templates/orgs/org_detail.html:51 msgid "Create by" msgstr "创建者" -#: ops/models/adhoc.py:270 +#: ops/models/adhoc.py:232 msgid "Task display" msgstr "任务展示" -#: ops/models/adhoc.py:271 +#: ops/models/adhoc.py:233 msgid "Host amount" msgstr "主机数量" -#: ops/models/adhoc.py:273 +#: ops/models/adhoc.py:235 msgid "Start time" msgstr "开始时间" -#: ops/models/adhoc.py:274 +#: ops/models/adhoc.py:236 msgid "End time" msgstr "完成时间" -#: ops/models/adhoc.py:275 ops/templates/ops/adhoc_history.html:55 +#: ops/models/adhoc.py:237 ops/templates/ops/adhoc_history.html:55 #: ops/templates/ops/task_history.html:61 ops/templates/ops/task_list.html:16 -#: xpack/plugins/change_auth_plan/models.py:252 -#: xpack/plugins/change_auth_plan/models.py:428 +#: xpack/plugins/change_auth_plan/models.py:198 +#: xpack/plugins/change_auth_plan/models.py:374 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:58 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:16 -#: xpack/plugins/gathered_user/models.py:143 +#: xpack/plugins/gathered_user/models.py:79 msgid "Time" msgstr "时间" -#: ops/models/adhoc.py:276 ops/templates/ops/adhoc_detail.html:104 +#: ops/models/adhoc.py:238 ops/templates/ops/adhoc_detail.html:104 #: ops/templates/ops/adhoc_history.html:53 #: ops/templates/ops/adhoc_history_detail.html:67 #: ops/templates/ops/task_detail.html:82 ops/templates/ops/task_history.html:59 msgid "Is finished" msgstr "是否完成" -#: ops/models/adhoc.py:277 ops/templates/ops/adhoc_history.html:54 +#: ops/models/adhoc.py:239 ops/templates/ops/adhoc_history.html:54 #: ops/templates/ops/task_history.html:60 msgid "Is success" msgstr "是否成功" -#: ops/models/adhoc.py:278 +#: ops/models/adhoc.py:240 msgid "Adhoc raw result" msgstr "结果" -#: ops/models/adhoc.py:279 +#: ops/models/adhoc.py:241 msgid "Adhoc result summary" msgstr "汇总" -#: ops/models/adhoc.py:323 +#: ops/models/adhoc.py:281 msgid "{} Start task: {}" msgstr "{} 任务开始: {}" -#: ops/models/adhoc.py:332 +#: ops/models/adhoc.py:290 msgid "{} Task finish" msgstr "{} 任务结束" -#: ops/models/command.py:23 +#: ops/models/command.py:24 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:56 -#: xpack/plugins/cloud/models.py:272 +#: xpack/plugins/cloud/models.py:209 msgid "Result" msgstr "结果" -#: ops/models/command.py:58 +#: ops/models/command.py:59 msgid "Task start" msgstr "任务开始" -#: ops/models/command.py:80 +#: ops/models/command.py:81 msgid "Command `{}` is forbidden ........" msgstr "命令 `{}` 不允许被执行 ......." -#: ops/models/command.py:86 +#: ops/models/command.py:88 msgid "Task end" msgstr "任务结束" @@ -3109,8 +3193,8 @@ msgid "Version detail" msgstr "版本详情" #: ops/templates/ops/adhoc_detail.html:20 -#: ops/templates/ops/adhoc_history.html:20 ops/views/adhoc.py:111 -msgid "Version run history" +#: ops/templates/ops/adhoc_history.html:20 ops/views/adhoc.py:106 +msgid "Version run execution" msgstr "执行历史" #: ops/templates/ops/adhoc_detail.html:51 @@ -3168,8 +3252,8 @@ msgstr "最后运行成功主机" #: ops/templates/ops/adhoc_history.html:28 #: ops/templates/ops/task_history.html:34 -msgid "History of " -msgstr "执行历史" +msgid "Executions of " +msgstr "执行历史 " #: ops/templates/ops/adhoc_history.html:51 #: ops/templates/ops/task_history.html:57 @@ -3181,18 +3265,18 @@ msgstr "失败/成功/总" msgid "Ratio" msgstr "比例" -#: ops/templates/ops/adhoc_history_detail.html:17 ops/views/adhoc.py:125 -msgid "Run history detail" +#: ops/templates/ops/adhoc_history_detail.html:17 ops/views/adhoc.py:120 +msgid "Execution detail" msgstr "执行历史详情" #: ops/templates/ops/adhoc_history_detail.html:20 #: ops/templates/ops/command_execution_list.html:69 -#: terminal/backends/command/models.py:16 +#: terminal/backends/command/models.py:22 msgid "Output" msgstr "输出" #: ops/templates/ops/adhoc_history_detail.html:28 -msgid "History detail of" +msgid "Execution detail of" msgstr "执行历史详情" #: ops/templates/ops/adhoc_history_detail.html:51 @@ -3217,8 +3301,8 @@ msgid "Task log" msgstr "任务列表" #: ops/templates/ops/command_execution_create.html:93 -#: terminal/templates/terminal/session_detail.html:95 -#: terminal/templates/terminal/session_detail.html:104 +#: terminal/templates/terminal/session_detail.html:97 +#: terminal/templates/terminal/session_detail.html:114 msgid "Go" msgstr "" @@ -3261,24 +3345,24 @@ msgid "Finished" msgstr "结束" #: ops/templates/ops/task_adhoc.html:17 ops/templates/ops/task_detail.html:18 -#: ops/templates/ops/task_history.html:17 ops/views/adhoc.py:55 +#: ops/templates/ops/task_history.html:17 ops/views/adhoc.py:50 msgid "Task detail" msgstr "任务详情" #: ops/templates/ops/task_adhoc.html:20 ops/templates/ops/task_detail.html:21 -#: ops/templates/ops/task_history.html:20 ops/views/adhoc.py:69 +#: ops/templates/ops/task_history.html:20 ops/views/adhoc.py:64 msgid "Task versions" msgstr "任务各版本" #: ops/templates/ops/task_adhoc.html:23 ops/templates/ops/task_detail.html:24 #: ops/templates/ops/task_history.html:23 -msgid "Run history" +msgid "Execution" msgstr "执行历史" #: ops/templates/ops/task_adhoc.html:26 ops/templates/ops/task_detail.html:27 #: ops/templates/ops/task_history.html:26 -msgid "Last run output" -msgstr "输出" +msgid "Last execution output" +msgstr "最后执行输出" #: ops/templates/ops/task_adhoc.html:34 msgid "Versions of " @@ -3305,17 +3389,17 @@ msgstr "执行" msgid "Task start: " msgstr "任务开始: " -#: ops/utils.py:53 +#: ops/utils.py:60 msgid "Update task content: {}" msgstr "更新任务内容: {}" -#: ops/utils.py:63 +#: ops/utils.py:70 msgid "Disk used more than 80%: {} => {}" msgstr "" -#: ops/views/adhoc.py:31 ops/views/adhoc.py:54 ops/views/adhoc.py:68 -#: ops/views/adhoc.py:82 ops/views/adhoc.py:96 ops/views/adhoc.py:110 -#: ops/views/adhoc.py:124 ops/views/command.py:48 ops/views/command.py:79 +#: ops/views/adhoc.py:31 ops/views/adhoc.py:49 ops/views/adhoc.py:63 +#: ops/views/adhoc.py:77 ops/views/adhoc.py:91 ops/views/adhoc.py:105 +#: ops/views/adhoc.py:119 ops/views/command.py:48 ops/views/command.py:79 msgid "Ops" msgstr "作业中心" @@ -3324,9 +3408,9 @@ msgstr "作业中心" msgid "Task list" msgstr "任务列表" -#: ops/views/adhoc.py:83 -msgid "Task run history" -msgstr "执行历史" +#: ops/views/adhoc.py:78 +msgid "Task execution list" +msgstr "任务执行列表" #: ops/views/command.py:49 msgid "Command execution list" @@ -3336,7 +3420,7 @@ msgstr "命令执行列表" msgid "Command execution" msgstr "命令执行" -#: orgs/mixins/models.py:44 orgs/mixins/serializers.py:26 orgs/models.py:31 +#: orgs/mixins/models.py:56 orgs/mixins/serializers.py:26 orgs/models.py:31 msgid "Organization" msgstr "组织" @@ -3362,7 +3446,7 @@ msgstr "提示:RDP 协议不支持单独控制上传或下载文件" #: perms/templates/perms/database_app_permission_list.html:16 #: perms/templates/perms/remote_app_permission_list.html:16 #: templates/_nav.html:21 users/forms/user.py:168 users/models/group.py:31 -#: users/models/user.py:444 users/templates/users/_select_user_modal.html:16 +#: users/models/user.py:446 users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_asset_permission.html:39 #: users/templates/users/user_asset_permission.html:67 #: users/templates/users/user_database_app_permission.html:38 @@ -3414,7 +3498,7 @@ msgstr "资产授权" #: perms/templates/perms/asset_permission_detail.html:85 #: perms/templates/perms/database_app_permission_detail.html:81 #: perms/templates/perms/remote_app_permission_detail.html:77 -#: users/models/user.py:476 users/templates/users/user_detail.html:93 +#: users/models/user.py:478 users/templates/users/user_detail.html:93 #: users/templates/users/user_profile.html:120 msgid "Date expired" msgstr "失效日期" @@ -3542,7 +3626,7 @@ msgstr "刷新授权缓存" #: users/templates/users/user_database_app_permission.html:41 #: users/templates/users/user_list.html:19 #: users/templates/users/user_remote_app_permission.html:41 -#: xpack/plugins/cloud/models.py:73 +#: xpack/plugins/cloud/models.py:50 #: xpack/plugins/cloud/templates/cloud/account_detail.html:55 #: xpack/plugins/cloud/templates/cloud/account_list.html:14 msgid "Validity" @@ -3726,27 +3810,15 @@ msgstr "远程应用授权用户列表" msgid "RemoteApp permission RemoteApp list" msgstr "远程应用授权远程应用列表" -#: settings/api.py:37 +#: settings/api.py:33 msgid "Test mail sent to {}, please check" msgstr "邮件已经发送{}, 请检查" -#: settings/api.py:76 -msgid "Test ldap success" -msgstr "连接LDAP成功" - -#: settings/api.py:107 -msgid "LDAP attr map not valid" -msgstr "" - -#: settings/api.py:116 -msgid "Match {} s users" -msgstr "匹配 {} 个用户" - -#: settings/api.py:226 +#: settings/api.py:227 msgid "Get ldap users is None" msgstr "获取 LDAP 用户为 None" -#: settings/api.py:233 +#: settings/api.py:234 msgid "Imported {} users successfully" msgstr "导入 {} 个用户成功" @@ -3768,7 +3840,7 @@ msgstr "Email主题前缀" #: settings/forms/basic.py:22 msgid "Tips: Some word will be intercept by mail provider" -msgstr "提示: 一些关键字可能会被邮件提供商拦截,如 跳板机、Jumpserver" +msgstr "提示: 一些关键字可能会被邮件提供商拦截,如 跳板机、JumpServer" #: settings/forms/email.py:15 msgid "SMTP host" @@ -3897,15 +3969,11 @@ msgstr "" msgid "Enable LDAP auth" msgstr "启用LDAP认证" -#: settings/forms/security.py:16 -msgid "MFA Secondary certification" -msgstr "MFA 二次认证" - #: settings/forms/security.py:18 msgid "" -"After opening, the user login must use MFA secondary authentication (valid " -"for all users, including administrators)" -msgstr "开启后,用户登录必须使用MFA二次认证(对所有用户有效,包括管理员)" +"After opening, all user login must use MFA(valid for all users, including " +"administrators)" +msgstr "开启后,所有用户登录必须使用多因子认证(对所有用户有效,包括管理员)" #: settings/forms/security.py:24 msgid "Batch execute commands" @@ -4022,6 +4090,10 @@ msgstr "密钥认证" msgid "Heartbeat interval" msgstr "心跳间隔" +#: settings/forms/terminal.py:34 +msgid "Units: seconds" +msgstr "单位: 秒" + #: settings/forms/terminal.py:37 msgid "List sort by" msgstr "资产列表排序" @@ -4069,7 +4141,7 @@ msgid "Refresh cache" msgstr "刷新缓存" #: settings/templates/settings/_ldap_list_users_modal.html:33 -#: users/forms/profile.py:89 users/models/user.py:440 +#: users/forms/profile.py:89 users/models/user.py:442 #: users/templates/users/user_detail.html:57 #: users/templates/users/user_profile.html:59 msgid "Email" @@ -4090,6 +4162,18 @@ msgstr "当前无勾选用户,请勾选你想要导入的用户" msgid "Import" msgstr "导入" +#: settings/templates/settings/_ldap_test_user_login_modal.html:4 +msgid "Test LDAP user login" +msgstr "测试LDAP 用户登录" + +#: settings/templates/settings/_ldap_test_user_login_modal.html:5 +msgid "Save the configuration before testing the login" +msgstr "请先提交LDAP配置再进行测试登录" + +#: settings/templates/settings/_ldap_test_user_login_modal.html:12 +msgid "Please input username" +msgstr "请输入用户名" + #: settings/templates/settings/_setting_tabs.html:4 #: settings/templates/settings/terminal_setting.html:31 settings/views.py:20 msgid "Basic setting" @@ -4121,6 +4205,10 @@ msgid "Create User setting" msgstr "创建用户设置" #: settings/templates/settings/ldap_setting.html:47 +msgid "Test login" +msgstr "测试登录" + +#: settings/templates/settings/ldap_setting.html:48 msgid "Bulk import" msgstr "一键导入" @@ -4140,6 +4228,103 @@ msgstr "会话管理 -> 终端管理 -> 存储配置" msgid "Here" msgstr "这里" +#: settings/utils/ldap.py:389 +msgid "Host or port is disconnected: {}" +msgstr "" + +#: settings/utils/ldap.py:391 +msgid "The port is not the port of the LDAP service: {}" +msgstr "" + +#: settings/utils/ldap.py:393 +msgid "Please enter the certificate: {}" +msgstr "" + +#: settings/utils/ldap.py:395 settings/utils/ldap.py:422 +#: settings/utils/ldap.py:452 settings/utils/ldap.py:480 +msgid "Unknown error: {}" +msgstr "" + +#: settings/utils/ldap.py:409 +msgid "bind dn or password incorrect" +msgstr "" + +#: settings/utils/ldap.py:416 +msgid "Please enter bind dn: {}" +msgstr "" + +#: settings/utils/ldap.py:418 +msgid "Please enter password: {}" +msgstr "请输入密码: {}" + +#: settings/utils/ldap.py:420 +msgid "Please enter correct bind dn and password: {}" +msgstr "请输入正确的dn和密码: {}" + +#: settings/utils/ldap.py:438 +msgid "Invalid search ou or filter: {}" +msgstr "不合法的ou或过滤器: {}" + +#: settings/utils/ldap.py:469 +msgid "LDAP attribute not include: {}" +msgstr "LDAP属性映射没有包含: {}" + +#: settings/utils/ldap.py:476 +msgid "LDAP attribute map is not dict" +msgstr "LDAP属性映射不合法" + +#: settings/utils/ldap.py:495 +msgid "LDAP authentication is not enabled" +msgstr "LDAP认证没有启用" + +#: settings/utils/ldap.py:513 +msgid "Error (Invalid server uri): {}" +msgstr "错误 (不合法的服务器地址): {}" + +#: settings/utils/ldap.py:515 +msgid "Error (Invalid bind dn): {}" +msgstr "错误 (不合法的DN): {}" + +#: settings/utils/ldap.py:517 +msgid "Error (Invalid attribute map): {}" +msgstr "错误 (不合法的属性映射): {}" + +#: settings/utils/ldap.py:519 +msgid "Error (Invalid search ou or filter): {}" +msgstr "错误 (不合法的搜索OU或过滤器): {}" + +#: settings/utils/ldap.py:521 +msgid "Error (Not enabled LDAP authentication): {}" +msgstr "错误 (没有启用LDAP认证): {}" + +#: settings/utils/ldap.py:523 +msgid "Error (Unknown): {}" +msgstr "错误 (未知): {}" + +#: settings/utils/ldap.py:526 +msgid "Succeed: Match {} s user" +msgstr "成功匹配 {} 个用户" + +#: settings/utils/ldap.py:559 +msgid "Authentication failed (configuration incorrect): {}" +msgstr "认证失败 (配置错误): {}" + +#: settings/utils/ldap.py:561 +msgid "Authentication failed (before login check failed): {}" +msgstr "认证失败 (登录前检查失败): {}" + +#: settings/utils/ldap.py:563 +msgid "Authentication failed (username or password incorrect): {}" +msgstr "认证失败 (用户名或密码不正确): {}" + +#: settings/utils/ldap.py:565 +msgid "Authentication failed (Unknown): {}" +msgstr "认证失败: (未知): {}" + +#: settings/utils/ldap.py:568 +msgid "Authentication success: {}" +msgstr "认证成功: {}" + #: settings/views.py:19 settings/views.py:46 settings/views.py:73 #: settings/views.py:105 settings/views.py:134 settings/views.py:161 #: templates/_nav.html:187 @@ -4151,6 +4336,43 @@ msgstr "系统设置" msgid "Update setting successfully" msgstr "更新设置成功" +#: templates/_base_only_msg_content.html:28 +#: xpack/plugins/interface/models.py:36 +msgid "Welcome to the JumpServer open source fortress" +msgstr "欢迎使用JumpServer开源堡垒机" + +#: templates/_base_only_msg_content.html:33 +msgid "" +"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." +msgstr "" +"全球首款完全开源的堡垒机,使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计" +"系统。" + +#: templates/_base_only_msg_content.html:36 +msgid "" +"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." +msgstr "" +"使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web " +"Terminal 解决方案,交互界面美观、用户体验好。" + +#: templates/_base_only_msg_content.html:39 +msgid "" +"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." +msgstr "" +"采纳分布式架构,支持多机房跨区域部署,中心节点提供 API,各机房部署登录节点," +"可横向扩展、无并发访问限制。" + +#: templates/_base_only_msg_content.html:42 +msgid "Changes the world, starting with a little bit." +msgstr "改变世界,从一点点开始。" + #: templates/_csv_import_modal.html:12 msgid "Download the imported template or use the exported CSV file format" msgstr "下载导入的模板或使用导出的csv格式" @@ -4195,7 +4417,8 @@ msgstr "商业支持" #: users/templates/users/user_profile.html:17 #: users/templates/users/user_profile_update.html:37 #: users/templates/users/user_profile_update.html:61 -#: users/templates/users/user_pubkey_update.html:37 users/views/profile.py:51 +#: users/templates/users/user_pubkey_update.html:37 +#: users/views/profile/base.py:27 msgid "Profile" msgstr "个人信息" @@ -4288,28 +4511,15 @@ msgstr "" "\"%(user_pubkey_update)s\"> 链接 更新\n" " " -#: templates/_nav.html:17 users/views/group.py:28 users/views/group.py:45 -#: users/views/group.py:63 users/views/group.py:82 users/views/group.py:99 -#: users/views/login.py:158 users/views/profile.py:90 -#: users/views/profile.py:125 users/views/user.py:50 users/views/user.py:67 -#: users/views/user.py:111 users/views/user.py:178 users/views/user.py:206 -#: users/views/user.py:220 users/views/user.py:234 users/views/user.py:248 -#: users/views/user.py:262 users/views/user.py:276 -msgid "Users" -msgstr "用户管理" - -#: templates/_nav.html:20 users/views/user.py:51 -msgid "User list" -msgstr "用户列表" - #: templates/_nav.html:47 msgid "Command filters" msgstr "命令过滤" #: templates/_nav.html:97 terminal/views/command.py:21 -#: terminal/views/session.py:43 terminal/views/session.py:54 -#: terminal/views/session.py:78 terminal/views/terminal.py:32 -#: terminal/views/terminal.py:48 terminal/views/terminal.py:61 +#: terminal/views/session.py:48 terminal/views/session.py:59 +#: terminal/views/session.py:74 terminal/views/session.py:97 +#: terminal/views/terminal.py:32 terminal/views/terminal.py:48 +#: terminal/views/terminal.py:61 msgid "Sessions" msgstr "会话管理" @@ -4317,11 +4527,12 @@ msgstr "会话管理" msgid "Session online" msgstr "在线会话" -#: templates/_nav.html:101 terminal/views/session.py:55 +#: templates/_nav.html:101 terminal/views/session.py:60 msgid "Session offline" msgstr "历史会话" -#: templates/_nav.html:102 +#: templates/_nav.html:102 terminal/templates/terminal/session_commands.html:21 +#: terminal/templates/terminal/session_detail.html:21 msgid "Commands" msgstr "命令记录" @@ -4406,136 +4617,128 @@ msgid "Online sessions" msgstr "在线会话" #: templates/index.html:61 -msgid " Top 5 Active user" -msgstr "活跃用户Top5" - -#: templates/index.html:62 msgid "In the past week, a total of " msgstr "过去一周, 共有 " -#: templates/index.html:62 +#: templates/index.html:61 msgid " users have logged in " msgstr " 位用户登录 " -#: templates/index.html:62 +#: templates/index.html:61 msgid " times asset." msgstr " 次资产." -#: templates/index.html:67 +#: templates/index.html:66 msgid " times/week" msgstr " 次/周" -#: templates/index.html:78 +#: templates/index.html:77 msgid "Active user asset ratio" msgstr "活跃用户资产占比" -#: templates/index.html:81 +#: templates/index.html:80 msgid "" "The following graphs describe the percentage of active users per month and " "assets per user host per month, respectively." msgstr "以下图形分别描述一个月活跃用户和资产占所有用户主机的百分比" -#: templates/index.html:106 templates/index.html:121 +#: templates/index.html:105 templates/index.html:120 msgid "Top 10 assets in a week" msgstr "一周Top10资产" -#: templates/index.html:122 +#: templates/index.html:121 msgid "Login frequency and last login record." msgstr "登录次数及最近一次登录记录." -#: templates/index.html:133 templates/index.html:221 +#: templates/index.html:132 templates/index.html:218 msgid " times" msgstr " 次" -#: templates/index.html:136 -msgid "The last time a user logged in" -msgstr "最近一次登录用户" +#: templates/index.html:135 templates/index.html:221 +msgid "The time last logged in" +msgstr "最近一次登录日期" -#: templates/index.html:138 templates/index.html:226 -msgid "At " +#: templates/index.html:136 templates/index.html:222 +msgid "At" msgstr "于" -#: templates/index.html:144 templates/index.html:183 templates/index.html:232 +#: templates/index.html:142 templates/index.html:180 templates/index.html:228 msgid "(No)" msgstr "(暂无)" -#: templates/index.html:152 +#: templates/index.html:150 msgid "Last 10 login" msgstr "最近十次登录" -#: templates/index.html:158 +#: templates/index.html:156 msgid "Login record" msgstr "登录记录" -#: templates/index.html:159 +#: templates/index.html:157 msgid "Last 10 login records." msgstr "最近十次登录记录." -#: templates/index.html:172 templates/index.html:174 +#: templates/index.html:170 templates/index.html:172 msgid "Before" msgstr "前" -#: templates/index.html:176 +#: templates/index.html:174 msgid "Login in " msgstr "登录了" -#: templates/index.html:194 templates/index.html:209 +#: templates/index.html:191 templates/index.html:206 msgid "Top 10 users in a week" msgstr "一周Top10用户" -#: templates/index.html:210 +#: templates/index.html:207 msgid "User login frequency and last login record." msgstr "用户登录次数及最近一次登录记录" -#: templates/index.html:224 -msgid "The last time logged on to the host" -msgstr "最近一次登录主机" - -#: templates/index.html:268 +#: templates/index.html:264 msgid "Monthly data overview" msgstr "月数据总览" -#: templates/index.html:269 +#: templates/index.html:265 msgid "History summary in one month" msgstr "一个月内历史汇总" -#: templates/index.html:277 templates/index.html:301 +#: templates/index.html:273 templates/index.html:297 msgid "Login count" msgstr "登录次数" -#: templates/index.html:277 templates/index.html:308 +#: templates/index.html:273 templates/index.html:304 msgid "Active users" msgstr "活跃用户" -#: templates/index.html:277 templates/index.html:315 +#: templates/index.html:273 templates/index.html:311 msgid "Active assets" msgstr "活跃资产" -#: templates/index.html:342 templates/index.html:392 +#: templates/index.html:338 templates/index.html:388 msgid "Monthly active users" msgstr "月活跃用户" -#: templates/index.html:342 templates/index.html:393 +#: templates/index.html:338 templates/index.html:389 msgid "Disable user" msgstr "禁用用户" -#: templates/index.html:342 templates/index.html:394 +#: templates/index.html:338 templates/index.html:390 msgid "Month not logged in user" msgstr "月未登录用户" -#: templates/index.html:368 templates/index.html:444 +#: templates/index.html:364 templates/index.html:440 msgid "Access to the source" msgstr "访问来源" -#: templates/index.html:418 templates/index.html:468 +#: templates/index.html:414 templates/index.html:464 msgid "Month is logged into the host" msgstr "月被登录主机" -#: templates/index.html:418 templates/index.html:469 +#: templates/index.html:414 templates/index.html:465 msgid "Disable host" msgstr "禁用主机" -#: templates/index.html:418 templates/index.html:470 +#: templates/index.html:414 templates/index.html:466 msgid "Month not logged on host" msgstr "月未登录主机" @@ -4559,16 +4762,34 @@ msgstr "测试成功" msgid "Test failure: Account invalid" msgstr "测试失败: 账户无效" +#: terminal/backends/command/models.py:14 +#: terminal/templates/terminal/command_list.html:110 +#: terminal/templates/terminal/command_list.html:194 +msgid "Ordinary" +msgstr "" + #: terminal/backends/command/models.py:15 +#: terminal/templates/terminal/command_list.html:111 +#: terminal/templates/terminal/command_list.html:191 +msgid "Dangerous" +msgstr "" + +#: terminal/backends/command/models.py:21 msgid "Input" msgstr "输入" -#: terminal/backends/command/models.py:17 -#: terminal/templates/terminal/command_list.html:32 +#: terminal/backends/command/models.py:23 +#: terminal/templates/terminal/command_list.html:33 #: terminal/templates/terminal/terminal_list.html:34 msgid "Session" msgstr "会话" +#: terminal/backends/command/models.py:24 +#: terminal/templates/terminal/command_list.html:29 +#: terminal/templates/terminal/command_list.html:109 +msgid "Risk level" +msgstr "" + #: terminal/forms/storage.py:41 msgid "Container name" msgstr "容器名称" @@ -4624,9 +4845,9 @@ msgid "" " " msgstr "" -#: terminal/forms/storage.py:143 xpack/plugins/cloud/models.py:303 +#: terminal/forms/storage.py:143 xpack/plugins/cloud/models.py:263 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:106 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:59 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:35 msgid "Region" msgstr "地域" @@ -4684,47 +4905,47 @@ msgstr "远端地址" msgid "HTTP Port" msgstr "HTTP端口" -#: terminal/models.py:145 +#: terminal/models.py:154 msgid "Session Online" msgstr "在线会话" -#: terminal/models.py:146 +#: terminal/models.py:155 msgid "CPU Usage" msgstr "CPU使用" -#: terminal/models.py:147 +#: terminal/models.py:156 msgid "Memory Used" msgstr "内存使用" -#: terminal/models.py:148 +#: terminal/models.py:157 msgid "Connections" msgstr "连接数" -#: terminal/models.py:149 +#: terminal/models.py:158 msgid "Threads" msgstr "线程数" -#: terminal/models.py:150 +#: terminal/models.py:159 msgid "Boot Time" msgstr "运行时间" -#: terminal/models.py:185 terminal/templates/terminal/session_list.html:135 +#: terminal/models.py:195 terminal/templates/terminal/session_list.html:135 msgid "Replay" msgstr "回放" -#: terminal/models.py:190 +#: terminal/models.py:200 terminal/templates/terminal/session_detail.html:76 msgid "Date end" msgstr "结束日期" -#: terminal/models.py:283 +#: terminal/models.py:335 msgid "Args" msgstr "参数" -#: terminal/templates/terminal/command_list.html:43 +#: terminal/templates/terminal/command_list.html:44 msgid "Export command" msgstr "导出命令" -#: terminal/templates/terminal/command_list.html:205 +#: terminal/templates/terminal/command_list.html:199 msgid "Goto" msgstr "转到" @@ -4738,40 +4959,52 @@ msgstr "创建命令存储" msgid "Create replay storage" msgstr "创建录像存储" -#: terminal/templates/terminal/session_detail.html:17 -#: terminal/views/session.py:79 +#: terminal/templates/terminal/session_commands.html:18 +#: terminal/templates/terminal/session_detail.html:18 +#: terminal/views/session.py:75 terminal/views/session.py:98 msgid "Session detail" msgstr "会话详情" -#: terminal/templates/terminal/session_detail.html:28 +#: terminal/templates/terminal/session_commands.html:29 +#: terminal/templates/terminal/session_detail.html:29 #: terminal/views/command.py:22 msgid "Command list" msgstr "命令记录列表" -#: terminal/templates/terminal/session_detail.html:67 +#: terminal/templates/terminal/session_commands.html:68 msgid "There is no command about this session" msgstr "该会话没有命令记录" -#: terminal/templates/terminal/session_detail.html:92 +#: terminal/templates/terminal/session_detail.html:64 +#: terminal/templates/terminal/session_list.html:30 +msgid "Login from" +msgstr "登录来源" + +#: terminal/templates/terminal/session_detail.html:94 msgid "Replay session" msgstr "回放会话" -#: terminal/templates/terminal/session_detail.html:101 +#: terminal/templates/terminal/session_detail.html:102 +msgid "Download replay" +msgstr "下载录像" + +#: terminal/templates/terminal/session_detail.html:105 +#: terminal/templates/terminal/session_list.html:137 +msgid "Download" +msgstr "下载" + +#: terminal/templates/terminal/session_detail.html:111 msgid "Monitor session" msgstr "监控" -#: terminal/templates/terminal/session_detail.html:109 +#: terminal/templates/terminal/session_detail.html:119 msgid "Terminate session" msgstr "终止会话" -#: terminal/templates/terminal/session_detail.html:144 +#: terminal/templates/terminal/session_detail.html:161 msgid "Terminate success" msgstr "终断成功" -#: terminal/templates/terminal/session_list.html:30 -msgid "Login from" -msgstr "登录来源" - #: terminal/templates/terminal/session_list.html:33 msgid "Duration" msgstr "时长" @@ -4788,14 +5021,18 @@ msgstr "确认已完成" msgid "Terminate task send, waiting ..." msgstr "终断任务已发送,请等待" -#: terminal/templates/terminal/session_list.html:141 +#: terminal/templates/terminal/session_list.html:143 msgid "Terminate" msgstr "终断" -#: terminal/templates/terminal/session_list.html:172 +#: terminal/templates/terminal/session_list.html:174 msgid "Finish session success" msgstr "标记会话完成成功" +#: terminal/templates/terminal/session_list.html:242 +msgid "Visit doc for replay play offline: " +msgstr "访问文档查看如何离线播放: " + #: terminal/templates/terminal/terminal_detail.html:13 #: terminal/views/terminal.py:62 msgid "Terminal detail" @@ -4839,7 +5076,7 @@ msgstr "接受终端注册" msgid "Info" msgstr "信息" -#: terminal/views/session.py:44 +#: terminal/views/session.py:49 msgid "Session online list" msgstr "在线会话" @@ -5026,12 +5263,7 @@ msgstr "工单详情" #: users/api/user.py:177 msgid "Could not reset self otp, use profile reset instead" -msgstr "不能再该页面重置MFA, 请去个人信息页面重置" - -#: users/forms/group.py:19 users/forms/user.py:143 users/forms/user.py:148 -#: xpack/plugins/orgs/forms.py:17 -msgid "Select users" -msgstr "选择用户" +msgstr "不能在该页面重置多因子认证, 请去个人信息页面重置" #: users/forms/profile.py:37 msgid "" @@ -5039,25 +5271,25 @@ msgid "" "in. you can also directly bind in \"personal information -> quick " "modification -> change MFA Settings\"!" msgstr "" -"启用之后您将会在下次登录时进入MFA绑定流程;您也可以在(个人信息->快速修改->更" -"改MFA设置)中直接绑定!" +"启用之后您将会在下次登录时进入多因子认证绑定流程;您也可以在(个人信息->快速" +"修改->更改多因子设置)中直接绑定!" -#: users/forms/profile.py:47 -msgid "* Enable MFA authentication to make the account more secure." -msgstr "* 启用MFA认证,使账号更加安全。" +#: users/forms/profile.py:48 +msgid "* Enable MFA to make the account more secure." +msgstr "* 启用多因子认证,使账号更加安全。" #: users/forms/profile.py:57 msgid "" "In order to protect you and your company, please keep your account, password " "and key sensitive information properly. (for example: setting complex " -"password, enabling MFA authentication)" +"password, enabling MFA)" msgstr "" "为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:" -"设置复杂密码,启用MFA认证)" +"设置复杂密码,并启用多因子认证)" #: users/forms/profile.py:64 users/templates/users/first_login.html:48 -#: users/templates/users/first_login.html:110 -#: users/templates/users/first_login.html:139 +#: users/templates/users/first_login.html:102 +#: users/templates/users/first_login.html:128 msgid "Finish" msgstr "完成" @@ -5103,11 +5335,11 @@ msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" #: users/forms/profile.py:137 users/forms/user.py:90 -#: users/serializers/user.py:122 +#: users/serializers/user.py:131 msgid "Not a valid ssh public key" msgstr "ssh密钥不合法" -#: users/forms/user.py:27 users/models/user.py:448 +#: users/forms/user.py:27 users/models/user.py:450 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:73 #: users/templates/users/user_list.html:16 @@ -5115,7 +5347,7 @@ msgstr "ssh密钥不合法" msgid "Role" msgstr "角色" -#: users/forms/user.py:31 users/models/user.py:483 +#: users/forms/user.py:31 users/models/user.py:485 #: users/templates/users/user_detail.html:89 #: users/templates/users/user_list.html:18 #: users/templates/users/user_profile.html:102 @@ -5130,7 +5362,8 @@ msgstr "复制用户公钥到这里" msgid "Join user groups" msgstr "添加到用户组" -#: users/forms/user.py:103 users/views/login.py:119 users/views/profile.py:107 +#: users/forms/user.py:103 users/views/login.py:119 +#: users/views/profile/password.py:57 msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" @@ -5142,7 +5375,7 @@ msgstr "生成重置密码链接,通过邮件发送给用户" msgid "Set password" msgstr "设置密码" -#: users/forms/user.py:132 xpack/plugins/change_auth_plan/models.py:88 +#: users/forms/user.py:132 xpack/plugins/change_auth_plan/models.py:77 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:45 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:67 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:57 @@ -5150,7 +5383,7 @@ msgstr "设置密码" msgid "Password strategy" msgstr "密码策略" -#: users/models/user.py:142 users/models/user.py:576 +#: users/models/user.py:142 users/models/user.py:591 msgid "Administrator" msgstr "管理员" @@ -5171,23 +5404,23 @@ msgstr "组织审计员" msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:428 +#: users/models/user.py:429 msgid "Local" msgstr "数据库" -#: users/models/user.py:451 +#: users/models/user.py:453 msgid "Avatar" msgstr "头像" -#: users/models/user.py:454 users/templates/users/user_detail.html:68 +#: users/models/user.py:456 users/templates/users/user_detail.html:68 msgid "Wechat" msgstr "微信" -#: users/models/user.py:487 +#: users/models/user.py:489 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:579 +#: users/models/user.py:594 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -5219,15 +5452,15 @@ msgstr "角色只能为 {}" msgid "Password does not match security rules" msgstr "密码不满足安全规则" -#: users/serializers/user.py:107 +#: users/serializers/user.py:116 msgid "Groups name" msgstr "用户组名" -#: users/serializers/user.py:108 +#: users/serializers/user.py:117 msgid "Source name" msgstr "用户来源名" -#: users/serializers/user.py:109 +#: users/serializers/user.py:118 msgid "Role name" msgstr "角色名" @@ -5241,7 +5474,7 @@ msgstr "安全令牌验证" #: users/templates/users/_base_otp.html:14 users/templates/users/_user.html:13 #: users/templates/users/user_profile_update.html:55 -#: xpack/plugins/cloud/models.py:146 +#: xpack/plugins/cloud/models.py:119 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:57 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:13 msgid "Account" @@ -5295,26 +5528,26 @@ msgstr "更新ssh密钥" msgid "First Login" msgstr "首次登录" -#: users/templates/users/first_login.html:72 +#: users/templates/users/first_login.html:65 msgid "I agree with the terms and conditions." msgstr "我同意条款和条件" -#: users/templates/users/first_login.html:73 +#: users/templates/users/first_login.html:66 msgid "Please choose the terms and conditions." msgstr "请选择同意条款和条件" -#: users/templates/users/first_login.html:77 +#: users/templates/users/first_login.html:70 #: users/templates/users/user_update.html:32 msgid "User auth from {}, ssh key login is not supported" msgstr "用户认证源来自 {}, 不支持使用 SSH Key 登录" -#: users/templates/users/first_login.html:104 +#: users/templates/users/first_login.html:96 msgid "Previous" msgstr "上一步" #: users/templates/users/first_login_done.html:31 msgid "Welcome to use jumpserver, visit " -msgstr "欢迎使用Jumpserver开源跳板机系统" +msgstr "欢迎使用JumpServer开源跳板机系统" #: users/templates/users/first_login_done.html:32 msgid "Use guide" @@ -5324,7 +5557,7 @@ msgstr "向导" msgid " for more information" msgstr "获取更多信息" -#: users/templates/users/forgot_password.html:20 +#: users/templates/users/forgot_password.html:15 msgid "Input your email, that will send a mail to your" msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中" @@ -5411,11 +5644,11 @@ msgstr "最后更新密码" #: users/templates/users/user_detail.html:148 msgid "Force enabled MFA" -msgstr "强制启用MFA" +msgstr "强制启用多因子认证" #: users/templates/users/user_detail.html:165 msgid "Reset MFA" -msgstr "重置MFA" +msgstr "重置多因子认证" #: users/templates/users/user_detail.html:174 msgid "Send reset password mail" @@ -5441,7 +5674,7 @@ msgstr "解除" #: users/templates/users/user_detail.html:365 msgid "Goto profile page enable MFA" -msgstr "请去个人信息页面启用自己的MFA" +msgstr "请去个人信息页面启用自己的多因子认证" #: users/templates/users/user_detail.html:401 msgid "An e-mail has been sent to the user`s mailbox." @@ -5480,16 +5713,27 @@ msgstr "解除用户登录限制后,此用户即可正常登录" #: users/templates/users/user_detail.html:520 msgid "Reset user MFA success" -msgstr "重置用户MFA成功" +msgstr "重置用户多因子认证成功" #: users/templates/users/user_disable_mfa.html:6 -#: users/templates/users/user_password_check.html:6 +#: users/templates/users/user_otp_check_password.html:6 msgid "Authenticate" msgstr "验证身份" -#: users/templates/users/user_disable_mfa.html:32 -msgid "Unbind" -msgstr "解绑 MFA" +#: users/templates/users/user_disable_mfa.html:11 +msgid "" +"The account protection has been opened, please complete the following " +"operations according to the prompts" +msgstr "账号保护已开启,请根据提示完成以下操作" + +#: users/templates/users/user_disable_mfa.html:13 +msgid "Open Authenticator and enter the 6-bit dynamic code" +msgstr "请打开 验证器,输入6位动态码" + +#: users/templates/users/user_disable_mfa.html:23 +#: users/templates/users/user_otp_enable_bind.html:22 +msgid "Six figures" +msgstr "6位数字" #: users/templates/users/user_group_detail.html:17 #: users/templates/users/user_group_granted_asset.html:18 @@ -5506,21 +5750,10 @@ msgstr "添加用户" msgid "Create user group" msgstr "创建用户组" -#: users/templates/users/user_group_list.html:90 -#: users/templates/users/user_list.html:135 -#: users/templates/users/user_profile.html:124 -msgid "User groups" -msgstr "用户组" - #: users/templates/users/user_list.html:32 msgid "Remove selected" msgstr "批量移除" -#: users/templates/users/user_list.html:106 -#: users/templates/users/user_list.html:110 -msgid "Remove" -msgstr "移除" - #: users/templates/users/user_list.html:179 msgid "This will delete the selected users !!!" msgstr "删除选中用户 !!!" @@ -5562,48 +5795,53 @@ msgid "User is inactive" msgstr "用户已禁用" #: users/templates/users/user_otp_enable_bind.html:6 -msgid "Bind" -msgstr "绑定 MFA" +msgid "Bind one-time password authenticator" +msgstr "绑定一次性密码验证器" -#: users/templates/users/user_otp_enable_bind.html:14 +#: users/templates/users/user_otp_enable_bind.html:13 msgid "" "Use the mobile Google Authenticator application to scan the following qr " "code for a 6-bit verification code" msgstr "使用手机 Google Authenticator 应用扫描以下二维码,获取6位验证码" #: users/templates/users/user_otp_enable_install_app.html:6 -msgid "Install" +msgid "Install app" msgstr "安装应用" -#: users/templates/users/user_otp_enable_install_app.html:11 +#: users/templates/users/user_otp_enable_install_app.html:13 msgid "Download and install the Google Authenticator application on your phone" msgstr "请在手机端下载并安装 Google Authenticator 应用" -#: users/templates/users/user_otp_enable_install_app.html:14 +#: users/templates/users/user_otp_enable_install_app.html:18 msgid "Android downloads" msgstr "Android手机下载" -#: users/templates/users/user_otp_enable_install_app.html:19 +#: users/templates/users/user_otp_enable_install_app.html:23 msgid "iPhone downloads" msgstr "iPhone手机下载" -#: users/templates/users/user_otp_enable_install_app.html:22 +#: users/templates/users/user_otp_enable_install_app.html:26 msgid "" "After installation, click the next step to enter the binding page (if " "installed, go to the next step directly)." msgstr "安装完成后点击下一步进入绑定页面(如已安装,直接进入下一步" +#: users/templates/users/user_password_verify.html:8 +#: users/templates/users/user_password_verify.html:9 +msgid "Verify password" +msgstr "校验密码" + #: users/templates/users/user_profile.html:97 msgid "Administrator Settings force MFA login" -msgstr "管理员设置强制使用MFA登录" +msgstr "管理员设置强制使用多因子认证" #: users/templates/users/user_profile.html:156 msgid "Set MFA" -msgstr "设置MFA" +msgstr "设置多因子认证" #: users/templates/users/user_profile.html:178 msgid "Update MFA" -msgstr "更改MFA" +msgstr "更改多因子认证" #: users/templates/users/user_profile.html:188 msgid "Update password" @@ -5891,37 +6129,37 @@ msgstr "Token错误或失效" msgid "First login" msgstr "首次登录" -#: users/views/profile.py:71 +#: users/views/profile/base.py:47 msgid "Profile setting" msgstr "个人信息设置" -#: users/views/profile.py:91 -msgid "Password update" -msgstr "密码更新" - -#: users/views/profile.py:126 -msgid "Public key update" -msgstr "密钥更新" - -#: users/views/profile.py:154 -msgid "Password invalid" -msgstr "用户名或密码无效" - -#: users/views/profile.py:265 +#: users/views/profile/otp.py:130 msgid "MFA enable success" -msgstr "MFA 绑定成功" +msgstr "多因子认证启用成功" -#: users/views/profile.py:266 +#: users/views/profile/otp.py:131 msgid "MFA enable success, return login page" -msgstr "MFA 绑定成功,返回到登录页面" +msgstr "多因子认证启用成功,返回到登录页面" -#: users/views/profile.py:268 +#: users/views/profile/otp.py:133 msgid "MFA disable success" -msgstr "MFA 解绑成功" +msgstr "多因子认证禁用成功" -#: users/views/profile.py:269 +#: users/views/profile/otp.py:134 msgid "MFA disable success, return login page" -msgstr "MFA 解绑成功,返回登录页面" +msgstr "多因子认证禁用成功,返回登录页面" + +#: users/views/profile/password.py:41 +msgid "Password update" +msgstr "密码更新" + +#: users/views/profile/password.py:72 +msgid "Password invalid" +msgstr "用户名或密码无效" + +#: users/views/profile/pubkey.py:37 +msgid "Public key update" +msgstr "密钥更新" #: users/views/user.py:130 msgid "Bulk update user success" @@ -5943,24 +6181,10 @@ msgstr "用户授权远程应用" msgid "User granted DatabaseApp" msgstr "用户授权数据库应用" -#: xpack/plugins/change_auth_plan/forms.py:20 +#: xpack/plugins/change_auth_plan/forms.py:21 msgid "Password length" msgstr "密码长度" -#: xpack/plugins/change_auth_plan/forms.py:75 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:54 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:79 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:17 -#: xpack/plugins/cloud/forms.py:33 xpack/plugins/cloud/forms.py:87 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:37 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:69 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:16 -#: xpack/plugins/gathered_user/forms.py:13 -#: xpack/plugins/gathered_user/forms.py:41 -#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:28 -msgid "Periodic perform" -msgstr "定时执行" - #: xpack/plugins/change_auth_plan/forms.py:79 msgid "" "Tips: The username of the user on the asset to be modified. if the user " @@ -5969,26 +6193,9 @@ msgstr "" "提示:用户名为将要修改的资产上的用户的用户名。如果用户存在,则修改密码;如果" "用户不存在,则创建用户。" -#: xpack/plugins/change_auth_plan/forms.py:83 xpack/plugins/cloud/forms.py:90 -#: xpack/plugins/gathered_user/forms.py:44 -msgid "Tips: (Units: hour)" -msgstr "提示:(单位: 时)" - -#: xpack/plugins/change_auth_plan/forms.py:84 xpack/plugins/cloud/forms.py:91 -#: xpack/plugins/gathered_user/forms.py:45 -msgid "" -"eg: Every Sunday 03:05 run <5 3 * * 0>
    Tips: Using 5 digits linux " -"crontab expressions (Online tools)
    Note: If both Regularly " -"perform and Cycle perform are set, give priority to Regularly perform" -msgstr "" -"eg:每周日 03:05 执行 <5 3 * * 0>
    提示: 使用5位 Linux crontab 表达式 <" -"分 时 日 月 星期> (在线工" -"具
    注意: 如果同时设置了定期执行和周期执行,优先使用定期执行" - #: xpack/plugins/change_auth_plan/meta.py:9 -#: xpack/plugins/change_auth_plan/models.py:116 -#: xpack/plugins/change_auth_plan/models.py:256 +#: xpack/plugins/change_auth_plan/models.py:105 +#: xpack/plugins/change_auth_plan/models.py:202 #: xpack/plugins/change_auth_plan/views.py:33 #: xpack/plugins/change_auth_plan/views.py:50 #: xpack/plugins/change_auth_plan/views.py:74 @@ -6011,57 +6218,37 @@ msgstr "所有资产使用相同的随机密码" msgid "All assets use different random password" msgstr "所有资产使用不同的随机密码" -#: xpack/plugins/change_auth_plan/models.py:78 -#: xpack/plugins/change_auth_plan/models.py:147 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:98 -#: xpack/plugins/cloud/models.py:164 xpack/plugins/cloud/models.py:218 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:88 -#: xpack/plugins/gathered_user/models.py:35 -#: xpack/plugins/gathered_user/models.py:72 -msgid "Cycle perform" -msgstr "周期执行" - -#: xpack/plugins/change_auth_plan/models.py:83 -#: xpack/plugins/change_auth_plan/models.py:145 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:90 -#: xpack/plugins/cloud/models.py:169 xpack/plugins/cloud/models.py:216 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:80 -#: xpack/plugins/gathered_user/models.py:40 -#: xpack/plugins/gathered_user/models.py:70 -msgid "Regularly perform" -msgstr "定期执行" - -#: xpack/plugins/change_auth_plan/models.py:92 +#: xpack/plugins/change_auth_plan/models.py:81 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:72 msgid "Password rules" msgstr "密码规则" -#: xpack/plugins/change_auth_plan/models.py:212 +#: xpack/plugins/change_auth_plan/models.py:158 msgid "* For security, do not change {} user's password" msgstr "* 为了安全,禁止更改 {} 用户的密码" -#: xpack/plugins/change_auth_plan/models.py:216 +#: xpack/plugins/change_auth_plan/models.py:162 msgid "Assets is empty, please add the asset" msgstr "资产为空,请添加资产" -#: xpack/plugins/change_auth_plan/models.py:260 +#: xpack/plugins/change_auth_plan/models.py:206 msgid "Change auth plan snapshot" msgstr "改密计划快照" -#: xpack/plugins/change_auth_plan/models.py:275 -#: xpack/plugins/change_auth_plan/models.py:432 +#: xpack/plugins/change_auth_plan/models.py:221 +#: xpack/plugins/change_auth_plan/models.py:378 msgid "Change auth plan execution" msgstr "改密计划执行" -#: xpack/plugins/change_auth_plan/models.py:441 +#: xpack/plugins/change_auth_plan/models.py:387 msgid "Change auth plan execution subtask" msgstr "改密计划执行子任务" -#: xpack/plugins/change_auth_plan/models.py:459 +#: xpack/plugins/change_auth_plan/models.py:405 msgid "Authentication failed" msgstr "认证失败" -#: xpack/plugins/change_auth_plan/models.py:461 +#: xpack/plugins/change_auth_plan/models.py:407 msgid "Connection timeout" msgstr "连接超时" @@ -6081,11 +6268,6 @@ msgstr "* 请输入正确的密码长度" msgid "* Password length range 6-30 bits" msgstr "* 密码长度范围 6-30 位" -#: xpack/plugins/change_auth_plan/serializers.py:97 -#: xpack/plugins/cloud/serializers.py:73 -msgid "* Please enter a valid crontab expression" -msgstr "* 请输入有效的 crontab 表达式" - #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:19 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:24 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:23 @@ -6123,10 +6305,10 @@ msgstr "执行失败" #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:31 msgid "Execution list of plan" -msgstr "执行列表" +msgstr "执行历史列表" #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:104 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:84 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:96 msgid "Log" msgstr "日志" @@ -6155,35 +6337,37 @@ msgstr "更新计划" msgid "Plan execution task list" msgstr "执行任务列表" -#: xpack/plugins/cloud/api.py:60 xpack/plugins/cloud/providers/base.py:93 +#: xpack/plugins/cloud/api.py:84 xpack/plugins/cloud/utils.py:37 msgid "Account unavailable" msgstr "账户无效" -#: xpack/plugins/cloud/forms.py:14 -msgid "Access Key ID" +#: xpack/plugins/cloud/forms.py:15 +msgid "Access Key" msgstr "" -#: xpack/plugins/cloud/forms.py:18 -msgid "Access Key Secret" -msgstr "" +#: xpack/plugins/cloud/forms.py:19 +#, fuzzy +#| msgid "Secret" +msgid "Secret Key" +msgstr "密文" -#: xpack/plugins/cloud/forms.py:58 +#: xpack/plugins/cloud/forms.py:56 msgid "Select account" msgstr "选择账户" -#: xpack/plugins/cloud/forms.py:64 +#: xpack/plugins/cloud/forms.py:62 msgid "Select regions" msgstr "选择地域" -#: xpack/plugins/cloud/forms.py:70 +#: xpack/plugins/cloud/forms.py:68 msgid "Select instances" msgstr "选择实例" -#: xpack/plugins/cloud/forms.py:76 +#: xpack/plugins/cloud/forms.py:74 msgid "Select node" msgstr "选择节点" -#: xpack/plugins/cloud/forms.py:82 xpack/plugins/orgs/forms.py:20 +#: xpack/plugins/cloud/forms.py:80 xpack/plugins/orgs/forms.py:20 msgid "Select admins" msgstr "选择管理员" @@ -6191,102 +6375,150 @@ msgstr "选择管理员" #: xpack/plugins/cloud/views.py:44 xpack/plugins/cloud/views.py:62 #: xpack/plugins/cloud/views.py:78 xpack/plugins/cloud/views.py:92 #: xpack/plugins/cloud/views.py:109 xpack/plugins/cloud/views.py:127 -#: xpack/plugins/cloud/views.py:143 xpack/plugins/cloud/views.py:159 -#: xpack/plugins/cloud/views.py:211 +#: xpack/plugins/cloud/views.py:143 xpack/plugins/cloud/views.py:158 +#: xpack/plugins/cloud/views.py:172 msgid "Cloud center" msgstr "云管中心" -#: xpack/plugins/cloud/models.py:52 +#: xpack/plugins/cloud/models.py:29 msgid "Available" msgstr "有效" -#: xpack/plugins/cloud/models.py:53 +#: xpack/plugins/cloud/models.py:30 msgid "Unavailable" msgstr "无效" -#: xpack/plugins/cloud/models.py:62 +#: xpack/plugins/cloud/models.py:39 #: xpack/plugins/cloud/templates/cloud/account_detail.html:51 #: xpack/plugins/cloud/templates/cloud/account_list.html:13 msgid "Provider" msgstr "云服务商" -#: xpack/plugins/cloud/models.py:65 +#: xpack/plugins/cloud/models.py:42 msgid "Access key id" msgstr "" -#: xpack/plugins/cloud/models.py:69 +#: xpack/plugins/cloud/models.py:46 msgid "Access key secret" msgstr "" -#: xpack/plugins/cloud/models.py:87 +#: xpack/plugins/cloud/models.py:64 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:26 msgid "Cloud account" msgstr "云账号" -#: xpack/plugins/cloud/models.py:149 +#: xpack/plugins/cloud/models.py:122 msgid "Regions" msgstr "地域" -#: xpack/plugins/cloud/models.py:152 +#: xpack/plugins/cloud/models.py:125 msgid "Instances" msgstr "实例" -#: xpack/plugins/cloud/models.py:175 +#: xpack/plugins/cloud/models.py:139 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:94 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:17 msgid "Date last sync" msgstr "最后同步日期" -#: xpack/plugins/cloud/models.py:186 xpack/plugins/cloud/models.py:270 +#: xpack/plugins/cloud/models.py:150 xpack/plugins/cloud/models.py:207 msgid "Sync instance task" msgstr "同步实例任务" -#: xpack/plugins/cloud/models.py:264 xpack/plugins/cloud/models.py:287 +#: xpack/plugins/cloud/models.py:202 msgid "Succeed" msgstr "成功" -#: xpack/plugins/cloud/models.py:265 -msgid "Partial succeed" -msgstr "" - -#: xpack/plugins/cloud/models.py:280 xpack/plugins/cloud/models.py:312 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:66 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:63 +#: xpack/plugins/cloud/models.py:217 xpack/plugins/cloud/models.py:272 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:41 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:38 msgid "Date sync" msgstr "同步日期" -#: xpack/plugins/cloud/models.py:288 -msgid "Exist" -msgstr "存在" +#: xpack/plugins/cloud/models.py:245 +msgid "Unsync" +msgstr "未同步" + +#: xpack/plugins/cloud/models.py:246 xpack/plugins/cloud/models.py:247 +msgid "Synced" +msgstr "已同步" -#: xpack/plugins/cloud/models.py:293 +#: xpack/plugins/cloud/models.py:248 +msgid "Released" +msgstr "已释放" + +#: xpack/plugins/cloud/models.py:253 msgid "Sync task" msgstr "同步任务" -#: xpack/plugins/cloud/models.py:297 +#: xpack/plugins/cloud/models.py:257 msgid "Sync instance task history" msgstr "同步实例任务历史" -#: xpack/plugins/cloud/models.py:300 +#: xpack/plugins/cloud/models.py:260 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:114 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:58 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:34 msgid "Instance" msgstr "实例" -#: xpack/plugins/cloud/providers/aliyun.py:14 -msgid "Aliyun" +#: xpack/plugins/cloud/providers/aliyun.py:16 +msgid "Alibaba Cloud" msgstr "阿里云" -#: xpack/plugins/cloud/providers/aws.py:11 +#: xpack/plugins/cloud/providers/aws.py:14 +msgid "AWS (International)" +msgstr "AWS (国际)" + +#: xpack/plugins/cloud/providers/aws_china.py:9 msgid "AWS (China)" msgstr "AWS (中国)" -#: xpack/plugins/cloud/providers/aws.py:12 -msgid "AWS (International)" -msgstr "AWS (国际)" +#: xpack/plugins/cloud/providers/huaweicloud.py:13 +msgid "Huawei Cloud" +msgstr "华为云" + +#: xpack/plugins/cloud/providers/huaweicloud.py:16 +msgid "CN North-Beijing4" +msgstr "华北-北京4" + +#: xpack/plugins/cloud/providers/huaweicloud.py:17 +msgid "CN East-Shanghai1" +msgstr "华东-上海1" + +#: xpack/plugins/cloud/providers/huaweicloud.py:18 +msgid "CN East-Shanghai2" +msgstr "华东-上海2" + +#: xpack/plugins/cloud/providers/huaweicloud.py:19 +msgid "CN South-Guangzhou" +msgstr "华南-广州" + +#: xpack/plugins/cloud/providers/huaweicloud.py:20 +msgid "CN Southwest-Guiyang1" +msgstr "西南-贵阳1" + +#: xpack/plugins/cloud/providers/huaweicloud.py:21 +msgid "AP-Hong-Kong" +msgstr "亚太-香港" + +#: xpack/plugins/cloud/providers/huaweicloud.py:22 +msgid "AP-Bangkok" +msgstr "亚太-曼谷" + +#: xpack/plugins/cloud/providers/huaweicloud.py:23 +msgid "AP-Singapore" +msgstr "亚太-新加坡" + +#: xpack/plugins/cloud/providers/huaweicloud.py:24 +msgid "AF-Johannesburg" +msgstr "非洲-约翰内斯堡" + +#: xpack/plugins/cloud/providers/huaweicloud.py:25 +msgid "LA-Santiago" +msgstr "拉美-圣地亚哥" #: xpack/plugins/cloud/providers/qcloud.py:14 -msgid "Qcloud" +msgid "Tencent Cloud" msgstr "腾讯云" #: xpack/plugins/cloud/templates/cloud/account_detail.html:17 @@ -6321,14 +6553,14 @@ msgstr "同步任务详情" #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:20 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:23 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:21 -#: xpack/plugins/cloud/views.py:160 +#: xpack/plugins/cloud/views.py:159 msgid "Sync task history" msgstr "同步历史列表" #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:23 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:26 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:24 -#: xpack/plugins/cloud/views.py:212 +#: xpack/plugins/cloud/views.py:173 msgid "Sync instance list" msgstr "同步实例列表" @@ -6341,21 +6573,21 @@ msgstr "手动执行任务" msgid "Sync success" msgstr "同步成功" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:60 -msgid "Total count" -msgstr "总数" +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:36 +msgid "New count" +msgstr "新增" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:61 -msgid "Succeed count" -msgstr "成功" +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:37 +msgid "Unsync count" +msgstr "未同步" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:62 -msgid "Failed count" -msgstr "失败" +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:38 +msgid "Synced count" +msgstr "已同步" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:63 -msgid "Exist count" -msgstr "存在" +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:39 +msgid "Released count" +msgstr "已释放" #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:5 msgid "Create sync instance task" @@ -6369,8 +6601,6 @@ msgstr "执行次数" msgid "Instance count" msgstr "实例个数" -# msgid "Sync success" -# msgstr "同步成功" #: xpack/plugins/cloud/views.py:63 msgid "Update account" msgstr "更新账户" @@ -6395,25 +6625,20 @@ msgstr "更新同步实例任务" msgid "Gathered user" msgstr "收集用户" -#: xpack/plugins/gathered_user/models.py:33 -#: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:18 -msgid "Periodic" -msgstr "定时执行" - -#: xpack/plugins/gathered_user/models.py:57 +#: xpack/plugins/gathered_user/models.py:39 #: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:13 msgid "Gather user task" msgstr "收集用户任务" -#: xpack/plugins/gathered_user/models.py:137 +#: xpack/plugins/gathered_user/models.py:73 msgid "Task" msgstr "任务" -#: xpack/plugins/gathered_user/models.py:149 +#: xpack/plugins/gathered_user/models.py:85 msgid "gather user task execution" msgstr "收集用户执行" -#: xpack/plugins/gathered_user/models.py:155 +#: xpack/plugins/gathered_user/models.py:91 msgid "Assets is empty, please change nodes" msgstr "资产为空,请更改节点" @@ -6426,6 +6651,10 @@ msgstr "资产用户" msgid "Create task" msgstr "创建任务" +#: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:18 +msgid "Periodic" +msgstr "定时执行" + #: xpack/plugins/gathered_user/views.py:22 msgid "Gathered user list" msgstr "收集用户列表" @@ -6441,8 +6670,8 @@ msgstr "登录页面标题" #: xpack/plugins/interface/forms.py:19 msgid "" "Tips: This will be displayed on the enterprise user login page. (eg: Welcome " -"to the Jumpserver open source fortress)" -msgstr "提示:将会显示在企业版用户登录页面(eg: 欢迎使用Jumpserver开源堡垒机)" +"to the JumpServer open source fortress)" +msgstr "提示:将会显示在企业版用户登录页面(eg: 欢迎使用JumpServer开源堡垒机)" #: xpack/plugins/interface/forms.py:25 xpack/plugins/interface/models.py:19 msgid "Image of login page" @@ -6650,7 +6879,7 @@ msgstr "密码匣子" msgid "Import vault" msgstr "导入密码" -#: xpack/plugins/vault/templates/vault/vault.html:66 +#: xpack/plugins/vault/templates/vault/vault.html:64 msgid "vault" msgstr "密码匣子" @@ -6662,6 +6891,93 @@ msgstr "密码匣子" msgid "vault create" msgstr "创建" +#~ msgid "Exist" +#~ msgstr "已存在" + +#~ msgid "Absent" +#~ msgstr "不存在" + +#~ msgid "Total count" +#~ msgstr "总数" + +#~ msgid "Failed count" +#~ msgstr "失败" + +#~ msgid "Exist count" +#~ msgstr "已存在" + +#~ msgid "Absent count" +#~ msgstr "不存在" + +#~ msgid "Succeed count" +#~ msgstr "成功" + +#~ msgid "Interval" +#~ msgstr "间隔" + +#~ msgid "Crontab" +#~ msgstr "Crontab" + +#~ msgid "5 * * * *" +#~ msgstr "5 * * * *" + +#~ msgid "Aliyun" +#~ msgstr "阿里云" + +#~ msgid "Qcloud" +#~ msgstr "腾讯云" + +#~ msgid "History detail of" +#~ msgstr "执行历史详情" + +#~ msgid "History of " +#~ msgstr "执行历史" + +#~ msgid "Assets count: {}" +#~ msgstr "资产数量" + +#~ msgid "Assets of " +#~ msgstr "资产" + +#~ msgid "MFA authentication failed" +#~ msgstr "多因子认证失败" + +#~ msgid "MFA Secondary certification" +#~ msgstr "MFA 二次认证" + +#~ msgid "Unbind" +#~ msgstr "解绑" + +#~ msgid "Bind" +#~ msgstr "绑定" + +#~ msgid "Have child node, cancel" +#~ msgstr "存在子节点,不能删除" + +#~ msgid "Have assets, cancel" +#~ msgstr "存在资产,不能删除" + +#~ msgid "Add to node" +#~ msgstr "添加到节点" + +#~ msgid "Test ldap success" +#~ msgstr "连接LDAP成功" + +#~ msgid " Top 5 Active user" +#~ msgstr "活跃用户Top5" + +#~ msgid "The last time logged on to the host" +#~ msgstr "最近一次登录主机" + +#~ msgid "No assets matched related system user protocol, stop task" +#~ msgstr "没有匹配到与系统用户协议相关的资产,结束任务" + +#~ msgid "Run history" +#~ msgstr "执行历史" + +#~ msgid "Task run history" +#~ msgstr "执行历史" + #~ msgid "Unlimited" #~ msgstr "无限制" @@ -6708,11 +7024,11 @@ msgstr "创建" #~ msgstr "导入用户" #~ msgid "" -#~ "Jumpserver is an open source desktop system developed using Python and " +#~ "JumpServer is an open source desktop system developed using Python and " #~ "Django that helps Internet businesses with efficient users, assets, " #~ "permissions, and audit management" #~ msgstr "" -#~ "Jumpserver是一款使用Python, Django开发的开源跳板机系统, 助力互联网企业高" +#~ "JumpServer是一款使用Python, Django开发的开源跳板机系统, 助力互联网企业高" #~ "效 用户、资产、权限、审计 管理" #~ msgid "" @@ -6774,9 +7090,6 @@ msgstr "创建" #~ msgid "Create succeed" #~ msgstr "创建成功" -#~ msgid "Delete succeed" -#~ msgstr "删除成功" - #~ msgid "Tips: If there are multiple hosts, separate them with a comma (,)" #~ msgstr "提示: 如果有多台主机,请使用逗号 ( , ) 进行分割" @@ -7044,9 +7357,6 @@ msgstr "创建" #~ msgid "Template" #~ msgstr "模板" -#~ msgid "Download" -#~ msgstr "下载" - #~ msgid "Asset csv file" #~ msgstr "资产csv文件" @@ -7065,9 +7375,6 @@ msgstr "创建" #~ msgid "Beijing Duizhan Tech, Inc." #~ msgstr "北京堆栈科技有限公司" -#~ msgid "Sync User" -#~ msgstr "同步用户" - #~ msgid "" #~ "Import {} users successfully;import {} users failed, Because’TypeError' " #~ "object has no attribute 'keys'" @@ -7076,8 +7383,8 @@ msgstr "创建" #~ msgid "Invalid private key" #~ msgstr "ssh密钥不合法" -#~ msgid "Login Jumpserver" -#~ msgstr "登录 Jumpserver" +#~ msgid "Login JumpServer" +#~ msgstr "登录 JumpServer" #~ msgid "Valid" #~ msgstr "账户状态" @@ -7161,6 +7468,3 @@ msgstr "创建" #~ msgid "Clear" #~ msgstr "清除" - -#~ msgid "MFA setting" -#~ msgstr "MFA 设置" diff --git a/apps/locale/zh/LC_MESSAGES/djangojs.mo b/apps/locale/zh/LC_MESSAGES/djangojs.mo index 24e68caf523648ed333a85c565401a6fbac6a37e..4bd390695a945e5f13744670c777354e7d85cc70 100644 GIT binary patch delta 312 zcmXZWzY76j7zgm@kRO*HB}y1940QG0p`77T$Y8xdGq6mDP<9-({S9Y&x7-NWB7 z4omWX-e9|IAI_0m@;>d^`Ub>PEs2^C`;-6a F{Q~9OCL#a; delta 273 zcmXZWKMnyw6oB!!OV;kjzlh1o5`~h4LM73PBe(!}(Cr1pDRc^Aqfprch)$u=XjN*3 zZ+O*j^1hkOdzhoyx11z$L{^?iQ$#iekve8L#2b<%@c&lT2#d_yIKm!IaG`yx{h*%J z3kKY~V*{VQB)`K)2Zm}xA1gRT`_T+zoMRai?BW{jf+s9thIa8Q*3b?A?~U3~2e_n< N!!&kx`Sk6*a$nFs8a@C3 diff --git a/apps/ops/ansible/runner.py b/apps/ops/ansible/runner.py index b6c6b8ee1..4486bdf1a 100644 --- a/apps/ops/ansible/runner.py +++ b/apps/ops/ansible/runner.py @@ -55,7 +55,7 @@ def get_default_options(): return options -# Jumpserver not use playbook +# JumpServer not use playbook class PlayBookRunner: """ 用于执行AnsiblePlaybook的接口.简化Playbook对象的使用. diff --git a/apps/ops/api/adhoc.py b/apps/ops/api/adhoc.py index 38c59d701..5f7c8318e 100644 --- a/apps/ops/api/adhoc.py +++ b/apps/ops/api/adhoc.py @@ -9,9 +9,9 @@ from django.db.models import Count, Q from common.permissions import IsOrgAdmin from common.serializers import CeleryTaskSerializer from orgs.utils import current_org -from ..models import Task, AdHoc, AdHocRunHistory +from ..models import Task, AdHoc, AdHocExecution from ..serializers import TaskSerializer, AdHocSerializer, \ - AdHocRunHistorySerializer + AdHocExecutionSerializer from ..tasks import run_ansible_task __all__ = [ @@ -28,11 +28,7 @@ class TaskViewSet(viewsets.ModelViewSet): def get_queryset(self): queryset = super().get_queryset() - if current_org.is_real(): - queryset = queryset.filter(created_by=current_org.id) - else: - queryset = queryset.filter(created_by='') - queryset = queryset.select_related('latest_history') + queryset = queryset.select_related('latest_execution') return queryset @@ -61,8 +57,8 @@ class AdHocViewSet(viewsets.ModelViewSet): class AdHocRunHistoryViewSet(viewsets.ModelViewSet): - queryset = AdHocRunHistory.objects.all() - serializer_class = AdHocRunHistorySerializer + queryset = AdHocExecution.objects.all() + serializer_class = AdHocExecutionSerializer permission_classes = (IsOrgAdmin,) def get_queryset(self): diff --git a/apps/ops/api/command.py b/apps/ops/api/command.py index f4deecc1a..48ead8f30 100644 --- a/apps/ops/api/command.py +++ b/apps/ops/api/command.py @@ -8,7 +8,7 @@ from django.conf import settings from orgs.mixins.api import RootOrgViewMixin from common.permissions import IsValidUser -from perms.utils import AssetPermissionUtilV2 +from perms.utils import AssetPermissionUtil from ..models import CommandExecution from ..serializers import CommandExecutionSerializer from ..tasks import run_command_execution @@ -27,7 +27,7 @@ class CommandExecutionViewSet(RootOrgViewMixin, viewsets.ModelViewSet): data = serializer.validated_data assets = data["hosts"] system_user = data["run_as"] - util = AssetPermissionUtilV2(self.request.user) + util = AssetPermissionUtil(self.request.user) util.filter_permissions(system_users=system_user.id) permed_assets = util.get_assets().filter(id__in=[a.id for a in assets]) invalid_assets = set(assets) - set(permed_assets) diff --git a/apps/ops/inventory.py b/apps/ops/inventory.py index ffd7e8619..59179b5c7 100644 --- a/apps/ops/inventory.py +++ b/apps/ops/inventory.py @@ -72,8 +72,8 @@ class JMSBaseInventory(BaseInventory): class JMSInventory(JMSBaseInventory): """ - JMS Inventory is the manager with jumpserver assets, so you can - write you own manager, construct you inventory, + JMS Inventory is the inventory with jumpserver assets, so you can + write you own inventory, construct you inventory, user_info is obtained from admin_user or asset_user """ def __init__(self, assets, run_as_admin=False, run_as=None, become_info=None): @@ -110,7 +110,7 @@ class JMSInventory(JMSBaseInventory): try: asset = self.assets.get(id=host.get('id')) manager = AssetUserManager() - run_user = manager.get(self.run_as, asset) + run_user = manager.get_latest(username=self.run_as, asset=asset) except Exception as e: logger.error(e, exc_info=True) return {} @@ -120,7 +120,7 @@ class JMSInventory(JMSBaseInventory): class JMSCustomInventory(JMSBaseInventory): """ - JMS Custom Inventory is the manager with jumpserver assets, + JMS Custom Inventory is the inventory with jumpserver assets, user_info is obtained from custom parameter """ diff --git a/apps/ops/migrations/0011_auto_20200106_1534.py b/apps/ops/migrations/0011_auto_20200106_1534.py new file mode 100644 index 000000000..0dba0160e --- /dev/null +++ b/apps/ops/migrations/0011_auto_20200106_1534.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.7 on 2020-01-06 07:34 + +import common.fields.model +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0010_auto_20191217_1758'), + ] + + operations = [ + migrations.AlterField( + model_name='adhoc', + name='become', + field=common.fields.model.EncryptJsonDictCharField(blank=True, default='', max_length=1024, null=True, verbose_name='Become'), + ), + ] diff --git a/apps/ops/migrations/0012_auto_20200108_1659.py b/apps/ops/migrations/0012_auto_20200108_1659.py new file mode 100644 index 000000000..d12bf4576 --- /dev/null +++ b/apps/ops/migrations/0012_auto_20200108_1659.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.7 on 2020-01-08 08:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0011_auto_20200106_1534'), + ] + + operations = [ + migrations.RenameField( + model_name='task', + old_name='created_by', + new_name='org_id', + ), + migrations.AlterUniqueTogether( + name='task', + unique_together={('name', 'org_id')}, + ), + ] diff --git a/apps/ops/migrations/0013_auto_20200108_1706.py b/apps/ops/migrations/0013_auto_20200108_1706.py new file mode 100644 index 000000000..e6ce99562 --- /dev/null +++ b/apps/ops/migrations/0013_auto_20200108_1706.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.7 on 2020-01-08 09:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0012_auto_20200108_1659'), + ] + + operations = [ + migrations.AddField( + model_name='adhoc', + name='org_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), + ), + migrations.AddField( + model_name='adhocrunhistory', + name='org_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), + ), + migrations.AlterField( + model_name='task', + name='org_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), + ), + ] diff --git a/apps/ops/migrations/0014_auto_20200108_1749.py b/apps/ops/migrations/0014_auto_20200108_1749.py new file mode 100644 index 000000000..a9b184e5c --- /dev/null +++ b/apps/ops/migrations/0014_auto_20200108_1749.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.7 on 2020-01-08 09:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0013_auto_20200108_1706'), + ] + + operations = [ + migrations.RenameModel( + old_name='AdHocRunHistory', + new_name='AdHocExecution', + ), + ] diff --git a/apps/ops/migrations/0015_auto_20200108_1809.py b/apps/ops/migrations/0015_auto_20200108_1809.py new file mode 100644 index 000000000..200d9b7e4 --- /dev/null +++ b/apps/ops/migrations/0015_auto_20200108_1809.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.7 on 2020-01-08 10:09 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0014_auto_20200108_1749'), + ] + + operations = [ + migrations.RenameField( + model_name='task', + old_name='latest_history', + new_name='latest_execution', + ), + migrations.AlterField( + model_name='adhocexecution', + name='adhoc', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='execution', to='ops.AdHoc'), + ), + migrations.AlterField( + model_name='adhocexecution', + name='task', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='execution', to='ops.Task'), + ), + migrations.AlterModelTable( + name='adhocexecution', + table='ops_adhoc_execution', + ), + ] diff --git a/apps/ops/migrations/0016_commandexecution_org_id.py b/apps/ops/migrations/0016_commandexecution_org_id.py new file mode 100644 index 000000000..f034a4f90 --- /dev/null +++ b/apps/ops/migrations/0016_commandexecution_org_id.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-02-27 09:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0015_auto_20200108_1809'), + ] + + operations = [ + migrations.AddField( + model_name='commandexecution', + name='org_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), + ), + ] diff --git a/apps/ops/migrations/0017_auto_20200306_1747.py b/apps/ops/migrations/0017_auto_20200306_1747.py new file mode 100644 index 000000000..8eb64257c --- /dev/null +++ b/apps/ops/migrations/0017_auto_20200306_1747.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.10 on 2020-03-06 09:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0016_commandexecution_org_id'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='crontab', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform'), + ), + migrations.AlterField( + model_name='task', + name='interval', + field=models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform'), + ), + ] diff --git a/apps/ops/mixin.py b/apps/ops/mixin.py new file mode 100644 index 000000000..d3d397220 --- /dev/null +++ b/apps/ops/mixin.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +# +import abc +import uuid +from django.utils.translation import ugettext_lazy as _ +from django.db import models +from django import forms +from rest_framework import serializers + +from .celery.utils import ( + create_or_update_celery_periodic_tasks, disable_celery_periodic_task, + delete_celery_periodic_task, +) + +__all__ = [ + 'PeriodTaskModelMixin', 'PeriodTaskSerializerMixin', + 'PeriodTaskFormMixin', +] + + +class PeriodTaskModelMixin(models.Model): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + name = models.CharField( + max_length=128, unique=False, verbose_name=_("Name") + ) + is_periodic = models.BooleanField(default=False) + interval = models.IntegerField( + default=24, null=True, blank=True, + verbose_name=_("Cycle perform"), + ) + crontab = models.CharField( + null=True, blank=True, max_length=128, + verbose_name=_("Regularly perform"), + ) + + @abc.abstractmethod + def get_register_task(self): + period_name, task, args, kwargs = None, None, (), {} + return period_name, task, args, kwargs + + @property + def interval_ratio(self): + return 3600, 'h' + + @property + def interval_display(self): + unit = self.interval_ratio[1] + if not self.interval: + return '' + return '{} {}'.format(self.interval, unit) + + def set_period_schedule(self): + name, task, args, kwargs = self.get_register_task() + if not self.is_periodic: + disable_celery_periodic_task(name) + return + + crontab = interval = None + if self.crontab: + crontab = self.crontab + elif self.interval: + interval = self.interval * self.interval_ratio[0] + + tasks = { + name: { + 'task': task, + 'interval': interval, + 'crontab': crontab, + 'args': args, + 'kwargs': kwargs, + 'enabled': True, + } + } + create_or_update_celery_periodic_tasks(tasks) + + def save(self, **kwargs): + instance = super().save(**kwargs) + self.set_period_schedule() + return instance + + def delete(self, using=None, keep_parents=False): + name = self.get_register_task()[0] + instance = super().delete(using=using, keep_parents=keep_parents) + delete_celery_periodic_task(name) + return instance + + @property + def periodic_display(self): + if self.is_periodic and self.crontab: + return _('Regularly perform') + " ( {} )".format(self.crontab) + if self.is_periodic and self.interval: + return _('Cycle perform') + " ( {} h )".format(self.interval) + return '-' + + @property + def schedule(self): + from django_celery_beat.models import PeriodicTask + try: + return PeriodicTask.objects.get(name=str(self)) + except PeriodicTask.DoesNotExist: + return None + + class Meta: + abstract = True + + +class PeriodTaskSerializerMixin(serializers.Serializer): + is_periodic = serializers.BooleanField(default=False, label=_("Periodic perform")) + crontab = serializers.CharField( + max_length=128, allow_blank=True, + allow_null=True, required=False, label=_('Regularly perform') + ) + interval = serializers.IntegerField(allow_null=True, required=False) + + INTERVAL_MAX = 65535 + INTERVAL_MIN = 1 + + def validate_crontab(self, crontab): + if not crontab: + return crontab + if isinstance(crontab, str) and len(crontab.strip().split()) != 5: + msg = _('* Please enter a valid crontab expression') + raise serializers.ValidationError(msg) + return crontab + + def validate_interval(self, interval): + if not interval: + return interval + msg = _("Range {} to {}").format(self.INTERVAL_MIN, self.INTERVAL_MAX) + if interval > self.INTERVAL_MAX or interval < self.INTERVAL_MIN: + raise serializers.ValidationError(msg) + return interval + + def validate_is_periodic(self, ok): + if not ok: + return ok + crontab = self.initial_data.get('crontab') + interval = self.initial_data.get('interval') + if ok and not any([crontab, interval]): + msg = _("Require periodic or regularly perform setting") + raise serializers.ValidationError(msg) + return ok + + +class PeriodTaskFormMixin(forms.Form): + is_periodic = forms.BooleanField( + initial=True, required=False, label=_('Periodic perform') + ) + crontab = forms.CharField( + max_length=128, required=False, label=_('Regularly perform'), + help_text=_("eg: Every Sunday 03:05 run <5 3 * * 0>
    " + "Tips: " + "Using 5 digits linux crontab expressions " + " " + "(Online tools)
    " + "Note: " + "If both Regularly perform and Cycle perform are set, " + "give priority to Regularly perform"), + ) + interval = forms.IntegerField( + required=False, initial=24, + help_text=_('Tips: (Units: hour)'), label=_("Cycle perform"), + ) + + def get_initial_for_field(self, field, field_name): + """ + Return initial data for field on form. Use initial data from the form + or the field, in that order. Evaluate callable values. + """ + if field_name not in ['is_periodic', 'crontab', 'interval']: + return super().get_initial_for_field(field, field_name) + instance = getattr(self, 'instance', None) + if instance is None: + return super().get_initial_for_field(field, field_name) + init_attr_name = field_name + '_initial' + value = getattr(self, init_attr_name, None) + if value is None: + return super().get_initial_for_field(field, field_name) + return value + + diff --git a/apps/ops/models/adhoc.py b/apps/ops/models/adhoc.py index 73dfd4339..01d1e1adb 100644 --- a/apps/ops/models/adhoc.py +++ b/apps/ops/models/adhoc.py @@ -10,45 +10,35 @@ from django.db import models from django.conf import settings from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from django_celery_beat.models import PeriodicTask from common.utils import get_logger, lazyproperty from common.fields.model import ( JsonListTextField, JsonDictCharField, EncryptJsonDictCharField, JsonDictTextField, ) -from orgs.utils import set_to_root_org, get_current_org, set_current_org -from ..celery.utils import ( - delete_celery_periodic_task, create_or_update_celery_periodic_tasks, - disable_celery_periodic_task -) +from orgs.mixins.models import OrgModelMixin from ..ansible import AdHocRunner, AnsibleError from ..inventory import JMSInventory +from ..mixin import PeriodTaskModelMixin -__all__ = ["Task", "AdHoc", "AdHocRunHistory"] +__all__ = ["Task", "AdHoc", "AdHocExecution"] logger = get_logger(__file__) -class Task(models.Model): +class Task(PeriodTaskModelMixin, OrgModelMixin): """ This task is different ansible task, Task like 'push system user', 'get asset info' .. One task can have some versions of adhoc, run a task only run the latest version adhoc """ - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - name = models.CharField(max_length=128, verbose_name=_('Name')) - interval = models.IntegerField(verbose_name=_("Interval"), null=True, blank=True, help_text=_("Units: seconds")) - crontab = models.CharField(verbose_name=_("Crontab"), null=True, blank=True, max_length=128, help_text=_("5 * * * *")) - is_periodic = models.BooleanField(default=False) callback = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Callback")) # Callback must be a registered celery task is_deleted = models.BooleanField(default=False) comment = models.TextField(blank=True, verbose_name=_("Comment")) - created_by = models.CharField(max_length=128, blank=True, default='') date_created = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name=_("Date created")) date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated")) latest_adhoc = models.ForeignKey('ops.AdHoc', on_delete=models.SET_NULL, null=True, related_name='task_latest') - latest_history = models.ForeignKey('ops.AdHocRunHistory', on_delete=models.SET_NULL, null=True, related_name='task_latest') + latest_execution = models.ForeignKey('ops.AdHocExecution', on_delete=models.SET_NULL, null=True, related_name='task_latest') total_run_amount = models.IntegerField(default=0) success_run_amount = models.IntegerField(default=0) _ignore_auto_created_by = True @@ -63,29 +53,29 @@ class Task(models.Model): @property def is_success(self): - if self.latest_history: - return self.latest_history.is_success + if self.latest_execution: + return self.latest_execution.is_success else: return False @property def timedelta(self): - if self.latest_history: - return self.latest_history.timedelta + if self.latest_execution: + return self.latest_execution.timedelta else: return 0 @property def date_start(self): - if self.latest_history: - return self.latest_history.date_start + if self.latest_execution: + return self.latest_execution.date_start else: return None @property def assets_amount(self): - if self.latest_history: - return self.latest_history.hosts_amount + if self.latest_execution: + return self.latest_execution.hosts_amount return 0 def get_latest_adhoc(self): @@ -106,8 +96,8 @@ class Task(models.Model): failed = total - success return {'total': total, 'success': success, 'failed': failed} - def get_run_history(self): - return self.history.all() + def get_run_execution(self): + return self.execution.all() def run(self): latest_adhoc = self.get_latest_adhoc() @@ -116,58 +106,29 @@ class Task(models.Model): else: return {'error': 'No adhoc'} - def register_as_period_task(self): - from ..tasks import run_ansible_task - interval = None - crontab = None - - if self.interval: - interval = self.interval - elif self.crontab: - crontab = self.crontab - - tasks = { - self.__str__(): { - "task": run_ansible_task.name, - "interval": interval, - "crontab": crontab, - "args": (str(self.id),), - "kwargs": {"callback": self.callback}, - "enabled": True, - } - } - create_or_update_celery_periodic_tasks(tasks) - - def save(self, **kwargs): - instance = super().save(**kwargs) - if self.is_periodic: - self.register_as_period_task() - else: - disable_celery_periodic_task(self.__str__()) - return instance - - def delete(self, using=None, keep_parents=False): - super().delete(using=using, keep_parents=keep_parents) - delete_celery_periodic_task(self.__str__()) - @property - def schedule(self): - try: - return PeriodicTask.objects.get(name=str(self)) - except PeriodicTask.DoesNotExist: - return None + def period_key(self): + return self.__str__() + + def get_register_task(self): + from ..tasks import run_ansible_task + name = self.__str__() + task = run_ansible_task.name + args = (str(self.id),) + kwargs = {"callback": self.callback} + return name, task, args, kwargs def __str__(self): - return self.name + '@' + str(self.created_by) + return self.name + '@' + str(self.org_id) class Meta: db_table = 'ops_task' - unique_together = ('name', 'created_by') + unique_together = ('name', 'org_id') ordering = ('-date_updated',) get_latest_by = 'date_created' -class AdHoc(models.Model): +class AdHoc(OrgModelMixin): """ task: A task reference _tasks: [{'name': 'task_name', 'action': {'module': '', 'args': ''}, 'other..': ''}, ] @@ -185,7 +146,7 @@ class AdHoc(models.Model): hosts = models.ManyToManyField('assets.Asset', verbose_name=_("Host")) run_as_admin = models.BooleanField(default=False, verbose_name=_('Run as admin')) run_as = models.CharField(max_length=64, default='', blank=True, null=True, verbose_name=_('Username')) - become = EncryptJsonDictCharField(max_length=1024, default='', null=True, blank=True, verbose_name=_("Become")) + become = EncryptJsonDictCharField(max_length=1024, default='', blank=True, null=True, verbose_name=_("Become")) created_by = models.CharField(max_length=64, default='', blank=True, null=True, verbose_name=_('Create by')) date_created = models.DateTimeField(auto_now_add=True, db_index=True) @@ -217,22 +178,23 @@ class AdHoc(models.Model): hid = current_task.request.id except AttributeError: hid = str(uuid.uuid4()) - history = AdHocRunHistory( + execution = AdHocExecution( id=hid, adhoc=self, task=self.task, - task_display=str(self.task) + task_display=str(self.task), + date_start=timezone.now(), ) - history.save() - return history.start() + execution.save() + return execution.start() @property def short_id(self): return str(self.id).split('-')[-1] @property - def latest_history(self): + def latest_execution(self): try: - return self.history.all().latest() - except AdHocRunHistory.DoesNotExist: + return self.execution.all().latest() + except AdHocExecution.DoesNotExist: return None def save(self, **kwargs): @@ -261,15 +223,15 @@ class AdHoc(models.Model): get_latest_by = 'date_created' -class AdHocRunHistory(models.Model): +class AdHocExecution(OrgModelMixin): """ AdHoc running history. """ id = models.UUIDField(default=uuid.uuid4, primary_key=True) - task = models.ForeignKey(Task, related_name='history', on_delete=models.SET_NULL, null=True) + task = models.ForeignKey(Task, related_name='execution', on_delete=models.SET_NULL, null=True) task_display = models.CharField(max_length=128, blank=True, default='', verbose_name=_("Task display")) hosts_amount = models.IntegerField(default=0, verbose_name=_("Host amount")) - adhoc = models.ForeignKey(AdHoc, related_name='history', on_delete=models.SET_NULL, null=True) + adhoc = models.ForeignKey(AdHoc, related_name='execution', on_delete=models.SET_NULL, null=True) date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Start time')) date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('End time')) timedelta = models.FloatField(default=0.0, verbose_name=_('Time'), null=True) @@ -308,13 +270,9 @@ class AdHocRunHistory(models.Model): return {}, {} def start(self): - self.task.latest_history = self + self.task.latest_execution = self self.task.save() - current_org = get_current_org() - set_to_root_org() time_start = time.time() - date_start = timezone.now() - is_success = False summary = {} raw = '' @@ -322,31 +280,32 @@ class AdHocRunHistory(models.Model): date_start_s = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') print(_("{} Start task: {}").format(date_start_s, self.task.name)) raw, summary = self.start_runner() - is_success = summary.get('success', False) except Exception as e: logger.error(e, exc_info=True) raw = {"dark": {"all": str(e)}, "contacted": []} finally: + self.clean_up(summary, time_start) date_end = timezone.now() date_end_s = date_end.strftime('%Y-%m-%d %H:%M:%S') print(_("{} Task finish").format(date_end_s)) print('.\n\n.') - task = Task.objects.get(id=self.task_id) - task.total_run_amount = models.F('total_run_amount') + 1 - if is_success: - task.success_run_amount = models.F('success_run_amount') + 1 - task.save() - AdHocRunHistory.objects.filter(id=self.id).update( - date_start=date_start, - is_finished=True, - is_success=is_success, - date_finished=timezone.now(), - timedelta=time.time() - time_start, - summary=summary - ) - set_current_org(current_org) return raw, summary + def clean_up(self, summary, time_start): + is_success = summary.get('success', False) + task = Task.objects.get(id=self.task_id) + task.total_run_amount = models.F('total_run_amount') + 1 + if is_success: + task.success_run_amount = models.F('success_run_amount') + 1 + task.save() + AdHocExecution.objects.filter(id=self.id).update( + is_finished=True, + is_success=is_success, + date_finished=timezone.now(), + timedelta=time.time() - time_start, + summary=summary + ) + @property def success_hosts(self): return self.summary.get('contacted', []) @@ -359,5 +318,5 @@ class AdHocRunHistory(models.Model): return self.short_id class Meta: - db_table = "ops_adhoc_history" + db_table = "ops_adhoc_execution" get_latest_by = 'date_start' diff --git a/apps/ops/models/command.py b/apps/ops/models/command.py index 9ce44e20f..53aeff2b9 100644 --- a/apps/ops/models/command.py +++ b/apps/ops/models/command.py @@ -11,11 +11,12 @@ from django.db import models from orgs.models import Organization +from orgs.mixins.models import OrgModelMixin from ..ansible.runner import CommandRunner from ..inventory import JMSInventory -class CommandExecution(models.Model): +class CommandExecution(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) hosts = models.ManyToManyField('assets.Asset') run_as = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE) @@ -80,6 +81,7 @@ class CommandExecution(models.Model): msg = _("Command `{}` is forbidden ........").format(self.command) print('\033[31m' + msg + '\033[0m') self.result = {"error": msg} + self.org_id = self.run_as.org_id self.is_finished = True self.date_finished = timezone.now() self.save() diff --git a/apps/ops/serializers/adhoc.py b/apps/ops/serializers/adhoc.py index 6a3464654..60fd39f90 100644 --- a/apps/ops/serializers/adhoc.py +++ b/apps/ops/serializers/adhoc.py @@ -3,14 +3,14 @@ from __future__ import unicode_literals from rest_framework import serializers from django.shortcuts import reverse -from ..models import Task, AdHoc, AdHocRunHistory, CommandExecution +from ..models import Task, AdHoc, AdHocExecution, CommandExecution -class AdHocRunHistorySerializer(serializers.ModelSerializer): +class AdHocExecutionSerializer(serializers.ModelSerializer): stat = serializers.SerializerMethodField() class Meta: - model = AdHocRunHistory + model = AdHocExecution fields = '__all__' @staticmethod @@ -31,7 +31,7 @@ class AdHocRunHistorySerializer(serializers.ModelSerializer): return fields -class AdHocRunHistoryExcludeResultSerializer(AdHocRunHistorySerializer): +class AdHocExecutionExcludeResultSerializer(AdHocExecutionSerializer): def get_field_names(self, declared_fields, info): fields = super().get_field_names(declared_fields, info) fields = [i for i in fields if i not in ['result', 'summary']] @@ -39,18 +39,18 @@ class AdHocRunHistoryExcludeResultSerializer(AdHocRunHistorySerializer): class TaskSerializer(serializers.ModelSerializer): - latest_history = AdHocRunHistoryExcludeResultSerializer(read_only=True) + latest_execution = AdHocExecutionExcludeResultSerializer(read_only=True) class Meta: model = Task fields = [ 'id', 'name', 'interval', 'crontab', 'is_periodic', - 'is_deleted', 'comment', 'created_by', 'date_created', - 'date_updated', 'latest_history', + 'is_deleted', 'comment', 'date_created', + 'date_updated', 'latest_execution', ] read_only_fields = [ - 'is_deleted', 'created_by', 'date_created', 'date_updated', - 'latest_adhoc', 'latest_history', 'total_run_amount', + 'is_deleted', 'date_created', 'date_updated', + 'latest_adhoc', 'latest_execution', 'total_run_amount', 'success_run_amount', ] @@ -63,11 +63,11 @@ class AdHocSerializer(serializers.ModelSerializer): fields = [ "id", "task", 'tasks', "pattern", "options", "hosts", "run_as_admin", "run_as", "become", - "created_by", "date_created", "short_id", + "date_created", "short_id", "become_display", ] read_only_fields = [ - 'created_by', 'date_created' + 'date_created' ] extra_kwargs = { "become": {'write_only': True} diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index 9746fdbe7..33d7cb850 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -67,7 +67,7 @@ def clean_tasks_adhoc_period(): for task in tasks: adhoc = task.adhoc.all().order_by('-date_created')[5:] for ad in adhoc: - ad.history.all().delete() + ad.execution.all().delete() ad.delete() @@ -75,17 +75,11 @@ def clean_tasks_adhoc_period(): @after_app_shutdown_clean_periodic @register_as_period_task(interval=3600*24, description=_("Clean celery log period")) def clean_celery_tasks_period(): - expire_days = 30 + expire_days = settings.TASK_LOG_KEEP_DAYS logger.debug("Start clean celery task history") one_month_ago = timezone.now() - timezone.timedelta(days=expire_days) tasks = CeleryTask.objects.filter(date_start__lt=one_month_ago) - for task in tasks: - if os.path.isfile(task.full_log_path): - try: - os.remove(task.full_log_path) - except (FileNotFoundError, PermissionError): - pass - task.delete() + tasks.delete() tasks = CeleryTask.objects.filter(date_start__isnull=True) tasks.delete() command = "find %s -mtime +%s -name '*.log' -type f -exec rm -f {} \\;" % ( @@ -108,13 +102,15 @@ def create_or_update_registered_periodic_tasks(): @register_as_period_task(interval=3600) def check_server_performance_period(): usages = get_disk_usage() - usages = {path: usage for path, usage in usages.items() - if not path.startswith('/etc')} + uncheck_paths = ['/etc', '/boot'] for path, usage in usages.items(): - if usage.percent > 80: + need_check = True + for uncheck_path in uncheck_paths: + if path.startswith(uncheck_path): + need_check = False + if need_check and usage.percent > 80: send_server_performance_mail(path, usage, usages) - return @shared_task(queue="ansible") diff --git a/apps/ops/templates/ops/adhoc_detail.html b/apps/ops/templates/ops/adhoc_detail.html index 777207da4..1a28ee0e8 100644 --- a/apps/ops/templates/ops/adhoc_detail.html +++ b/apps/ops/templates/ops/adhoc_detail.html @@ -17,7 +17,7 @@ {% trans 'Version detail' %}
  • - {% trans 'Version run history' %} + {% trans 'Version run execution' %}
  • @@ -73,7 +73,7 @@ {% else %} {% trans 'Run as' %}: - {{ object.get_latest_history.date_start }} + {{ object.get_latest_execution.date_start }} {% endif %} @@ -90,23 +90,23 @@ {% trans 'Run times' %}: - {{ object.history.all | length }} + {{ object.execution.all | length }} {% trans 'Last run' %}: - {{ object.latest_history.short_id }} + {{ object.latest_execution.short_id }} {% trans 'Time delta' %}: - {{ object.latest_history.timedelta|floatformat}} s + {{ object.latest_execution.timedelta|floatformat}} s {% trans 'Is finished' %}: - {{ object.latest_history.is_finished|yesno:"Yes,No,Unkown" }} + {{ object.latest_execution.is_finished|yesno:"Yes,No,Unkown" }} {% trans 'Is success ' %}: - {{ object.latest_history.is_success|yesno:"Yes,No,Unkown" }} + {{ object.latest_execution.is_success|yesno:"Yes,No,Unkown" }} {% trans 'Tasks' %}: @@ -131,7 +131,7 @@
    - {% for host, task in object.latest_history.failed_hosts.items %} + {% for host, task in object.latest_execution.failed_hosts.items %} {% if forloop.first %} {% else %} @@ -161,7 +161,7 @@
    - {% for host in object.latest_history.success_hosts %} + {% for host in object.latest_execution.success_hosts %} {% if forloop.first %} {% else %} @@ -189,12 +189,12 @@ {% endblock %} diff --git a/apps/ops/templates/ops/adhoc_history.html b/apps/ops/templates/ops/adhoc_history.html index 8272279d4..e9662cb45 100644 --- a/apps/ops/templates/ops/adhoc_history.html +++ b/apps/ops/templates/ops/adhoc_history.html @@ -17,7 +17,7 @@ {% trans 'Version detail' %}
  • - {% trans 'Version run history' %} + {% trans 'Version run execution' %}
  • @@ -25,7 +25,7 @@
    - {% trans 'History of ' %} {{ object.task.name }}:{{ object.short_id }} + {% trans 'Executions of ' %} {{ object.task.name }}:{{ object.short_id }}
    -
    +
    - + - + + + + + {% else %} @@ -127,36 +137,75 @@ $ {{ command.input }} {% endblock %} {% block custom_foot_js %} - - + + + + } + + $(document).ready(function () { + l = $('.btn-download').ladda(); + l.click(function () { + l.ladda('start'); + interval = setInterval(getReplayDetail, 2000) + }); + var downloadDirect = getUrlParam('download'); + if (downloadDirect) { + $('.btn-download').trigger('click'); + } + }).on('click', '.btn-term', function () { + var $this = $(this); + var session_id = $this.attr('value'); + var terminal_id = $this.attr('terminal'); + alert(session_id); + var data = { + name: "kill_session", + args: session_id, + terminal: terminal_id + }; + terminateSession(data) + }) + {% endblock %} diff --git a/apps/terminal/templates/terminal/session_list.html b/apps/terminal/templates/terminal/session_list.html index eee137084..6b50bb9db 100644 --- a/apps/terminal/templates/terminal/session_list.html +++ b/apps/terminal/templates/terminal/session_list.html @@ -132,11 +132,13 @@ function initTable() { }}, {targets: 11, createdCell: function (td, cellData, rowData) { var btnGroup = ""; - var replayBtn = '{% trans "Replay" %}' - {#var replayBtn = '{% trans "Replay" %}'#} + var replayBtn = '{% trans "Replay" %}'; replayBtn = replayBtn.replace("sessionID", rowData.id); + var downloadBtn = ' {% trans "Download" %}'; + downloadBtn = downloadBtn.replace("sessionID", rowData.id); if (rowData.can_replay) { - replayBtn = replayBtn.replace("disabled", "") + replayBtn = replayBtn.replace("disabled", ""); + downloadBtn = downloadBtn.replace("disabled", ""); } var termBtn = '{% trans "Terminate" %}'; if ("{{ request.user.can_admin_current_org }}" === "True") { @@ -145,7 +147,7 @@ function initTable() { .replace("terminalID", rowData.terminal) } if (rowData.is_finished) { - btnGroup += replayBtn + btnGroup += replayBtn + downloadBtn } else { btnGroup += termBtn; } @@ -231,6 +233,19 @@ $(document).ready(function() { var replayUrl = "/luna/replay/" + sessionID; window.open(replayUrl, "height=600, width=800, top=400, left=400, toolbar=no, menubar=no, scrollbars=no, location=no, status=no"); }) +.on('click', '.btn-download', function () { + var sessionID = $(this).data("session"); + var downloadUrl = "{% url 'terminal:session-replay-download' pk=DEFAULT_PK %}" + .replace("{{ DEFAULT_PK }}", sessionID); + var hasConfirm = getCookie('replayConfirm'); + if (!hasConfirm) { + var help_text = "{% trans "Visit doc for replay play offline: " %}"; + help_text += "http://docs.jumpserver.org"; + var r = confirm(help_text); + setCookie("replayConfirm", "1") + } + window.open(downloadUrl) +}) .on("click", '#session_table_filter input', function (e) { e.preventDefault(); e.stopPropagation(); diff --git a/apps/terminal/templates/terminal/terminal_detail.html b/apps/terminal/templates/terminal/terminal_detail.html index ea0ba6614..14aff902c 100644 --- a/apps/terminal/templates/terminal/terminal_detail.html +++ b/apps/terminal/templates/terminal/terminal_detail.html @@ -38,33 +38,33 @@
    @@ -74,7 +74,7 @@ {% endblock %} diff --git a/apps/ops/templates/ops/task_detail.html b/apps/ops/templates/ops/task_detail.html index d2e46cc35..39696b3d9 100644 --- a/apps/ops/templates/ops/task_detail.html +++ b/apps/ops/templates/ops/task_detail.html @@ -21,10 +21,10 @@ {% trans 'Task versions' %}
  • - {% trans 'Run history' %} + {% trans 'Execution' %}
  • - {% trans 'Last run output' %} + {% trans 'Last execution output' %}
  • @@ -72,16 +72,16 @@
    {% trans 'Last run' %}:{{ object.latest_history.date_start }}{{ object.latest_execution.date_start }}
    {% trans 'Time delta' %}:{{ object.latest_history.timedelta|floatformat}} s{{ object.latest_execution.timedelta|floatformat}} s
    {% trans 'Is finished' %}: - {% if object.latest_history.is_finished %} + {% if object.latest_execution.is_finished %} {% trans 'Yes' %} {% else %} {% trans 'No' %} @@ -91,7 +91,7 @@
    {% trans 'Is success ' %}: - {% if object.latest_history.is_success %} + {% if object.latest_execution.is_success %} {% trans 'Yes' %} {% else %} {% trans 'No' %} @@ -121,7 +121,7 @@
    - {% for host, task in object.latest_history.failed_hosts.items %} + {% for host, task in object.latest_execution.failed_hosts.items %} {% if forloop.first %} {% else %} @@ -151,7 +151,7 @@
    - {% for host in object.latest_history.success_hosts %} + {% for host in object.latest_execution.success_hosts %} {% if forloop.first %} {% else %} @@ -179,12 +179,12 @@ diff --git a/apps/ops/templates/ops/task_history.html b/apps/ops/templates/ops/task_history.html index 5121a8045..06dc3416c 100644 --- a/apps/ops/templates/ops/task_history.html +++ b/apps/ops/templates/ops/task_history.html @@ -20,10 +20,10 @@ {% trans 'Task versions' %}
  • - {% trans 'Run history' %} + {% trans 'Execution' %}
  • - {% trans 'Last run output' %} + {% trans 'Last execution output' %}
  • @@ -31,7 +31,7 @@
    - {% trans 'History of ' %} {{ object.name }}:{{ object.short_id }} + {% trans 'Executions of ' %} {{ object.name }}:{{ object.short_id }}
    -
    +
    + @@ -60,13 +61,7 @@ - - +{% include '_filter_dropdown.html' %} {% endblock %} {% block content_bottom_left %}{% endblock %} {% block custom_foot_js %} @@ -105,6 +100,19 @@ $(document).ready(function () { table.ajax.reload(); }); }); + + var filterMenu = [ + {title: "{% trans 'User' %}", value: "user"}, + {title: "{% trans 'Asset' %}", value: "asset"}, + {title: "{% trans 'System user' %}", value: "system_user"}, + {title: "{% trans 'Command' %}", value: "input"}, + {title: "{% trans 'Risk level' %}", value: "risk_level", submenu: [ + {title: "{% trans 'Ordinary' %}", value: "0"}, + {title: "{% trans 'Dangerous' %}", value: "5"}, + ]}, + ]; + initTableFilterDropdown('#command_table_filter input', filterMenu, 15, 50) + }) .on('click', '#btn_bulk_update', function(){ // var action = $('#slct_bulk_update').val(); @@ -113,30 +121,6 @@ $(document).ready(function () { var exportPath = "{% url 'api-terminal:command-export' %}"; var url = exportPath + "?" + params; window.open(url); -}).on("click", '#command_table_filter input', function (e) { - e.preventDefault(); - e.stopPropagation(); - var offset1 = $('#command_table_filter input').offset(); - var x = offset1.left; - var y = offset1.top; - var offset = $(".search-help").parent().offset(); - x -= offset.left; - y -= offset.top; - x += 18; - y += 80; - $('.search-help').css({"top":y+"px", "left":x+"px", "position": "absolute"}); - $('.dropdown-menu.search-help').show(); -}) -.on('click', '.search-item', function (e) { - e.preventDefault(); - e.stopPropagation(); - var keyword = $("#command_table_filter input"); - var value = $(this).data('value'); - var old_value = keyword.val(); - var new_value = old_value + ' ' + value + ':'; - keyword.val(new_value.trim()); - $('.dropdown-menu.search-help').hide(); - keyword.focus() }) .on('click', 'body', function (e) { $('.dropdown-menu.search-help').hide() @@ -201,12 +185,22 @@ function initTable() { interHtml.html(data); $(td).html(interHtml); }}, - {targets: 5, createdCell: function (td, cellData) { + {targets: 2, createdCell: function (td, cellData){ + let risk; + if(cellData === 5){ + risk = "{% trans 'Dangerous' %}"; + } + else{ + risk = "{% trans 'Ordinary' %}"; + } + $(td).html(risk) + }}, + {targets: 6, createdCell: function (td, cellData) { var data = '{% trans "Goto" %}' .replace('{{ DEFAULT_PK }}', cellData); $(td).html(data); }}, - {targets: 6, createdCell: function (td, cellData) { + {targets: 7, createdCell: function (td, cellData) { var data = toSafeLocalDateStr(cellData*1000); $(td).html(data); }}, @@ -214,7 +208,8 @@ function initTable() { toggle: true, ajax_url: commandListUrl, columns: [ - {data: "id"}, {data: "input", orderable: false, width: "40%"}, {data: "user", orderable: false}, + {data: "id"}, {data: "input", orderable: false, width: "40%"}, + {data: "risk_level", orderable: false}, {data: "user", orderable: false}, {data: "asset", orderable: false}, {data: "system_user", orderable: false}, {data: "session", orderable: false}, {data: "timestamp", width: "160px", orderable: false}, ], diff --git a/apps/terminal/templates/terminal/session_commands.html b/apps/terminal/templates/terminal/session_commands.html new file mode 100644 index 000000000..3aeb2989f --- /dev/null +++ b/apps/terminal/templates/terminal/session_commands.html @@ -0,0 +1,96 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} +{% load common_tags %} + +{% block custom_head_css_js %} + + +{% endblock %} +{% block content %} +
    +
    +
    +
    + +
    +
    +
    +
    + {% trans 'Command list' %} +
    + + + + + + + + + + +
    +
    +
    +
    @@ -80,7 +80,7 @@ diff --git a/apps/ops/templates/ops/task_list.html b/apps/ops/templates/ops/task_list.html index 0d2095d78..6f393cb1c 100644 --- a/apps/ops/templates/ops/task_list.html +++ b/apps/ops/templates/ops/task_list.html @@ -79,10 +79,10 @@ $(document).ready(function () { ajax_url: '{% url "api-ops:task-list" %}', columns: [ {data: "id"}, {data: "name", className: "text-left"}, - {data: "latest_history", orderable: false}, - {data: "latest_history", orderable: false}, - {data: "latest_history", orderable: false}, {data: "latest_history"}, - {data: "latest_history", orderable:false}, {data: "id", orderable: false}, + {data: "latest_execution", orderable: false}, + {data: "latest_execution", orderable: false}, + {data: "latest_execution", orderable: false}, {data: "latest_execution"}, + {data: "latest_execution", orderable:false}, {data: "id", orderable: false}, ], order: [], op_html: $('#actions').html() diff --git a/apps/ops/urls/api_urls.py b/apps/ops/urls/api_urls.py index cc242f649..366524aa4 100644 --- a/apps/ops/urls/api_urls.py +++ b/apps/ops/urls/api_urls.py @@ -11,7 +11,7 @@ app_name = "ops" router = DefaultRouter() router.register(r'tasks', api.TaskViewSet, 'task') router.register(r'adhoc', api.AdHocViewSet, 'adhoc') -router.register(r'history', api.AdHocRunHistoryViewSet, 'history') +router.register(r'adhoc-executions', api.AdHocRunHistoryViewSet, 'execution') router.register(r'command-executions', api.CommandExecutionViewSet, 'command-execution') router.register(r'celery/period-tasks', api.CeleryPeriodTaskViewSet, 'celery-period-task') diff --git a/apps/ops/urls/view_urls.py b/apps/ops/urls/view_urls.py index 13759c3f2..592ae9d93 100644 --- a/apps/ops/urls/view_urls.py +++ b/apps/ops/urls/view_urls.py @@ -13,12 +13,12 @@ urlpatterns = [ path('task/', views.TaskListView.as_view(), name='task-list'), path('task//', views.TaskDetailView.as_view(), name='task-detail'), path('task//adhoc/', views.TaskAdhocView.as_view(), name='task-adhoc'), - path('task//history/', views.TaskHistoryView.as_view(), name='task-history'), + path('task//executions/', views.TaskExecutionView.as_view(), name='task-execution'), path('adhoc//', views.AdHocDetailView.as_view(), name='adhoc-detail'), - path('adhoc//history/', views.AdHocHistoryView.as_view(), name='adhoc-history'), - path('adhoc/history//', views.AdHocHistoryDetailView.as_view(), name='adhoc-history-detail'), + path('adhoc//executions/', views.AdHocExecutionView.as_view(), name='adhoc-history'), + path('adhoc/executions//', views.AdHocExecutionDetailView.as_view(), name='adhoc-execution-detail'), path('celery/task//log/', views.CeleryTaskLogView.as_view(), name='celery-task-log'), - path('command-execution/', views.CommandExecutionListView.as_view(), name='command-execution-list'), - path('command-execution/create/', views.CommandExecutionCreateView.as_view(), name='command-execution-create'), + path('command-executions/', views.CommandExecutionListView.as_view(), name='command-execution-list'), + path('command-executions/create/', views.CommandExecutionCreateView.as_view(), name='command-execution-create'), ] diff --git a/apps/ops/utils.py b/apps/ops/utils.py index 3322890f2..16ba8e648 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -3,37 +3,44 @@ from django.utils.translation import ugettext_lazy as _ from common.utils import get_logger, get_object_or_none from common.tasks import send_mail_async -from orgs.utils import set_to_root_org +from orgs.utils import tmp_to_org, org_aware_func + from .models import Task, AdHoc logger = get_logger(__file__) +DEFAULT_TASK_OPTIONS = { + 'timeout': 10, + 'forks': 10, +} + def get_task_by_id(task_id): return get_object_or_none(Task, id=task_id) +@org_aware_func("hosts") def update_or_create_ansible_task( - task_name, hosts, tasks, created_by, + task_name, hosts, tasks, interval=None, crontab=None, is_periodic=False, callback=None, pattern='all', options=None, run_as_admin=False, run_as=None, become_info=None, ): if not hosts or not tasks or not task_name: return None, None - set_to_root_org() + if options is None: + options = DEFAULT_TASK_OPTIONS defaults = { 'name': task_name, 'interval': interval, 'crontab': crontab, 'is_periodic': is_periodic, 'callback': callback, - 'created_by': created_by, } created = False task, ok = Task.objects.update_or_create( - defaults=defaults, name=task_name, created_by=created_by + defaults=defaults, name=task_name ) adhoc = task.get_latest_adhoc() new_adhoc = AdHoc(task=task, pattern=pattern, diff --git a/apps/ops/views/adhoc.py b/apps/ops/views/adhoc.py index 4df75305b..b8d90f092 100644 --- a/apps/ops/views/adhoc.py +++ b/apps/ops/views/adhoc.py @@ -7,13 +7,13 @@ from django.views.generic import ListView, DetailView, TemplateView from common.mixins import DatetimeSearchMixin from common.permissions import PermissionsMixin, IsOrgAdmin from orgs.utils import current_org -from ..models import Task, AdHoc, AdHocRunHistory +from ..models import Task, AdHoc, AdHocExecution __all__ = [ - 'TaskListView', 'TaskDetailView', 'TaskHistoryView', - 'TaskAdhocView', 'AdHocDetailView', 'AdHocHistoryDetailView', - 'AdHocHistoryView' + 'TaskListView', 'TaskDetailView', 'TaskExecutionView', + 'TaskAdhocView', 'AdHocDetailView', 'AdHocExecutionDetailView', + 'AdHocExecutionView' ] @@ -42,11 +42,6 @@ class TaskDetailView(PermissionsMixin, DetailView): def get_queryset(self): queryset = super().get_queryset() - # Todo: 需要整理默认组织等东西 - if current_org.is_real(): - queryset = queryset.filter(created_by=current_org.id) - else: - queryset = queryset.filter(created_by='') return queryset def get_context_data(self, **kwargs): @@ -72,7 +67,7 @@ class TaskAdhocView(PermissionsMixin, DetailView): return super().get_context_data(**kwargs) -class TaskHistoryView(PermissionsMixin, DetailView): +class TaskExecutionView(PermissionsMixin, DetailView): model = Task template_name = 'ops/task_history.html' permission_classes = [IsOrgAdmin] @@ -80,7 +75,7 @@ class TaskHistoryView(PermissionsMixin, DetailView): def get_context_data(self, **kwargs): context = { 'app': _('Ops'), - 'action': _('Task run history'), + 'action': _('Task execution list'), } kwargs.update(context) return super().get_context_data(**kwargs) @@ -100,7 +95,7 @@ class AdHocDetailView(PermissionsMixin, DetailView): return super().get_context_data(**kwargs) -class AdHocHistoryView(PermissionsMixin, DetailView): +class AdHocExecutionView(PermissionsMixin, DetailView): model = AdHoc template_name = 'ops/adhoc_history.html' permission_classes = [IsOrgAdmin] @@ -108,21 +103,21 @@ class AdHocHistoryView(PermissionsMixin, DetailView): def get_context_data(self, **kwargs): context = { 'app': _('Ops'), - 'action': _('Version run history'), + 'action': _('Version run execution'), } kwargs.update(context) return super().get_context_data(**kwargs) -class AdHocHistoryDetailView(PermissionsMixin, DetailView): - model = AdHocRunHistory +class AdHocExecutionDetailView(PermissionsMixin, DetailView): + model = AdHocExecution template_name = 'ops/adhoc_history_detail.html' permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { 'app': _('Ops'), - 'action': _('Run history detail'), + 'action': _('Execution detail'), } kwargs.update(context) return super().get_context_data(**kwargs) diff --git a/apps/ops/views/command.py b/apps/ops/views/command.py index 87e0528c6..4dfc3d2e7 100644 --- a/apps/ops/views/command.py +++ b/apps/ops/views/command.py @@ -66,10 +66,10 @@ class CommandExecutionCreateView(PermissionsMixin, TemplateView): return super().get_permissions() def get_user_system_users(self): - from perms.utils import AssetPermissionUtilV2 + from perms.utils import AssetPermissionUtil user = self.request.user with tmp_to_root_org(): - util = AssetPermissionUtilV2(user) + util = AssetPermissionUtil(user) system_users = util.get_system_users() return system_users diff --git a/apps/orgs/mixins/api.py b/apps/orgs/mixins/api.py index 64314a3f8..7695292c3 100644 --- a/apps/orgs/mixins/api.py +++ b/apps/orgs/mixins/api.py @@ -33,10 +33,10 @@ class OrgQuerySetMixin: if hasattr(self, 'swagger_fake_view'): return queryset[:1] - if hasattr(self, 'action') and self.action == 'list' and \ - hasattr(self, 'serializer_class') and \ - hasattr(self.serializer_class, 'setup_eager_loading'): - queryset = self.serializer_class.setup_eager_loading(queryset) + if hasattr(self, 'action') and self.action == 'list': + serializer_class = self.get_serializer_class() + if serializer_class and hasattr(serializer_class, 'setup_eager_loading'): + queryset = serializer_class.setup_eager_loading(queryset) return queryset diff --git a/apps/orgs/mixins/models.py b/apps/orgs/mixins/models.py index 30429b874..649c450cc 100644 --- a/apps/orgs/mixins/models.py +++ b/apps/orgs/mixins/models.py @@ -20,6 +20,18 @@ __all__ = [ class OrgManager(models.Manager): + def all_group_by_org(self): + from ..models import Organization + orgs = list(Organization.objects.all()) + orgs.append(Organization.default()) + querysets = {} + for org in orgs: + if org.is_real(): + org_id = org.id + else: + org_id = '' + querysets[org] = super(OrgManager, self).get_queryset().filter(org_id=org_id) + return querysets def get_queryset(self): queryset = super(OrgManager, self).get_queryset() diff --git a/apps/orgs/models.py b/apps/orgs/models.py index a9040b378..bae5c408c 100644 --- a/apps/orgs/models.py +++ b/apps/orgs/models.py @@ -99,7 +99,7 @@ class Organization(models.Model): if self.is_real(): return self.id elif self.is_root(): - return None + return self.ROOT_ID else: return '' @@ -209,3 +209,9 @@ class Organization(models.Model): def change_to(self): from .utils import set_current_org set_current_org(self) + + @classmethod + def all_orgs(cls): + orgs = list(cls.objects.all()) + orgs.append(cls.default()) + return orgs diff --git a/apps/orgs/utils.py b/apps/orgs/utils.py index 2a7cfca6b..c69af25b8 100644 --- a/apps/orgs/utils.py +++ b/apps/orgs/utils.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # -import traceback +import uuid +from inspect import signature +from functools import wraps from werkzeug.local import LocalProxy from contextlib import contextmanager @@ -28,7 +30,7 @@ def get_org_from_request(request): def set_current_org(org): - if isinstance(org, str): + if isinstance(org, (str, uuid.UUID)): org = Organization.get_instance(org) setattr(thread_local, 'current_org_id', org.id) @@ -110,4 +112,29 @@ def filter_org_queryset(queryset): return queryset +def org_aware_func(org_arg_name): + """ + :param org_arg_name: 函数中包含org_id的对象是哪个参数 + :return: + """ + def decorate(func): + @wraps(func) + def wrapper(*args, **kwargs): + sig = signature(func) + values = sig.bind(*args, **kwargs) + org_aware_resource = values.arguments.get(org_arg_name) + if not org_aware_resource: + return func(*args, **kwargs) + if hasattr(org_aware_resource, '__getitem__'): + org_aware_resource = org_aware_resource[0] + if not hasattr(org_aware_resource, "org_id"): + print("Error: {} not has org_id attr".format(org_aware_resource)) + return func(*args, **kwargs) + with tmp_to_org(org_aware_resource.org_id): + # print("Current org id: {}".format(org_aware_resource.org_id)) + return func(*args, **kwargs) + return wrapper + return decorate + + current_org = LocalProxy(get_current_org) diff --git a/apps/perms/api/mixin.py b/apps/perms/api/mixin.py index 86e1a8c0a..cbbfa825f 100644 --- a/apps/perms/api/mixin.py +++ b/apps/perms/api/mixin.py @@ -4,7 +4,7 @@ from rest_framework.generics import get_object_or_404 from common.permissions import IsValidUser, IsOrgAdminOrAppUser from common.utils import get_logger -from orgs.utils import set_to_root_org, get_current_org, set_current_org, tmp_to_root_org +from orgs.utils import set_to_root_org, set_current_org, get_current_org from ..hands import User, UserGroup @@ -22,25 +22,15 @@ class UserPermissionMixin: def initial(self, *args, **kwargs): super().initial(*args, *kwargs) - self.current_org = get_current_org() - set_to_root_org() self.obj = self.get_obj() - # def dispatch(self, request, *args, **kwargs): - # """不能这么做,校验权限时拿不到组织了""" - # with tmp_to_root_org(): - # return super().dispatch(request, *args, **kwargs) - - # def get(self, request, *args, **kwargs): - # """有的api重写了get方法""" - # with tmp_to_root_org(): - # return super().get(request, *args, **kwargs) - def get_obj(self): user_id = self.kwargs.get('pk', '') if user_id: user = get_object_or_404(User, id=user_id) else: + self.current_org = get_current_org() + set_to_root_org() user = self.request.user return user diff --git a/apps/perms/api/user_permission/common.py b/apps/perms/api/user_permission/common.py index badd5cbbc..6af3bb6ba 100644 --- a/apps/perms/api/user_permission/common.py +++ b/apps/perms/api/user_permission/common.py @@ -11,7 +11,7 @@ from rest_framework.generics import ( from common.permissions import IsOrgAdminOrAppUser, IsOrgAdmin from common.utils import get_logger from ...utils import ( - AssetPermissionUtilV2 + AssetPermissionUtil ) from ...hands import User, Asset, SystemUser from ... import serializers @@ -51,8 +51,8 @@ class GetUserAssetPermissionActionsApi(UserAssetPermissionMixin, asset = get_object_or_404(Asset, id=asset_id) system_user = get_object_or_404(SystemUser, id=system_id) - system_users_actions = self.util.get_asset_system_users_with_actions(asset) - actions = system_users_actions.get(system_user) + system_users_actions = self.util.get_asset_system_users_id_with_actions(asset) + actions = system_users_actions.get(system_user.id) return {"actions": actions} @@ -81,9 +81,8 @@ class ValidateUserAssetPermissionApi(UserAssetPermissionMixin, APIView): asset = get_object_or_404(Asset, id=asset_id) system_user = get_object_or_404(SystemUser, id=system_id) - system_users_actions = self.util.get_asset_system_users_with_actions( - asset) - actions = system_users_actions.get(system_user) + system_users_actions = self.util.get_asset_system_users_id_with_actions(asset) + actions = system_users_actions.get(system_user.id) if actions is None: return Response({'msg': False}, status=403) if action_name in Action.value_to_choices(actions): @@ -95,7 +94,7 @@ class RefreshAssetPermissionCacheApi(RetrieveAPIView): permission_classes = (IsOrgAdmin,) def retrieve(self, request, *args, **kwargs): - AssetPermissionUtilV2.expire_all_user_tree_cache() + AssetPermissionUtil.expire_all_user_tree_cache() return Response({'msg': True}, status=200) @@ -107,11 +106,14 @@ class UserGrantedAssetSystemUsersApi(UserAssetPermissionMixin, ListAPIView): def get_queryset(self): asset_id = self.kwargs.get('asset_id') asset = get_object_or_404(Asset, id=asset_id) - system_users_with_actions = self.util.get_asset_system_users_with_actions(asset) - system_users = [] - for system_user, actions in system_users_with_actions.items(): + system_users_with_actions = self.util.get_asset_system_users_id_with_actions(asset) + system_users_id = system_users_with_actions.keys() + system_users = SystemUser.objects.filter(id__in=system_users_id)\ + .only(*self.serializer_class.Meta.only_fields) \ + .order_by('priority') + system_users = list(system_users) + for system_user in system_users: + actions = system_users_with_actions.get(system_user.id, 0) system_user.actions = actions - system_users.append(system_user) - system_users.sort(key=lambda x: x.priority) return system_users diff --git a/apps/perms/api/user_permission/mixin.py b/apps/perms/api/user_permission/mixin.py index bbc926ffe..4b0cc32a5 100644 --- a/apps/perms/api/user_permission/mixin.py +++ b/apps/perms/api/user_permission/mixin.py @@ -3,7 +3,7 @@ from common.utils import lazyproperty from common.tree import TreeNodeSerializer from ..mixin import UserPermissionMixin -from ...utils import AssetPermissionUtilV2, ParserNode +from ...utils import AssetPermissionUtil, ParserNode from ...hands import Node, Asset @@ -17,7 +17,7 @@ class UserAssetPermissionMixin(UserPermissionMixin): def util(self): cache_policy = self.get_cache_policy() system_user_id = self.request.query_params.get("system_user") - util = AssetPermissionUtilV2(self.obj, cache_policy=cache_policy) + util = AssetPermissionUtil(self.obj, cache_policy=cache_policy) if system_user_id: util.filter_permissions(system_users=system_user_id) return util diff --git a/apps/perms/api/user_permission/user_permission_assets.py b/apps/perms/api/user_permission/user_permission_assets.py index b2ed1fe65..82f46dd6f 100644 --- a/apps/perms/api/user_permission/user_permission_assets.py +++ b/apps/perms/api/user_permission/user_permission_assets.py @@ -2,9 +2,7 @@ # from django.shortcuts import get_object_or_404 -from rest_framework.generics import ( - ListAPIView, get_object_or_404 -) +from rest_framework.generics import ListAPIView from common.permissions import IsOrgAdminOrAppUser from common.utils import get_logger, timeit @@ -16,8 +14,7 @@ from .mixin import UserAssetPermissionMixin, UserAssetTreeMixin logger = get_logger(__name__) __all__ = [ - 'UserGrantedAssetsApi', - 'UserGrantedAssetsAsTreeApi', + 'UserGrantedAssetsApi', 'UserGrantedAssetsAsTreeApi', 'UserGrantedNodeAssetsApi', ] diff --git a/apps/perms/mixins.py b/apps/perms/mixins.py index 30a588ffd..98ac8bf76 100644 --- a/apps/perms/mixins.py +++ b/apps/perms/mixins.py @@ -4,70 +4,10 @@ from orgs.utils import set_to_root_org __all__ = [ - 'AssetsFilterMixin', 'ChangeOrgIfNeedMixin', + 'ChangeOrgIfNeedMixin', ] -class AssetsFilterMixin(object): - """ - 对资产进行过滤(查询,排序) - """ - - def filter_queryset(self, queryset): - queryset = self.search_assets(queryset) - queryset = self.filter_labels(queryset) - queryset = self.sort_assets(queryset) - return queryset - - def search_assets(self, queryset): - from perms.utils import is_obj_attr_has - value = self.request.query_params.get('search') - if not value: - return queryset - queryset = [asset for asset in queryset if is_obj_attr_has(asset, value)] - return queryset - - def sort_assets(self, queryset): - from perms.utils import sort_assets - order_by = self.request.query_params.get('order') - if not order_by: - order_by = 'hostname' - - if order_by.startswith('-'): - order_by = order_by.lstrip('-') - reverse = True - else: - reverse = False - - queryset = sort_assets(queryset, order_by=order_by, reverse=reverse) - return queryset - - def filter_labels(self, queryset): - from assets.models import Label - query_keys = self.request.query_params.keys() - all_label_keys = Label.objects.values_list('name', flat=True) - valid_keys = set(all_label_keys) & set(query_keys) - labels_query = {} - for key in valid_keys: - labels_query[key] = self.request.query_params.get(key) - if not labels_query: - return queryset - - labels = set() - for k, v in labels_query.items(): - label = Label.objects.filter(name=k, value=v).first() - if not label: - continue - labels.add(label) - - _queryset = [] - for asset in queryset: - _labels = set(asset.labels.all()) & set(labels) - if _labels and len(_labels) == len(set(labels)): - _queryset.append(asset) - return _queryset - - class ChangeOrgIfNeedMixin(object): @staticmethod diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py index c194cf64a..ac248947f 100644 --- a/apps/perms/serializers/user_permission.py +++ b/apps/perms/serializers/user_permission.py @@ -27,6 +27,7 @@ class AssetSystemUserSerializer(serializers.ModelSerializer): model = SystemUser only_fields = ( 'id', 'name', 'username', 'priority', 'protocol', 'login_mode', + 'sftp_root', 'username_same_with_user', ) fields = list(only_fields) + ["actions"] read_only_fields = fields diff --git a/apps/perms/signals_handler.py b/apps/perms/signals_handler.py index 4a7fe389c..1be6ce9f4 100644 --- a/apps/perms/signals_handler.py +++ b/apps/perms/signals_handler.py @@ -5,8 +5,8 @@ from django.dispatch import receiver from common.utils import get_logger from common.decorator import on_transaction_commit -from .models import AssetPermission -from .utils.asset_permission import AssetPermissionUtilV2 +from .models import AssetPermission, RemoteAppPermission +from .utils.asset_permission import AssetPermissionUtil logger = get_logger(__file__) @@ -16,45 +16,64 @@ logger = get_logger(__file__) @on_transaction_commit def on_permission_change(sender, action='', **kwargs): logger.debug('Asset permission changed, refresh user tree cache') - AssetPermissionUtilV2.expire_all_user_tree_cache() + AssetPermissionUtil.expire_all_user_tree_cache() # Todo: 检查授权规则到期,从而修改授权规则 @receiver(m2m_changed, sender=AssetPermission.nodes.through) -def on_permission_nodes_changed(sender, instance=None, action='', **kwargs): - if action != 'post_add': +def on_permission_nodes_changed(sender, instance=None, action='', reverse=None, **kwargs): + if action != 'post_add' and reverse: return - if isinstance(instance, AssetPermission): - logger.debug("Asset permission nodes change signal received") - nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) - system_users = instance.system_users.all() - for system_user in system_users: - system_user.nodes.add(*tuple(nodes)) + logger.debug("Asset permission nodes change signal received") + nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) + system_users = instance.system_users.all() + for system_user in system_users: + system_user.nodes.add(*tuple(nodes)) @receiver(m2m_changed, sender=AssetPermission.assets.through) -def on_permission_assets_changed(sender, instance=None, action='', **kwargs): - if action != 'post_add': +def on_permission_assets_changed(sender, instance=None, action='', reverse=None, **kwargs): + if action != 'post_add' and reverse: return - if isinstance(instance, AssetPermission): - logger.debug("Asset permission assets change signal received") - assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) - system_users = instance.system_users.all() - for system_user in system_users: - system_user.assets.add(*tuple(assets)) + logger.debug("Asset permission assets change signal received") + assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) + system_users = instance.system_users.all() + for system_user in system_users: + system_user.assets.add(*tuple(assets)) @receiver(m2m_changed, sender=AssetPermission.system_users.through) -def on_permission_system_users_changed(sender, instance=None, action='', **kwargs): - if action != 'post_add': +def on_asset_permission_system_users_changed(sender, instance=None, action='', + reverse=False, **kwargs): + if action != 'post_add' and reverse: return - if isinstance(instance, AssetPermission): - system_users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) - logger.debug("Asset permission system_users change signal received") - assets = instance.assets.all().values_list('id', flat=True) - nodes = instance.nodes.all().values_list('id', flat=True) - for system_user in system_users: - system_user.nodes.add(*tuple(nodes)) - system_user.assets.add(*tuple(assets)) + logger.debug("Asset permission system_users change signal received") + system_users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) + assets = instance.assets.all().values_list('id', flat=True) + nodes = instance.nodes.all().values_list('id', flat=True) + users = instance.users.all().values_list('id', flat=True) + groups = instance.user_groups.all().values_list('id', flat=True) + for system_user in system_users: + system_user.nodes.add(*tuple(nodes)) + system_user.assets.add(*tuple(assets)) + if system_user.username_same_with_user: + system_user.groups.add(*tuple(groups)) + system_user.users.add(*tuple(users)) + +@receiver(m2m_changed, sender=RemoteAppPermission.system_users.through) +def on_remote_app_permission_system_users_changed(sender, instance=None, + action='', reverse=False, **kwargs): + if action != 'post_add' or reverse: + return + system_users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) + logger.debug("Remote app permission system_users change signal received") + assets = instance.remote_apps.all().values_list('asset__id', flat=True) + users = instance.users.all().values_list('id', flat=True) + groups = instance.user_groups.all().values_list('id', flat=True) + for system_user in system_users: + system_user.assets.add(*tuple(assets)) + if system_user.username_same_with_user: + system_user.groups.add(*tuple(groups)) + system_user.users.add(*tuple(users)) diff --git a/apps/perms/templates/perms/asset_permission_list.html b/apps/perms/templates/perms/asset_permission_list.html index 5b507350a..1f44f7de1 100644 --- a/apps/perms/templates/perms/asset_permission_list.html +++ b/apps/perms/templates/perms/asset_permission_list.html @@ -193,7 +193,7 @@ $(document).ready(function(){ {title: "{% trans 'Exclude' %}", value: "0"}, ]}, ]; - initTableFilterDropdown('#permission_list_table_filter input', filterMenu) + initTableFilterDropdown('#permission_list_table_filter input', filterMenu, 15) }) .on('click', '.btn-del', function () { var $this = $(this); diff --git a/apps/perms/urls/asset_permission.py b/apps/perms/urls/asset_permission.py index 4a649c992..c14db7c3e 100644 --- a/apps/perms/urls/asset_permission.py +++ b/apps/perms/urls/asset_permission.py @@ -25,7 +25,7 @@ user_permission_urlpatterns = [ path('nodes/', api.UserGrantedNodesApi.as_view(), name='my-nodes'), # Node children - path('/nodes/children/', api.UserGrantedNodesApi.as_view(), name='user-nodes-children'), + path('/nodes/children/', api.UserGrantedNodeChildrenApi.as_view(), name='user-nodes-children'), path('nodes/children/', api.UserGrantedNodesApi.as_view(), name='my-nodes-children'), # Node as tree diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py index 3ba5c68d6..1905bfe62 100644 --- a/apps/perms/utils/asset_permission.py +++ b/apps/perms/utils/asset_permission.py @@ -1,5 +1,5 @@ # coding: utf-8 - +import time import pickle from collections import defaultdict from functools import reduce @@ -8,7 +8,7 @@ from django.core.cache import cache from django.db.models import Q from django.conf import settings -from orgs.utils import set_to_root_org +from orgs.utils import current_org from common.utils import get_logger, timeit, lazyproperty from common.tree import TreeNode from assets.utils import TreeService @@ -19,8 +19,7 @@ logger = get_logger(__file__) __all__ = [ - 'is_obj_attr_has', 'sort_assets', - 'ParserNode', 'AssetPermissionUtilV2', + 'ParserNode', 'AssetPermissionUtil', ] @@ -59,25 +58,48 @@ def get_system_user_permissions(system_user): class AssetPermissionUtilCacheMixin: - user_tree_cache_key = 'USER_PERM_TREE_{}_{}' + user_tree_cache_key = 'USER_PERM_TREE_{}_{}_{}' user_tree_cache_ttl = settings.ASSETS_PERM_CACHE_TIME user_tree_cache_enable = settings.ASSETS_PERM_CACHE_ENABLE + user_tree_map = {} cache_policy = '0' obj_id = '' _filter_id = 'None' @property def cache_key(self): - return self.user_tree_cache_key.format(self.obj_id, self._filter_id) + return self.get_cache_key() + + def get_cache_key(self, org_id=None): + if org_id is None: + org_id = current_org.org_id() + + key = self.user_tree_cache_key.format( + org_id, self.obj_id, self._filter_id + ) + return key def expire_user_tree_cache(self): cache.delete(self.cache_key) @classmethod def expire_all_user_tree_cache(cls): - key = cls.user_tree_cache_key.format('*', '*') - key = key.split('_')[:-1] - key = '_'.join(key) + expire_cache_key = "USER_TREE_EXPIRED_AT" + latest_expired = cache.get(expire_cache_key, 0) + now = time.time() + if now - latest_expired < 60: + return + key = cls.user_tree_cache_key.format('*', '1', '1') + key = key.replace('_1', '') + cache.delete_pattern(key) + cache.set(expire_cache_key, now) + + @classmethod + def expire_org_tree_cache(cls, org_id=None): + if org_id is None: + org_id = current_org.org_id() + key = cls.user_tree_cache_key.format(org_id, '*', '1') + key = key.replace('_1', '') cache.delete_pattern(key) def set_user_tree_to_cache(self, user_tree): @@ -111,7 +133,7 @@ class AssetPermissionUtilCacheMixin: self.set_user_tree_to_cache(user_tree) -class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): +class AssetPermissionUtil(AssetPermissionUtilCacheMixin): get_permissions_map = { "User": get_user_permissions, "UserGroup": get_user_group_permissions, @@ -134,9 +156,12 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): self._user_tree = None self._user_tree_filter_id = 'None' + if not isinstance(obj, User): + self.cache_policy = '0' + @staticmethod def change_org_if_need(): - set_to_root_org() + pass @lazyproperty def full_tree(self): @@ -157,9 +182,7 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): @timeit def filter_permissions(self, **filters): self.cache_policy = '0' - # filters_json = json.dumps(filters, sort_keys=True) self._permissions = self.permissions.filter(**filters) - # self._filter_id = md5(filters_json.encode()).hexdigest() @lazyproperty def user_tree(self): @@ -253,6 +276,8 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): # 获取单独授权资产,并没有在授权的节点上 for key, assets in nodes_single_assets.items(): + if not self.full_tree.contains(key): + continue node = self.full_tree.get_node(key, deep=True) parent_id = self.full_tree.parent(key).identifier parent = user_tree.get_node(parent_id) @@ -280,21 +305,6 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): if not ancestors: continue user_tree.safe_add_ancestors(child, ancestors) - # parent_id = ancestors[0].identifier - # user_tree.move_node(child.identifier, parent_id) - - @staticmethod - def add_empty_node_if_need(user_tree): - """ - 添加空节点,如果根节点没有子节点的话 - """ - if not user_tree.children(user_tree.root): - node_key = Node.empty_key - node_value = Node.empty_value - user_tree.create_node( - identifier=node_key, tag=node_value, - parent=user_tree.root, - ) def add_favorite_node_if_need(self, user_tree): if not isinstance(self.object, User): @@ -321,16 +331,10 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): @timeit def get_user_tree(self): - # 使用锁,保证多次获取tree的时候顺序执行,可以使用缓存 - user_tree = self.get_user_tree_from_local() - if user_tree: - return user_tree user_tree = self.get_user_tree_from_cache_if_need() if user_tree: - self.set_user_tree_to_local(user_tree) return user_tree user_tree = TreeService() - user_tree._invalid_assets = self.full_tree._invalid_assets full_tree_root = self.full_tree.root_node() user_tree.create_node( tag=full_tree_root.tag, @@ -340,13 +344,12 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): self.add_single_assets_node_to_user_tree(user_tree) self.parse_user_tree_to_full_tree(user_tree) self.add_favorite_node_if_need(user_tree) - self.add_empty_node_if_need(user_tree) self.set_user_tree_to_cache_if_need(user_tree) self.set_user_tree_to_local(user_tree) return user_tree # Todo: 是否可以获取多个资产的系统用户 - def get_asset_system_users_with_actions(self, asset): + def get_asset_system_users_id_with_actions(self, asset): nodes = asset.get_nodes() nodes_keys_related = set() for node in nodes: @@ -367,35 +370,35 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): queryset = queryset.filter(args) else: queryset = queryset.none() - queryset = queryset.distinct().prefetch_related('system_users') + asset_protocols = asset.protocols_as_dict.keys() + values = queryset.filter(system_users__protocol__in=asset_protocols).distinct()\ + .values_list('system_users', 'actions') system_users_actions = defaultdict(int) - for perm in queryset: - system_users = perm.system_users.all() - if not system_users or not perm.actions: + for system_user_id, actions in values: + if None in (system_user_id, actions): continue - for s in system_users: - if not asset.has_protocol(s.protocol): - continue - system_users_actions[s] |= perm.actions + for i, action in values: + system_users_actions[i] |= actions return system_users_actions def get_permissions_nodes_and_assets(self): from assets.models import Node - permissions = self.permissions.values_list('assets', 'nodes__key').distinct() - nodes_keys = set() - assets_ids = set() - for asset_id, node_key in permissions: - if asset_id: - assets_ids.add(asset_id) - if node_key: - nodes_keys.add(node_key) + permissions = self.permissions + nodes_keys = permissions.exclude(nodes__isnull=True)\ + .values_list('nodes__key', flat=True) + assets_ids = permissions.exclude(assets__isnull=True)\ + .values_list('assets', flat=True) + nodes_keys = set(nodes_keys) + assets_ids = set(assets_ids) nodes_keys = Node.clean_children_keys(nodes_keys) return nodes_keys, assets_ids @timeit def get_assets(self): nodes_keys, assets_ids = self.get_permissions_nodes_and_assets() - queryset = Node.get_nodes_all_assets(nodes_keys, extra_assets_ids=assets_ids) + queryset = Node.get_nodes_all_assets( + nodes_keys, extra_assets_ids=assets_ids + ) return queryset.valid() def get_nodes_assets(self, node, deep=False): @@ -414,30 +417,9 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): return SystemUser.objects.filter(id__in=system_users_id) -def is_obj_attr_has(obj, val, attrs=("hostname", "ip", "comment")): - if not attrs: - vals = [val for val in obj.__dict__.values() if isinstance(val, (str, int))] - else: - vals = [getattr(obj, attr) for attr in attrs if - hasattr(obj, attr) and isinstance(hasattr(obj, attr), (str, int))] - - for v in vals: - if str(v).find(val) != -1: - return True - return False - - -def sort_assets(assets, order_by='hostname', reverse=False): - if order_by == 'ip': - assets = sorted(assets, key=lambda asset: [int(d) for d in asset.ip.split('.') if d.isdigit()], reverse=reverse) - else: - assets = sorted(assets, key=lambda asset: getattr(asset, order_by), reverse=reverse) - return assets - - class ParserNode: nodes_only_fields = ("key", "value", "id") - assets_only_fields = ("hostname", "id", "ip", "protocols", "org_id") + assets_only_fields = ("hostname", "id", "ip", "protocols", "domain", "org_id") system_users_only_fields = ( "id", "name", "username", "protocol", "priority", "login_mode", ) @@ -490,6 +472,7 @@ class ParserNode: 'ip': asset.ip, 'protocols': asset.protocols_as_list, 'platform': asset.platform_base, + 'domain': asset.domain_id, 'org_name': asset.org_name, }, } diff --git a/apps/settings/api.py b/apps/settings/api.py index 025db8ae8..7b5383755 100644 --- a/apps/settings/api.py +++ b/apps/settings/api.py @@ -12,15 +12,14 @@ from django.utils.translation import ugettext_lazy as _ from .utils import ( LDAPServerUtil, LDAPCacheUtil, LDAPImportUtil, LDAPSyncUtil, - LDAP_USE_CACHE_FLAGS - + LDAP_USE_CACHE_FLAGS, LDAPTestUtil, ) from .tasks import sync_ldap_user_task from common.permissions import IsOrgAdmin, IsSuperUser from common.utils import get_logger from .serializers import ( - MailTestSerializer, LDAPTestSerializer, LDAPUserSerializer, - PublicSettingSerializer, + MailTestSerializer, LDAPTestConfigSerializer, LDAPUserSerializer, + PublicSettingSerializer, LDAPTestLoginSerializer, ) from users.models import User @@ -67,10 +66,18 @@ class MailTestingAPI(APIView): return Response({"error": str(serializer.errors)}, status=401) -class LDAPTestingAPI(APIView): +class LDAPTestingConfigAPI(APIView): permission_classes = (IsSuperUser,) - serializer_class = LDAPTestSerializer - success_message = _("Test ldap success") + serializer_class = LDAPTestConfigSerializer + + def post(self, request): + serializer = self.serializer_class(data=request.data) + if not serializer.is_valid(): + return Response({"error": str(serializer.errors)}, status=401) + config = self.get_ldap_config(serializer) + ok, msg = LDAPTestUtil(config).test_config() + status = 200 if ok else 401 + return Response(msg, status=status) @staticmethod def get_ldap_config(serializer): @@ -78,39 +85,36 @@ class LDAPTestingAPI(APIView): bind_dn = serializer.validated_data["AUTH_LDAP_BIND_DN"] password = serializer.validated_data["AUTH_LDAP_BIND_PASSWORD"] use_ssl = serializer.validated_data.get("AUTH_LDAP_START_TLS", False) - search_ougroup = serializer.validated_data["AUTH_LDAP_SEARCH_OU"] + search_ou = serializer.validated_data["AUTH_LDAP_SEARCH_OU"] search_filter = serializer.validated_data["AUTH_LDAP_SEARCH_FILTER"] attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"] + auth_ldap = serializer.validated_data.get('AUTH_LDAP', False) config = { 'server_uri': server_uri, 'bind_dn': bind_dn, 'password': password, 'use_ssl': use_ssl, - 'search_ougroup': search_ougroup, + 'search_ou': search_ou, 'search_filter': search_filter, - 'attr_map': json.loads(attr_map), + 'attr_map': attr_map, + 'auth_ldap': auth_ldap } return config + +class LDAPTestingLoginAPI(APIView): + permission_classes = (IsSuperUser,) + serializer_class = LDAPTestLoginSerializer + def post(self, request): serializer = self.serializer_class(data=request.data) if not serializer.is_valid(): return Response({"error": str(serializer.errors)}, status=401) - - attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"] - try: - json.loads(attr_map) - except json.JSONDecodeError: - return Response({"error": _("LDAP attr map not valid")}, status=401) - - config = self.get_ldap_config(serializer) - util = LDAPServerUtil(config=config) - try: - users = util.search() - except Exception as e: - return Response({"error": str(e)}, status=401) - - return Response({"msg": _("Match {} s users").format(len(users))}) + username = serializer.validated_data['username'] + password = serializer.validated_data['password'] + ok, msg = LDAPTestUtil().test_login(username, password) + status = 200 if ok else 401 + return Response(msg, status=status) class LDAPUserListApi(generics.ListAPIView): diff --git a/apps/settings/forms/security.py b/apps/settings/forms/security.py index 7b7cc247d..6d30d8b73 100644 --- a/apps/settings/forms/security.py +++ b/apps/settings/forms/security.py @@ -13,10 +13,10 @@ __all__ = ['SecuritySettingForm'] class SecuritySettingForm(BaseForm): # MFA global setting SECURITY_MFA_AUTH = forms.BooleanField( - required=False, label=_("MFA Secondary certification"), + required=False, label=_("MFA"), help_text=_( - 'After opening, the user login must use MFA secondary ' - 'authentication (valid for all users, including administrators)' + 'After opening, all user login must use MFA' + '(valid for all users, including administrators)' ) ) # Execute commands for user diff --git a/apps/settings/serializers/ldap.py b/apps/settings/serializers/ldap.py index 4009c0705..06ae3051f 100644 --- a/apps/settings/serializers/ldap.py +++ b/apps/settings/serializers/ldap.py @@ -3,10 +3,12 @@ from rest_framework import serializers -__all__ = ['LDAPTestSerializer', 'LDAPUserSerializer'] +__all__ = [ + 'LDAPTestConfigSerializer', 'LDAPUserSerializer', 'LDAPTestLoginSerializer' +] -class LDAPTestSerializer(serializers.Serializer): +class LDAPTestConfigSerializer(serializers.Serializer): AUTH_LDAP_SERVER_URI = serializers.CharField(max_length=1024) AUTH_LDAP_BIND_DN = serializers.CharField(max_length=1024, required=False, allow_blank=True) AUTH_LDAP_BIND_PASSWORD = serializers.CharField(required=False, allow_blank=True) @@ -14,6 +16,12 @@ class LDAPTestSerializer(serializers.Serializer): AUTH_LDAP_SEARCH_FILTER = serializers.CharField() AUTH_LDAP_USER_ATTR_MAP = serializers.CharField() AUTH_LDAP_START_TLS = serializers.BooleanField(required=False) + AUTH_LDAP = serializers.BooleanField(required=False) + + +class LDAPTestLoginSerializer(serializers.Serializer): + username = serializers.CharField(max_length=1024, required=True) + password = serializers.CharField(max_length=2014, required=True) class LDAPUserSerializer(serializers.Serializer): diff --git a/apps/settings/templates/settings/_ldap_test_user_login_modal.html b/apps/settings/templates/settings/_ldap_test_user_login_modal.html new file mode 100644 index 000000000..3359d2468 --- /dev/null +++ b/apps/settings/templates/settings/_ldap_test_user_login_modal.html @@ -0,0 +1,58 @@ +{% extends '_modal.html' %} +{% load i18n %} +{% block modal_id %}test_user_login_modal{% endblock %} +{% block modal_title%}{% trans "Test LDAP user login" %}{% endblock %} +{% block modal_comment %}{% trans "Save the configuration before testing the login" %}{% endblock %} +{% block modal_body %} +
    + {% csrf_token %} +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +{% endblock %} +{% block modal_confirm_id %}btn_test_user_login_modal_confirm{% endblock %} diff --git a/apps/settings/templates/settings/ldap_setting.html b/apps/settings/templates/settings/ldap_setting.html index 943a69322..42902391b 100644 --- a/apps/settings/templates/settings/ldap_setting.html +++ b/apps/settings/templates/settings/ldap_setting.html @@ -41,9 +41,10 @@ {% endfor %}
    -
    +
    - + +
    @@ -58,27 +59,28 @@
    {% include 'settings/_ldap_list_users_modal.html' %} + {% include 'settings/_ldap_test_user_login_modal.html' %} {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py index abee1c0d0..689e1ea82 100644 --- a/apps/settings/urls/api_urls.py +++ b/apps/settings/urls/api_urls.py @@ -8,7 +8,8 @@ app_name = 'common' urlpatterns = [ path('mail/testing/', api.MailTestingAPI.as_view(), name='mail-testing'), - path('ldap/testing/', api.LDAPTestingAPI.as_view(), name='ldap-testing'), + path('ldap/testing/config/', api.LDAPTestingConfigAPI.as_view(), name='ldap-testing-config'), + path('ldap/testing/login/', api.LDAPTestingLoginAPI.as_view(), name='ldap-testing-login'), path('ldap/users/', api.LDAPUserListApi.as_view(), name='ldap-user-list'), path('ldap/users/import/', api.LDAPUserImportAPI.as_view(), name='ldap-user-import'), path('ldap/cache/refresh/', api.LDAPCacheRefreshAPI.as_view(), name='ldap-cache-refresh'), diff --git a/apps/settings/utils/ldap.py b/apps/settings/utils/ldap.py index 726c74c82..36fe36f7f 100644 --- a/apps/settings/utils/ldap.py +++ b/apps/settings/utils/ldap.py @@ -1,21 +1,38 @@ # coding: utf-8 # -from ldap3 import Server, Connection +import json +from ldap3 import Server, Connection, SIMPLE +from ldap3.core.exceptions import ( + LDAPSocketOpenError, + LDAPSocketReceiveError, + LDAPSessionTerminatedByServerError, + LDAPUserNameIsMandatoryError, + LDAPPasswordIsMandatoryError, + LDAPInvalidDnError, + LDAPInvalidServerError, + LDAPBindError, + LDAPInvalidFilterError, + LDAPExceptionError, + LDAPConfigurationError, + LDAPAttributeError, +) from django.conf import settings from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ +from copy import deepcopy from common.const import LDAP_AD_ACCOUNT_DISABLE from common.utils import timeit, get_logger from users.utils import construct_user_email from users.models import User +from authentication.backends.ldap import LDAPAuthorizationBackend, LDAPUser logger = get_logger(__file__) __all__ = [ 'LDAPConfig', 'LDAPServerUtil', 'LDAPCacheUtil', 'LDAPImportUtil', - 'LDAPSyncUtil', 'LDAP_USE_CACHE_FLAGS' + 'LDAPSyncUtil', 'LDAP_USE_CACHE_FLAGS', 'LDAPTestUtil', ] LDAP_USE_CACHE_FLAGS = [1, '1', 'true', 'True', True] @@ -28,9 +45,10 @@ class LDAPConfig(object): self.bind_dn = None self.password = None self.use_ssl = None - self.search_ougroup = None + self.search_ou = None self.search_filter = None self.attr_map = None + self.auth_ldap = None if isinstance(config, dict): self.load_from_config(config) else: @@ -41,18 +59,20 @@ class LDAPConfig(object): self.bind_dn = config.get('bind_dn') self.password = config.get('password') self.use_ssl = config.get('use_ssl') - self.search_ougroup = config.get('search_ougroup') + self.search_ou = config.get('search_ou') self.search_filter = config.get('search_filter') self.attr_map = config.get('attr_map') + self.auth_ldap = config.get('auth_ldap') def load_from_settings(self): self.server_uri = settings.AUTH_LDAP_SERVER_URI self.bind_dn = settings.AUTH_LDAP_BIND_DN self.password = settings.AUTH_LDAP_BIND_PASSWORD self.use_ssl = settings.AUTH_LDAP_START_TLS - self.search_ougroup = settings.AUTH_LDAP_SEARCH_OU + self.search_ou = settings.AUTH_LDAP_SEARCH_OU self.search_filter = settings.AUTH_LDAP_SEARCH_FILTER self.attr_map = settings.AUTH_LDAP_USER_ATTR_MAP + self.auth_ldap = settings.AUTH_LDAP class LDAPServerUtil(object): @@ -93,7 +113,7 @@ class LDAPServerUtil(object): cookie = self.connection.result['controls']['1.2.840.113556.1.4.319']['value']['cookie'] return cookie except Exception as e: - logger.error(e) + logger.error(e, exc_info=True) return None def get_search_filter_extra(self): @@ -129,7 +149,7 @@ class LDAPServerUtil(object): def search_user_entries(self): logger.info("Search user entries") user_entries = list() - search_ous = str(self.config.search_ougroup).split('|') + search_ous = str(self.config.search_ou).split('|') for search_ou in search_ous: logger.info("Search user entries ou: {}".format(search_ou)) self.search_user_entries_ou(search_ou) @@ -332,4 +352,221 @@ class LDAPImportUtil(object): return errors +class LDAPTestUtil(object): + class LDAPInvalidSearchOuOrFilterError(LDAPExceptionError): + pass + + class LDAPInvalidAttributeMapError(LDAPExceptionError): + pass + + class LDAPNotEnabledAuthError(LDAPExceptionError): + pass + + class LDAPBeforeLoginCheckError(LDAPExceptionError): + pass + + def __init__(self, config=None): + self.config = LDAPConfig(config) + self.user_entries = [] + + def _test_connection_bind(self, authentication=None, user=None, password=None): + server = Server(self.config.server_uri) + connection = Connection( + server, user=user, password=password, authentication=authentication + ) + ret = connection.bind() + return ret + + # test server uri + + def _test_server_uri(self): + self._test_connection_bind() + + def test_server_uri(self): + try: + self._test_server_uri() + except LDAPSocketOpenError as e: + error = _("Host or port is disconnected: {}".format(e)) + except LDAPSessionTerminatedByServerError as e: + error = _('The port is not the port of the LDAP service: {}'.format(e)) + except LDAPSocketReceiveError as e: + error = _('Please enter the certificate: {}'.format(e)) + except Exception as e: + error = _('Unknown error: {}'.format(e)) + else: + return + raise LDAPInvalidServerError(error) + + # test bind dn + + def _test_bind_dn(self): + user = self.config.bind_dn + password = self.config.password + ret = self._test_connection_bind( + authentication=SIMPLE, user=user, password=password + ) + if not ret: + msg = _('bind dn or password incorrect') + raise LDAPInvalidDnError(msg) + + def test_bind_dn(self): + try: + self._test_bind_dn() + except LDAPUserNameIsMandatoryError as e: + error = _('Please enter bind dn: {}'.format(e)) + except LDAPPasswordIsMandatoryError as e: + error = _('Please enter password: {}'.format(e)) + except LDAPInvalidDnError as e: + error = _('Please enter correct bind dn and password: {}'.format(e)) + except Exception as e: + error = _('Unknown error: {}'.format(e)) + else: + return + raise LDAPBindError(error) + + # test search ou + + def _test_search_ou_and_filter(self): + config = deepcopy(self.config) + util = LDAPServerUtil(config=config) + search_ous = str(self.config.search_ou).split('|') + for search_ou in search_ous: + util.config.search_ou = search_ou + user_entries = util.search_user_entries() + logger.debug('Search ou: {}, count user: {}'.format(search_ou, len(user_entries))) + if len(user_entries) == 0: + error = _('Invalid search ou or filter: {}'.format(search_ou)) + raise self.LDAPInvalidSearchOuOrFilterError(error) + + def test_search_ou_and_filter(self): + try: + self._test_search_ou_and_filter() + except LDAPInvalidFilterError as e: + error = e + except self.LDAPInvalidSearchOuOrFilterError as e: + error = e + except LDAPAttributeError as e: + error = e + raise self.LDAPInvalidAttributeMapError(error) + except Exception as e: + error = _('Unknown error: {}'.format(e)) + else: + return + raise self.LDAPInvalidSearchOuOrFilterError(error) + + # test attr map + + def _test_attr_map(self): + attr_map = self.config.attr_map + if not isinstance(attr_map, dict): + attr_map = json.loads(attr_map) + self.config.attr_map = attr_map + + should_contain_attr = {'username', 'name', 'email'} + actually_contain_attr = set(attr_map.keys()) + result = should_contain_attr - actually_contain_attr + if len(result) != 0: + error = _('LDAP attribute not include: {}'.format(result)) + raise self.LDAPInvalidAttributeMapError(error) + + def test_attr_map(self): + try: + self._test_attr_map() + except json.JSONDecodeError: + error = _('LDAP attribute map is not dict') + except self.LDAPInvalidAttributeMapError as e: + error = e + except Exception as e: + error = _('Unknown error: {}'.format(e)) + else: + return + raise self.LDAPInvalidAttributeMapError(error) + + # test search + + def test_search(self): + util = LDAPServerUtil(config=self.config) + self.user_entries = util.search_user_entries() + + # test auth ldap enabled + + def test_enabled_auth_ldap(self): + if not self.config.auth_ldap: + error = _('LDAP authentication is not enabled') + raise self.LDAPNotEnabledAuthError(error) + + # test config + + def _test_config(self): + self.test_server_uri() + self.test_bind_dn() + self.test_attr_map() + self.test_search_ou_and_filter() + self.test_search() + self.test_enabled_auth_ldap() + + def test_config(self): + status = False + try: + self._test_config() + except LDAPInvalidServerError as e: + msg = _('Error (Invalid server uri): {}'.format(e)) + except LDAPBindError as e: + msg = _('Error (Invalid bind dn): {}'.format(e)) + except self.LDAPInvalidAttributeMapError as e: + msg = _('Error (Invalid attribute map): {}'.format(e)) + except self.LDAPInvalidSearchOuOrFilterError as e: + msg = _('Error (Invalid search ou or filter): {}'.format(e)) + except self.LDAPNotEnabledAuthError as e: + msg = _('Error (Not enabled LDAP authentication): {}'.format(e)) + except Exception as e: + msg = _('Error (Unknown): {}').format(e) + else: + status = True + msg = _('Succeed: Match {} s user'.format(len(self.user_entries))) + + if not status: + logger.error(msg, exc_info=True) + return status, msg + + # test login + + def _test_before_login_check(self, username, password): + ok, msg = self.test_config() + if not ok: + raise LDAPConfigurationError(msg) + + backend = LDAPAuthorizationBackend() + ok, msg = backend.pre_check(username, password) + if not ok: + raise self.LDAPBeforeLoginCheckError(msg) + + @staticmethod + def _test_login_auth(username, password): + backend = LDAPAuthorizationBackend() + ldap_user = LDAPUser(backend, username=username.strip()) + ldap_user._authenticate_user_dn(password) + + def _test_login(self, username, password): + self._test_before_login_check(username, password) + self._test_login_auth(username, password) + + def test_login(self, username, password): + status = False + try: + self._test_login(username, password) + except LDAPConfigurationError as e: + msg = _('Authentication failed (configuration incorrect): {}'.format(e)) + except self.LDAPBeforeLoginCheckError as e: + msg = _('Authentication failed (before login check failed): {}'.format(e)) + except LDAPUser.AuthenticationFailed as e: + msg = _('Authentication failed (username or password incorrect): {}'.format(e)) + except Exception as e: + msg = _("Authentication failed (Unknown): {}".format(e)) + else: + status = True + msg = _("Authentication success: {}".format(username)) + if not status: + logger.error(msg, exc_info=True) + return status, msg diff --git a/apps/static/img/logo_text.png b/apps/static/img/logo_text.png index 7471c24d51a4e64602e06b59962eca27077da965..8d741116c1a599de5ef54eef7953be1836682961 100644 GIT binary patch delta 11197 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 Su!<1eKNMxvWU8gU1^q8OT7LEb delta 20649 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"col-md-6 text-center"i>><"col-md-4"p>>'; var table = ele.DataTable({ @@ -767,6 +777,7 @@ jumpserver.initServerSideDataTable = function (options) { $('#uc').html(options.uc_html || ''); $('#fb').html(options.fb_html || ''); $('#fa').html(options.fa_html || ''); + $('#lb').html(options.lb_html || ''); }); var table_id = table.settings()[0].sTableId; $('#' + table_id + ' .ipt_check_all').on('click', function () { @@ -1205,6 +1216,14 @@ function objectAttrsIsBool(obj, attrs) { }) } +function objectAttrsIsNumber(obj, attrs) { + attrs.forEach(function (attr) { + if (!obj[attr]) { + obj[attr] = null; + } + }) +} + function cleanDateStr(d) { for (var i = 0; i < 3; i++) { if (!isNaN(Date.parse(d))) { @@ -1396,8 +1415,8 @@ function showCeleryTaskLog(taskId) { } function getUserLang(){ - let userLangZh = document.cookie.indexOf('django_language=en'); - if (userLangZh === -1){ + let userLangEN = document.cookie.indexOf('django_language=en'); + if (userLangEN === -1){ return 'zh-CN' } else{ @@ -1448,3 +1467,34 @@ function initDateRangePicker(selector, options) { function reloadPage() { setTimeout( function () {window.location.reload();}, 300); } + +function isEmptyObject(obj) { + return Object.keys(obj).length === 0 +} + +function getStatusIcon(status, mapping, title) { + var navy = ''; + var danger = ''; + var warning = ''; + var icons = { + navy: navy, + danger: danger, + warning: warning + }; + var defaultMapping = { + true: 'navy', + false: 'danger', + 1: 'navy', + 0: 'danger', + default: 'navy' + }; + if (!mapping) { + mapping = defaultMapping; + } + var name = mapping[status] || mapping['default']; + var icon = icons[name]; + if (title) { + icon = icon.replace('title=""', 'title="' + title + '"') + } + return icon; +} diff --git a/apps/templates/_base_only_msg_content.html b/apps/templates/_base_only_msg_content.html new file mode 100644 index 000000000..1d9ca6c8d --- /dev/null +++ b/apps/templates/_base_only_msg_content.html @@ -0,0 +1,67 @@ +{% load static %} +{% load i18n %} + + + + + + + JumpServer + + {% include '_head_css_js.html' %} + + + + + + + +
    +
    +
    +

    + {% block msg_title %} + {% trans 'Welcome to the JumpServer open source fortress' %} + {% endblock %} +

    + {% block msg_content %} +

    + {% 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." %} +

    +

    + {% 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." %} +

    +

    + {% 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.' %} +

    +

    + {% trans "Changes the world, starting with a little bit." %} +

    + {% endblock %} +
    +
    +
    +
    + + + {% block content_title %} + {% trans 'Login' %} + {% endblock %} +
    + {% block content %} {% endblock %} +
    +
    +
    +
    +
    +
    + {% include '_copyright.html' %} +
    +
    +
    + + diff --git a/apps/templates/_csv_update_modal.html b/apps/templates/_csv_update_modal.html index c4c31abda..05709bced 100644 --- a/apps/templates/_csv_update_modal.html +++ b/apps/templates/_csv_update_modal.html @@ -6,7 +6,7 @@ {% block modal_title%}csv {% trans 'Update' %}{% endblock %} {% block modal_body %} -
    + {% csrf_token %}
    diff --git a/apps/templates/_filter_dropdown.html b/apps/templates/_filter_dropdown.html index 3fff427bc..c3a749dcd 100644 --- a/apps/templates/_filter_dropdown.html +++ b/apps/templates/_filter_dropdown.html @@ -30,7 +30,7 @@ function addItem(menuRef, menuItem, parent) { } }) } -function initTableFilterDropdown(selector, menu) { +function initTableFilterDropdown(selector, menu, offset_x=0, offset_y=0) { /* menu = [ {title: "Title", value: "title"}, @@ -52,6 +52,8 @@ function initTableFilterDropdown(selector, menu) { y -= offset.top; {#x += 18;#} y += 30; + x += offset_x; + y += offset_y; $('.search-help').css({"top":y+"px", "left":x+"px", "position": "absolute"}); $('.dropdown-menu.search-help').show(); }); diff --git a/apps/templates/_nav.html b/apps/templates/_nav.html index e517b901e..2222c5fd0 100644 --- a/apps/templates/_nav.html +++ b/apps/templates/_nav.html @@ -122,7 +122,7 @@
    -

    {% trans ' Top 5 Active user' %}

    {% trans 'In the past week, a total of ' %}{{ user_visit_count_weekly }}{% trans ' users have logged in ' %}{{ asset_visit_count_weekly }}{% trans ' times asset.' %}
      {% for data in user_visit_count_top_five %} @@ -133,9 +132,8 @@ {{ data.total }}{% trans ' times' %}
    -

    {% trans 'The last time a user logged in' %}

    -

    {{ data.last.user }}

    -

    {% trans 'At ' %}{{ data.last.date_start |date:"Y-m-d H:i:s" }}

    +

    {% trans 'The time last logged in' %}

    +

    {% trans 'At' %} {{ data.last|date:"Y-m-d H:i:s" }}

    @@ -175,7 +173,6 @@ {% endifequal %} {{ login.user }} {% trans 'Login in ' %}{{ login.asset }}
    {{ login.date_start }} - {% endfor %} @@ -221,9 +218,8 @@ {{ data.total }}{% trans ' times' %}
    -

    {% trans 'The last time logged on to the host' %}

    -

    {{ data.last.asset }}

    -

    {% trans 'At ' %}{{ data.last.date_start |date:"Y-m-d H:i:s" }}

    +

    {% trans 'The time last logged in' %}

    +

    {% trans 'At' %} {{ data.last|date:"Y-m-d H:i:s" }}

    @@ -298,16 +294,16 @@ $(document).ready(function(){ ], series : [ { - name:"{% trans 'Login count' %}", + name: "{% trans 'Login count' %}", type:'line', - smooth:true, + smooth: true, itemStyle: {normal: {areaStyle: {type: 'default'}}}, data: {{ month_total_visit_count|safe}} }, { - name:"{% trans 'Active users' %}", - type:'line', - smooth:true, + name: "{% trans 'Active users' %}", + type: 'line', + smooth: true, itemStyle: {normal: {areaStyle: {type: 'default'}}}, data: {{ month_user|safe }} }, diff --git a/apps/terminal/api/command.py b/apps/terminal/api/command.py index 1dfd0ad3b..1bfa122eb 100644 --- a/apps/terminal/api/command.py +++ b/apps/terminal/api/command.py @@ -9,6 +9,7 @@ from rest_framework.response import Response from django.template import loader +from orgs.utils import current_org from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor from common.utils import get_logger from ..backends import ( @@ -28,6 +29,22 @@ class CommandQueryMixin: ] default_days_ago = 5 + @staticmethod + def get_org_id(): + if current_org.is_default(): + org_id = '' + else: + org_id = current_org.id + return org_id + + def get_query_risk_level(self): + risk_level = self.request.query_params.get('risk_level') + if risk_level is None: + return None + if risk_level.isdigit(): + return int(risk_level) + return None + def get_queryset(self): # 解决访问 /docs/ 问题 if hasattr(self, 'swagger_fake_view'): @@ -38,7 +55,8 @@ class CommandQueryMixin: queryset = multi_command_storage.filter( date_from=date_from, date_to=date_to, input=q.get("input"), user=q.get("user"), asset=q.get("asset"), - system_user=q.get("system_user") + system_user=q.get("system_user"), + risk_level=self.get_query_risk_level(), org_id=self.get_org_id(), ) return queryset diff --git a/apps/terminal/api/session.py b/apps/terminal/api/session.py index 7607d46de..b036f376c 100644 --- a/apps/terminal/api/session.py +++ b/apps/terminal/api/session.py @@ -1,21 +1,18 @@ # -*- coding: utf-8 -*- # -import os - -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, reverse from django.core.files.storage import default_storage -from django.http import HttpResponseNotFound -from django.conf import settings from rest_framework import viewsets from rest_framework.response import Response -import jms_storage from common.utils import is_uuid, get_logger +from common.mixins.api import AsyncApiMixin from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor from common.drf.filters import DatetimeRangeFilter from orgs.mixins.api import OrgBulkModelViewSet +from ..utils import find_session_replay_local, download_session_replay from ..hands import SystemUser -from ..models import Session, ReplayStorage +from ..models import Session from .. import serializers @@ -25,7 +22,10 @@ logger = get_logger(__name__) class SessionViewSet(OrgBulkModelViewSet): model = Session - serializer_class = serializers.SessionSerializer + serializer_classes = { + 'default': serializers.SessionSerializer, + 'display': serializers.SessionDisplaySerializer, + } permission_classes = (IsOrgAdminOrAppUser, ) filterset_fields = [ "user", "asset", "system_user", "remote_addr", @@ -59,10 +59,11 @@ class SessionViewSet(OrgBulkModelViewSet): return super().get_permissions() -class SessionReplayViewSet(viewsets.ViewSet): +class SessionReplayViewSet(AsyncApiMixin, viewsets.ViewSet): serializer_class = serializers.ReplaySerializer permission_classes = (IsOrgAdminOrAppUser | IsOrgAuditor,) session = None + download_cache_key = "SESSION_REPLAY_DOWNLOAD_{}" def create(self, request, *args, **kwargs): session_id = kwargs.get('pk') @@ -83,46 +84,36 @@ class SessionReplayViewSet(viewsets.ViewSet): logger.error(msg) return Response({'msg': serializer.errors}, status=401) - def retrieve(self, request, *args, **kwargs): - session_id = kwargs.get('pk') - session = get_object_or_404(Session, id=session_id) - + @staticmethod + def get_replay_data(session, url): tp = 'json' if session.protocol in ('rdp', 'vnc'): tp = 'guacamole' - data = {'type': tp, 'src': ''} - - # 新版本和老版本的文件后缀不同 - session_path = session.get_rel_replay_path() # 存在外部存储上的路径 - local_path = session.get_local_path() - local_path_v1 = session.get_local_path(version=1) - - # 去default storage中查找 - for _local_path in (local_path, local_path_v1, session_path): - if default_storage.exists(_local_path): - url = default_storage.url(_local_path) - data['src'] = url - return Response(data) - - replay_storages = ReplayStorage.objects.all() - configs = { - storage.name: storage.config - for storage in replay_storages - if not storage.in_defaults() + download_url = reverse('terminal:session-replay-download', kwargs={'pk': session.id}) + data = { + 'type': tp, 'src': url, + 'user': session.user, 'asset': session.asset, + 'system_user': session.system_user, + 'date_start': session.date_start, + 'date_end': session.date_end, + 'download_url': download_url, } - if not configs: - return HttpResponseNotFound() - - target_path = os.path.join(default_storage.base_location, local_path) # 保存到storage的路径 - target_dir = os.path.dirname(target_path) - if not os.path.isdir(target_dir): - os.makedirs(target_dir, exist_ok=True) - storage = jms_storage.get_multi_object_storage(configs) - ok, err = storage.download(session_path, target_path) - if not ok: - logger.error("Failed download replay file: {}".format(err)) - return HttpResponseNotFound() - data['src'] = default_storage.url(local_path) - return Response(data) + return data + def is_need_async(self): + if self.action != 'retrieve': + return False + return True + + def retrieve(self, request, *args, **kwargs): + session_id = kwargs.get('pk') + session = get_object_or_404(Session, id=session_id) + local_path, url = find_session_replay_local(session) + + if not local_path: + local_path, url = download_session_replay(session) + if not local_path: + return Response({"error": url}) + data = self.get_replay_data(session, url) + return Response(data) diff --git a/apps/terminal/backends/command/db.py b/apps/terminal/backends/command/db.py index 113b7ce17..bb4fa957a 100644 --- a/apps/terminal/backends/command/db.py +++ b/apps/terminal/backends/command/db.py @@ -18,12 +18,12 @@ class CommandStore(CommandBase): """ 保存命令到数据库 """ - self.model.objects.create( user=command["user"], asset=command["asset"], system_user=command["system_user"], input=command["input"], output=command["output"], session=command["session"], - org_id=command["org_id"], timestamp=command["timestamp"] + risk_level=command.get("risk_level", 0), org_id=command["org_id"], + timestamp=command["timestamp"] ) def bulk_save(self, commands): @@ -35,7 +35,8 @@ class CommandStore(CommandBase): _commands.append(self.model( user=c["user"], asset=c["asset"], system_user=c["system_user"], input=c["input"], output=c["output"], session=c["session"], - org_id=c["org_id"], timestamp=c["timestamp"] + risk_level=c.get("risk_level", 0), org_id=c["org_id"], + timestamp=c["timestamp"] )) error = False try: @@ -61,7 +62,7 @@ class CommandStore(CommandBase): def make_filter_kwargs( date_from=None, date_to=None, user=None, asset=None, system_user=None, - input=None, session=None): + input=None, session=None, risk_level=None, org_id=None): filter_kwargs = {} date_from_default = timezone.now() - datetime.timedelta(days=7) date_to_default = timezone.now() @@ -89,15 +90,19 @@ class CommandStore(CommandBase): filter_kwargs['input__icontains'] = input if session: filter_kwargs['session'] = session + if org_id is not None: + filter_kwargs['org_id'] = org_id + if risk_level is not None: + filter_kwargs['risk_level'] = risk_level return filter_kwargs def filter(self, date_from=None, date_to=None, user=None, asset=None, system_user=None, - input=None, session=None): + input=None, session=None, risk_level=None, org_id=None): filter_kwargs = self.make_filter_kwargs( date_from=date_from, date_to=date_to, user=user, asset=asset, system_user=system_user, input=input, - session=session, + session=session, risk_level=risk_level, org_id=org_id, ) queryset = self.model.objects.filter(**filter_kwargs) return queryset diff --git a/apps/terminal/backends/command/es.py b/apps/terminal/backends/command/es.py index 5aa16dfbf..43bd52c02 100644 --- a/apps/terminal/backends/command/es.py +++ b/apps/terminal/backends/command/es.py @@ -3,17 +3,21 @@ from datetime import datetime from jms_storage.es import ESStorage +from common.utils import get_logger from .base import CommandBase from .models import AbstractSessionCommand +logger = get_logger(__file__) + + class CommandStore(ESStorage, CommandBase): def __init__(self, params): super().__init__(params) def filter(self, date_from=None, date_to=None, user=None, asset=None, system_user=None, - input=None, session=None): + input=None, session=None, risk_level=None, org_id=None): if date_from is not None: if isinstance(date_from, float): @@ -22,9 +26,28 @@ class CommandStore(ESStorage, CommandBase): if isinstance(date_to, float): date_to = datetime.fromtimestamp(date_to) - data = super().filter(date_from=date_from, date_to=date_to, - user=user, asset=asset, system_user=system_user, - input=input, session=session) - return AbstractSessionCommand.from_multi_dict( - [item["_source"] for item in data["hits"] if item] - ) + try: + data = super().filter(date_from=date_from, date_to=date_to, + user=user, asset=asset, system_user=system_user, + input=input, session=session, + risk_level=risk_level, org_id=org_id) + except Exception as e: + logger.error(e, exc_info=True) + return [] + else: + return AbstractSessionCommand.from_multi_dict( + [item["_source"] for item in data["hits"] if item] + ) + + def count(self, date_from=None, date_to=None, user=None, asset=None, + system_user=None, input=None, session=None): + try: + count = super().count( + date_from=date_from, date_to=date_to, user=user, asset=asset, + system_user=system_user, input=input, session=session + ) + except Exception as e: + logger.error(e, exc_info=True) + return 0 + else: + return count diff --git a/apps/terminal/backends/command/models.py b/apps/terminal/backends/command/models.py index 39b3f9853..d0ec04bec 100644 --- a/apps/terminal/backends/command/models.py +++ b/apps/terminal/backends/command/models.py @@ -8,6 +8,12 @@ from orgs.mixins.models import OrgModelMixin class AbstractSessionCommand(OrgModelMixin): + RISK_LEVEL_ORDINARY = 0 + RISK_LEVEL_DANGEROUS = 5 + RISK_LEVEL_CHOICES = ( + (RISK_LEVEL_ORDINARY, _('Ordinary')), + (RISK_LEVEL_DANGEROUS, _('Dangerous')), + ) id = models.UUIDField(default=uuid.uuid4, primary_key=True) user = models.CharField(max_length=64, db_index=True, verbose_name=_("User")) asset = models.CharField(max_length=128, db_index=True, verbose_name=_("Asset")) @@ -15,6 +21,7 @@ class AbstractSessionCommand(OrgModelMixin): input = models.CharField(max_length=128, db_index=True, verbose_name=_("Input")) output = models.CharField(max_length=1024, blank=True, verbose_name=_("Output")) session = models.CharField(max_length=36, db_index=True, verbose_name=_("Session")) + risk_level = models.SmallIntegerField(default=RISK_LEVEL_ORDINARY, choices=RISK_LEVEL_CHOICES, db_index=True, verbose_name=_("Risk level")) timestamp = models.IntegerField(db_index=True) class Meta: diff --git a/apps/terminal/backends/command/serializers.py b/apps/terminal/backends/command/serializers.py index bea8c4764..657aa2356 100644 --- a/apps/terminal/backends/command/serializers.py +++ b/apps/terminal/backends/command/serializers.py @@ -12,6 +12,7 @@ class SessionCommandSerializer(serializers.Serializer): input = serializers.CharField(max_length=128) output = serializers.CharField(max_length=1024, allow_blank=True) session = serializers.CharField(max_length=36) + risk_level = serializers.IntegerField(required=False) org_id = serializers.CharField(max_length=36, required=False, default='', allow_null=True, allow_blank=True) timestamp = serializers.IntegerField() diff --git a/apps/terminal/migrations/0022_session_is_success.py b/apps/terminal/migrations/0022_session_is_success.py new file mode 100644 index 000000000..72ef9b283 --- /dev/null +++ b/apps/terminal/migrations/0022_session_is_success.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-02-27 08:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0021_auto_20200213_1316'), + ] + + operations = [ + migrations.AddField( + model_name='session', + name='is_success', + field=models.BooleanField(db_index=True, default=True), + ), + ] diff --git a/apps/terminal/migrations/0023_command_risk_level.py b/apps/terminal/migrations/0023_command_risk_level.py new file mode 100644 index 000000000..6ada1b826 --- /dev/null +++ b/apps/terminal/migrations/0023_command_risk_level.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-03-03 08:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0022_session_is_success'), + ] + + operations = [ + migrations.AddField( + model_name='command', + name='risk_level', + field=models.SmallIntegerField(choices=[(0, 'Ordinary'), (5, 'Dangerous')], db_index=True, default=0, verbose_name='Risk level'), + ), + ] diff --git a/apps/terminal/models.py b/apps/terminal/models.py index eebeb611c..f73eada2b 100644 --- a/apps/terminal/models.py +++ b/apps/terminal/models.py @@ -190,6 +190,7 @@ class Session(OrgModelMixin): system_user_id = models.CharField(blank=True, default='', max_length=36, db_index=True) login_from = models.CharField(max_length=2, choices=LOGIN_FROM_CHOICES, default="ST") remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) + is_success = models.BooleanField(default=True, db_index=True) is_finished = models.BooleanField(default=False, db_index=True) has_replay = models.BooleanField(default=False, verbose_name=_("Replay")) has_command = models.BooleanField(default=False, verbose_name=_("Command")) @@ -274,6 +275,48 @@ class Session(OrgModelMixin): def login_from_display(self): return self.get_login_from_display() + @classmethod + def generate_fake(cls, count=100, is_finished=True): + import random + from orgs.models import Organization + from users.models import User + from assets.models import Asset, SystemUser + from orgs.utils import get_current_org + from common.utils.random import random_datetime, random_ip + + org = get_current_org() + if not org or not org.is_real(): + Organization.default().change_to() + i = 0 + users = User.objects.all()[:100] + assets = Asset.objects.all()[:100] + system_users = SystemUser.objects.all()[:100] + while i < count: + user_random = random.choices(users, k=10) + assets_random = random.choices(assets, k=10) + system_users = random.choices(system_users, k=10) + + ziped = zip(user_random, assets_random, system_users) + sessions = [] + now = timezone.now() + month_ago = now - timezone.timedelta(days=30) + for user, asset, system_user in ziped: + ip = random_ip() + date_start = random_datetime(month_ago, now) + date_end = random_datetime(date_start, date_start+timezone.timedelta(hours=2)) + data = dict( + user=str(user), user_id=user.id, + asset=str(asset), asset_id=asset.id, + system_user=str(system_user), system_user_id=system_user.id, + remote_addr=ip, + date_start=date_start, + date_end=date_end, + is_finished=is_finished, + ) + sessions.append(Session(**data)) + cls.objects.bulk_create(sessions) + i += 10 + class Meta: db_table = "terminal_session" ordering = ["-date_start"] diff --git a/apps/terminal/serializers/session.py b/apps/terminal/serializers/session.py index 8d71b2a95..006529b15 100644 --- a/apps/terminal/serializers/session.py +++ b/apps/terminal/serializers/session.py @@ -4,21 +4,34 @@ from orgs.mixins.serializers import BulkOrgResourceModelSerializer from common.serializers import AdaptedBulkListSerializer from ..models import Session +__all__ = [ + 'SessionSerializer', 'SessionDisplaySerializer', + 'ReplaySerializer', +] + class SessionSerializer(BulkOrgResourceModelSerializer): - command_amount = serializers.IntegerField(read_only=True) org_id = serializers.CharField(allow_blank=True) class Meta: model = Session list_serializer_class = AdaptedBulkListSerializer fields = [ - "id", "user", "asset", "system_user", "login_from", - "login_from_display", "remote_addr", "is_finished", - "has_replay", "can_replay", "protocol", "date_start", "date_end", - "terminal", "command_amount", + "id", "user", "asset", "system_user", + "user_id", "asset_id", "system_user_id", + "login_from", "login_from_display", "remote_addr", + "is_success", "is_finished", "has_replay", "can_replay", + "protocol", "date_start", "date_end", + "terminal", ] +class SessionDisplaySerializer(SessionSerializer): + command_amount = serializers.IntegerField(read_only=True) + + class Meta(SessionSerializer.Meta): + fields = SessionSerializer.Meta.fields + ['command_amount'] + + class ReplaySerializer(serializers.Serializer): file = serializers.FileField(allow_empty_file=True) diff --git a/apps/terminal/templates/terminal/command_list.html b/apps/terminal/templates/terminal/command_list.html index 0e5ba0a32..eb51aaac4 100644 --- a/apps/terminal/templates/terminal/command_list.html +++ b/apps/terminal/templates/terminal/command_list.html @@ -26,6 +26,7 @@
    {% trans 'Command' %}{% trans 'Risk level' %} {% trans 'User' %} {% trans 'Asset' %} {% trans 'System user'%}
    + + + + + + + + + + {% for command in object_list %} + + + + + + + {% empty %} + + + + {% endfor %} + + + + + + +
    ID{% trans 'Command' %}{% trans 'Datetime' %}
    {{ forloop.counter }}{{ command.input | truncatechars:40 }}
    +$ {{ command.input }}
    +
    +{{ command.output }}
    +                                                
    {{ command.timestamp|ts_to_date}}
    {% trans "There is no command about this session" %}
    +
      +
      +
      + + + + + + + +{% endblock %} +{% block custom_foot_js %} + + +{% endblock %} diff --git a/apps/terminal/templates/terminal/session_commands_list_modal.html b/apps/terminal/templates/terminal/session_commands_list_modal.html deleted file mode 100644 index 1a1cee6a5..000000000 --- a/apps/terminal/templates/terminal/session_commands_list_modal.html +++ /dev/null @@ -1,58 +0,0 @@ -{% load static %} - - - - - - - {% include '_head_css_js.html' %} - - - - - - - -
      -
      -
      - - - - - - - - - - - - {% for command in object_list %} - - - - - - - {% endfor %} - - - - - - -
      IDCommandOutputDatetime
      {{ command.command_no }}{{ command.command }}{{ command.output_decode |safe }}{{ command.datetime }}
      -
        -
        -
        -
        -
        - - - - diff --git a/apps/terminal/templates/terminal/session_detail.html b/apps/terminal/templates/terminal/session_detail.html index ea5ba9b00..26ec1c785 100644 --- a/apps/terminal/templates/terminal/session_detail.html +++ b/apps/terminal/templates/terminal/session_detail.html @@ -4,6 +4,7 @@ {% load common_tags %} {% block custom_head_css_js %} + {% endblock %} {% block content %} @@ -14,11 +15,11 @@
        @@ -41,40 +42,41 @@
        - - +
        + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - {% for command in object_list %} - - - - - - - {% empty %} - - - - {% endfor %} - - - - -
        {% trans 'User' %}:{{ object.user }}
        ID{% trans 'Command' %}{% trans 'Datetime' %}{% trans 'Asset' %}:{{ object.asset }}
        {% trans 'System user' %}:{{ object.system_user }}
        {% trans 'Protocol' %}:{{ object.protocol }}
        {% trans 'Login from' %}:{{ object.login_from_display }}
        {% trans 'Remote addr' %}:{{ object.remote_addr }}
        {% trans 'Date start' %}:{{ object.date_start }}
        {% trans 'Date end' %}:{{ object.date_end }}
        {{ forloop.counter }}{{ command.input | truncatechars:40 }}
        -$ {{ command.input }}
        -
        -{{ command.output }}
        -                                                
        {{ command.timestamp|ts_to_date}}
        {% trans "There is no command about this session" %}
        -
          -
          @@ -96,6 +98,14 @@ $ {{ command.input }}
          {% trans 'Download replay' %}: + + + +
          - - - - - - - - - - - - - - - - - - - - - - - - - - -
          {% trans 'Name' %}:{{ terminal.name }}
          {% trans 'Remote addr' %}:{{ terminal.remote_addr }}
          {% trans 'SSH port' %}:{{ terminal.ssh_port }}
          {% trans 'Http port' %}:{{ terminal.http_port }}
          {% trans 'Date created' %}:{{ terminal.date_created }}
          {% trans 'Comment' %}:{{ asset.comment }}
          + + + {% trans 'Name' %}: + {{ terminal.name }} + + + {% trans 'Remote addr' %}: + {{ terminal.remote_addr }} + + + {% trans 'SSH port' %}: + {{ terminal.ssh_port }} + + + {% trans 'Http port' %}: + {{ terminal.http_port }} + + + {% trans 'Date created' %}: + {{ terminal.date_created }} + + + {% trans 'Comment' %}: + {{ asset.comment }} + + +
          diff --git a/apps/terminal/urls/views_urls.py b/apps/terminal/urls/views_urls.py index 5caec1e75..048a79d71 100644 --- a/apps/terminal/urls/views_urls.py +++ b/apps/terminal/urls/views_urls.py @@ -22,6 +22,8 @@ urlpatterns = [ path('session-online/', views.SessionOnlineListView.as_view(), name='session-online-list'), path('session-offline/', views.SessionOfflineListView.as_view(), name='session-offline-list'), path('session//', views.SessionDetailView.as_view(), name='session-detail'), + path('session//commands/', views.SessionCommandsView.as_view(), name='session-commands'), + path('session//replay/download/', views.SessionReplayDownloadView.as_view(), name='session-replay-download'), # Command view path('command/', views.CommandListView.as_view(), name='command-list'), diff --git a/apps/terminal/utils.py b/apps/terminal/utils.py index d7dace0a9..cb8308fcd 100644 --- a/apps/terminal/utils.py +++ b/apps/terminal/utils.py @@ -1,11 +1,18 @@ # -*- coding: utf-8 -*- # +import os + from django.core.cache import cache +from django.core.files.storage import default_storage +import jms_storage from assets.models import Asset, SystemUser from users.models import User - +from common.utils import get_logger from .const import USERS_CACHE_KEY, ASSETS_CACHE_KEY, SYSTEM_USER_CACHE_KEY +from .models import ReplayStorage + +logger = get_logger(__name__) def get_session_asset_list(): @@ -32,4 +39,50 @@ def get_system_user_list_from_cache(): return cache.get(SYSTEM_USER_CACHE_KEY) - +def find_session_replay_local(session): + # 新版本和老版本的文件后缀不同 + session_path = session.get_rel_replay_path() # 存在外部存储上的路径 + local_path = session.get_local_path() + local_path_v1 = session.get_local_path(version=1) + + # 去default storage中查找 + for _local_path in (local_path, local_path_v1, session_path): + if default_storage.exists(_local_path): + url = default_storage.url(_local_path) + return _local_path, url + return None, None + + +def download_session_replay(session): + session_path = session.get_rel_replay_path() # 存在外部存储上的路径 + local_path = session.get_local_path() + replay_storages = ReplayStorage.objects.all() + configs = { + storage.name: storage.config + for storage in replay_storages + if not storage.in_defaults() + } + if not configs: + msg = "Not found replay file, and not remote storage set" + return None, msg + + # 保存到storage的路径 + target_path = os.path.join(default_storage.base_location, local_path) + target_dir = os.path.dirname(target_path) + if not os.path.isdir(target_dir): + os.makedirs(target_dir, exist_ok=True) + storage = jms_storage.get_multi_object_storage(configs) + ok, err = storage.download(session_path, target_path) + if not ok: + msg = "Failed download replay file: {}".format(err) + logger.error(msg) + return None, msg + url = default_storage.url(local_path) + return local_path, url + + +def get_session_replay_url(session): + local_path, url = find_session_replay_local(session) + if local_path is None: + local_path, url = download_session_replay(session) + return local_path, url diff --git a/apps/terminal/views/session.py b/apps/terminal/views/session.py index 38558f292..a7e56cd57 100644 --- a/apps/terminal/views/session.py +++ b/apps/terminal/views/session.py @@ -1,22 +1,27 @@ # -*- coding: utf-8 -*- # +import os +import tarfile -from django.views.generic import ListView, TemplateView +from django.views.generic import ListView, TemplateView, DetailView from django.views.generic.edit import SingleObjectMixin from django.utils.translation import ugettext as _ from django.utils import timezone -from django.conf import settings +from django.utils.encoding import escape_uri_path +from django.http import FileResponse, HttpResponse +from django.core.files.storage import default_storage from common.permissions import PermissionsMixin, IsOrgAdmin, IsOrgAuditor -from common.mixins import DatetimeSearchMixin -from ..models import Session, Command, Terminal +from common.utils import model_to_json +from ..models import Session from ..backends import get_multi_command_storage from .. import utils __all__ = [ 'SessionOnlineListView', 'SessionOfflineListView', - 'SessionDetailView', + 'SessionDetailView', 'SessionReplayDownloadView', + 'SessionCommandsView', ] @@ -59,9 +64,23 @@ class SessionOfflineListView(SessionListView): return super().get_context_data(**kwargs) -class SessionDetailView(SingleObjectMixin, PermissionsMixin, ListView): +class SessionDetailView(PermissionsMixin, DetailView): template_name = 'terminal/session_detail.html' model = Session + permission_classes = [IsOrgAdmin | IsOrgAuditor] + + def get_context_data(self, **kwargs): + context = { + 'app': _('Sessions'), + 'action': _('Session detail'), + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class SessionCommandsView(SingleObjectMixin, PermissionsMixin, ListView): + template_name = 'terminal/session_commands.html' + model = Session object = None permission_classes = [IsOrgAdmin | IsOrgAuditor] @@ -81,3 +100,43 @@ class SessionDetailView(SingleObjectMixin, PermissionsMixin, ListView): kwargs.update(context) return super().get_context_data(**kwargs) + +class SessionReplayDownloadView(PermissionsMixin, DetailView): + permission_classes = [IsOrgAdmin | IsOrgAuditor] + model = Session + + @staticmethod + def prepare_offline_file(session, local_path): + replay_path = default_storage.path(local_path) + current_dir = os.getcwd() + dir_path = os.path.dirname(replay_path) + replay_filename = os.path.basename(replay_path) + meta_filename = '{}.json'.format(session.id) + offline_filename = '{}.tar'.format(session.id) + os.chdir(dir_path) + + with open(meta_filename, 'wt') as f: + f.write(model_to_json(session)) + + with tarfile.open(offline_filename, 'w') as f: + f.add(replay_filename) + f.add(meta_filename) + file = open(offline_filename, 'rb') + os.chdir(current_dir) + return file + + def get(self, request, *args, **kwargs): + session = self.get_object() + local_path, url = utils.get_session_replay_url(session) + if local_path is None: + error = url + return HttpResponse(error) + file = self.prepare_offline_file(session, local_path) + response = FileResponse(file) + response['Content-Type'] = 'application/octet-stream' + # 这里要注意哦,网上查到的方法都是response['Content-Disposition']='attachment;filename="filename.py"', + # 但是如果文件名是英文名没问题,如果文件名包含中文,下载下来的文件名会被改为url中的path。 + filename = escape_uri_path('{}.tar'.format(session.id)) + disposition = "attachment; filename*=UTF-8''{}".format(filename) + response["Content-Disposition"] = disposition + return response diff --git a/apps/users/forms/profile.py b/apps/users/forms/profile.py index bd1047733..775e33b1f 100644 --- a/apps/users/forms/profile.py +++ b/apps/users/forms/profile.py @@ -37,15 +37,15 @@ class UserMFAForm(forms.ModelForm): 'When enabled, ' 'you will enter the MFA binding process the next time you log in. ' 'you can also directly bind in ' - '"personal information -> quick modification -> change MFA Settings"!') + '"personal information -> quick modification -> change MFA Settings"!' + ) class Meta: model = User fields = ['mfa_level'] widgets = {'mfa_level': forms.RadioSelect()} help_texts = { - 'mfa_level': _('* Enable MFA authentication ' - 'to make the account more secure.'), + 'mfa_level': _('* Enable MFA to make the account more secure.'), } @@ -57,7 +57,7 @@ class UserFirstLoginFinishForm(forms.Form): 'In order to protect you and your company, ' 'please keep your account, ' 'password and key sensitive information properly. ' - '(for example: setting complex password, enabling MFA authentication)' + '(for example: setting complex password, enabling MFA)' ) diff --git a/apps/users/hands.py b/apps/users/hands.py index 5e2007c8a..50229f386 100644 --- a/apps/users/hands.py +++ b/apps/users/hands.py @@ -6,7 +6,7 @@ Other module of this app shouldn't connect with other app. - :copyright: (c) 2014-2018 by Jumpserver Team. + :copyright: (c) 2014-2018 by JumpServer Team. :license: GPL v2, see LICENSE for more details. """ diff --git a/apps/users/migrations/0025_auto_20200206_1216.py b/apps/users/migrations/0025_auto_20200206_1216.py new file mode 100644 index 000000000..7eed2ad54 --- /dev/null +++ b/apps/users/migrations/0025_auto_20200206_1216.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-02-06 04:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0024_auto_20191118_1612'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='source', + field=models.CharField(choices=[('local', 'Local'), ('ldap', 'LDAP/AD'), ('openid', 'OpenID'), ('radius', 'Radius'), ('cas', 'CAS')], default='local', max_length=30, verbose_name='Source'), + ), + ] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index a062f6c99..21cc9bf03 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -412,11 +412,11 @@ class MFAMixin: return self.check_otp(code) def mfa_enabled_but_not_set(self): - if self.mfa_enabled and \ - self.mfa_is_otp() and \ - not self.otp_secret_key: - return True - return False + if not self.mfa_enabled: + return False, None + if self.mfa_is_otp() and not self.otp_secret_key: + return True, reverse('users:user-otp-enable-start') + return False, None class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): @@ -424,11 +424,13 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): SOURCE_LDAP = 'ldap' SOURCE_OPENID = 'openid' SOURCE_RADIUS = 'radius' + SOURCE_CAS = 'cas' SOURCE_CHOICES = ( (SOURCE_LOCAL, _('Local')), (SOURCE_LDAP, 'LDAP/AD'), (SOURCE_OPENID, 'OpenID'), (SOURCE_RADIUS, 'Radius'), + (SOURCE_CAS, 'CAS'), ) id = models.UUIDField(default=uuid.uuid4, primary_key=True) @@ -532,9 +534,17 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): def is_local(self): return self.source == self.SOURCE_LOCAL - def save(self, *args, **kwargs): + def set_unprovide_attr_if_need(self): if not self.name: self.name = self.username + if not self.email or '@' not in self.email: + email = '{}@{}'.format(self.username, settings.EMAIL_SUFFIX) + if '@' in self.username: + email = self.username + self.email = email + + def save(self, *args, **kwargs): + self.set_unprovide_attr_if_need() if self.username == 'admin': self.role = 'Admin' self.is_active = True @@ -548,6 +558,11 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): def set_avatar(self, f): self.avatar.save(self.username, f) + @classmethod + def get_avatar_url(cls, username): + user_default = settings.STATIC_URL + "img/avatar/user.png" + return user_default + def avatar_url(self): admin_default = settings.STATIC_URL + "img/avatar/admin.png" user_default = settings.STATIC_URL + "img/avatar/user.png" diff --git a/apps/users/signals_handler.py b/apps/users/signals_handler.py index 902dfa4d7..45c03f3cf 100644 --- a/apps/users/signals_handler.py +++ b/apps/users/signals_handler.py @@ -3,6 +3,7 @@ from django.dispatch import receiver from django.db.models.signals import m2m_changed +from django_cas_ng.signals import cas_user_authenticated from common.utils import get_logger from .signals import post_user_create @@ -11,16 +12,6 @@ from .models import User logger = get_logger(__file__) -# @receiver(post_save, sender=User) -# def on_user_created(sender, instance=None, created=False, **kwargs): -# if created: -# logger.debug("Receive user `{}` create signal".format(instance.name)) -# from .utils import send_user_created_mail -# logger.info(" - Sending welcome mail ...".format(instance.name)) -# if instance.email: -# send_user_created_mail(instance) - - @receiver(post_user_create) def on_user_create(sender, user=None, **kwargs): logger.debug("Receive user `{}` create signal".format(user.name)) @@ -37,5 +28,12 @@ def on_user_groups_change(sender, instance=None, action='', **kwargs): """ if action.startswith('post'): logger.debug("User group member change signal recv: {}".format(instance)) - from perms.utils import AssetPermissionUtilV2 - AssetPermissionUtilV2.expire_all_user_tree_cache() + from perms.utils import AssetPermissionUtil + AssetPermissionUtil.expire_all_user_tree_cache() + + +@receiver(cas_user_authenticated) +def on_cas_user_authenticated(sender, user, created, **kwargs): + if created: + user.source = user.SOURCE_CAS + user.save() diff --git a/apps/users/templates/users/_base_otp.html b/apps/users/templates/users/_base_otp.html index 89ac8d656..a9589c50b 100644 --- a/apps/users/templates/users/_base_otp.html +++ b/apps/users/templates/users/_base_otp.html @@ -10,7 +10,7 @@ {% endblock %}
          -
          +
          {% trans 'Security token validation' %}  {% trans 'Account' %} {{ user.username }}  {% trans 'Follow these steps to complete the binding operation' %}

          {% block content %} diff --git a/apps/users/templates/users/_granted_assets.html b/apps/users/templates/users/_granted_assets.html index ce6bf3cd4..9d5006910 100644 --- a/apps/users/templates/users/_granted_assets.html +++ b/apps/users/templates/users/_granted_assets.html @@ -18,11 +18,6 @@
          -{#
          #} -{# #} -{# #} -{#
          #} @@ -131,6 +126,9 @@ function initTree(refresh) { }; $.get(treeUrl, function(data, status) { + if (data.length === 0) { + data.push({"name": "{% trans 'empty' %}", "id": ""}) + } zTree = $.fn.zTree.init($("#assetTree"), setting, data); if (!refresh) { initTable(); diff --git a/apps/users/templates/users/first_login.html b/apps/users/templates/users/first_login.html index fb8af6257..417d20a3d 100644 --- a/apps/users/templates/users/first_login.html +++ b/apps/users/templates/users/first_login.html @@ -58,13 +58,6 @@ {% csrf_token %} {{ wizard.management_form }} - {#{% if wizard.form.forms %}#} - {#{{ wizard.form.management_form }}#} - {#{% for form in wizard.form %}#} - {#{% bootstrap_form form %}#} - {#{% endfor %}#} - {#{% else %}#} - {#{% endif %}#} {% if form.finish_description %} {{ form.finish_description }}
          @@ -86,7 +79,6 @@ {% if form.pubkey_description and request.user.can_update_ssh_key %} {{ form.pubkey_description }} {% endif %} -
          @@ -111,11 +103,8 @@ {% endif %}
          -
          - - diff --git a/apps/users/templates/users/forgot_password.html b/apps/users/templates/users/forgot_password.html index d48cf0277..cbaadaa73 100644 --- a/apps/users/templates/users/forgot_password.html +++ b/apps/users/templates/users/forgot_password.html @@ -3,11 +3,6 @@ {% load i18n %} {% load bootstrap3 %} {% block custom_head_css_js %} - {% endblock %} {% block html_title %}{% trans 'Forgot password' %}{% endblock %} {% block title %} {% trans 'Forgot password' %}?{% endblock %} diff --git a/apps/users/templates/users/user_asset_permission.html b/apps/users/templates/users/user_asset_permission.html index 21ae72722..d0975f4b8 100644 --- a/apps/users/templates/users/user_asset_permission.html +++ b/apps/users/templates/users/user_asset_permission.html @@ -162,7 +162,7 @@ $(document).ready(function() { {title: "{% trans 'Exclude' %}", value: "0"}, ]}, ]; - initTableFilterDropdown('#permission_list_table_filter input', filterMenu) + initTableFilterDropdown('#permission_list_table_filter input', filterMenu, 15, 38) }) .on('click', '.toggle', function (e) { e.preventDefault(); diff --git a/apps/users/templates/users/user_detail.html b/apps/users/templates/users/user_detail.html index 7bac7a454..e9bdf8b92 100644 --- a/apps/users/templates/users/user_detail.html +++ b/apps/users/templates/users/user_detail.html @@ -74,7 +74,7 @@ - + {% endif %} - +
          {{ object.role_display }}
          {% trans 'MFA certification' %}:{% trans 'MFA' %}: {% if object.mfa_force_enabled %} {% trans 'Force enabled' %} diff --git a/apps/users/templates/users/user_disable_mfa.html b/apps/users/templates/users/user_disable_mfa.html index e95fd26a5..e1f6aa644 100644 --- a/apps/users/templates/users/user_disable_mfa.html +++ b/apps/users/templates/users/user_disable_mfa.html @@ -29,7 +29,7 @@ {% endblock %} diff --git a/apps/users/templates/users/user_password_check.html b/apps/users/templates/users/user_otp_check_password.html similarity index 96% rename from apps/users/templates/users/user_password_check.html rename to apps/users/templates/users/user_otp_check_password.html index 217bdc88b..f83e03b78 100644 --- a/apps/users/templates/users/user_password_check.html +++ b/apps/users/templates/users/user_otp_check_password.html @@ -10,15 +10,11 @@
          {% csrf_token %}
          - +
          - - {% if 'password' in form.errors %}

          {{ form.password.errors.as_text }}

          {% endif %} -
          {% endblock %} - diff --git a/apps/users/templates/users/user_otp_enable_bind.html b/apps/users/templates/users/user_otp_enable_bind.html index 7aaa25236..65d770cde 100644 --- a/apps/users/templates/users/user_otp_enable_bind.html +++ b/apps/users/templates/users/user_otp_enable_bind.html @@ -3,11 +3,10 @@ {% load i18n %} {% block small_title %} - {% trans 'Bind' %} + {% trans 'Bind one-time password authenticator' %} {% endblock %} {% block content %} -

          @@ -17,21 +16,15 @@

          Secret: {{ otp_secret_key }}
          -
          {% csrf_token %} -
          - - - {% if 'otp_code' in form.errors %}

          {{ form.otp_code.errors.as_text }}

          {% endif %} -
          diff --git a/apps/users/templates/users/user_otp_enable_install_app.html b/apps/users/templates/users/user_otp_enable_install_app.html index 809296b2c..e3462cd3a 100644 --- a/apps/users/templates/users/user_otp_enable_install_app.html +++ b/apps/users/templates/users/user_otp_enable_install_app.html @@ -3,12 +3,16 @@ {% load static %} {% block small_title %} - {% trans 'Install' %} + {% trans 'Install app' %} {% endblock %} {% block content %}
          -

          {% trans 'Download and install the Google Authenticator application on your phone' %}

          +

          + + {% trans 'Download and install the Google Authenticator application on your phone' %} + +

          {% trans 'Android downloads' %}

          @@ -27,7 +31,6 @@ diff --git a/apps/users/templates/users/user_password_verify.html b/apps/users/templates/users/user_password_verify.html new file mode 100644 index 000000000..bfc72ee87 --- /dev/null +++ b/apps/users/templates/users/user_password_verify.html @@ -0,0 +1,24 @@ +{% extends '_base_only_content.html' %} +{% load static %} +{% load i18n %} +{% load bootstrap3 %} +{% block custom_head_css_js %} +{% endblock %} + +{% block html_title %}{% trans 'Verify password' %}{% endblock %} +{% block title %} {% trans 'Verify password' %}{% endblock %} + +{% block content %} +
          + {% if 'password' in form.errors %} +

          {{ form.password.errors.as_text }}

          + {% endif %} + {% csrf_token %} +
          + +
          + +
          +{% endblock %} + + diff --git a/apps/users/templates/users/user_profile.html b/apps/users/templates/users/user_profile.html index 8c9088504..8f76871f3 100644 --- a/apps/users/templates/users/user_profile.html +++ b/apps/users/templates/users/user_profile.html @@ -84,7 +84,7 @@
          {% trans 'MFA certification' %}{% trans 'MFA' %} {% if user.mfa_force_enabled %} {% trans 'Force enable' %} @@ -166,7 +166,7 @@ ">{% trans 'Disable' %} {% endif %} {% else %} - {% url 'users:user-otp-enable-authentication' %} + {% url 'users:user-otp-enable-start' %} ">{% trans 'Enable' %} {% endif %} diff --git a/apps/users/urls/views_urls.py b/apps/users/urls/views_urls.py index 7773ca7d6..23f209d5c 100644 --- a/apps/users/urls/views_urls.py +++ b/apps/users/urls/views_urls.py @@ -13,6 +13,7 @@ urlpatterns = [ path('password/forgot/sendmail-success/', views.UserForgotPasswordSendmailSuccessView.as_view(), name='forgot-password-sendmail-success'), path('password/reset/', views.UserResetPasswordView.as_view(), name='reset-password'), path('password/reset/success/', views.UserResetPasswordSuccessView.as_view(), name='reset-password-success'), + path('password/verify/', views.UserVerifyPasswordView.as_view(), name='user-verify-password'), # Profile path('profile/', views.UserProfileView.as_view(), name='user-profile'), @@ -20,7 +21,8 @@ urlpatterns = [ path('profile/password/update/', views.UserPasswordUpdateView.as_view(), name='user-password-update'), path('profile/pubkey/update/', views.UserPublicKeyUpdateView.as_view(), name='user-pubkey-update'), path('profile/pubkey/generate/', views.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'), - path('profile/otp/enable/authentication/', views.UserCheckPasswordView.as_view(), name='user-otp-enable-authentication'), + + path('profile/otp/enable/start/', views.UserOtpEnableStartView.as_view(), name='user-otp-enable-start'), 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.UserDisableMFAView.as_view(), name='user-otp-disable-authentication'), diff --git a/apps/users/utils.py b/apps/users/utils.py index 5acb4df9a..831c6571f 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -1,6 +1,5 @@ # ~*~ coding: utf-8 ~*~ # -from __future__ import unicode_literals import os import re import pyotp @@ -14,7 +13,8 @@ from django.core.cache import cache from datetime import datetime from common.tasks import send_mail_async -from common.utils import reverse +from common.utils import reverse, get_object_or_none +from .models import User logger = logging.getLogger('jumpserver') @@ -193,34 +193,17 @@ def send_reset_ssh_key_mail(user): send_mail_async.delay(subject, message, recipient_list, html_message=message) -def get_user_or_tmp_user(request): +def get_user_or_pre_auth_user(request): user = request.user - tmp_user = get_tmp_user_from_cache(request) if user.is_authenticated: return user - elif tmp_user: - return tmp_user - else: - raise Http404("Not found this user") - - -def get_tmp_user_from_cache(request): - if not request.session.session_key: - return None - user = cache.get(request.session.session_key+'user') + pre_auth_user_id = request.session.get('user_id') + user = None + if pre_auth_user_id: + user = get_object_or_none(User, pk=pre_auth_user_id) return user -def set_tmp_user_to_cache(request, user, ttl=3600): - cache.set(request.session.session_key+'user', user, ttl) - - -def delete_tmp_user_for_cache(request): - if not request.session.session_key: - return None - cache.delete(request.session.session_key+'user') - - def redirect_user_first_login_or_index(request, redirect_field_name): if request.user.is_first_login: return reverse('users:user-first-login') @@ -231,15 +214,13 @@ def redirect_user_first_login_or_index(request, redirect_field_name): return url_in_get -def generate_otp_uri(request, issuer="Jumpserver"): - user = get_user_or_tmp_user(request) - otp_secret_key = cache.get(request.session.session_key+'otp_key', '') - if not otp_secret_key: +def generate_otp_uri(username, otp_secret_key=None, issuer="JumpServer"): + if otp_secret_key is None: otp_secret_key = base64.b32encode(os.urandom(10)).decode('utf-8') - cache.set(request.session.session_key+'otp_key', otp_secret_key, 600) totp = pyotp.TOTP(otp_secret_key) otp_issuer_name = settings.OTP_ISSUER_NAME or issuer - return totp.provisioning_uri(name=user.username, issuer_name=otp_issuer_name), otp_secret_key + uri = totp.provisioning_uri(name=username, issuer_name=otp_issuer_name) + return uri, otp_secret_key def check_otp_code(otp_secret_key, otp_code): diff --git a/apps/users/views/profile.py b/apps/users/views/profile.py deleted file mode 100644 index dc0359fa9..000000000 --- a/apps/users/views/profile.py +++ /dev/null @@ -1,272 +0,0 @@ -# ~*~ coding: utf-8 ~*~ - -from __future__ import unicode_literals - - -from django.contrib.auth import authenticate -from django.core.cache import cache -from django.conf import settings -from django.http import HttpResponse -from django.shortcuts import redirect -from django.urls import reverse_lazy, reverse -from django.utils.translation import ugettext as _ -from django.views import View -from django.views.generic.base import TemplateView -from django.views.generic.edit import ( - UpdateView, FormView -) -from django.contrib.auth import logout as auth_logout - -from common.utils import get_logger, ssh_key_gen -from common.permissions import ( - PermissionsMixin, IsValidUser, - UserCanUpdatePassword, UserCanUpdateSSHKey, -) -from .. import forms -from ..models import User -from ..utils import ( - generate_otp_uri, check_otp_code, get_user_or_tmp_user, - delete_tmp_user_for_cache, check_password_rules, get_password_check_rules, -) - -__all__ = [ - 'UserProfileView', - 'UserProfileUpdateView', 'UserPasswordUpdateView', - 'UserPublicKeyUpdateView', 'UserPublicKeyGenerateView', - 'UserCheckPasswordView', 'UserOtpEnableInstallAppView', - 'UserOtpEnableBindView', 'UserOtpSettingsSuccessView', - 'UserDisableMFAView', 'UserOtpUpdateView', -] - -logger = get_logger(__name__) - - -class UserProfileView(PermissionsMixin, TemplateView): - template_name = 'users/user_profile.html' - permission_classes = [IsValidUser] - - def get_context_data(self, **kwargs): - mfa_setting = settings.SECURITY_MFA_AUTH - context = { - 'action': _('Profile'), - 'mfa_setting': mfa_setting if mfa_setting is not None else False, - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - -class UserProfileUpdateView(PermissionsMixin, UpdateView): - template_name = 'users/user_profile_update.html' - model = User - permission_classes = [IsValidUser] - form_class = forms.UserProfileForm - success_url = reverse_lazy('users:user-profile') - - def get_object(self, queryset=None): - return self.request.user - - def get_context_data(self, **kwargs): - context = { - 'app': _('User'), - 'action': _('Profile setting'), - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - -class UserPasswordUpdateView(PermissionsMixin, UpdateView): - template_name = 'users/user_password_update.html' - model = User - form_class = forms.UserPasswordForm - success_url = reverse_lazy('users:user-profile') - permission_classes = [IsValidUser, UserCanUpdatePassword] - - def get_object(self, queryset=None): - return self.request.user - - def get_context_data(self, **kwargs): - check_rules = get_password_check_rules() - context = { - 'app': _('Users'), - 'action': _('Password update'), - 'password_check_rules': check_rules, - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - def get_success_url(self): - auth_logout(self.request) - return super().get_success_url() - - def form_valid(self, form): - password = form.cleaned_data.get('new_password') - is_ok = check_password_rules(password) - if not is_ok: - form.add_error( - "new_password", - _("* Your password does not meet the requirements") - ) - return self.form_invalid(form) - return super().form_valid(form) - - -class UserPublicKeyUpdateView(PermissionsMixin, UpdateView): - template_name = 'users/user_pubkey_update.html' - model = User - form_class = forms.UserPublicKeyForm - permission_classes = [IsValidUser, UserCanUpdateSSHKey] - success_url = reverse_lazy('users:user-profile') - - def get_object(self, queryset=None): - return self.request.user - - def get_context_data(self, **kwargs): - context = { - 'app': _('Users'), - 'action': _('Public key update'), - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - -class UserPublicKeyGenerateView(PermissionsMixin, View): - permission_classes = [IsValidUser] - - def get(self, request, *args, **kwargs): - private, public = ssh_key_gen(username=request.user.username, hostname='jumpserver') - request.user.public_key = public - request.user.save() - response = HttpResponse(private, content_type='text/plain') - filename = "{0}-jumpserver.pem".format(request.user.username) - response['Content-Disposition'] = 'attachment; filename={}'.format(filename) - return response - - -class UserCheckPasswordView(FormView): - template_name = 'users/user_password_check.html' - form_class = forms.UserCheckPasswordForm - - def form_valid(self, form): - user = get_user_or_tmp_user(self.request) - password = form.cleaned_data.get('password') - user = authenticate(username=user.username, password=password) - if not user: - form.add_error("password", _("Password invalid")) - return self.form_invalid(form) - if not user.mfa_is_otp(): - user.enable_mfa() - user.save() - return redirect(self.get_success_url()) - - def get_success_url(self): - if settings.OTP_IN_RADIUS: - success_url = reverse_lazy('users:user-otp-settings-success') - else: - success_url = reverse('users:user-otp-enable-install-app') - return success_url - - def get_context_data(self, **kwargs): - context = { - 'user': get_user_or_tmp_user(self.request) - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - -class UserOtpEnableInstallAppView(TemplateView): - template_name = 'users/user_otp_enable_install_app.html' - - def get_context_data(self, **kwargs): - user = get_user_or_tmp_user(self.request) - context = { - 'user': user - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - -class UserOtpEnableBindView(TemplateView, FormView): - template_name = 'users/user_otp_enable_bind.html' - form_class = forms.UserCheckOtpCodeForm - success_url = reverse_lazy('users:user-otp-settings-success') - - def form_valid(self, form): - otp_code = form.cleaned_data.get('otp_code') - otp_secret_key = cache.get(self.request.session.session_key+'otp_key', '') - - if check_otp_code(otp_secret_key, otp_code): - self.save_otp(otp_secret_key) - return super().form_valid(form) - - else: - 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): - user = get_user_or_tmp_user(self.request) - user.enable_mfa() - user.otp_secret_key = otp_secret_key - user.save() - - def get_context_data(self, **kwargs): - user = get_user_or_tmp_user(self.request) - otp_uri, otp_secret_key = generate_otp_uri(self.request) - context = { - 'otp_uri': otp_uri, - 'otp_secret_key': otp_secret_key, - 'user': user - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - -class UserDisableMFAView(FormView): - template_name = 'users/user_disable_mfa.html' - form_class = forms.UserCheckOtpCodeForm - success_url = reverse_lazy('users:user-otp-settings-success') - - def form_valid(self, form): - user = self.request.user - otp_code = form.cleaned_data.get('otp_code') - - valid = user.check_mfa(otp_code) - if valid: - user.disable_mfa() - user.save() - return super().form_valid(form) - else: - form.add_error('otp_code', _('MFA code invalid, or ntp sync server time')) - return super().form_invalid(form) - - -class UserOtpUpdateView(UserDisableMFAView): - success_url = reverse_lazy('users:user-otp-enable-bind') - - -class UserOtpSettingsSuccessView(TemplateView): - template_name = 'flash_message_standalone.html' - - def get_context_data(self, **kwargs): - title, describe = self.get_title_describe() - context = { - 'title': title, - 'messages': describe, - 'interval': 1, - 'redirect_url': reverse('authentication:login'), - 'auto_redirect': True, - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - def get_title_describe(self): - user = get_user_or_tmp_user(self.request) - if self.request.user.is_authenticated: - auth_logout(self.request) - title = _('MFA enable success') - describe = _('MFA enable success, return login page') - if not user.mfa_enabled: - title = _('MFA disable success') - describe = _('MFA disable success, return login page') - delete_tmp_user_for_cache(self.request) - return title, describe - diff --git a/apps/users/views/profile/__init__.py b/apps/users/views/profile/__init__.py new file mode 100644 index 000000000..1bc58d06f --- /dev/null +++ b/apps/users/views/profile/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# +from .base import * +from .password import * +from .pubkey import * +from .mfa import * +from .otp import * diff --git a/apps/users/views/profile/base.py b/apps/users/views/profile/base.py new file mode 100644 index 000000000..2044a0c94 --- /dev/null +++ b/apps/users/views/profile/base.py @@ -0,0 +1,50 @@ +# ~*~ coding: utf-8 ~*~ +from django.conf import settings +from django.urls import reverse_lazy +from django.utils.translation import ugettext as _ +from django.views.generic.base import TemplateView +from django.views.generic.edit import UpdateView + +from common.utils import get_logger +from common.permissions import ( + PermissionsMixin, IsValidUser, +) +from ... import forms +from ...models import User + + +__all__ = ['UserProfileView', 'UserProfileUpdateView'] +logger = get_logger(__name__) + + +class UserProfileView(PermissionsMixin, TemplateView): + template_name = 'users/user_profile.html' + permission_classes = [IsValidUser] + + def get_context_data(self, **kwargs): + mfa_setting = settings.SECURITY_MFA_AUTH + context = { + 'action': _('Profile'), + 'mfa_setting': mfa_setting if mfa_setting is not None else False, + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class UserProfileUpdateView(PermissionsMixin, UpdateView): + template_name = 'users/user_profile_update.html' + model = User + permission_classes = [IsValidUser] + form_class = forms.UserProfileForm + success_url = reverse_lazy('users:user-profile') + + def get_object(self, queryset=None): + return self.request.user + + def get_context_data(self, **kwargs): + context = { + 'app': _('User'), + 'action': _('Profile setting'), + } + kwargs.update(context) + return super().get_context_data(**kwargs) diff --git a/apps/users/views/profile/mfa.py b/apps/users/views/profile/mfa.py new file mode 100644 index 000000000..ec51c5a2b --- /dev/null +++ b/apps/users/views/profile/mfa.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# diff --git a/apps/users/views/profile/otp.py b/apps/users/views/profile/otp.py new file mode 100644 index 000000000..7bae70c58 --- /dev/null +++ b/apps/users/views/profile/otp.py @@ -0,0 +1,136 @@ +# ~*~ coding: utf-8 ~*~ + +from django.urls import reverse_lazy, reverse +from django.utils.translation import ugettext as _ +from django.views.generic.base import TemplateView +from django.views.generic.edit import FormView +from django.contrib.auth import logout as auth_logout +from django.conf import settings + +from common.utils import get_logger +from common.permissions import IsValidUser +from ... import forms +from .password import UserVerifyPasswordView +from ...utils import ( + generate_otp_uri, check_otp_code, get_user_or_pre_auth_user, +) + +__all__ = [ + 'UserOtpEnableStartView', + 'UserOtpEnableInstallAppView', + 'UserOtpEnableBindView', 'UserOtpSettingsSuccessView', + 'UserDisableMFAView', 'UserOtpUpdateView', +] + +logger = get_logger(__name__) + + +class UserOtpEnableStartView(UserVerifyPasswordView): + template_name = 'users/user_otp_check_password.html' + + def get_success_url(self): + if settings.OTP_IN_RADIUS: + success_url = reverse_lazy('users:user-otp-settings-success') + else: + success_url = reverse('users:user-otp-enable-install-app') + return success_url + + +class UserOtpEnableInstallAppView(TemplateView): + template_name = 'users/user_otp_enable_install_app.html' + + def get_context_data(self, **kwargs): + user = get_user_or_pre_auth_user(self.request) + context = {'user': user} + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class UserOtpEnableBindView(TemplateView, FormView): + template_name = 'users/user_otp_enable_bind.html' + form_class = forms.UserCheckOtpCodeForm + success_url = reverse_lazy('users:user-otp-settings-success') + + def form_valid(self, form): + otp_code = form.cleaned_data.get('otp_code') + otp_secret_key = self.request.session.get('otp_secret_key', '') + + valid = check_otp_code(otp_secret_key, otp_code) + if valid: + self.save_otp(otp_secret_key) + return super().form_valid(form) + else: + error = _("MFA code invalid, or ntp sync server time") + form.add_error("otp_code", error) + return self.form_invalid(form) + + def save_otp(self, otp_secret_key): + user = get_user_or_pre_auth_user(self.request) + user.enable_mfa() + user.otp_secret_key = otp_secret_key + user.save() + + def get_context_data(self, **kwargs): + user = get_user_or_pre_auth_user(self.request) + otp_uri, otp_secret_key = generate_otp_uri(user.username) + self.request.session['otp_secret_key'] = otp_secret_key + context = { + 'otp_uri': otp_uri, + 'otp_secret_key': otp_secret_key, + 'user': user + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class UserDisableMFAView(FormView): + template_name = 'users/user_disable_mfa.html' + form_class = forms.UserCheckOtpCodeForm + success_url = reverse_lazy('users:user-otp-settings-success') + permission_classes = [IsValidUser] + + def form_valid(self, form): + user = self.request.user + otp_code = form.cleaned_data.get('otp_code') + + valid = user.check_mfa(otp_code) + if valid: + user.disable_mfa() + user.save() + return super().form_valid(form) + else: + error = _('MFA code invalid, or ntp sync server time') + form.add_error('otp_code', error) + return super().form_invalid(form) + + +class UserOtpUpdateView(UserDisableMFAView): + success_url = reverse_lazy('users:user-otp-enable-bind') + + +class UserOtpSettingsSuccessView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get_context_data(self, **kwargs): + title, describe = self.get_title_describe() + context = { + 'title': title, + 'messages': describe, + 'interval': 1, + 'redirect_url': reverse('authentication:login'), + 'auto_redirect': True, + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + def get_title_describe(self): + user = get_user_or_pre_auth_user(self.request) + if self.request.user.is_authenticated: + auth_logout(self.request) + title = _('MFA enable success') + describe = _('MFA enable success, return login page') + if not user.mfa_enabled: + title = _('MFA disable success') + describe = _('MFA disable success, return login page') + return title, describe + diff --git a/apps/users/views/profile/password.py b/apps/users/views/profile/password.py new file mode 100644 index 000000000..c9bb97f38 --- /dev/null +++ b/apps/users/views/profile/password.py @@ -0,0 +1,94 @@ +# ~*~ coding: utf-8 ~*~ + +from django.contrib.auth import authenticate +from django.shortcuts import redirect +from django.urls import reverse_lazy, reverse +from django.utils.translation import ugettext as _ +from django.views.generic.edit import UpdateView, FormView +from django.contrib.auth import logout as auth_logout + +from common.utils import get_logger +from common.permissions import ( + PermissionsMixin, IsValidUser, + UserCanUpdatePassword +) +from ... import forms +from ...models import User +from ...utils import ( + get_user_or_pre_auth_user, + check_password_rules, get_password_check_rules, +) + +__all__ = ['UserPasswordUpdateView', 'UserVerifyPasswordView'] + +logger = get_logger(__name__) + + +class UserPasswordUpdateView(PermissionsMixin, UpdateView): + template_name = 'users/user_password_update.html' + model = User + form_class = forms.UserPasswordForm + success_url = reverse_lazy('users:user-profile') + permission_classes = [IsValidUser, UserCanUpdatePassword] + + def get_object(self, queryset=None): + return self.request.user + + def get_context_data(self, **kwargs): + check_rules = get_password_check_rules() + context = { + 'app': _('Users'), + 'action': _('Password update'), + 'password_check_rules': check_rules, + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + def get_success_url(self): + auth_logout(self.request) + return super().get_success_url() + + def form_valid(self, form): + password = form.cleaned_data.get('new_password') + is_ok = check_password_rules(password) + if not is_ok: + form.add_error( + "new_password", + _("* Your password does not meet the requirements") + ) + return self.form_invalid(form) + return super().form_valid(form) + + +class UserVerifyPasswordView(FormView): + template_name = 'users/user_password_verify.html' + form_class = forms.UserCheckPasswordForm + + def form_valid(self, form): + user = get_user_or_pre_auth_user(self.request) + password = form.cleaned_data.get('password') + user = authenticate(username=user.username, password=password) + if not user: + form.add_error("password", _("Password invalid")) + return self.form_invalid(form) + if not user.mfa_is_otp(): + user.enable_mfa() + user.save() + self.request.session['user_id'] = str(user.id) + self.request.session['auth_password'] = 1 + return redirect(self.get_success_url()) + + def get_success_url(self): + referer = self.request.META.get('HTTP_REFERER') + next_url = self.request.GET.get("next") + if next_url: + return next_url + else: + return referer + + def get_context_data(self, **kwargs): + context = { + 'user': get_user_or_pre_auth_user(self.request) + } + kwargs.update(context) + return super().get_context_data(**kwargs) diff --git a/apps/users/views/profile/pubkey.py b/apps/users/views/profile/pubkey.py new file mode 100644 index 000000000..29b557232 --- /dev/null +++ b/apps/users/views/profile/pubkey.py @@ -0,0 +1,54 @@ +# ~*~ coding: utf-8 ~*~ + +from django.http import HttpResponse +from django.urls import reverse_lazy +from django.utils.translation import ugettext as _ +from django.views import View +from django.views.generic.edit import UpdateView + +from common.utils import get_logger, ssh_key_gen +from common.permissions import ( + PermissionsMixin, IsValidUser, + UserCanUpdateSSHKey, +) +from ... import forms +from ...models import User + +__all__ = [ + 'UserPublicKeyUpdateView', 'UserPublicKeyGenerateView', +] + +logger = get_logger(__name__) + + +class UserPublicKeyUpdateView(PermissionsMixin, UpdateView): + template_name = 'users/user_pubkey_update.html' + model = User + form_class = forms.UserPublicKeyForm + permission_classes = [IsValidUser, UserCanUpdateSSHKey] + success_url = reverse_lazy('users:user-profile') + + def get_object(self, queryset=None): + return self.request.user + + def get_context_data(self, **kwargs): + context = { + 'app': _('Users'), + 'action': _('Public key update'), + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class UserPublicKeyGenerateView(PermissionsMixin, View): + permission_classes = [IsValidUser] + + def get(self, request, *args, **kwargs): + username = request.user.username + private, public = ssh_key_gen(username, hostname='jumpserver') + request.user.public_key = public + request.user.save() + response = HttpResponse(private, content_type='text/plain') + filename = "{0}-jumpserver.pem".format(username) + response['Content-Disposition'] = 'attachment; filename={}'.format(filename) + return response diff --git a/jms b/jms index fcbe189a8..d61e53da1 100755 --- a/jms +++ b/jms @@ -7,6 +7,7 @@ import threading import datetime import logging import logging.handlers +import psutil import time import argparse import sys @@ -351,6 +352,7 @@ def watch_services(): rotate_log_if_need() time.sleep(30) except KeyboardInterrupt: + print("Start stop service") time.sleep(1) break clean_up() @@ -437,6 +439,11 @@ def stop_service(srv, sig=15): os.kill(pid, sig) with LOCK: process = processes.pop(s, None) + if process is None: + try: + process = psutil.Process(pid) + except: + pass if process is None: print("\033[31m No process found\033[0m") continue @@ -529,6 +536,7 @@ if __name__ == '__main__': start_services_and_watch(srv) os._exit(0) elif action == "stop": + print("Stop service") if args.force: stop_service_force(srv) else: diff --git a/requirements/requirements.txt b/requirements/requirements.txt index c3edaf181..1fad7c89b 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -3,18 +3,18 @@ ansible==2.8.2 asn1crypto==0.24.0 bcrypt==3.1.4 billiard==3.5.0.3 -boto3==1.6.5 +boto3==1.12.14 botocore==1.9.5 -celery==4.1.0 +celery==4.1.1 certifi==2018.1.18 -cffi==1.11.5 +cffi==1.13.2 chardet==3.0.4 configparser==3.5.0 coreapi==2.3.3 coreschema==0.0.4 -cryptography==2.3.1 +cryptography==2.8 decorator==4.1.2 -Django==2.1.11 +Django==2.2.10 django-auth-ldap==1.7.0 django-bootstrap3==9.1.0 django-celery-beat==1.4.0 @@ -24,6 +24,7 @@ django-ranged-response==0.2.0 django-redis-cache==1.7.1 django-rest-swagger==2.1.2 django-simple-captcha==0.5.6 +django-timezone-field==3.1 djangorestframework==3.9.4 djangorestframework-bulk==0.2.1 docutils==0.14 @@ -31,6 +32,7 @@ ecdsa==0.13.3 enum-compat==0.0.2 ephem==3.7.6.0 eventlet==0.24.1 +future==0.16.0 ForgeryPy==0.1 greenlet==0.4.14 gunicorn==19.9.0 @@ -39,16 +41,16 @@ itsdangerous==0.24 itypes==1.1.0 Jinja2==2.10.1 jmespath==0.9.3 -kombu==4.0.2 +kombu==4.2.1 ldap3==2.4 -MarkupSafe==1.0 +MarkupSafe==1.1.1 mysqlclient==1.3.14 olefile==0.44 openapi-codec==1.3.2 paramiko==2.4.2 passlib==1.7.1 Pillow==6.2.0 -pyasn1==0.4.2 +pyasn1==0.4.8 pycparser==2.19 pycrypto==2.6.1 pyotp==2.2.6 @@ -77,7 +79,7 @@ python-keycloak-client==0.1.3 rest_condition==1.0.3 python-ldap==3.1.0 tencentcloud-sdk-python==3.0.40 -django-radius==1.3.3 +django-radius==1.4.0 ipip-ipdb==1.2.1 django-redis-sessions==0.6.1 unicodecsv==0.14.1 @@ -90,3 +92,7 @@ channels-redis==2.4.0 channels==2.3.0 daphne==2.3.0 psutil==5.6.5 +django-cas-ng==4.0.1 +python-cas==1.5.0 +ipython +huaweicloud-sdk-python==1.0.21 diff --git a/requirements/rpm_requirements.txt b/requirements/rpm_requirements.txt index 14fe36fde..2e02f07bd 100644 --- a/requirements/rpm_requirements.txt +++ b/requirements/rpm_requirements.txt @@ -1 +1 @@ -gcc krb5-devel libtiff-devel libjpeg-devel libzip-devel freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel sshpass openldap-devel mariadb-devel mysql-devel libffi-devel openssh-clients telnet openldap-clients +gcc krb5-devel libtiff-devel libjpeg-devel libzip-devel freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel sshpass openldap-devel mariadb-devel mysql-devel mysql libffi-devel openssh-clients telnet openldap-clients diff --git a/utils/create_test_data.py b/utils/create_test_data.py new file mode 100644 index 000000000..3d98261b1 --- /dev/null +++ b/utils/create_test_data.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# +