From 9ca4a8c941d277791619e7017d24e19d1f8b07d0 Mon Sep 17 00:00:00 2001 From: BaiJiangJie <32935519+BaiJiangJie@users.noreply.github.com> Date: Tue, 25 Jun 2019 15:24:41 +0800 Subject: [PATCH] Dev (#2838) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Dev ansible windows 2 (#2783) * [Update] 改密支持windows * [Update] 修改asset表结构 * [Feature] Windows支持批量改密、测试可连接性等功能 * [Update] 处理创建资产时labels的问题 * [Update] 优化测试管理系统、系统用户可连接性任务执行逻辑 * [Update] 优化ansible任务逻辑;添加自动推送rdp系统用户功能 * [Update] 添加翻译 * [Update] 优化ansible任务逻辑(测试系统用户可连接性, 通过协议过滤资产) * [Update] 更新翻译 * [Update] 更新翻译 * [Update] 推送windows系统用户,默认添加到Users、Remote Desktop Users组中 * [Update] 优化小细节 * [Update] 更新翻译,删除多余代码 * [Update] 更新翻译信息 * [Bugfix] 修复windows推送系统用户小bug (#2794) * [Update] 邮件设置添加配置项:发送账号 (#2796) * [Bugfix] 和资产相关的Serializer添加protocols字段; (#2800) * [Bugfix] 和资产相关的Serializer添加protocols字段; * [Bugfix] RemoteApp Form 修改过滤RDP协议资产 * [Bugfix] 修改小问题 * [Update] 用户授权相关API,如果需要切换到root org (#2803) * [Update] 用户授权相关API,如果需要切换到root org * [Update] 优化小问题 * [Update] 增加审计员权限控制 (#2792) * [Update] 审计员 * [Update] 增加审计员的权限控制 * [Update] 增加审计员Api全校的控制 * [Update] 优化auditor的api权限控制 * [Update] 优化审计员权限控制 * [Update]优化管理员权限的View * [Update] 优化超级管理权限的View * [Update] 添加审计员切换组织查询会话管理数据 * [Update] 前端禁用审计员在线会话终断按钮 * [Update]优化细节问题 * [Update] Auth Info (#2806) * [Update] 修改支持auth info导出 * [Update] 统一认证查看 * [Update] 修改auth book manager * [Update] 修改auth info * [Update] 完成修改auth info * [Update] 优化api * [Update] 修改assets 的related * [Update] serializer mixin继承 (#2810) * [Update] serializer mixin继承 * [Update] 修改system user更新serialzier * [Update] 修改success message * [Update] 添加一键禁用LDAP认证脚本 (#2813) * [Update] 修改资产创建格式 * [Update] 兼容之前的protocols格式 * [Update] Merge master_bugfix to dev_bugfix (#2817) * [Update] 邮件设置添加配置项:发送账号 (#2795) * [Bugfix] 修复普通用户被授权的RemoteApp列表加载为空的bug * [Bugfix] 修复普通用户加载被授权的RemoteApp为空的bug * [Update] 修改邮件测试的接受者为发送者 * [Update] 修改小问题 * [Update] 修改资产授权序列类返回资产protocols的协议格式/, 同时添加protocol和port字段 * [Update] 修改文案 (#2823) * [Update] 修改文案 * [Update] 修改文案2 * [Bugfix] 修复资产没有管理用户时获取connectivity字段失败的bug * [Update] 优化测试可连接性时结果获取 (#2825) * [Update] 修改资产使用patch方法更新时页面不提示messages信息 * [Update] 添加迁移文件,修改设置资产可连接性时管理用户为None的bug * [Update] 修改org.middleware自动切换组织的bug (#2829) * [Update] 修改org.middleware自动切换组织的bug * [Update] 将切换组织逻辑移动到PermsUtil中 * [Update] 修改首页组织名称显示来源 --- apps/applications/forms/remote_app.py | 4 +- apps/applications/hands.py | 1 - apps/applications/serializers/remote_app.py | 4 +- apps/applications/views/remote_app.py | 26 +- apps/assets/api/asset.py | 13 +- apps/assets/api/asset_user.py | 141 +- apps/assets/backends/__init__.py | 1 + apps/assets/backends/admin_user.py | 10 + apps/assets/backends/asset_user.py | 35 + apps/assets/backends/base.py | 110 +- apps/assets/backends/{external => }/db.py | 12 +- apps/assets/backends/external/__init__.py | 2 - apps/assets/backends/external/vault.py | 19 - apps/assets/backends/internal/__init__.py | 4 - apps/assets/backends/internal/admin_user.py | 38 - apps/assets/backends/internal/asset_user.py | 32 - apps/assets/backends/internal/system_user.py | 75 - apps/assets/backends/internal/utils.py | 26 - apps/assets/backends/manager.py | 113 ++ apps/assets/backends/multi.py | 40 - apps/assets/backends/system_user.py | 30 + apps/assets/backends/{external => }/utils.py | 0 apps/assets/backends/vault.py | 11 + apps/assets/const.py | 29 +- apps/assets/forms/asset.py | 38 +- apps/assets/forms/user.py | 10 +- apps/assets/hands.py | 1 - .../migrations/0027_auto_20190521_1703.py | 5 + apps/assets/migrations/0028_protocol.py | 29 + .../migrations/0029_auto_20190522_1114.py | 22 + .../migrations/0030_auto_20190619_1135.py | 19 + .../migrations/0031_auto_20190621_1332.py | 53 + apps/assets/models/__init__.py | 1 - apps/assets/models/asset.py | 182 +- apps/assets/models/authbook.py | 32 +- apps/assets/models/base.py | 46 +- apps/assets/models/user.py | 8 +- apps/assets/serializers/admin_user.py | 5 +- apps/assets/serializers/asset.py | 160 +- apps/assets/serializers/asset_user.py | 75 +- apps/assets/serializers/cmd_filter.py | 5 +- apps/assets/serializers/domain.py | 7 +- apps/assets/serializers/label.py | 5 +- apps/assets/serializers/node.py | 3 +- apps/assets/serializers/system_user.py | 18 +- apps/assets/signals_handler.py | 5 + apps/assets/tasks.py | 254 ++- .../templates/assets/_asset_list_modal.html | 1 + .../assets/_asset_user_auth_modal.html | 28 - .../assets/_asset_user_auth_update_modal.html | 87 + ....html => _asset_user_auth_view_modal.html} | 68 +- .../templates/assets/_asset_user_list.html | 145 ++ .../assets/templates/assets/_system_user.html | 131 +- .../templates/assets/admin_user_assets.html | 109 +- .../assets/asset_asset_user_list.html | 125 +- .../assets/templates/assets/asset_create.html | 173 +- .../assets/templates/assets/asset_detail.html | 16 +- .../assets/templates/assets/asset_update.html | 92 +- .../templates/assets/system_user_asset.html | 134 +- .../templates/assets/system_user_detail.html | 14 +- .../templates/assets/system_user_list.html | 2 +- apps/assets/urls/api_urls.py | 1 + apps/assets/views/admin_user.py | 22 +- apps/assets/views/asset.py | 68 +- apps/assets/views/cmd_filter.py | 23 +- apps/assets/views/domain.py | 26 +- apps/assets/views/label.py | 16 +- apps/assets/views/system_user.py | 20 +- apps/audits/api.py | 4 +- apps/audits/views.py | 21 +- apps/authentication/backends/api.py | 24 +- .../authentication/_mfa_confirm_modal.html | 54 + apps/common/api.py | 3 +- apps/common/decorator.py | 12 + apps/common/mixins.py | 29 + apps/common/permissions.py | 17 + apps/common/validators.py | 19 +- apps/jumpserver/conf.py | 5 +- apps/jumpserver/settings.py | 2 +- apps/jumpserver/views.py | 7 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 76824 -> 77458 bytes apps/locale/zh/LC_MESSAGES/django.po | 1477 +++++++++-------- apps/locale/zh/LC_MESSAGES/djangojs.mo | Bin 2698 -> 2727 bytes apps/locale/zh/LC_MESSAGES/djangojs.po | 68 +- apps/ops/inventory.py | 35 +- apps/ops/models/adhoc.py | 1 + apps/ops/tasks.py | 8 + .../ops/command_execution_create.html | 7 +- apps/ops/utils.py | 2 +- apps/ops/views/adhoc.py | 23 +- apps/ops/views/celery.py | 5 +- apps/ops/views/command.py | 10 +- apps/orgs/context_processor.py | 4 +- apps/orgs/middleware.py | 15 + apps/orgs/mixins.py | 31 +- apps/orgs/models.py | 2 +- apps/perms/api/user_permission.py | 28 +- apps/perms/hands.py | 4 +- apps/perms/serializers/asset_permission.py | 38 +- .../serializers/remote_app_permission.py | 7 +- apps/perms/utils/asset_permission.py | 19 +- apps/perms/utils/remote_app_permission.py | 6 + apps/perms/views/asset_permission.py | 23 +- apps/perms/views/remote_app_permission.py | 23 +- apps/settings/views.py | 26 +- apps/static/css/jumpserver.css | 14 +- apps/static/js/jumpserver.js | 29 +- apps/templates/_left_side_bar.html | 2 + apps/templates/_nav_audits.html | 31 + apps/templates/_nav_user.html | 4 + apps/terminal/api/session.py | 6 +- .../templates/terminal/session_list.html | 4 +- apps/terminal/views/command.py | 10 +- apps/terminal/views/session.py | 17 +- apps/terminal/views/terminal.py | 31 +- apps/users/forms.py | 1 + .../migrations/0020_auto_20190612_1825.py | 18 + apps/users/models/user.py | 8 +- apps/users/serializers/v1.py | 3 +- apps/users/utils.py | 10 - apps/users/views/group.py | 17 +- apps/users/views/login.py | 6 +- apps/users/views/user.py | 38 +- requirements/requirements.txt | 1 - utils/disable_ldap_auth.sh | 22 + ...isable_user_mfa.py => disable_user_mfa.sh} | 0 126 files changed, 3231 insertions(+), 2160 deletions(-) create mode 100644 apps/assets/backends/admin_user.py create mode 100644 apps/assets/backends/asset_user.py rename apps/assets/backends/{external => }/db.py (76%) delete mode 100644 apps/assets/backends/external/__init__.py delete mode 100644 apps/assets/backends/external/vault.py delete mode 100644 apps/assets/backends/internal/__init__.py delete mode 100644 apps/assets/backends/internal/admin_user.py delete mode 100644 apps/assets/backends/internal/asset_user.py delete mode 100644 apps/assets/backends/internal/system_user.py delete mode 100644 apps/assets/backends/internal/utils.py create mode 100644 apps/assets/backends/manager.py delete mode 100644 apps/assets/backends/multi.py create mode 100644 apps/assets/backends/system_user.py rename apps/assets/backends/{external => }/utils.py (100%) create mode 100644 apps/assets/backends/vault.py create mode 100644 apps/assets/migrations/0028_protocol.py create mode 100644 apps/assets/migrations/0029_auto_20190522_1114.py create mode 100644 apps/assets/migrations/0030_auto_20190619_1135.py create mode 100644 apps/assets/migrations/0031_auto_20190621_1332.py delete mode 100644 apps/assets/templates/assets/_asset_user_auth_modal.html create mode 100644 apps/assets/templates/assets/_asset_user_auth_update_modal.html rename apps/assets/templates/assets/{_asset_user_view_auth_modal.html => _asset_user_auth_view_modal.html} (63%) create mode 100644 apps/assets/templates/assets/_asset_user_list.html create mode 100644 apps/authentication/templates/authentication/_mfa_confirm_modal.html create mode 100644 apps/common/decorator.py create mode 100644 apps/templates/_nav_audits.html create mode 100644 apps/users/migrations/0020_auto_20190612_1825.py create mode 100644 utils/disable_ldap_auth.sh rename utils/{disable_user_mfa.py => disable_user_mfa.sh} (100%) diff --git a/apps/applications/forms/remote_app.py b/apps/applications/forms/remote_app.py index b5317fa1c..cfa841f83 100644 --- a/apps/applications/forms/remote_app.py +++ b/apps/applications/forms/remote_app.py @@ -5,7 +5,7 @@ from django.utils.translation import ugettext as _ from django import forms from orgs.mixins import OrgModelForm -from assets.models import Asset, SystemUser +from assets.models import SystemUser, Protocol from ..models import RemoteApp from .. import const @@ -89,7 +89,7 @@ class RemoteAppCreateUpdateForm(RemoteAppTypeForms, OrgModelForm): super().__init__(*args, **kwargs) field_asset = self.fields['asset'] field_asset.queryset = field_asset.queryset.filter( - protocol=Asset.PROTOCOL_RDP + protocols__name=Protocol.PROTOCOL_RDP ) field_system_user = self.fields['system_user'] field_system_user.queryset = field_system_user.queryset.filter( diff --git a/apps/applications/hands.py b/apps/applications/hands.py index ffe1e35c5..7c83e1332 100644 --- a/apps/applications/hands.py +++ b/apps/applications/hands.py @@ -11,6 +11,5 @@ """ -from common.permissions import AdminUserRequiredMixin from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser from users.models import User, UserGroup diff --git a/apps/applications/serializers/remote_app.py b/apps/applications/serializers/remote_app.py index 6957d11d5..9b5c56315 100644 --- a/apps/applications/serializers/remote_app.py +++ b/apps/applications/serializers/remote_app.py @@ -4,8 +4,8 @@ from rest_framework import serializers -from common.mixins import BulkSerializerMixin from common.serializers import AdaptedBulkListSerializer +from orgs.mixins import BulkOrgResourceModelSerializer from .. import const from ..models import RemoteApp @@ -66,7 +66,7 @@ class RemoteAppParamsDictField(serializers.DictField): return value -class RemoteAppSerializer(BulkSerializerMixin, serializers.ModelSerializer): +class RemoteAppSerializer(BulkOrgResourceModelSerializer): params = RemoteAppParamsDictField() class Meta: diff --git a/apps/applications/views/remote_app.py b/apps/applications/views/remote_app.py index e21db3ad8..5576ed3bb 100644 --- a/apps/applications/views/remote_app.py +++ b/apps/applications/views/remote_app.py @@ -6,11 +6,10 @@ from django.views.generic import TemplateView from django.views.generic.edit import CreateView, UpdateView from django.views.generic.detail import DetailView from django.contrib.messages.views import SuccessMessageMixin -from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse_lazy -from common.permissions import AdminUserRequiredMixin +from common.permissions import PermissionsMixin, IsOrgAdmin, IsValidUser from common.const import create_success_msg, update_success_msg from ..models import RemoteApp @@ -23,27 +22,29 @@ __all__ = [ ] -class RemoteAppListView(AdminUserRequiredMixin, TemplateView): +class RemoteAppListView(PermissionsMixin, TemplateView): template_name = 'applications/remote_app_list.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { - 'app': _('Assets'), + 'app': _('Applications'), 'action': _('RemoteApp list'), } kwargs.update(context) return super().get_context_data(**kwargs) -class RemoteAppCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): +class RemoteAppCreateView(PermissionsMixin, SuccessMessageMixin, CreateView): template_name = 'applications/remote_app_create_update.html' model = RemoteApp form_class = forms.RemoteAppCreateUpdateForm success_url = reverse_lazy('applications:remote-app-list') + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { - 'app': _('Assets'), + 'app': _('Applications'), 'action': _('Create RemoteApp'), } kwargs.update(context) @@ -53,18 +54,19 @@ class RemoteAppCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateVie return create_success_msg % ({'name': cleaned_data['name']}) -class RemoteAppUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): +class RemoteAppUpdateView(PermissionsMixin, SuccessMessageMixin, UpdateView): template_name = 'applications/remote_app_create_update.html' model = RemoteApp form_class = forms.RemoteAppCreateUpdateForm success_url = reverse_lazy('applications:remote-app-list') + permission_classes = [IsOrgAdmin] def get_initial(self): return {k: v for k, v in self.object.params.items()} def get_context_data(self, **kwargs): context = { - 'app': _('Assets'), + 'app': _('Applications'), 'action': _('Update RemoteApp'), } kwargs.update(context) @@ -74,22 +76,24 @@ class RemoteAppUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateVie return update_success_msg % ({'name': cleaned_data['name']}) -class RemoteAppDetailView(AdminUserRequiredMixin, DetailView): +class RemoteAppDetailView(PermissionsMixin, DetailView): template_name = 'applications/remote_app_detail.html' model = RemoteApp context_object_name = 'remote_app' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { - 'app': _('Assets'), + 'app': _('Applications'), 'action': _('RemoteApp detail'), } kwargs.update(context) return super().get_context_data(**kwargs) -class UserRemoteAppListView(LoginRequiredMixin, TemplateView): +class UserRemoteAppListView(PermissionsMixin, TemplateView): template_name = 'applications/user_remote_app_list.html' + permission_classes = [IsValidUser] def get_context_data(self, **kwargs): context = { diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index 39182fbf6..f8ed14cb9 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -16,7 +16,7 @@ from django.urls import reverse_lazy from django.core.cache import cache from django.db.models import Q -from common.mixins import IDInCacheFilterMixin +from common.mixins import IDInCacheFilterMixin, ApiMessageMixin from common.utils import get_logger, get_object_or_none from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser @@ -36,17 +36,18 @@ __all__ = [ ] -class AssetViewSet(IDInCacheFilterMixin, LabelFilter, BulkModelViewSet): +class AssetViewSet(IDInCacheFilterMixin, LabelFilter, ApiMessageMixin, BulkModelViewSet): """ API endpoint that allows Asset to be viewed or edited. """ - filter_fields = ("hostname", "ip") - search_fields = filter_fields + filter_fields = ("hostname", "ip", "systemuser__id", "admin_user__id") + search_fields = ("hostname", "ip") ordering_fields = ("hostname", "ip", "port", "cpu_cores") queryset = Asset.objects.all() serializer_class = serializers.AssetSerializer pagination_class = LimitOffsetPagination permission_classes = (IsOrgAdminOrAppUser,) + success_message = _("%(hostname)s was %(action)s successfully") def set_assets_node(self, assets): if not isinstance(assets, list): @@ -169,8 +170,8 @@ class AssetGatewayApi(generics.RetrieveAPIView): asset = get_object_or_404(Asset, pk=asset_id) if asset.domain and \ - asset.domain.gateways.filter(protocol=asset.protocol).exists(): - gateway = random.choice(asset.domain.gateways.filter(protocol=asset.protocol)) + 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: diff --git a/apps/assets/api/asset_user.py b/apps/assets/api/asset_user.py index 7000a95a5..2b2eb18a5 100644 --- a/apps/assets/api/asset_user.py +++ b/apps/assets/api/asset_user.py @@ -1,58 +1,121 @@ # -*- coding: utf-8 -*- # +import time from rest_framework.response import Response from rest_framework import viewsets, status, generics from rest_framework.pagination import LimitOffsetPagination +from rest_framework import filters +from rest_framework_bulk import BulkModelViewSet +from django.shortcuts import get_object_or_404 from common.permissions import IsOrgAdminOrAppUser from common.utils import get_object_or_none, get_logger - -from ..backends.multi import AssetUserManager -from ..models import Asset +from common.mixins import IDInCacheFilterMixin +from ..backends import AssetUserManager +from ..models import Asset, Node, SystemUser, AdminUser from .. import serializers from ..tasks import test_asset_users_connectivity_manual __all__ = [ 'AssetUserViewSet', 'AssetUserAuthInfoApi', 'AssetUserTestConnectiveApi', + 'AssetUserExportViewSet', ] logger = get_logger(__name__) -class AssetUserViewSet(viewsets.GenericViewSet): +class AssetUserFilterBackend(filters.BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + kwargs = {} + for field in view.filter_fields: + value = request.GET.get(field) + if not value: + continue + if field in ("node_id", "system_user_id", "admin_user_id"): + continue + kwargs[field] = value + return queryset.filter(**kwargs) + + +class AssetUserSearchBackend(filters.BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + 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 + + +class AssetUserViewSet(IDInCacheFilterMixin, BulkModelViewSet): pagination_class = LimitOffsetPagination serializer_class = serializers.AssetUserSerializer permission_classes = (IsOrgAdminOrAppUser, ) http_method_names = ['get', 'post'] - - def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - - def list(self, request, *args, **kwargs): - queryset = self.filter_queryset(self.get_queryset()) - serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data) + filter_fields = [ + "id", "ip", "hostname", "username", "asset_id", "node_id", + "system_user_id", "admin_user_id" + ] + search_fields = filter_fields + filter_backends = ( + filters.OrderingFilter, + AssetUserFilterBackend, AssetUserSearchBackend, + ) def get_queryset(self): + # 尽可能先返回更少的数据 username = self.request.GET.get('username') asset_id = self.request.GET.get('asset_id') - asset = get_object_or_none(Asset, pk=asset_id) - queryset = AssetUserManager.filter(username=username, asset=asset) + 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") + + kwargs = {} + assets = [] + + manager = AssetUserManager() + if system_user_id: + system_user = get_object_or_404(SystemUser, id=system_user_id) + assets = system_user.assets.all() + 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_none(Asset, pk=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: + kwargs['assets'] = assets + + queryset = manager.filter(**kwargs) return queryset - def filter_queryset(self, queryset): - queryset = sorted( - queryset, - key=lambda q: (q.asset.hostname, q.connectivity, q.username) - ) - return queryset + +class AssetUserExportViewSet(AssetUserViewSet): + serializer_class = serializers.AssetUserExportSerializer + http_method_names = ['get'] + + def list(self, request, *args, **kwargs): + otp_last_verify = request.session.get("OTP_LAST_VERIFY_TIME") + if not otp_last_verify or time.time() - int(otp_last_verify) > 600: + return Response({"error": "Need MFA confirm mfa auth"}, status=403) + return super().list(request, *args, **kwargs) class AssetUserAuthInfoApi(generics.RetrieveAPIView): @@ -60,6 +123,10 @@ class AssetUserAuthInfoApi(generics.RetrieveAPIView): permission_classes = (IsOrgAdminOrAppUser,) def retrieve(self, request, *args, **kwargs): + otp_last_verify = request.session.get("OTP_LAST_VERIFY_TIME") + if not otp_last_verify or time.time() - int(otp_last_verify) > 600: + return Response({"error": "Need MFA confirm mfa auth"}, status=403) + instance = self.get_object() serializer = self.get_serializer(instance) status_code = status.HTTP_200_OK @@ -70,9 +137,13 @@ class AssetUserAuthInfoApi(generics.RetrieveAPIView): def get_object(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) try: - instance = AssetUserManager.get(username, asset) + manger = AssetUserManager() + if prefer: + manger.prefer(prefer) + instance = manger.get(username, asset) except Exception as e: logger.error(e, exc_info=True) return None @@ -84,18 +155,36 @@ class AssetUserTestConnectiveApi(generics.RetrieveAPIView): """ Test asset users connective """ + permission_classes = (IsOrgAdminOrAppUser,) def get_asset_users(self): username = self.request.GET.get('username') asset_id = self.request.GET.get('asset_id') asset = get_object_or_none(Asset, pk=asset_id) - asset_users = AssetUserManager.filter(username=username, asset=asset) + manager = AssetUserManager() + asset_users = manager.filter(username=username, assets=[asset]) return asset_users def retrieve(self, request, *args, **kwargs): asset_users = self.get_asset_users() - task = test_asset_users_connectivity_manual.delay(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 diff --git a/apps/assets/backends/__init__.py b/apps/assets/backends/__init__.py index e69de29bb..9a22a23dd 100644 --- a/apps/assets/backends/__init__.py +++ b/apps/assets/backends/__init__.py @@ -0,0 +1 @@ +from .manager import AssetUserManager diff --git a/apps/assets/backends/admin_user.py b/apps/assets/backends/admin_user.py new file mode 100644 index 000000000..8f3644b1b --- /dev/null +++ b/apps/assets/backends/admin_user.py @@ -0,0 +1,10 @@ +# -*- 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 new file mode 100644 index 000000000..62ab24b4d --- /dev/null +++ b/apps/assets/backends/asset_user.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# +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() + 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 = [] + for asset_user in asset_users: + if not assets: + assets = asset_user.assets.all() + for asset in 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 c93ea6a31..d6f30f940 100644 --- a/apps/assets/backends/base.py +++ b/apps/assets/backends/base.py @@ -1,60 +1,84 @@ # -*- coding: utf-8 -*- # - -from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist +import uuid from abc import abstractmethod -class NotSupportError(Exception): - pass - - class BaseBackend: - 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 {}!' - - @classmethod - def get(cls, username, asset): - instances = cls.filter(username, asset) - if len(instances) == 1: - return instances[0] - elif len(instances) == 0: - cls.raise_does_not_exist(cls.__name__) - else: - cls.raise_multiple_return(cls.__name__, len(instances)) - @classmethod @abstractmethod - def filter(cls, username=None, asset=None, latest=True): + def filter(cls, username=None, assets=None, latest=True): """ :param username: 用户名 - :param asset: 对象 + :param assets: 对象 :param latest: 是否是最新记录 :return: 元素为的可迭代对象( or ) """ pass - @classmethod - @abstractmethod - def create(cls, **kwargs): - """ - :param kwargs: - { - name, username, asset, comment, password, public_key, private_key, - (org_id) - } - :return: 对象 - """ - pass - @classmethod - def raise_does_not_exist(cls, name): - raise cls.ObjectDoesNotExist(cls.MSG_NOT_EXIST.format(name)) +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 - @classmethod - def raise_multiple_return(cls, name, length): - raise cls.MultipleObjectsReturned(cls.MSG_MULTIPLE.format(name, length)) + 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: + in_kwargs[k] = v + for k in in_kwargs: + kwargs.pop(k) + + if len(in_kwargs) == 0: + return self + for i in self: + matched = True + for k, v in in_kwargs.items(): + key = k.split('__')[0] + attr = getattr(i, key, 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 not in v: + matched = False + 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) + if isinstance(value, uuid.UUID): + value = str(value) + real.append(value) + return wanted == real + if len(kwargs) > 0: + queryset = AssetUserQuerySet([i for i in self if filter_it(i)]) + else: + queryset = self + return queryset + + def filter(self, **kwargs): + queryset = self.filter_in(kwargs).filter_equal(kwargs) + return queryset + + def __or__(self, other): + self.extend(other) + return self diff --git a/apps/assets/backends/external/db.py b/apps/assets/backends/db.py similarity index 76% rename from apps/assets/backends/external/db.py rename to apps/assets/backends/db.py index eedafd336..f37569e51 100644 --- a/apps/assets/backends/external/db.py +++ b/apps/assets/backends/db.py @@ -1,20 +1,18 @@ # -*- coding: utf-8 -*- # -from assets.models import AuthBook - -from ..base import BaseBackend +from ..models import AuthBook +from .base import BaseBackend class AuthBookBackend(BaseBackend): - @classmethod - def filter(cls, username=None, asset=None, latest=True): + def filter(cls, username=None, assets=None, latest=True): queryset = AuthBook.objects.all() if username is not None: queryset = queryset.filter(username=username) - if asset: - queryset = queryset.filter(asset=asset) + if assets: + queryset = queryset.filter(asset__in=assets) if latest: queryset = queryset.latest_version() return queryset diff --git a/apps/assets/backends/external/__init__.py b/apps/assets/backends/external/__init__.py deleted file mode 100644 index ec51c5a2b..000000000 --- a/apps/assets/backends/external/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- -# diff --git a/apps/assets/backends/external/vault.py b/apps/assets/backends/external/vault.py deleted file mode 100644 index da7583458..000000000 --- a/apps/assets/backends/external/vault.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from ..base import BaseBackend - - -class VaultBackend(BaseBackend): - - @classmethod - def get(cls, username, asset): - pass - - @classmethod - def filter(cls, username=None, asset=None, latest=True): - pass - - @classmethod - def create(cls, **kwargs): - pass diff --git a/apps/assets/backends/internal/__init__.py b/apps/assets/backends/internal/__init__.py deleted file mode 100644 index f19a64d9a..000000000 --- a/apps/assets/backends/internal/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -# - - diff --git a/apps/assets/backends/internal/admin_user.py b/apps/assets/backends/internal/admin_user.py deleted file mode 100644 index abd32b5ae..000000000 --- a/apps/assets/backends/internal/admin_user.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from assets.models import Asset - -from ..base import BaseBackend -from .utils import construct_authbook_object - - -class AdminUserBackend(BaseBackend): - - @classmethod - def filter(cls, username=None, asset=None, **kwargs): - instances = cls.construct_authbook_objects(username, asset) - return instances - - @classmethod - def _get_assets(cls, asset): - if not asset: - assets = Asset.objects.all().prefetch_related('admin_user') - else: - assets = [asset] - return assets - - @classmethod - def construct_authbook_objects(cls, username, asset): - instances = [] - assets = cls._get_assets(asset) - for asset in assets: - if username is not None and asset.admin_user.username != username: - continue - instance = construct_authbook_object(asset.admin_user, asset) - instances.append(instance) - return instances - - @classmethod - def create(cls, **kwargs): - raise cls.NotSupportError("Not support create") diff --git a/apps/assets/backends/internal/asset_user.py b/apps/assets/backends/internal/asset_user.py deleted file mode 100644 index 8502cf5d9..000000000 --- a/apps/assets/backends/internal/asset_user.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from ..base import BaseBackend -from .admin_user import AdminUserBackend -from .system_user import SystemUserBackend - - -class AssetUserBackend(BaseBackend): - @classmethod - def filter(cls, username=None, asset=None, **kwargs): - admin_user_instances = AdminUserBackend.filter(username, asset) - system_user_instances = SystemUserBackend.filter(username, asset) - instances = cls._merge_instances(admin_user_instances, system_user_instances) - return instances - - @classmethod - def _merge_instances(cls, admin_user_instances, system_user_instances): - admin_user_instances_keyword_list = [ - {'username': instance.username, 'asset': instance.asset} - for instance in admin_user_instances - ] - instances = [ - instance for instance in system_user_instances - if instance.keyword not in admin_user_instances_keyword_list - ] - admin_user_instances.extend(instances) - return admin_user_instances - - @classmethod - def create(cls, **kwargs): - raise cls.NotSupportError("Not support create") diff --git a/apps/assets/backends/internal/system_user.py b/apps/assets/backends/internal/system_user.py deleted file mode 100644 index b1413fedb..000000000 --- a/apps/assets/backends/internal/system_user.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- -# - -import itertools - -from assets.models import Asset - -from ..base import BaseBackend -from .utils import construct_authbook_object - - -class SystemUserBackend(BaseBackend): - - @classmethod - def filter(cls, username=None, asset=None, **kwargs): - instances = cls.construct_authbook_objects(username, asset) - return instances - - @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 - - @classmethod - def _filter_system_users_by_username(cls, system_users, username): - _system_users = cls._distinct_system_users_by_username(system_users) - if username is not None: - _system_users = [su for su in _system_users if username == su.username] - return _system_users - - @classmethod - def _construct_authbook_objects(cls, system_users, asset): - instances = [] - for system_user in system_users: - instance = construct_authbook_object(system_user, asset) - instances.append(instance) - return instances - - @classmethod - def _get_assets_with_system_users(cls, asset=None): - """ - { 'asset': set(, , ...) } - """ - if not asset: - _assets = Asset.objects.all().prefetch_related('systemuser_set') - else: - _assets = [asset] - - assets = {asset: set(asset.systemuser_set.all()) for asset in _assets} - return assets - - @classmethod - def construct_authbook_objects(cls, username, asset): - """ - :return: [, , ...] - """ - instances = [] - assets = cls._get_assets_with_system_users(asset) - for _asset, _system_users in assets.items(): - _system_users = cls._filter_system_users_by_username(_system_users, username) - _instances = cls._construct_authbook_objects(_system_users, _asset) - instances.extend(_instances) - return instances - - @classmethod - def create(cls, **kwargs): - raise Exception("Not support create") - - diff --git a/apps/assets/backends/internal/utils.py b/apps/assets/backends/internal/utils.py deleted file mode 100644 index 65b4fa821..000000000 --- a/apps/assets/backends/internal/utils.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from assets.models import AuthBook - - -def construct_authbook_object(asset_user, asset): - """ - 作用: 将对象构造成为对象并返回 - - :param asset_user: 对象 - :param asset: 对象 - :return: 对象 - """ - fields = [ - 'id', 'name', 'username', 'comment', 'org_id', - '_password', '_private_key', '_public_key', - 'date_created', 'date_updated', 'created_by' - ] - - obj = AuthBook(asset=asset, version=0, is_latest=True) - for field in fields: - value = getattr(asset_user, field) - setattr(obj, field, value) - return obj - diff --git a/apps/assets/backends/manager.py b/apps/assets/backends/manager.py new file mode 100644 index 000000000..180c2861c --- /dev/null +++ b/apps/assets/backends/manager.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# +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 + + +class NotSupportError(Exception): + pass + + +class AssetUserManager: + """ + 资产用户管理器 + """ + 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), + ) + + _prefer = "system_user" + _using = None + + def filter(self, username=None, assets=None, latest=True): + if self._using: + backend = dict(self.backends).get(self._using) + if not backend: + return self.none() + instances = backend.filter(username=username, assets=assets, latest=latest) + return AssetUserQuerySet(instances) + + instances_map = {} + instances = [] + for name, backend in self.backends: + _instances = backend.filter( + username=username, assets=assets, latest=latest + ) + 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"]) + 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): + instances = self.filter(username, assets=[asset]) + if len(instances) == 1: + return instances[0] + elif len(instances) == 0: + self.raise_does_not_exist(self.__name__) + else: + self.raise_multiple_return(self.__name__, len(instances)) + + def raise_does_not_exist(self, name): + raise self.ObjectDoesNotExist(self.MSG_NOT_EXIST.format(name)) + + def raise_multiple_return(self, name, length): + raise self.MultipleObjectsReturned(self.MSG_MULTIPLE.format(name, length)) + + @staticmethod + def create(**kwargs): + instance = AuthBookBackend.create(**kwargs) + return instance + + def all(self): + return self.filter() + + def prefer(self, s): + self._prefer = s + return self + + def using(self, s): + self._using = s + return self + + @staticmethod + def none(): + return AssetUserQuerySet() + + @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 diff --git a/apps/assets/backends/multi.py b/apps/assets/backends/multi.py deleted file mode 100644 index 785176885..000000000 --- a/apps/assets/backends/multi.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from .base import BaseBackend - -from .external.utils import get_backend -from .internal.asset_user import AssetUserBackend - - -class AssetUserManager(BaseBackend): - """ - 资产用户管理器 - """ - external_backend = get_backend() - internal_backend = AssetUserBackend - - @classmethod - def filter(cls, username=None, asset=None, **kwargs): - external_instance = list(cls.external_backend.filter(username, asset)) - internal_instance = list(cls.internal_backend.filter(username, asset)) - instances = cls._merge_instances(external_instance, internal_instance) - return instances - - @classmethod - def create(cls, **kwargs): - instance = cls.external_backend.create(**kwargs) - return instance - - @classmethod - def _merge_instances(cls, external_instances, internal_instances): - external_instances_keyword_list = [ - {'username': instance.username, 'asset': instance.asset} - for instance in external_instances - ] - instances = [ - instance for instance in internal_instances - if instance.keyword not in external_instances_keyword_list - ] - external_instances.extend(instances) - return external_instances diff --git a/apps/assets/backends/system_user.py b/apps/assets/backends/system_user.py new file mode 100644 index 000000000..8dda67d2a --- /dev/null +++ b/apps/assets/backends/system_user.py @@ -0,0 +1,30 @@ +# -*- 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/external/utils.py b/apps/assets/backends/utils.py similarity index 100% rename from apps/assets/backends/external/utils.py rename to apps/assets/backends/utils.py diff --git a/apps/assets/backends/vault.py b/apps/assets/backends/vault.py new file mode 100644 index 000000000..d21245247 --- /dev/null +++ b/apps/assets/backends/vault.py @@ -0,0 +1,11 @@ +# -*- 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/const.py b/apps/assets/const.py index 0cd74ba0d..eebb5ecca 100644 --- a/apps/assets/const.py +++ b/apps/assets/const.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- # -from django.utils.translation import ugettext_lazy as _ - UPDATE_ASSETS_HARDWARE_TASKS = [ { @@ -22,6 +20,14 @@ TEST_ADMIN_USER_CONN_TASKS = [ } } ] +TEST_WINDOWS_ADMIN_USER_CONN_TASKS = [ + { + "name": "ping", + "action": { + "module": "win_ping", + } + } +] ASSET_ADMIN_CONN_CACHE_KEY = "ASSET_ADMIN_USER_CONN_{}" @@ -34,9 +40,16 @@ TEST_SYSTEM_USER_CONN_TASKS = [ } } ] +TEST_WINDOWS_SYSTEM_USER_CONN_TASKS = [ + { + "name": "ping", + "action": { + "module": "win_ping", + } + } +] - -ASSET_USER_CONN_CACHE_KEY = 'ASSET_USER_CONN_{}_{}' +ASSET_USER_CONN_CACHE_KEY = 'ASSET_USER_CONN_{}' TEST_ASSET_USER_CONN_TASKS = [ { "name": "ping", @@ -45,6 +58,14 @@ TEST_ASSET_USER_CONN_TASKS = [ } } ] +TEST_WINDOWS_ASSET_USER_CONN_TASKS = [ + { + "name": "ping", + "action": { + "module": "win_ping", + } + } +] TASK_OPTIONS = { diff --git a/apps/assets/forms/asset.py b/apps/assets/forms/asset.py index 774867a6b..624b6c818 100644 --- a/apps/assets/forms/asset.py +++ b/apps/assets/forms/asset.py @@ -6,21 +6,39 @@ from django.utils.translation import gettext_lazy as _ from common.utils import get_logger from orgs.mixins import OrgModelForm -from ..models import Asset, AdminUser +from ..models import Asset, Protocol logger = get_logger(__file__) -__all__ = ['AssetCreateForm', 'AssetUpdateForm', 'AssetBulkUpdateForm'] +__all__ = [ + 'AssetCreateForm', 'AssetUpdateForm', 'AssetBulkUpdateForm', + 'ProtocolForm' +] + + +class ProtocolForm(forms.ModelForm): + class Meta: + model = Protocol + fields = ['name', 'port'] + widgets = { + 'name': forms.Select(attrs={ + 'class': 'form-control protocol-name' + }), + 'port': forms.TextInput(attrs={ + 'class': 'form-control protocol-port' + }), + } class AssetCreateForm(OrgModelForm): + PROTOCOL_CHOICES = Protocol.PROTOCOL_CHOICES + class Meta: model = Asset fields = [ - 'hostname', 'ip', 'public_ip', 'port', 'comment', + 'hostname', 'ip', 'public_ip', 'protocols', 'comment', 'nodes', 'is_active', 'admin_user', 'labels', 'platform', - 'domain', 'protocol', - + 'domain', ] widgets = { 'nodes': forms.SelectMultiple(attrs={ @@ -32,7 +50,6 @@ class AssetCreateForm(OrgModelForm): 'labels': forms.SelectMultiple(attrs={ 'class': 'select2', 'data-placeholder': _('Label') }), - 'port': forms.TextInput(), 'domain': forms.Select(attrs={ 'class': 'select2', 'data-placeholder': _('Domain') }), @@ -54,9 +71,9 @@ class AssetUpdateForm(OrgModelForm): class Meta: model = Asset fields = [ - 'hostname', 'ip', 'port', 'nodes', 'is_active', 'platform', + 'hostname', 'ip', 'protocols', 'nodes', 'is_active', 'platform', 'public_ip', 'number', 'comment', 'admin_user', 'labels', - 'domain', 'protocol', + 'domain', ] widgets = { 'nodes': forms.SelectMultiple(attrs={ @@ -68,7 +85,6 @@ class AssetUpdateForm(OrgModelForm): 'labels': forms.SelectMultiple(attrs={ 'class': 'select2', 'data-placeholder': _('Label') }), - 'port': forms.TextInput(), 'domain': forms.Select(attrs={ 'class': 'select2', 'data-placeholder': _('Domain') }), @@ -101,8 +117,8 @@ class AssetBulkUpdateForm(OrgModelForm): class Meta: model = Asset fields = [ - 'assets', 'port', 'admin_user', 'labels', 'platform', - 'protocol', 'domain', + 'assets', 'admin_user', 'labels', 'platform', + 'domain', ] widgets = { 'labels': forms.SelectMultiple( diff --git a/apps/assets/forms/user.py b/apps/assets/forms/user.py index e832ab158..5c7d3c770 100644 --- a/apps/assets/forms/user.py +++ b/apps/assets/forms/user.py @@ -100,16 +100,18 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm): private_key, public_key = super().gen_keys() if login_mode == SystemUser.LOGIN_MANUAL or \ - protocol in [SystemUser.PROTOCOL_RDP, - SystemUser.PROTOCOL_TELNET, + protocol in [SystemUser.PROTOCOL_TELNET, SystemUser.PROTOCOL_VNC]: system_user.auto_push = 0 - auto_generate_key = False system_user.save() + auto_generate_key = False if auto_generate_key: logger.info('Auto generate key and set system user auth') - system_user.auto_gen_auth() + if protocol == SystemUser.PROTOCOL_SSH: + system_user.auto_gen_auth() + elif protocol == SystemUser.PROTOCOL_RDP: + system_user.auto_gen_auth_password() else: system_user.set_auth(password=password, private_key=private_key, public_key=public_key) diff --git a/apps/assets/hands.py b/apps/assets/hands.py index ffe1e35c5..7c83e1332 100644 --- a/apps/assets/hands.py +++ b/apps/assets/hands.py @@ -11,6 +11,5 @@ """ -from common.permissions import AdminUserRequiredMixin from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser from users.models import User, UserGroup diff --git a/apps/assets/migrations/0027_auto_20190521_1703.py b/apps/assets/migrations/0027_auto_20190521_1703.py index da38b1791..91446019f 100644 --- a/apps/assets/migrations/0027_auto_20190521_1703.py +++ b/apps/assets/migrations/0027_auto_20190521_1703.py @@ -15,4 +15,9 @@ class Migration(migrations.Migration): name='ip', field=models.CharField(db_index=True, max_length=128, verbose_name='IP'), ), + migrations.AlterField( + model_name='asset', + name='public_ip', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Public IP'), + ), ] diff --git a/apps/assets/migrations/0028_protocol.py b/apps/assets/migrations/0028_protocol.py new file mode 100644 index 000000000..230c43773 --- /dev/null +++ b/apps/assets/migrations/0028_protocol.py @@ -0,0 +1,29 @@ +# Generated by Django 2.1.7 on 2019-05-22 02:58 + +import django.core.validators +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0027_auto_20190521_1703'), + ] + + operations = [ + migrations.CreateModel( + name='Protocol', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)'), ('vnc', 'vnc')], default='ssh', max_length=16, verbose_name='Name')), + ('port', models.IntegerField(default=22, validators=[django.core.validators.MaxValueValidator(65535), django.core.validators.MinValueValidator(1)], verbose_name='Port')), + ], + ), + migrations.AddField( + model_name='asset', + name='protocols', + field=models.ManyToManyField(to='assets.Protocol', + verbose_name='Protocol'), + ), + ] diff --git a/apps/assets/migrations/0029_auto_20190522_1114.py b/apps/assets/migrations/0029_auto_20190522_1114.py new file mode 100644 index 000000000..e1d20bb5f --- /dev/null +++ b/apps/assets/migrations/0029_auto_20190522_1114.py @@ -0,0 +1,22 @@ +# Generated by Django 2.1.7 on 2019-05-22 03:14 + +from django.db import migrations + + +def migrate_assets_protocol(apps, schema_editor): + asset_model = apps.get_model("assets", "Asset") + db_alias = schema_editor.connection.alias + assets = asset_model.objects.using(db_alias).all() + for asset in assets: + asset.protocols.create(name=asset.protocol, port=asset.port) + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0028_protocol'), + ] + + operations = [ + migrations.RunPython(migrate_assets_protocol), + ] diff --git a/apps/assets/migrations/0030_auto_20190619_1135.py b/apps/assets/migrations/0030_auto_20190619_1135.py new file mode 100644 index 000000000..a4b6969aa --- /dev/null +++ b/apps/assets/migrations/0030_auto_20190619_1135.py @@ -0,0 +1,19 @@ +# Generated by Django 2.1.7 on 2019-06-19 03:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0029_auto_20190522_1114'), + ] + + operations = [ + migrations.AlterField( + model_name='asset', + name='admin_user', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='assets', to='assets.AdminUser', verbose_name='Admin user'), + ), + ] diff --git a/apps/assets/migrations/0031_auto_20190621_1332.py b/apps/assets/migrations/0031_auto_20190621_1332.py new file mode 100644 index 000000000..c2e614cbc --- /dev/null +++ b/apps/assets/migrations/0031_auto_20190621_1332.py @@ -0,0 +1,53 @@ +# Generated by Django 2.1.7 on 2019-06-21 05:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0030_auto_20190619_1135'), + ] + + operations = [ + migrations.AlterField( + model_name='adminuser', + name='date_created', + field=models.DateTimeField(auto_now_add=True, verbose_name='Date created'), + ), + migrations.AlterField( + model_name='adminuser', + name='date_updated', + field=models.DateTimeField(auto_now=True, verbose_name='Date updated'), + ), + migrations.AlterField( + model_name='authbook', + name='date_created', + field=models.DateTimeField(auto_now_add=True, verbose_name='Date created'), + ), + migrations.AlterField( + model_name='authbook', + name='date_updated', + field=models.DateTimeField(auto_now=True, verbose_name='Date updated'), + ), + migrations.AlterField( + model_name='gateway', + name='date_created', + field=models.DateTimeField(auto_now_add=True, verbose_name='Date created'), + ), + migrations.AlterField( + model_name='gateway', + name='date_updated', + field=models.DateTimeField(auto_now=True, verbose_name='Date updated'), + ), + migrations.AlterField( + model_name='systemuser', + name='date_created', + field=models.DateTimeField(auto_now_add=True, verbose_name='Date created'), + ), + migrations.AlterField( + model_name='systemuser', + name='date_updated', + field=models.DateTimeField(auto_now=True, verbose_name='Date updated'), + ), + ] diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index c82c66a69..b87a18796 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -8,4 +8,3 @@ from .asset import * from .cmd_filter import * from .utils import * from .authbook import * -from applications.models.remote_app import * diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index 47a34243d..cb094dfeb 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -12,11 +12,12 @@ from django.db import models from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from django.core.cache import cache +from django.core.validators import MinValueValidator, MaxValueValidator from .user import AdminUser, SystemUser from orgs.mixins import OrgModelMixin, OrgManager -__all__ = ['Asset'] +__all__ = ['Asset', 'Protocol'] logger = logging.getLogger(__name__) @@ -47,6 +48,35 @@ class AssetQuerySet(models.QuerySet): return self.active() +class AssetManager(OrgManager): + def get_queryset(self): + queryset = super().get_queryset().prefetch_related("nodes", "protocols") + return queryset + + +class Protocol(models.Model): + PROTOCOL_SSH = 'ssh' + PROTOCOL_RDP = 'rdp' + PROTOCOL_TELNET = 'telnet' + PROTOCOL_VNC = 'vnc' + PROTOCOL_CHOICES = ( + (PROTOCOL_SSH, 'ssh'), + (PROTOCOL_RDP, 'rdp'), + (PROTOCOL_TELNET, 'telnet (beta)'), + (PROTOCOL_VNC, 'vnc'), + ) + PORT_VALIDATORS = [MaxValueValidator(65535), MinValueValidator(1)] + + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + name = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, + default=PROTOCOL_SSH, verbose_name=_("Name")) + port = models.IntegerField(default=22, verbose_name=_("Port"), + validators=PORT_VALIDATORS) + + def __str__(self): + return "{}/{}".format(self.name, self.port) + + class Asset(OrgModelMixin): # Important PLATFORM_CHOICES = ( @@ -59,32 +89,25 @@ class Asset(OrgModelMixin): ('Other', 'Other'), ) - PROTOCOL_SSH = 'ssh' - PROTOCOL_RDP = 'rdp' - PROTOCOL_TELNET = 'telnet' - PROTOCOL_VNC = 'vnc' - PROTOCOL_CHOICES = ( - (PROTOCOL_SSH, 'ssh'), - (PROTOCOL_RDP, 'rdp'), - (PROTOCOL_TELNET, 'telnet (beta)'), - (PROTOCOL_VNC, 'vnc'), - ) - id = models.UUIDField(default=uuid.uuid4, primary_key=True) ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True) hostname = models.CharField(max_length=128, verbose_name=_('Hostname')) - protocol = models.CharField(max_length=128, default=PROTOCOL_SSH, choices=PROTOCOL_CHOICES, verbose_name=_('Protocol')) + protocol = models.CharField(max_length=128, default=Protocol.PROTOCOL_SSH, + choices=Protocol.PROTOCOL_CHOICES, + verbose_name=_('Protocol')) port = models.IntegerField(default=22, verbose_name=_('Port')) + + protocols = models.ManyToManyField('Protocol', verbose_name=_("Protocol")) platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform')) domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL) nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes")) is_active = models.BooleanField(default=True, verbose_name=_('Is active')) # Auth - admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user")) + admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user"), related_name='assets') # Some information - public_ip = models.GenericIPAddressField(max_length=32, blank=True, null=True, verbose_name=_('Public IP')) + public_ip = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Public IP')) number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number')) # Collect @@ -110,7 +133,7 @@ class Asset(OrgModelMixin): date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) - objects = OrgManager.from_queryset(AssetQuerySet)() + objects = AssetManager.from_queryset(AssetQuerySet)() CONNECTIVITY_CACHE_KEY = '_JMS_ASSET_CONNECTIVITY_{}' UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3) CONNECTIVITY_CHOICES = ( @@ -131,19 +154,53 @@ class Asset(OrgModelMixin): return True, '' return False, warning - def support_ansible(self): - if self.platform in ("Windows", "Windows2016", "Other"): - return False - if self.protocol != 'ssh': - return False - return True + @property + def protocols_name(self): + names = [] + for protocol in self.protocols.all(): + names.append(protocol.name) + return names - def is_unixlike(self): - if self.platform not in ("Windows", "Windows2016"): + def has_protocol(self, name): + return name in self.protocols_name + + def get_protocol_by_name(self, name): + for i in self.protocols.all(): + if i.name.lower() == name.lower(): + return i + return None + + @property + def protocol_ssh(self): + return self.get_protocol_by_name("ssh") + + @property + def protocol_rdp(self): + return self.get_protocol_by_name("rdp") + + @property + def ssh_port(self): + if self.protocol_ssh: + port = self.protocol_ssh.port + else: + port = 22 + return port + + def is_windows(self): + if self.platform in ("Windows", "Windows2016"): return True else: return False + def is_unixlike(self): + if self.platform not in ("Windows", "Windows2016", "Other"): + return True + else: + return False + + def is_support_ansible(self): + return self.has_protocol('ssh') and self.platform not in ("Other",) + def get_nodes(self): from .node import Node nodes = self.nodes.all() or [Node.root()] @@ -172,6 +229,15 @@ class Asset(OrgModelMixin): filter_arg |= Q(Q(org_id__isnull=True) | Q(org_id=''), hostname__in=hosts) return Asset.objects.filter(filter_arg) + @property + def cpu_info(self): + info = "" + if self.cpu_model: + info += self.cpu_model + if self.cpu_count and self.cpu_cores: + info += "{}*{}".format(self.cpu_count, self.cpu_cores) + return info + @property def hardware_info(self): if self.cpu_count: @@ -184,26 +250,27 @@ class Asset(OrgModelMixin): @property def connectivity(self): - if not self.is_unixlike(): - return self.REACHABLE - key = self.CONNECTIVITY_CACHE_KEY.format(str(self.id)) - cached = cache.get(key, None) - return cached if cached is not None else self.UNKNOWN + if not self.admin_user: + return self.UNKNOWN + return self.admin_user.get_connectivity_of(self) @connectivity.setter def connectivity(self, value): - key = self.CONNECTIVITY_CACHE_KEY.format(str(self.id)) - cache.set(key, value, 3600*2) + if not self.admin_user: + return + self.admin_user.set_connectivity_of(self, value) def get_auth_info(self): - if self.admin_user: - self.admin_user.load_specific_asset_auth(self) - return { - 'username': self.admin_user.username, - 'password': self.admin_user.password, - 'private_key': self.admin_user.private_key_file, - 'become': self.admin_user.become_info, - } + if not self.admin_user: + return {} + + self.admin_user.load_specific_asset_auth(self) + info = { + 'username': self.admin_user.username, + 'password': self.admin_user.password, + 'private_key': self.admin_user.private_key_file, + } + return info def as_node(self): from .node import Node @@ -215,35 +282,6 @@ class Asset(OrgModelMixin): fake_node.is_node = False return fake_node - def to_json(self): - info = { - 'id': self.id, - 'hostname': self.hostname, - 'ip': self.ip, - 'port': self.port, - } - if self.domain and self.domain.gateway_set.all(): - info["gateways"] = [d.id for d in self.domain.gateway_set.all()] - return info - - def _to_secret_json(self): - """ - Ansible use it create inventory - Todo: May be move to ops implements it - """ - data = self.to_json() - if self.admin_user: - self.admin_user.load_specific_asset_auth(self) - admin_user = self.admin_user - data.update({ - 'username': admin_user.username, - 'password': admin_user.password, - 'private_key': admin_user.private_key_file, - 'become': admin_user.become_info, - 'groups': [node.value for node in self.nodes.all()], - }) - return data - def as_tree_node(self, parent_node): from common.tree import TreeNode icon_skin = 'file' @@ -265,9 +303,11 @@ class Asset(OrgModelMixin): 'id': self.id, 'hostname': self.hostname, 'ip': self.ip, - 'port': self.port, + 'protocols': [ + {"name": p.name, "port": p.port} + for p in self.protocols.all() + ], 'platform': self.platform, - 'protocol': self.protocol, } } } @@ -291,10 +331,10 @@ class Asset(OrgModelMixin): asset = cls(ip='.'.join(ip), hostname=forgery_py.internet.user_name(True), admin_user=choice(AdminUser.objects.all()), - port=22, created_by='Fake') try: asset.save() + asset.protocols.create(name="ssh", port=22) if nodes and len(nodes) > 3: _nodes = random.sample(nodes, 3) else: diff --git a/apps/assets/models/authbook.py b/apps/assets/models/authbook.py index 94e4f4cf4..e61c3423d 100644 --- a/apps/assets/models/authbook.py +++ b/apps/assets/models/authbook.py @@ -29,6 +29,9 @@ class AuthBook(AssetUser): version = models.IntegerField(default=1, verbose_name=_('Version')) objects = AuthBookManager.from_queryset(AuthBookQuerySet)() + backend = "db" + # 用于system user和admin_user的动态设置 + _connectivity = None class Meta: verbose_name = _('AuthBook') @@ -40,7 +43,8 @@ class AuthBook(AssetUser): def _get_pre_obj(self): pre_obj = self.__class__.objects.filter( - username=self.username, asset=self.asset).latest_version().first() + username=self.username, asset=self.asset + ).latest_version().first() return pre_obj def _remove_pre_obj_latest(self): @@ -63,30 +67,30 @@ class AuthBook(AssetUser): @property def _conn_cache_key(self): - return ASSET_USER_CONN_CACHE_KEY.format(self.id, self.asset.id) + return ASSET_USER_CONN_CACHE_KEY.format(self.id) @property def connectivity(self): + if self._connectivity: + return self._connectivity value = cache.get(self._conn_cache_key, self.UNKNOWN) return value @connectivity.setter def connectivity(self, value): - _connectivity = self.UNKNOWN - - for host in value.get('dark', {}).keys(): - if host == self.asset.hostname: - _connectivity = self.UNREACHABLE - - for host in value.get('contacted', []): - if host == self.asset.hostname: - _connectivity = self.REACHABLE - - cache.set(self._conn_cache_key, _connectivity, 3600) + cache.set(self._conn_cache_key, value, 3600) @property def keyword(self): - return {'username': self.username, 'asset': self.asset} + return '{}_#_{}'.format(self.username, str(self.asset.id)) + + @property + def hostname(self): + return self.asset.hostname + + @property + def ip(self): + return self.asset.ip def __str__(self): return '{}@{}'.format(self.username, self.asset) diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 30d8ae0f5..d58b97a18 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -6,6 +6,7 @@ from hashlib import md5 import sshpubkeys from django.db import models +from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ from django.conf import settings @@ -29,8 +30,8 @@ class AssetUser(OrgModelMixin): _private_key = models.TextField(max_length=4096, blank=True, null=True, verbose_name=_('SSH private key'), validators=[private_key_validator, ]) _public_key = models.TextField(max_length=4096, blank=True, verbose_name=_('SSH public key')) comment = models.TextField(blank=True, verbose_name=_('Comment')) - date_created = models.DateTimeField(auto_now_add=True) - date_updated = models.DateTimeField(auto_now=True) + 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')) UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3) @@ -39,6 +40,8 @@ class AssetUser(OrgModelMixin): (REACHABLE, _('Reachable')), (UNKNOWN, _("Unknown")), ) + CONNECTIVITY_CACHE_KEY = "CONNECTIVITY_{}" + _prefer = "system_user" @property def password(self): @@ -124,10 +127,21 @@ class AssetUser(OrgModelMixin): def get_auth(self, asset=None): pass + def get_connectivity_of(self, asset): + i = self.generate_id_with_asset(asset) + key = self.CONNECTIVITY_CACHE_KEY.format(i) + return cache.get(key) + + def set_connectivity_of(self, asset, c): + i = self.generate_id_with_asset(asset) + key = self.CONNECTIVITY_CACHE_KEY.format(i) + cache.set(key, c, 3600) + def load_specific_asset_auth(self, asset): - from ..backends.multi import AssetUserManager + from ..backends import AssetUserManager try: - other = AssetUserManager.get(username=self.username, asset=asset) + manager = AssetUserManager().prefer(self._prefer) + other = manager.get(username=self.username, asset=asset) except Exception as e: logger.error(e, exc_info=True) else: @@ -158,6 +172,10 @@ class AssetUser(OrgModelMixin): private_key=private_key, public_key=public_key) + def auto_gen_auth_password(self): + password = str(uuid.uuid4()) + self.set_auth(password=password) + def _to_secret_json(self): """Push system user use it""" return { @@ -168,5 +186,25 @@ class AssetUser(OrgModelMixin): 'private_key': self.private_key_file, } + def generate_id_with_asset(self, asset): + id_ = '{}_{}'.format(asset.id, self.id) + id_ = uuid.UUID(md5(id_.encode()).hexdigest()) + return id_ + + 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' + ] + id_ = self.generate_id_with_asset(asset) + obj = AuthBook(id=id_, asset=asset, version=0, is_latest=True) + obj._connectivity = self.get_connectivity_of(asset) + for field in fields: + value = getattr(self, field) + setattr(obj, field, value) + return obj + class Meta: abstract = True diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 1e661d631..2cded41a1 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -32,6 +32,7 @@ class AdminUser(AssetUser): become_user = models.CharField(default='root', max_length=64) _become_pass = models.CharField(default='', max_length=128) CONNECTIVE_CACHE_KEY = '_JMS_ADMIN_USER_CONNECTIVE_{}' + _prefer = "admin_user" def __str__(self): return self.name @@ -61,7 +62,7 @@ class AdminUser(AssetUser): return info def get_related_assets(self): - assets = self.asset_set.all() + assets = self.assets.all() return assets @property @@ -174,17 +175,20 @@ class SystemUser(AssetUser): data = self.connectivity unreachable = data['unreachable'] reachable = data['reachable'] + assets = {asset.hostname: asset for asset in self.assets.all()} for host in value.get('dark', {}).keys(): if host not in unreachable: unreachable.append(host) if host in reachable: reachable.remove(host) + self.set_connectivity_of(assets.get(host), self.UNREACHABLE) for host in value.get('contacted'): if host not in reachable: reachable.append(host) if host in unreachable: unreachable.remove(host) + self.set_connectivity_of(assets.get(host), self.REACHABLE) cache_key = self.CONNECTIVE_CACHE_KEY.format(str(self.id)) cache.set(cache_key, data, 3600) @@ -201,7 +205,7 @@ class SystemUser(AssetUser): return self.get_login_mode_display() def is_need_push(self): - if self.auto_push and self.protocol == self.PROTOCOL_SSH: + if self.auto_push and self.protocol in [self.PROTOCOL_SSH, self.PROTOCOL_RDP]: return True else: return False diff --git a/apps/assets/serializers/admin_user.py b/apps/assets/serializers/admin_user.py index 66f25db87..765559e70 100644 --- a/apps/assets/serializers/admin_user.py +++ b/apps/assets/serializers/admin_user.py @@ -8,11 +8,12 @@ from common.serializers import AdaptedBulkListSerializer from ..models import Node, AdminUser from ..const import ADMIN_USER_CONN_CACHE_KEY +from orgs.mixins import BulkOrgResourceModelSerializer from .base import AuthSerializer -class AdminUserSerializer(serializers.ModelSerializer): +class AdminUserSerializer(BulkOrgResourceModelSerializer): """ 管理用户 """ @@ -27,7 +28,7 @@ class AdminUserSerializer(serializers.ModelSerializer): list_serializer_class = AdaptedBulkListSerializer model = AdminUser fields = [ - 'id', 'org_id', 'name', 'username', 'assets_amount', + 'id', 'name', 'username', 'assets_amount', 'reachable_amount', 'unreachable_amount', 'password', 'comment', 'date_created', 'date_updated', 'become', 'become_method', 'become_user', 'created_by', diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index 3e1cf39bb..77715b351 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -1,49 +1,76 @@ # -*- coding: utf-8 -*- # from rest_framework import serializers +from rest_framework.validators import ValidationError from django.utils.translation import ugettext_lazy as _ -from orgs.mixins import OrgResourceSerializerMixin -from common.mixins import BulkSerializerMixin +from orgs.mixins import BulkOrgResourceModelSerializer from common.serializers import AdaptedBulkListSerializer -from ..models import Asset +from ..models import Asset, Protocol from .system_user import AssetSystemUserSerializer __all__ = [ - 'AssetSerializer', 'AssetGrantedSerializer', 'MyAssetGrantedSerializer', - 'AssetAsNodeSerializer', 'AssetSimpleSerializer', + 'AssetSerializer', 'AssetGrantedSerializer', 'AssetSimpleSerializer', + 'ProtocolSerializer', ] -class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer, OrgResourceSerializerMixin): +class ProtocolSerializer(serializers.ModelSerializer): + class Meta: + model = Protocol + fields = ["name", "port"] + + +class ProtocolsRelatedField(serializers.RelatedField): + def to_representation(self, value): + return str(value) + + def to_internal_value(self, data): + if isinstance(data, dict): + return data + if '/' not in data: + raise ValidationError("protocol not contain /: {}".format(data)) + v = data.split("/") + if len(v) != 2: + raise ValidationError("protocol format should be name/port: {}".format(data)) + name, port = v + cleaned_data = {"name": name, "port": port} + return cleaned_data + + +class AssetSerializer(BulkOrgResourceModelSerializer): + protocols = ProtocolsRelatedField( + many=True, queryset=Protocol.objects.all(), label=_("Protocols") + ) + """ 资产的数据结构 """ class Meta: model = Asset list_serializer_class = AdaptedBulkListSerializer - # validators = [] # 解决批量导入时unique_together字段校验失败 fields = [ - 'id', 'org_id', 'org_name', 'ip', 'hostname', 'protocol', 'port', - 'platform', 'is_active', 'public_ip', 'domain', 'admin_user', - 'nodes', 'labels', 'number', 'vendor', 'model', 'sn', + 'id', 'ip', 'hostname', 'protocol', 'port', + 'protocols', 'platform', 'is_active', 'public_ip', 'domain', + 'admin_user', 'nodes', 'labels', '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' ] read_only_fields = ( - 'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count', + 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info', 'os', 'os_version', 'os_arch', 'hostname_raw', 'created_by', 'date_created', ) extra_kwargs = { + 'protocol': {'write_only': True}, + 'port': {'write_only': True}, 'hardware_info': {'label': _('Hardware info')}, 'connectivity': {'label': _('Connectivity')}, 'org_name': {'label': _('Org name')} - } @classmethod @@ -53,24 +80,79 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer, OrgResou .select_related('admin_user') return queryset - def get_field_names(self, declared_fields, info): - fields = super().get_field_names(declared_fields, info) - fields.extend([ - 'hardware_info', 'connectivity', 'org_name' - ]) - return fields + @staticmethod + def validate_protocols(attr): + protocols_serializer = ProtocolSerializer(data=attr, many=True) + protocols_serializer.is_valid(raise_exception=True) + protocols_name = [i.get("name", "ssh") for i in attr] + errors = [{} for i in protocols_name] + for i, name in enumerate(protocols_name): + if name in protocols_name[:i]: + errors[i] = {"name": _("Protocol duplicate: {}").format(name)} + if any(errors): + raise ValidationError(errors) + return attr + + def create(self, validated_data): + protocols_data = validated_data.pop("protocols", []) + + # 兼容老的api + protocol = validated_data.get("protocol") + port = validated_data.get("port") + if not protocols_data and protocol and port: + protocols_data = [{"name": protocol, "port": port}] + + if not protocol and not port and protocols_data: + validated_data["protocol"] = protocols_data[0]["name"] + validated_data["port"] = protocols_data[0]["port"] + + protocols_serializer = ProtocolSerializer(data=protocols_data, many=True) + protocols_serializer.is_valid(raise_exception=True) + protocols = protocols_serializer.save() + instance = super().create(validated_data) + instance.protocols.set(protocols) + return instance + + def update(self, instance, validated_data): + protocols_data = validated_data.pop("protocols", []) + + # 兼容老的api + protocol = validated_data.get("protocol") + port = validated_data.get("port") + if not protocols_data and protocol and port: + protocols_data = [{"name": protocol, "port": port}] + + if not protocol and not port and protocols_data: + validated_data["protocol"] = protocols_data[0]["name"] + validated_data["port"] = protocols_data[0]["port"] + protocols = None + if protocols_data: + protocols_serializer = ProtocolSerializer(data=protocols_data, many=True) + protocols_serializer.is_valid(raise_exception=True) + protocols = protocols_serializer.save() + + instance = super().update(instance, validated_data) + if protocols: + instance.protocols.all().delete() + instance.protocols.set(protocols) + return instance -class AssetAsNodeSerializer(serializers.ModelSerializer): - class Meta: - model = Asset - fields = ['id', 'hostname', 'ip', 'port', 'platform', 'protocol'] +# class AssetAsNodeSerializer(serializers.ModelSerializer): +# protocols = ProtocolSerializer(many=True) +# +# class Meta: +# model = Asset +# fields = ['id', 'hostname', 'ip', 'platform', 'protocols'] class AssetGrantedSerializer(serializers.ModelSerializer): """ 被授权资产的数据结构 """ + protocols = ProtocolsRelatedField( + many=True, queryset=Protocol.objects.all(), label=_("Protocols") + ) system_users_granted = AssetSystemUserSerializer(many=True, read_only=True) system_users_join = serializers.SerializerMethodField() # nodes = NodeTMPSerializer(many=True, read_only=True) @@ -78,9 +160,9 @@ class AssetGrantedSerializer(serializers.ModelSerializer): class Meta: model = Asset fields = ( - "id", "hostname", "ip", "port", "system_users_granted", - "is_active", "system_users_join", "os", 'domain', - "platform", "comment", "protocol", "org_id", "org_name", + "id", "hostname", "ip", "protocol", "port", "protocols", + "system_users_granted", "is_active", "system_users_join", "os", + 'domain', "platform", "comment", "org_id", "org_name", ) @staticmethod @@ -89,21 +171,23 @@ class AssetGrantedSerializer(serializers.ModelSerializer): return ', '.join(system_users) -class MyAssetGrantedSerializer(AssetGrantedSerializer): - """ - 普通用户获取授权的资产定义的数据结构 - """ - - class Meta: - model = Asset - fields = ( - "id", "hostname", "system_users_granted", - "is_active", "system_users_join", "org_name", - "os", "platform", "comment", "org_id", "protocol" - ) +# class MyAssetGrantedSerializer(AssetGrantedSerializer): +# """ +# 普通用户获取授权的资产定义的数据结构 +# """ +# protocols = ProtocolSerializer(many=True) +# +# class Meta: +# model = Asset +# fields = ( +# "id", "hostname", "system_users_granted", +# "is_active", "system_users_join", "org_name", +# "os", "platform", "comment", "org_id", "protocols" +# ) class AssetSimpleSerializer(serializers.ModelSerializer): + class Meta: model = Asset - fields = ['id', 'hostname', 'port', 'ip', 'connectivity'] + fields = ['id', 'hostname', 'ip', 'connectivity', 'port'] diff --git a/apps/assets/serializers/asset_user.py b/apps/assets/serializers/asset_user.py index f7345437e..bb5f83142 100644 --- a/apps/assets/serializers/asset_user.py +++ b/apps/assets/serializers/asset_user.py @@ -4,49 +4,70 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers -from ..models import AuthBook -from ..backends.multi import AssetUserManager +from ..models import AuthBook, Asset +from ..backends import AssetUserManager +from common.utils import validate_ssh_private_key +from common.serializers import AdaptedBulkListSerializer +from orgs.mixins import BulkOrgResourceModelSerializer + __all__ = [ 'AssetUserSerializer', 'AssetUserAuthInfoSerializer', + 'AssetUserExportSerializer', 'AssetUserPushSerializer', ] -class AssetUserSerializer(serializers.ModelSerializer): +class BasicAssetSerializer(serializers.ModelSerializer): + class Meta: + model = Asset + fields = ['hostname', 'ip'] + + +class AssetUserSerializer(BulkOrgResourceModelSerializer): + hostname = serializers.CharField(read_only=True, label=_("Hostname")) + ip = serializers.CharField(read_only=True, label=_("IP")) + connectivity = serializers.CharField(read_only=True, label=_("Connectivity")) password = serializers.CharField( max_length=256, allow_blank=True, allow_null=True, write_only=True, - required=False, help_text=_('Password') + required=False, label=_('Password') ) public_key = serializers.CharField( max_length=4096, allow_blank=True, allow_null=True, write_only=True, - required=False, help_text=_('Public key') + required=False, label=_('Public key') ) private_key = serializers.CharField( max_length=4096, allow_blank=True, allow_null=True, write_only=True, - required=False, help_text=_('Private key') + required=False, label=_('Private key') ) + backend = serializers.CharField(read_only=True, label=_("Backend")) class Meta: model = AuthBook + list_serializer_class = AdaptedBulkListSerializer read_only_fields = ( 'date_created', 'date_updated', 'created_by', 'is_latest', 'version', 'connectivity', ) - fields = '__all__' + fields = [ + "id", "hostname", "ip", "username", "password", "asset", "version", + "is_latest", "connectivity", "backend", + "date_created", "date_updated", "private_key", "public_key", + ] extra_kwargs = { - 'username': {'required': True} + 'username': {'required': True}, } - def get_field_names(self, declared_fields, info): - fields = super().get_field_names(declared_fields, info) - fields = [f for f in fields if not f.startswith('_') and f != 'id'] - fields.extend(['connectivity']) - return fields + def validate_private_key(self, key): + password = self.initial_data.get("password") + valid = validate_ssh_private_key(key, password) + if not valid: + raise serializers.ValidationError(_("private key invalid")) + return key def create(self, validated_data): kwargs = { - 'name': validated_data.get('name'), + 'name': validated_data.get('username'), 'username': validated_data.get('username'), 'asset': validated_data.get('asset'), 'comment': validated_data.get('comment', ''), @@ -59,7 +80,33 @@ class AssetUserSerializer(serializers.ModelSerializer): return instance +class AssetUserExportSerializer(AssetUserSerializer): + password = serializers.CharField( + max_length=256, allow_blank=True, allow_null=True, + required=False, label=_('Password') + ) + public_key = serializers.CharField( + max_length=4096, allow_blank=True, allow_null=True, + required=False, label=_('Public key') + ) + private_key = serializers.CharField( + max_length=4096, allow_blank=True, allow_null=True, + required=False, label=_('Private key') + ) + + class AssetUserAuthInfoSerializer(serializers.ModelSerializer): class Meta: model = AuthBook fields = ['password', 'private_key', 'public_key'] + + +class AssetUserPushSerializer(serializers.Serializer): + asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects.all(), label=_("Asset")) + username = serializers.CharField(max_length=1024) + + def create(self, validated_data): + pass + + def update(self, instance, validated_data): + pass diff --git a/apps/assets/serializers/cmd_filter.py b/apps/assets/serializers/cmd_filter.py index 26040f6aa..b523a1779 100644 --- a/apps/assets/serializers/cmd_filter.py +++ b/apps/assets/serializers/cmd_filter.py @@ -5,9 +5,10 @@ from rest_framework import serializers from common.fields import ChoiceDisplayField from common.serializers import AdaptedBulkListSerializer from ..models import CommandFilter, CommandFilterRule, SystemUser +from orgs.mixins import BulkOrgResourceModelSerializer -class CommandFilterSerializer(serializers.ModelSerializer): +class CommandFilterSerializer(BulkOrgResourceModelSerializer): rules = serializers.PrimaryKeyRelatedField(queryset=CommandFilterRule.objects.all(), many=True) system_users = serializers.PrimaryKeyRelatedField(queryset=SystemUser.objects.all(), many=True) @@ -17,7 +18,7 @@ class CommandFilterSerializer(serializers.ModelSerializer): fields = '__all__' -class CommandFilterRuleSerializer(serializers.ModelSerializer): +class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer): serializer_choice_field = ChoiceDisplayField class Meta: diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index 553911eb8..cda208b9f 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -3,11 +3,12 @@ from rest_framework import serializers from common.serializers import AdaptedBulkListSerializer +from orgs.mixins import BulkOrgResourceModelSerializer from ..models import Domain, Gateway -class DomainSerializer(serializers.ModelSerializer): +class DomainSerializer(BulkOrgResourceModelSerializer): asset_count = serializers.SerializerMethodField() gateway_count = serializers.SerializerMethodField() @@ -25,7 +26,7 @@ class DomainSerializer(serializers.ModelSerializer): return obj.gateway_set.all().count() -class GatewaySerializer(serializers.ModelSerializer): +class GatewaySerializer(BulkOrgResourceModelSerializer): class Meta: model = Gateway list_serializer_class = AdaptedBulkListSerializer @@ -45,7 +46,7 @@ class GatewayWithAuthSerializer(GatewaySerializer): return fields -class DomainWithGatewaySerializer(serializers.ModelSerializer): +class DomainWithGatewaySerializer(BulkOrgResourceModelSerializer): gateways = GatewayWithAuthSerializer(many=True, read_only=True) class Meta: diff --git a/apps/assets/serializers/label.py b/apps/assets/serializers/label.py index 9fbc9e804..526580216 100644 --- a/apps/assets/serializers/label.py +++ b/apps/assets/serializers/label.py @@ -3,11 +3,12 @@ from rest_framework import serializers from common.serializers import AdaptedBulkListSerializer +from orgs.mixins import BulkOrgResourceModelSerializer from ..models import Label -class LabelSerializer(serializers.ModelSerializer): +class LabelSerializer(BulkOrgResourceModelSerializer): asset_count = serializers.SerializerMethodField() class Meta: @@ -25,7 +26,7 @@ class LabelSerializer(serializers.ModelSerializer): return fields -class LabelDistinctSerializer(serializers.ModelSerializer): +class LabelDistinctSerializer(BulkOrgResourceModelSerializer): value = serializers.SerializerMethodField() class Meta: diff --git a/apps/assets/serializers/node.py b/apps/assets/serializers/node.py index f0ff3062b..61df12f64 100644 --- a/apps/assets/serializers/node.py +++ b/apps/assets/serializers/node.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from rest_framework import serializers +from orgs.mixins import BulkOrgResourceModelSerializer from ..models import Asset, Node @@ -10,7 +11,7 @@ __all__ = [ ] -class NodeSerializer(serializers.ModelSerializer): +class NodeSerializer(BulkOrgResourceModelSerializer): assets_amount = serializers.IntegerField(read_only=True) class Meta: diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index 88ae6eb38..c4cb5be05 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -3,12 +3,12 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from common.serializers import AdaptedBulkListSerializer - -from ..models import SystemUser, Asset +from orgs.mixins import BulkOrgResourceModelSerializer +from ..models import SystemUser from .base import AuthSerializer -class SystemUserSerializer(serializers.ModelSerializer): +class SystemUserSerializer(BulkOrgResourceModelSerializer): """ 系统用户 """ @@ -31,7 +31,7 @@ class SystemUserSerializer(serializers.ModelSerializer): model = SystemUser list_serializer_class = AdaptedBulkListSerializer fields = [ - 'id', 'org_id', 'name', 'username', 'login_mode', + 'id', 'name', 'username', 'login_mode', 'login_mode_display', 'login_mode_display', 'priority', 'protocol', 'auto_push', 'password', 'assets_amount', 'reachable_amount', 'reachable_assets', 'unreachable_amount', 'unreachable_assets', 'cmd_filters', 'sudo', @@ -39,17 +39,9 @@ class SystemUserSerializer(serializers.ModelSerializer): ] extra_kwargs = { 'login_mode_display': {'label': _('Login mode display')}, - 'created_by': {'read_only': True}, 'nodes': {'read_only': True}, - 'assets': {'read_only': True} + 'created_by': {'read_only': True}, } - def get_field_names(self, declared_fields, info): - fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info) - fields.extend([ - 'login_mode_display', - ]) - return fields - @staticmethod def get_unreachable_assets(obj): return obj.assets_unreachable diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index 4afb97e75..a145437d0 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -5,6 +5,7 @@ from django.db.models.signals import post_save, m2m_changed, post_delete from django.dispatch import receiver from common.utils import get_logger +from common.decorator import on_transaction_commit from .models import Asset, SystemUser, Node, AuthBook from .tasks import ( update_assets_hardware_info_util, @@ -32,9 +33,12 @@ def set_asset_root_node(asset): @receiver(post_save, sender=Asset, dispatch_uid="my_unique_identifier") +@on_transaction_commit def on_asset_created_or_update(sender, instance=None, created=False, **kwargs): if created: logger.info("Asset `{}` create signal received".format(instance)) + + # 获取资产硬件信息 update_asset_hardware_info_on_created(instance) test_asset_conn_on_created(instance) @@ -61,6 +65,7 @@ def on_system_user_update(sender, instance=None, created=True, **kwargs): @receiver(m2m_changed, sender=SystemUser.nodes.through) def on_system_user_nodes_change(sender, instance=None, **kwargs): if instance and kwargs["action"] == "post_add": + logger.info("System user `{}` nodes update signal received".format(instance)) assets = set() nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) for node in nodes: diff --git a/apps/assets/tasks.py b/apps/assets/tasks.py index 172175f38..149e9fa2c 100644 --- a/apps/assets/tasks.py +++ b/apps/assets/tasks.py @@ -3,6 +3,7 @@ import json import re import os +from collections import defaultdict from celery import shared_task from django.utils.translation import ugettext as _ from django.core.cache import cache @@ -31,7 +32,7 @@ def check_asset_can_run_ansible(asset): msg = _("Asset has been disabled, skipped: {}").format(asset) logger.info(msg) return False - if not asset.support_ansible(): + if not asset.is_support_ansible(): msg = _("Asset may not be support ansible, skipped: {}").format(asset) logger.info(msg) return False @@ -45,10 +46,21 @@ def clean_hosts(assets): continue clean_assets.append(asset) if not clean_assets: - print(_("No assets matched, stop task")) + logger.info(_("No assets matched, stop task")) return clean_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 + + @shared_task def set_assets_hardware_info(assets, result, **kwargs): """ @@ -96,7 +108,7 @@ def set_assets_hardware_info(assets, result, **kwargs): ___disk_total = '%s %s' % sum_capacity(disk_info.values()) ___disk_info = json.dumps(disk_info) - ___platform = info.get('ansible_system', 'Unknown') + # ___platform = info.get('ansible_system', 'Unknown') ___os = info.get('ansible_distribution', 'Unknown') ___os_version = info.get('ansible_distribution_version', 'Unknown') ___os_arch = info.get('ansible_architecture', 'Unknown') @@ -163,25 +175,57 @@ def test_asset_connectivity_util(assets, task_name=None): if task_name is None: task_name = _("Test assets connectivity") + hosts = clean_hosts(assets) if not hosts: return {} - tasks = const.TEST_ADMIN_USER_CONN_TASKS - created_by = assets[0].org_id - task, created = update_or_create_ansible_task( - task_name=task_name, hosts=hosts, tasks=tasks, pattern='all', - options=const.TASK_OPTIONS, run_as_admin=True, created_by=created_by, + + hosts_category = { + 'linux': { + 'hosts': [], + 'tasks': const.TEST_ADMIN_USER_CONN_TASKS + }, + 'windows': { + 'hosts': [], + 'tasks': const.TEST_WINDOWS_ADMIN_USER_CONN_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 ) - result = task.run() - summary = result[1] + created_by = assets[0].org_id + 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'], + pattern='all', options=const.TASK_OPTIONS, run_as_admin=True, + created_by=created_by, + ) + result = task.run() + summary = result[1] + success = summary.get('success', False) + contacted = summary.get('contacted', {}) + dark = summary.get('dark', {}) + + results_summary['success'] &= success + results_summary['contacted'].update(contacted) + results_summary['dark'].update(dark) + for asset in assets: - if asset.hostname in summary.get('dark', {}): + if asset.hostname in results_summary.get('dark', {}): asset.connectivity = asset.UNREACHABLE - elif asset.hostname in summary.get('contacted', []): + elif asset.hostname in results_summary.get('contacted', []): asset.connectivity = asset.REACHABLE else: asset.connectivity = asset.UNKNOWN - return summary + + return results_summary @shared_task @@ -243,8 +287,7 @@ def test_admin_user_connectivity_manual(admin_user): ## System user connective ## @shared_task -def set_system_user_connectivity_info(system_user, result): - summary = result[1] +def set_system_user_connectivity_info(system_user, summary): system_user.connectivity = summary @@ -258,18 +301,53 @@ def test_system_user_connectivity_util(system_user, assets, task_name): :return: """ from ops.utils import update_or_create_ansible_task - tasks = const.TEST_SYSTEM_USER_CONN_TASKS + hosts = clean_hosts(assets) if not hosts: return {} - task, created = update_or_create_ansible_task( - task_name, hosts=hosts, tasks=tasks, pattern='all', - options=const.TASK_OPTIONS, - run_as=system_user.username, created_by=system_user.org_id, + + 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 + } + } + 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 ) - result = task.run() - set_system_user_connectivity_info(system_user, result) - return result + 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'], + pattern='all', options=const.TASK_OPTIONS, + run_as=system_user.username, created_by=system_user.org_id, + ) + result = task.run() + summary = result[1] + success = summary.get('success', False) + contacted = summary.get('contacted', {}) + dark = summary.get('dark', {}) + + results_summary['success'] &= success + results_summary['contacted'].update(contacted) + results_summary['dark'].update(dark) + + set_system_user_connectivity_info(system_user, results_summary) + return results_summary @shared_task @@ -301,11 +379,7 @@ def test_system_user_connectivity_period(): #### Push system user tasks #### -def get_push_system_user_tasks(system_user): - # Set root as system user is dangerous - if system_user.username == "root": - return [] - +def get_push_linux_system_user_tasks(system_user): tasks = [] if system_user.password: tasks.append({ @@ -320,12 +394,12 @@ def get_push_system_user_tasks(system_user): }) tasks.extend([ { - 'name': 'Check home dir exists', - 'action': { - 'module': 'stat', - 'args': 'path=/home/{}'.format(system_user.username) - }, - 'register': 'home_existed' + 'name': 'Check home dir exists', + 'action': { + 'module': 'stat', + 'args': 'path=/home/{}'.format(system_user.username) + }, + 'register': 'home_existed' }, { 'name': "Set home dir permission", @@ -364,6 +438,46 @@ def get_push_system_user_tasks(system_user): ) } }) + + return tasks + + +def get_push_windows_system_user_tasks(system_user): + tasks = [] + if system_user.password: + tasks.append({ + 'name': 'Add user {}'.format(system_user.username), + 'action': { + 'module': 'win_user', + 'args': 'fullname={} ' + 'name={} ' + 'password={} ' + 'state=present ' + 'update_password=always ' + 'password_expired=no ' + 'password_never_expires=yes ' + 'groups="Users,Remote Desktop Users" ' + 'groups_action=add ' + ''.format(system_user.name, + system_user.username, + system_user.password), + } + }) + 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 = [] return tasks @@ -372,16 +486,29 @@ def push_system_user_util(system_user, assets, task_name): 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: {}").format(system_user.name) + "protocol is not ssh or rdp: {}").format(system_user.name) logger.info(msg) - return + 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) if not hosts: return {} + + hosts = clean_hosts_by_protocol(system_user, hosts) + if not hosts: + return {} + for host in hosts: system_user.load_specific_asset_auth(host) - tasks = get_push_system_user_tasks(system_user) + tasks = get_push_system_user_tasks(host, system_user) + if not tasks: + continue task, created = update_or_create_ansible_task( task_name=task_name, hosts=[host], tasks=tasks, pattern='all', options=const.TASK_OPTIONS, run_as_admin=True, @@ -423,41 +550,74 @@ def test_admin_user_connectability_period(): pass +#### Test Asset user connectivity task #### + +def get_test_asset_user_connectivity_tasks(asset): + if asset.is_unixlike(): + tasks = const.TEST_ASSET_USER_CONN_TASKS + elif asset.is_windows(): + tasks = const.TEST_WINDOWS_ASSET_USER_CONN_TASKS + else: + msg = _( + "The asset {} system platform {} does not " + "support run Ansible tasks".format(asset.hostname, asset.platform) + ) + logger.info(msg) + tasks = [] + return tasks + + @shared_task def set_asset_user_connectivity_info(asset_user, result): summary = result[1] - asset_user.connectivity = summary + if summary.get('contacted'): + connectivity = 1 + elif summary.get("dark"): + connectivity = 0 + else: + connectivity = 3 + asset_user.connectivity = connectivity @shared_task -def test_asset_user_connectivity_util(asset_user, task_name): +def test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=False): """ :param asset_user: 对象 :param task_name: :return: """ from ops.utils import update_or_create_ansible_task - tasks = const.TEST_ASSET_USER_CONN_TASKS + if not check_asset_can_run_ansible(asset_user.asset): return - task, created = update_or_create_ansible_task( - task_name, hosts=[asset_user.asset], tasks=tasks, pattern='all', - options=const.TASK_OPTIONS, - run_as=asset_user.username, created_by=asset_user.org_id - ) + tasks = get_test_asset_user_connectivity_tasks(asset_user.asset) + if not tasks: + 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) result = task.run() set_asset_user_connectivity_info(asset_user, result) @shared_task -def test_asset_users_connectivity_manual(asset_users): +def test_asset_users_connectivity_manual(asset_users, run_as_admin=False): """ :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) + test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=run_as_admin) # @shared_task diff --git a/apps/assets/templates/assets/_asset_list_modal.html b/apps/assets/templates/assets/_asset_list_modal.html index d62dc7a59..4abfa0587 100644 --- a/apps/assets/templates/assets/_asset_list_modal.html +++ b/apps/assets/templates/assets/_asset_list_modal.html @@ -76,6 +76,7 @@ function initTable2() { function onNodeSelected2(event, treeNode) { var url = asset_table2.ajax.url(); url = setUrlParam(url, "node_id", treeNode.meta.node.id); + url = setUrlParam(url, "show_current_asset", ""); asset_table2.ajax.url(url); asset_table2.ajax.reload(); } diff --git a/apps/assets/templates/assets/_asset_user_auth_modal.html b/apps/assets/templates/assets/_asset_user_auth_modal.html deleted file mode 100644 index be615ce52..000000000 --- a/apps/assets/templates/assets/_asset_user_auth_modal.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends '_modal.html' %} -{% load i18n %} -{% block modal_id %}asset_user_auth_modal{% endblock %} -{% block modal_title%}{% trans "Update asset user auth" %}{% endblock %} -{% block modal_body %} -
- {% csrf_token %} -
- -
-

-
-
-
- -
-

-
-
-
- -
- -
-
-
-{% endblock %} -{% block modal_confirm_id %}btn_asset_user_auth_modal_confirm{% endblock %} diff --git a/apps/assets/templates/assets/_asset_user_auth_update_modal.html b/apps/assets/templates/assets/_asset_user_auth_update_modal.html new file mode 100644 index 000000000..28a1a956d --- /dev/null +++ b/apps/assets/templates/assets/_asset_user_auth_update_modal.html @@ -0,0 +1,87 @@ +{% extends '_modal.html' %} +{% load i18n %} +{% block modal_id %}asset_user_auth_update_modal{% endblock %} +{% block modal_title%}{% trans "Update asset user auth" %}{% endblock %} +{% block modal_body %} +
+ {% csrf_token %} +
+ +
+

+
+
+
+ +
+

+
+
+
+ +
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+ +{% endblock %} +{% block modal_confirm_id %}btn_asset_user_auth_update_modal_confirm{% endblock %} diff --git a/apps/assets/templates/assets/_asset_user_view_auth_modal.html b/apps/assets/templates/assets/_asset_user_auth_view_modal.html similarity index 63% rename from apps/assets/templates/assets/_asset_user_view_auth_modal.html rename to apps/assets/templates/assets/_asset_user_auth_view_modal.html index 05f3bf619..6fbd48fcd 100644 --- a/apps/assets/templates/assets/_asset_user_view_auth_modal.html +++ b/apps/assets/templates/assets/_asset_user_auth_view_modal.html @@ -10,17 +10,7 @@ }
-
- -
- - {% trans "Need otp auth for view auth" %} -
- -
-
@@ -132,50 +120,9 @@ - {% include 'assets/_asset_user_auth_modal.html' %} - {% include 'assets/_asset_user_view_auth_modal.html' %} {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/assets/templates/assets/system_user_detail.html b/apps/assets/templates/assets/system_user_detail.html index 26e9bbef0..55f625d81 100644 --- a/apps/assets/templates/assets/system_user_detail.html +++ b/apps/assets/templates/assets/system_user_detail.html @@ -118,7 +118,7 @@
- + {% if system_user.auto_push %} - + {% endif %} - +
{% trans 'Auto push' %}: @@ -135,7 +135,7 @@
{% trans 'Push system user now' %}: @@ -144,7 +144,7 @@
{% trans 'Test assets connective' %}: @@ -219,8 +219,12 @@ function updateCommandFilters(command_filters) { }); } $(document).ready(function () { - if($('#id_protocol_type').text() === 'rdp'){ - $('.only-ssh').addClass('hidden') + var protocol = $('#id_protocol_type').text(); + if(protocol !== 'ssh'){ + $('.only-ssh').addClass("hidden") + } + if(["ssh", "rdp"].indexOf(protocol) === -1){ + $('.only-ssh-rdp').addClass('hidden'); } $(".panel-body .table tr:visible:first").addClass('no-borders-tr'); $('.select2').select2() diff --git a/apps/assets/templates/assets/system_user_list.html b/apps/assets/templates/assets/system_user_list.html index 2b1a63115..0744b793b 100644 --- a/apps/assets/templates/assets/system_user_list.html +++ b/apps/assets/templates/assets/system_user_list.html @@ -9,7 +9,7 @@ {# 目前还不支持Windows的自动推送#} {% trans 'System user is Jumpserver jump login assets used by the users, can be understood as the user login assets, such as web, sa, the dba (` ssh web@some-host `), rather than using a user the username login server jump (` ssh xiaoming@some-host `); '%} {% trans 'In simple terms, users log into Jumpserver using their own username, and Jumpserver uses system users to log into assets. '%} - {% trans 'When system users are created, if you choose auto push Jumpserver to use ansible push system users into the asset, if the asset (Switch, Windows) does not support ansible, please manually fill in the account password. Automatic push for Windows is not currently supported.' %} + {% 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 %} diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index fec960dab..3309e44c2 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -18,6 +18,7 @@ router.register(r'domain', api.DomainViewSet, 'domain') router.register(r'gateway', api.GatewayViewSet, 'gateway') router.register(r'cmd-filter', api.CommandFilterViewSet, 'cmd-filter') router.register(r'asset-user', api.AssetUserViewSet, 'asset-user') +router.register(r'asset-user-info', api.AssetUserExportViewSet, 'asset-user-info') cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filter', lookup='filter') cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule') diff --git a/apps/assets/views/admin_user.py b/apps/assets/views/admin_user.py index fe00e29ac..089efe352 100644 --- a/apps/assets/views/admin_user.py +++ b/apps/assets/views/admin_user.py @@ -11,7 +11,7 @@ from django.views.generic.detail import DetailView, SingleObjectMixin from common.const import create_success_msg, update_success_msg from .. import forms from ..models import AdminUser, Node -from common.permissions import AdminUserRequiredMixin +from common.permissions import PermissionsMixin, IsOrgAdmin __all__ = [ 'AdminUserCreateView', 'AdminUserDetailView', @@ -20,9 +20,10 @@ __all__ = [ ] -class AdminUserListView(AdminUserRequiredMixin, TemplateView): +class AdminUserListView(PermissionsMixin, TemplateView): model = AdminUser template_name = 'assets/admin_user_list.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -33,7 +34,7 @@ class AdminUserListView(AdminUserRequiredMixin, TemplateView): return super().get_context_data(**kwargs) -class AdminUserCreateView(AdminUserRequiredMixin, +class AdminUserCreateView(PermissionsMixin, SuccessMessageMixin, CreateView): model = AdminUser @@ -41,6 +42,7 @@ class AdminUserCreateView(AdminUserRequiredMixin, template_name = 'assets/admin_user_create_update.html' success_url = reverse_lazy('assets:admin-user-list') success_message = create_success_msg + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -51,12 +53,13 @@ class AdminUserCreateView(AdminUserRequiredMixin, return super().get_context_data(**kwargs) -class AdminUserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): +class AdminUserUpdateView(PermissionsMixin, SuccessMessageMixin, UpdateView): model = AdminUser form_class = forms.AdminUserForm template_name = 'assets/admin_user_create_update.html' success_url = reverse_lazy('assets:admin-user-list') success_message = update_success_msg + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -67,11 +70,12 @@ class AdminUserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateVie return super().get_context_data(**kwargs) -class AdminUserDetailView(AdminUserRequiredMixin, DetailView): +class AdminUserDetailView(PermissionsMixin, DetailView): model = AdminUser template_name = 'assets/admin_user_detail.html' context_object_name = 'admin_user' object = None + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -83,18 +87,19 @@ class AdminUserDetailView(AdminUserRequiredMixin, DetailView): return super().get_context_data(**kwargs) -class AdminUserAssetsView(AdminUserRequiredMixin, SingleObjectMixin, ListView): +class AdminUserAssetsView(PermissionsMixin, SingleObjectMixin, ListView): paginate_by = settings.DISPLAY_PER_PAGE template_name = 'assets/admin_user_assets.html' context_object_name = 'admin_user' object = None + permission_classes = [IsOrgAdmin] def get(self, request, *args, **kwargs): self.object = self.get_object(queryset=AdminUser.objects.all()) return super().get(request, *args, **kwargs) def get_queryset(self): - self.queryset = self.object.asset_set.all() + self.queryset = self.object.assets.all() return self.queryset def get_context_data(self, **kwargs): @@ -108,9 +113,10 @@ class AdminUserAssetsView(AdminUserRequiredMixin, SingleObjectMixin, ListView): return super().get_context_data(**kwargs) -class AdminUserDeleteView(AdminUserRequiredMixin, DeleteView): +class AdminUserDeleteView(PermissionsMixin, DeleteView): model = AdminUser template_name = 'delete_confirm.html' success_url = reverse_lazy('assets:admin-user-list') + permission_classes = [IsOrgAdmin] diff --git a/apps/assets/views/asset.py b/apps/assets/views/asset.py index 4a3ce2273..aef420171 100644 --- a/apps/assets/views/asset.py +++ b/apps/assets/views/asset.py @@ -20,18 +20,16 @@ from django.views.decorators.csrf import csrf_exempt from django.utils.decorators import method_decorator from django.core.cache import cache from django.utils import timezone -from django.contrib.auth.mixins import LoginRequiredMixin from django.shortcuts import redirect from django.contrib.messages.views import SuccessMessageMixin +from django.forms.formsets import formset_factory from common.mixins import JSONResponseMixin from common.utils import get_object_or_none, get_logger -from common.permissions import AdminUserRequiredMixin +from common.permissions import PermissionsMixin, IsOrgAdmin, IsValidUser from common.const import ( create_success_msg, update_success_msg, KEY_CACHE_RESOURCES_ID ) -from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX -from orgs.utils import current_org from .. import forms from ..models import Asset, AdminUser, SystemUser, Label, Node, Domain @@ -44,8 +42,9 @@ __all__ = [ logger = get_logger(__file__) -class AssetListView(AdminUserRequiredMixin, TemplateView): +class AssetListView(PermissionsMixin, TemplateView): template_name = 'assets/asset_list.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): Node.root() @@ -59,10 +58,11 @@ class AssetListView(AdminUserRequiredMixin, TemplateView): return super().get_context_data(**kwargs) -class AssetUserListView(AdminUserRequiredMixin, DetailView): +class AssetUserListView(PermissionsMixin, DetailView): model = Asset context_object_name = 'asset' template_name = 'assets/asset_asset_user_list.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -73,8 +73,9 @@ class AssetUserListView(AdminUserRequiredMixin, DetailView): return super().get_context_data(**kwargs) -class UserAssetListView(LoginRequiredMixin, TemplateView): +class UserAssetListView(PermissionsMixin, TemplateView): template_name = 'assets/user_asset_list.html' + permission_classes = [IsValidUser] def get_context_data(self, **kwargs): context = { @@ -86,11 +87,12 @@ class UserAssetListView(LoginRequiredMixin, TemplateView): return super().get_context_data(**kwargs) -class AssetCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): +class AssetCreateView(PermissionsMixin, SuccessMessageMixin, CreateView): model = Asset form_class = forms.AssetCreateForm template_name = 'assets/asset_create.html' success_url = reverse_lazy('assets:asset-list') + permission_classes = [IsOrgAdmin] def get_form(self, form_class=None): form = super().get_form(form_class=form_class) @@ -102,10 +104,30 @@ class AssetCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): form["nodes"].initial = node return form + def get_protocol_formset(self): + ProtocolFormset = formset_factory(forms.ProtocolForm, extra=0, min_num=1, max_num=5) + if self.request.method == "POST": + formset = ProtocolFormset(self.request.POST) + else: + formset = ProtocolFormset() + return formset + + def form_valid(self, form): + formset = self.get_protocol_formset() + valid = formset.is_valid() + if not valid: + return self.form_invalid(form) + protocols = formset.save() + instance = super().form_valid(form) + instance.protocols.set(protocols) + return instance + def get_context_data(self, **kwargs): + formset = self.get_protocol_formset() context = { 'app': _('Assets'), 'action': _('Create asset'), + 'formset': formset, } kwargs.update(context) return super().get_context_data(**kwargs) @@ -114,7 +136,7 @@ class AssetCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): return create_success_msg % ({"name": cleaned_data["hostname"]}) -class AssetBulkUpdateView(AdminUserRequiredMixin, ListView): +class AssetBulkUpdateView(PermissionsMixin, ListView): model = Asset form_class = forms.AssetBulkUpdateForm template_name = 'assets/asset_bulk_update.html' @@ -122,6 +144,7 @@ class AssetBulkUpdateView(AdminUserRequiredMixin, ListView): success_message = _("Bulk update asset success") id_list = None form = None + permission_classes = [IsOrgAdmin] def get(self, request, *args, **kwargs): spm = request.GET.get('spm', '') @@ -154,16 +177,28 @@ class AssetBulkUpdateView(AdminUserRequiredMixin, ListView): return super().get_context_data(**kwargs) -class AssetUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): +class AssetUpdateView(PermissionsMixin, SuccessMessageMixin, UpdateView): model = Asset form_class = forms.AssetUpdateForm template_name = 'assets/asset_update.html' success_url = reverse_lazy('assets:asset-list') + permission_classes = [IsOrgAdmin] + + def get_protocol_formset(self): + ProtocolFormset = formset_factory(forms.ProtocolForm, extra=0, min_num=1, max_num=5) + if self.request.method == "POST": + formset = ProtocolFormset(self.request.POST) + else: + initial_data = [{"name": p.name, "port": p.port} for p in self.object.protocols.all()] + formset = ProtocolFormset(initial=initial_data) + return formset def get_context_data(self, **kwargs): + formset = self.get_protocol_formset() context = { 'app': _('Assets'), 'action': _('Update asset'), + 'formset': formset, } kwargs.update(context) return super().get_context_data(**kwargs) @@ -172,16 +207,18 @@ class AssetUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): return update_success_msg % ({"name": cleaned_data["hostname"]}) -class AssetDeleteView(AdminUserRequiredMixin, DeleteView): +class AssetDeleteView(PermissionsMixin, DeleteView): model = Asset template_name = 'delete_confirm.html' success_url = reverse_lazy('assets:asset-list') + permission_classes = [IsOrgAdmin] -class AssetDetailView(LoginRequiredMixin, DetailView): +class AssetDetailView(PermissionsMixin, DetailView): model = Asset context_object_name = 'asset' template_name = 'assets/asset_detail.html' + permission_classes = [IsValidUser] def get_context_data(self, **kwargs): nodes_remain = Node.objects.exclude(assets=self.object) @@ -195,7 +232,9 @@ class AssetDetailView(LoginRequiredMixin, DetailView): @method_decorator(csrf_exempt, name='dispatch') -class AssetExportView(LoginRequiredMixin, View): +class AssetExportView(PermissionsMixin, View): + permission_classes = [IsValidUser] + def get(self, request): spm = request.GET.get('spm', '') assets_id_default = [Asset.objects.first().id] if Asset.objects.first() else [] @@ -242,8 +281,9 @@ class AssetExportView(LoginRequiredMixin, View): return JsonResponse({'redirect': url}) -class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView): +class BulkImportAssetView(PermissionsMixin, JSONResponseMixin, FormView): form_class = forms.FileForm + permission_classes = [IsOrgAdmin] def form_valid(self, form): node_id = self.request.GET.get("node_id") diff --git a/apps/assets/views/cmd_filter.py b/apps/assets/views/cmd_filter.py index 56c15885b..354c1d852 100644 --- a/apps/assets/views/cmd_filter.py +++ b/apps/assets/views/cmd_filter.py @@ -8,7 +8,7 @@ from django.utils.translation import ugettext_lazy as _ from django.urls import reverse_lazy from django.shortcuts import get_object_or_404, reverse -from common.permissions import AdminUserRequiredMixin +from common.permissions import PermissionsMixin, IsOrgAdmin from common.const import create_success_msg, update_success_msg from ..models import CommandFilter, CommandFilterRule, SystemUser from ..forms import CommandFilterForm, CommandFilterRuleForm @@ -22,8 +22,9 @@ __all__ = ( ) -class CommandFilterListView(AdminUserRequiredMixin, TemplateView): +class CommandFilterListView(PermissionsMixin, TemplateView): template_name = 'assets/cmd_filter_list.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -34,12 +35,13 @@ class CommandFilterListView(AdminUserRequiredMixin, TemplateView): return super().get_context_data(**kwargs) -class CommandFilterCreateView(AdminUserRequiredMixin, CreateView): +class CommandFilterCreateView(PermissionsMixin, CreateView): model = CommandFilter template_name = 'assets/cmd_filter_create_update.html' form_class = CommandFilterForm success_url = reverse_lazy('assets:cmd-filter-list') success_message = create_success_msg + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -50,12 +52,13 @@ class CommandFilterCreateView(AdminUserRequiredMixin, CreateView): return super().get_context_data(**kwargs) -class CommandFilterUpdateView(AdminUserRequiredMixin, UpdateView): +class CommandFilterUpdateView(PermissionsMixin, UpdateView): model = CommandFilter template_name = 'assets/cmd_filter_create_update.html' form_class = CommandFilterForm success_url = reverse_lazy('assets:cmd-filter-list') success_message = update_success_msg + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -66,9 +69,10 @@ class CommandFilterUpdateView(AdminUserRequiredMixin, UpdateView): return super().get_context_data(**kwargs) -class CommandFilterDetailView(AdminUserRequiredMixin, DetailView): +class CommandFilterDetailView(PermissionsMixin, DetailView): model = CommandFilter template_name = 'assets/cmd_filter_detail.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): system_users_remain = SystemUser.objects\ @@ -83,10 +87,11 @@ class CommandFilterDetailView(AdminUserRequiredMixin, DetailView): return super().get_context_data(**kwargs) -class CommandFilterRuleListView(AdminUserRequiredMixin, SingleObjectMixin, TemplateView): +class CommandFilterRuleListView(PermissionsMixin, SingleObjectMixin, TemplateView): template_name = 'assets/cmd_filter_rule_list.html' model = CommandFilter object = None + permission_classes = [IsOrgAdmin] def get(self, request, *args, **kwargs): self.object = self.get_object(queryset=self.model.objects.all()) @@ -102,12 +107,13 @@ class CommandFilterRuleListView(AdminUserRequiredMixin, SingleObjectMixin, Templ return super().get_context_data(**kwargs) -class CommandFilterRuleCreateView(AdminUserRequiredMixin, CreateView): +class CommandFilterRuleCreateView(PermissionsMixin, CreateView): template_name = 'assets/cmd_filter_rule_create_update.html' model = CommandFilterRule form_class = CommandFilterRuleForm success_message = create_success_msg cmd_filter = None + permission_classes = [IsOrgAdmin] def get_success_url(self): return reverse('assets:cmd-filter-rule-list', kwargs={ @@ -135,12 +141,13 @@ class CommandFilterRuleCreateView(AdminUserRequiredMixin, CreateView): return super().get_context_data(**kwargs) -class CommandFilterRuleUpdateView(AdminUserRequiredMixin, UpdateView): +class CommandFilterRuleUpdateView(PermissionsMixin, UpdateView): template_name = 'assets/cmd_filter_rule_create_update.html' model = CommandFilterRule form_class = CommandFilterRuleForm success_message = create_success_msg cmd_filter = None + permission_classes = [IsOrgAdmin] def get_success_url(self): return reverse('assets:cmd-filter-rule-list', kwargs={ diff --git a/apps/assets/views/domain.py b/apps/assets/views/domain.py index 0bfce6905..797bae1f4 100644 --- a/apps/assets/views/domain.py +++ b/apps/assets/views/domain.py @@ -7,7 +7,7 @@ from django.views.generic.detail import SingleObjectMixin from django.utils.translation import ugettext_lazy as _ from django.urls import reverse_lazy, reverse -from common.permissions import AdminUserRequiredMixin +from common.permissions import PermissionsMixin ,IsOrgAdmin from common.const import create_success_msg, update_success_msg from common.utils import get_object_or_none from ..models import Domain, Gateway @@ -21,8 +21,9 @@ __all__ = ( ) -class DomainListView(AdminUserRequiredMixin, TemplateView): +class DomainListView(PermissionsMixin, TemplateView): template_name = 'assets/domain_list.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -33,12 +34,13 @@ class DomainListView(AdminUserRequiredMixin, TemplateView): return super().get_context_data(**kwargs) -class DomainCreateView(AdminUserRequiredMixin, CreateView): +class DomainCreateView(PermissionsMixin, CreateView): model = Domain template_name = 'assets/domain_create_update.html' form_class = DomainForm success_url = reverse_lazy('assets:domain-list') success_message = create_success_msg + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -49,12 +51,13 @@ class DomainCreateView(AdminUserRequiredMixin, CreateView): return super().get_context_data(**kwargs) -class DomainUpdateView(AdminUserRequiredMixin, UpdateView): +class DomainUpdateView(PermissionsMixin, UpdateView): model = Domain template_name = 'assets/domain_create_update.html' form_class = DomainForm success_url = reverse_lazy('assets:domain-list') success_message = update_success_msg + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -65,9 +68,10 @@ class DomainUpdateView(AdminUserRequiredMixin, UpdateView): return super().get_context_data(**kwargs) -class DomainDetailView(AdminUserRequiredMixin, DetailView): +class DomainDetailView(PermissionsMixin, DetailView): model = Domain template_name = 'assets/domain_detail.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -78,16 +82,18 @@ class DomainDetailView(AdminUserRequiredMixin, DetailView): return super().get_context_data(**kwargs) -class DomainDeleteView(AdminUserRequiredMixin, DeleteView): +class DomainDeleteView(PermissionsMixin, DeleteView): model = Domain template_name = 'delete_confirm.html' success_url = reverse_lazy('assets:domain-list') + permission_classes = [IsOrgAdmin] -class DomainGatewayListView(AdminUserRequiredMixin, SingleObjectMixin, TemplateView): +class DomainGatewayListView(PermissionsMixin, SingleObjectMixin, TemplateView): template_name = 'assets/domain_gateway_list.html' model = Domain object = None + permission_classes = [IsOrgAdmin] def get(self, request, *args, **kwargs): self.object = self.get_object(queryset=self.model.objects.all()) @@ -103,11 +109,12 @@ class DomainGatewayListView(AdminUserRequiredMixin, SingleObjectMixin, TemplateV return super().get_context_data(**kwargs) -class DomainGatewayCreateView(AdminUserRequiredMixin, CreateView): +class DomainGatewayCreateView(PermissionsMixin, CreateView): model = Gateway template_name = 'assets/gateway_create_update.html' form_class = GatewayForm success_message = create_success_msg + permission_classes = [IsOrgAdmin] def get_success_url(self): domain = self.object.domain @@ -130,11 +137,12 @@ class DomainGatewayCreateView(AdminUserRequiredMixin, CreateView): return super().get_context_data(**kwargs) -class DomainGatewayUpdateView(AdminUserRequiredMixin, UpdateView): +class DomainGatewayUpdateView(PermissionsMixin, UpdateView): model = Gateway template_name = 'assets/gateway_create_update.html' form_class = GatewayForm success_message = update_success_msg + permission_classes = [IsOrgAdmin] def get_success_url(self): domain = self.object.domain diff --git a/apps/assets/views/label.py b/apps/assets/views/label.py index 9ed289e3e..b53a5d040 100644 --- a/apps/assets/views/label.py +++ b/apps/assets/views/label.py @@ -6,7 +6,7 @@ from django.views.generic import TemplateView, CreateView, \ from django.utils.translation import ugettext_lazy as _ from django.urls import reverse_lazy -from common.permissions import AdminUserRequiredMixin +from common.permissions import PermissionsMixin, IsOrgAdmin from common.const import create_success_msg, update_success_msg from ..models import Label from ..forms import LabelForm @@ -18,8 +18,9 @@ __all__ = ( ) -class LabelListView(AdminUserRequiredMixin, TemplateView): +class LabelListView(PermissionsMixin, TemplateView): template_name = 'assets/label_list.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -30,13 +31,14 @@ class LabelListView(AdminUserRequiredMixin, TemplateView): return super().get_context_data(**kwargs) -class LabelCreateView(AdminUserRequiredMixin, CreateView): +class LabelCreateView(PermissionsMixin, CreateView): model = Label template_name = 'assets/label_create_update.html' form_class = LabelForm success_url = reverse_lazy('assets:label-list') success_message = create_success_msg disable_name = ['draw', 'search', 'limit', 'offset', '_'] + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -57,12 +59,13 @@ class LabelCreateView(AdminUserRequiredMixin, CreateView): return super().form_valid(form) -class LabelUpdateView(AdminUserRequiredMixin, UpdateView): +class LabelUpdateView(PermissionsMixin, UpdateView): model = Label template_name = 'assets/label_create_update.html' form_class = LabelForm success_url = reverse_lazy('assets:label-list') success_message = update_success_msg + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -73,11 +76,12 @@ class LabelUpdateView(AdminUserRequiredMixin, UpdateView): return super().get_context_data(**kwargs) -class LabelDetailView(AdminUserRequiredMixin, DetailView): +class LabelDetailView(PermissionsMixin, DetailView): pass -class LabelDeleteView(AdminUserRequiredMixin, DeleteView): +class LabelDeleteView(PermissionsMixin, DeleteView): model = Label template_name = 'delete_confirm.html' success_url = reverse_lazy('assets:label-list') + permission_classes = [IsOrgAdmin] diff --git a/apps/assets/views/system_user.py b/apps/assets/views/system_user.py index c31fd8da0..3e400cd29 100644 --- a/apps/assets/views/system_user.py +++ b/apps/assets/views/system_user.py @@ -10,7 +10,7 @@ from django.views.generic.detail import DetailView from common.const import create_success_msg, update_success_msg from ..forms import SystemUserForm from ..models import SystemUser, Node, CommandFilter -from common.permissions import AdminUserRequiredMixin +from common.permissions import PermissionsMixin, IsOrgAdmin __all__ = [ @@ -20,8 +20,9 @@ __all__ = [ ] -class SystemUserListView(AdminUserRequiredMixin, TemplateView): +class SystemUserListView(PermissionsMixin, TemplateView): template_name = 'assets/system_user_list.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -32,12 +33,13 @@ class SystemUserListView(AdminUserRequiredMixin, TemplateView): return super().get_context_data(**kwargs) -class SystemUserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): +class SystemUserCreateView(PermissionsMixin, SuccessMessageMixin, CreateView): model = SystemUser form_class = SystemUserForm template_name = 'assets/system_user_create.html' success_url = reverse_lazy('assets:system-user-list') success_message = create_success_msg + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -48,12 +50,13 @@ class SystemUserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateVi return super().get_context_data(**kwargs) -class SystemUserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): +class SystemUserUpdateView(PermissionsMixin, SuccessMessageMixin, UpdateView): model = SystemUser form_class = SystemUserForm template_name = 'assets/system_user_update.html' success_url = reverse_lazy('assets:system-user-list') success_message = update_success_msg + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -64,10 +67,11 @@ class SystemUserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateVi return super().get_context_data(**kwargs) -class SystemUserDetailView(AdminUserRequiredMixin, DetailView): +class SystemUserDetailView(PermissionsMixin, DetailView): template_name = 'assets/system_user_detail.html' context_object_name = 'system_user' model = SystemUser + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -79,16 +83,18 @@ class SystemUserDetailView(AdminUserRequiredMixin, DetailView): return super().get_context_data(**kwargs) -class SystemUserDeleteView(AdminUserRequiredMixin, DeleteView): +class SystemUserDeleteView(PermissionsMixin, DeleteView): model = SystemUser template_name = 'delete_confirm.html' success_url = reverse_lazy('assets:system-user-list') + permission_classes = [IsOrgAdmin] -class SystemUserAssetView(AdminUserRequiredMixin, DetailView): +class SystemUserAssetView(PermissionsMixin, DetailView): model = SystemUser template_name = 'assets/system_user_asset.html' context_object_name = 'system_user' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): nodes_remain = sorted(Node.objects.exclude(systemuser=self.object), reverse=True) diff --git a/apps/audits/api.py b/apps/audits/api.py index 83c75e243..daf111ed8 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -3,7 +3,7 @@ from rest_framework import viewsets -from common.permissions import IsOrgAdminOrAppUser +from common.permissions import IsOrgAdminOrAppUser, IsAuditor from .models import FTPLog from .serializers import FTPLogSerializer @@ -11,4 +11,4 @@ from .serializers import FTPLogSerializer class FTPLogViewSet(viewsets.ModelViewSet): queryset = FTPLog.objects.all() serializer_class = FTPLogSerializer - permission_classes = (IsOrgAdminOrAppUser,) + permission_classes = (IsOrgAdminOrAppUser | IsAuditor,) diff --git a/apps/audits/views.py b/apps/audits/views.py index 372159b74..8c9b6467d 100644 --- a/apps/audits/views.py +++ b/apps/audits/views.py @@ -14,12 +14,11 @@ from django.views import View from django.views.decorators.csrf import csrf_exempt from django.views.generic import ListView from django.utils.translation import ugettext as _ -from django.contrib.auth.mixins import LoginRequiredMixin from django.db.models import Q from audits.utils import get_excel_response, write_content_to_excel from common.mixins import DatetimeSearchMixin -from common.permissions import AdminUserRequiredMixin +from common.permissions import PermissionsMixin, IsOrgAdmin, IsAuditor, IsValidUser from orgs.utils import current_org from ops.views import CommandExecutionListView as UserCommandExecutionListView @@ -42,12 +41,13 @@ def get_resource_type_list(): return [model._meta.verbose_name for model in models] -class FTPLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): +class FTPLogListView(PermissionsMixin, DatetimeSearchMixin, ListView): model = FTPLog template_name = 'audits/ftp_log_list.html' paginate_by = settings.DISPLAY_PER_PAGE user = asset = system_user = filename = '' date_from = date_to = None + permission_classes = [IsOrgAdmin | IsAuditor] def get_queryset(self): self.queryset = super().get_queryset() @@ -89,13 +89,14 @@ class FTPLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): return super().get_context_data(**kwargs) -class OperateLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): +class OperateLogListView(PermissionsMixin, DatetimeSearchMixin, ListView): model = OperateLog template_name = 'audits/operate_log_list.html' paginate_by = settings.DISPLAY_PER_PAGE user = action = resource_type = '' date_from = date_to = None actions_dict = dict(OperateLog.ACTION_CHOICES) + permission_classes = [IsOrgAdmin | IsAuditor] def get_queryset(self): self.queryset = super().get_queryset() @@ -124,7 +125,6 @@ class OperateLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): 'date_from': self.date_from, 'date_to': self.date_to, 'user': self.user, - 'action': self.action, 'resource_type': self.resource_type, "app": _("Audits"), "action": _("Operate log"), @@ -133,12 +133,13 @@ class OperateLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): return super().get_context_data(**kwargs) -class PasswordChangeLogList(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): +class PasswordChangeLogList(PermissionsMixin, DatetimeSearchMixin, ListView): model = PasswordChangeLog template_name = 'audits/password_change_log_list.html' paginate_by = settings.DISPLAY_PER_PAGE user = '' date_from = date_to = None + permission_classes = [IsOrgAdmin | IsAuditor] def get_queryset(self): users = current_org.get_org_users() @@ -169,12 +170,13 @@ class PasswordChangeLogList(AdminUserRequiredMixin, DatetimeSearchMixin, ListVie return super().get_context_data(**kwargs) -class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): +class LoginLogListView(PermissionsMixin, DatetimeSearchMixin, ListView): template_name = 'audits/login_log_list.html' model = UserLoginLog paginate_by = settings.DISPLAY_PER_PAGE user = keyword = "" date_to = date_from = None + permission_classes = [IsOrgAdmin | IsAuditor] @staticmethod def get_org_users(): @@ -246,11 +248,12 @@ class CommandExecutionListView(UserCommandExecutionListView): 'keyword': self.keyword, 'user_id': self.user_id, }) - return super().get_context_data(**context) + return context @method_decorator(csrf_exempt, name='dispatch') -class LoginLogExportView(LoginRequiredMixin, View): +class LoginLogExportView(PermissionsMixin, View): + permission_classes = [IsValidUser] def get(self, request): fields = [ diff --git a/apps/authentication/backends/api.py b/apps/authentication/backends/api.py index 75b9bdb3f..2b1cc0b5d 100644 --- a/apps/authentication/backends/api.py +++ b/apps/authentication/backends/api.py @@ -147,7 +147,23 @@ class PrivateTokenAuthentication(authentication.TokenAuthentication): class SessionAuthentication(authentication.SessionAuthentication): - def enforce_csrf(self, request): - reason = CSRFCheck().process_view(request, None, (), {}) - if reason: - raise exceptions.AuthenticationFailed(reason) + def authenticate(self, request): + """ + Returns a `User` if the request session currently has a logged in user. + Otherwise returns `None`. + """ + + # Get the session-based user from the underlying HttpRequest object + user = getattr(request._request, 'user', None) + + # Unauthenticated, CSRF validation not required + if not user or not user.is_active: + return None + + try: + self.enforce_csrf(request) + except exceptions.AuthenticationFailed: + return None + + # CSRF passed with authenticated user + return user, None diff --git a/apps/authentication/templates/authentication/_mfa_confirm_modal.html b/apps/authentication/templates/authentication/_mfa_confirm_modal.html new file mode 100644 index 000000000..0d7b794bb --- /dev/null +++ b/apps/authentication/templates/authentication/_mfa_confirm_modal.html @@ -0,0 +1,54 @@ +{% extends '_modal.html' %} +{% load i18n %} +{% load static %} +{% block modal_id %}mfa_auth_confirm{% endblock %} +{% block modal_title%}{% trans "MFA confirm" %}{% endblock %} +{% block modal_body %} + +
+
+ +
+ + {% trans "Need otp auth for view auth" %} +
+ +
+
+ +{% endblock %} +{% block modal_button %} + +{% endblock %} diff --git a/apps/common/api.py b/apps/common/api.py index 4f5f8da30..bf41312d7 100644 --- a/apps/common/api.py +++ b/apps/common/api.py @@ -48,7 +48,7 @@ class LogTailApi(generics.RetrieveAPIView): return line def read_from_file(self): - with open(self.log_path, 'r') as f: + with open(self.log_path, 'rt', encoding='utf8') as f: offset = cache.get(self.mark, 0) f.seek(offset) data = f.read(self.buff_size).replace('\n', '\r\n') @@ -79,7 +79,6 @@ class LogTailApi(generics.RetrieveAPIView): class ResourcesIDCacheApi(APIView): - def post(self, request, *args, **kwargs): spm = str(uuid.uuid4()) resources_id = request.data.get('resources') diff --git a/apps/common/decorator.py b/apps/common/decorator.py new file mode 100644 index 000000000..6edc4f3c3 --- /dev/null +++ b/apps/common/decorator.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# +from django.db import transaction + + +def on_transaction_commit(func): + """ + 如果不调用on_commit, 对象创建时添加多对多字段值失败 + """ + def inner(*args, **kwargs): + transaction.on_commit(lambda: func(*args, **kwargs)) + return inner diff --git a/apps/common/mixins.py b/apps/common/mixins.py index 8e4af26dd..b46673cd6 100644 --- a/apps/common/mixins.py +++ b/apps/common/mixins.py @@ -5,6 +5,7 @@ from django.http import JsonResponse from django.utils import timezone from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ +from django.contrib import messages from rest_framework.utils import html from rest_framework.settings import api_settings from rest_framework.exceptions import ValidationError @@ -203,3 +204,31 @@ class DatetimeSearchMixin: def get(self, request, *args, **kwargs): self.get_date_range() return super().get(request, *args, **kwargs) + + +class ApiMessageMixin: + success_message = _("%(name)s was %(action)s successfully") + _action_map = {"create": _("create"), "update": _("update")} + + def get_success_message(self, cleaned_data): + if not isinstance(cleaned_data, dict): + return '' + data = {k: v for k, v in cleaned_data.items()} + action = getattr(self, "action", "create") + data["action"] = self._action_map.get(action) + try: + message = self.success_message % data + except: + message = '' + return message + + def dispatch(self, request, *args, **kwargs): + resp = super().dispatch(request, *args, **kwargs) + if request.method.lower() in ("get", "delete", "patch"): + return resp + if resp.status_code >= 400: + return resp + message = self.get_success_message(resp.data) + if message: + messages.success(request, message) + return resp diff --git a/apps/common/permissions.py b/apps/common/permissions.py index 025d44ba3..ec004df0b 100644 --- a/apps/common/permissions.py +++ b/apps/common/permissions.py @@ -27,6 +27,12 @@ class IsAppUser(IsValidUser): and request.user.is_app +class IsAuditor(IsValidUser): + def has_permission(self, request, view): + return super(IsAuditor, self).has_permission(request, view) \ + and request.user.is_auditor + + class IsSuperUser(IsValidUser): def has_permission(self, request, view): return super(IsSuperUser, self).has_permission(request, view) \ @@ -115,3 +121,14 @@ class WithBootstrapToken(permissions.BasePermission): return False request_bootstrap_token = authorization.split()[-1] return settings.BOOTSTRAP_TOKEN == request_bootstrap_token + + +class PermissionsMixin(UserPassesTestMixin): + permission_classes = [] + + def test_func(self): + permission_classes = self.permission_classes + for permission_class in permission_classes: + if not permission_class().has_permission(self.request, self): + return False + return True diff --git a/apps/common/validators.py b/apps/common/validators.py index b273bd1de..8106f27bd 100644 --- a/apps/common/validators.py +++ b/apps/common/validators.py @@ -3,5 +3,22 @@ from django.core.validators import RegexValidator from django.utils.translation import ugettext_lazy as _ +from rest_framework.validators import ( + UniqueTogetherValidator, ValidationError +) -alphanumeric = RegexValidator(r'^[0-9a-zA-Z_@\-\.]*$', _('Special char not allowed')) \ No newline at end of file + +alphanumeric = RegexValidator(r'^[0-9a-zA-Z_@\-\.]*$', _('Special char not allowed')) + + +class ProjectUniqueValidator(UniqueTogetherValidator): + def __call__(self, attrs): + try: + super().__call__(attrs) + except ValidationError as e: + errors = {} + for field in self.fields: + if field == "org_id": + continue + errors[field] = _('This field must be unique.') + raise ValidationError(errors) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 28ac12456..0362a68e3 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -287,7 +287,10 @@ class Config(dict): return v try: - v = tp(v) + if tp in [list, dict]: + v = json.loads(v) + else: + v = tp(v) except Exception: pass return v diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 7f88e980a..453b1b9e7 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -358,7 +358,7 @@ EMAIL_USE_SSL = False EMAIL_USE_TLS = False EMAIL_SUBJECT_PREFIX = '[JMS] ' -#Email custom content +# Email custom content EMAIL_CUSTOM_USER_CREATED_SUBJECT = '' EMAIL_CUSTOM_USER_CREATED_HONORIFIC = '' EMAIL_CUSTOM_USER_CREATED_BODY = '' diff --git a/apps/jumpserver/views.py b/apps/jumpserver/views.py index 52fa31273..f3272fb17 100644 --- a/apps/jumpserver/views.py +++ b/apps/jumpserver/views.py @@ -8,7 +8,6 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django.db.models import Count from django.shortcuts import redirect -from django.contrib.auth.mixins import LoginRequiredMixin from rest_framework.response import Response from django.views.decorators.csrf import csrf_exempt from django.http import HttpResponse @@ -18,10 +17,12 @@ 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 -class IndexView(LoginRequiredMixin, TemplateView): +class IndexView(PermissionsMixin, TemplateView): template_name = 'index.html' + permission_classes = [IsValidUser] session_week = None session_month = None @@ -31,6 +32,8 @@ class IndexView(LoginRequiredMixin, TemplateView): def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated: return self.handle_no_permission() + if request.user.is_auditor: + return super(IndexView, self).dispatch(request, *args, **kwargs) if not request.user.is_org_admin: return redirect('assets:user-asset-list') if not current_org or not current_org.can_admin_by(request.user): diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 4d24e37a15f6f9752e44d095855d013808ab31ba..c4e24380b3cf885b4f8afcd4e1406bd158425e35 100644 GIT binary patch delta 23885 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 delta 23263 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 G_x}LTU_r6~ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 65c757425..2780ee85f 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Jumpserver 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-06-14 10:30+0800\n" +"POT-Creation-Date: 2019-06-25 15:22+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: Jumpserver team\n" @@ -76,8 +76,9 @@ msgstr "运行参数" #: applications/templates/applications/remote_app_list.html:22 #: applications/templates/applications/user_remote_app_list.html:18 #: assets/forms/domain.py:15 assets/forms/label.py:13 -#: assets/models/asset.py:279 assets/models/authbook.py:27 -#: assets/serializers/admin_user.py:23 assets/serializers/system_user.py:28 +#: assets/models/asset.py:319 assets/models/authbook.py:27 +#: assets/serializers/admin_user.py:24 assets/serializers/asset_user.py:105 +#: assets/serializers/system_user.py:28 #: assets/templates/assets/admin_user_list.html:49 #: assets/templates/assets/domain_detail.html:60 #: assets/templates/assets/domain_list.html:26 @@ -95,7 +96,7 @@ msgstr "运行参数" #: terminal/templates/terminal/session_list.html:41 #: terminal/templates/terminal/session_list.html:72 #: xpack/plugins/change_auth_plan/forms.py:114 -#: xpack/plugins/change_auth_plan/models.py:409 +#: xpack/plugins/change_auth_plan/models.py:413 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:46 #: 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 @@ -103,6 +104,7 @@ msgstr "运行参数" #: xpack/plugins/cloud/models.py:187 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:63 #: xpack/plugins/orgs/templates/orgs/org_list.html:16 +#: xpack/plugins/vault/forms.py:13 xpack/plugins/vault/forms.py:15 msgid "Asset" msgstr "资产" @@ -110,7 +112,7 @@ msgstr "资产" #: applications/templates/applications/remote_app_detail.html:61 #: applications/templates/applications/remote_app_list.html:23 #: applications/templates/applications/user_remote_app_list.html:19 -#: assets/models/user.py:247 assets/templates/assets/user_asset_list.html:172 +#: assets/models/user.py:251 assets/templates/assets/user_asset_list.html:172 #: audits/models.py:20 audits/templates/audits/ftp_log_list.html:49 #: audits/templates/audits/ftp_log_list.html:72 #: perms/forms/asset_permission.py:52 perms/models/asset_permission.py:39 @@ -132,11 +134,11 @@ msgstr "系统用户" #: applications/templates/applications/remote_app_detail.html:53 #: applications/templates/applications/remote_app_list.html:20 #: applications/templates/applications/user_remote_app_list.html:16 -#: assets/forms/domain.py:73 assets/forms/user.py:84 assets/forms/user.py:146 -#: assets/models/base.py:26 assets/models/cluster.py:18 -#: assets/models/cmd_filter.py:20 assets/models/domain.py:20 -#: assets/models/group.py:20 assets/models/label.py:18 -#: assets/templates/assets/admin_user_detail.html:56 +#: assets/forms/domain.py:73 assets/forms/user.py:84 assets/forms/user.py:148 +#: assets/models/asset.py:72 assets/models/base.py:27 +#: assets/models/cluster.py:18 assets/models/cmd_filter.py:20 +#: assets/models/domain.py:20 assets/models/group.py:20 +#: assets/models/label.py:18 assets/templates/assets/admin_user_detail.html:56 #: assets/templates/assets/admin_user_list.html:47 #: assets/templates/assets/cmd_filter_detail.html:61 #: assets/templates/assets/cmd_filter_list.html:24 @@ -165,7 +167,7 @@ msgstr "系统用户" #: settings/templates/settings/terminal_setting.html:105 terminal/models.py:22 #: terminal/models.py:258 terminal/templates/terminal/terminal_detail.html:43 #: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14 -#: users/models/user.py:61 users/templates/users/_select_user_modal.html:13 +#: users/models/user.py:63 users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_detail.html:63 #: users/templates/users/user_group_detail.html:55 #: users/templates/users/user_group_list.html:35 @@ -173,7 +175,7 @@ msgstr "系统用户" #: users/templates/users/user_profile.html:51 #: users/templates/users/user_pubkey_update.html:53 #: xpack/plugins/change_auth_plan/forms.py:97 -#: xpack/plugins/change_auth_plan/models.py:58 +#: xpack/plugins/change_auth_plan/models.py:61 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:61 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:12 #: xpack/plugins/cloud/models.py:49 xpack/plugins/cloud/models.py:119 @@ -204,7 +206,7 @@ msgstr "参数" #: applications/models/remote_app.py:43 #: applications/templates/applications/remote_app_detail.html:77 -#: assets/models/asset.py:109 assets/models/base.py:34 +#: assets/models/asset.py:132 assets/models/base.py:35 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:25 #: assets/models/cmd_filter.py:58 assets/models/group.py:21 #: assets/templates/assets/admin_user_detail.html:68 @@ -216,9 +218,9 @@ msgstr "参数" #: perms/models/asset_permission.py:62 perms/models/base.py:41 #: perms/templates/perms/asset_permission_detail.html:98 #: perms/templates/perms/remote_app_permission_detail.html:90 -#: users/models/user.py:102 users/serializers/v1.py:72 +#: users/models/user.py:104 users/serializers/v1.py:73 #: users/templates/users/user_detail.html:111 -#: xpack/plugins/change_auth_plan/models.py:103 +#: xpack/plugins/change_auth_plan/models.py:106 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:113 #: xpack/plugins/cloud/models.py:55 xpack/plugins/cloud/models.py:127 msgid "Created by" @@ -228,9 +230,10 @@ msgstr "创建者" # msgstr "创建者" #: applications/models/remote_app.py:46 #: applications/templates/applications/remote_app_detail.html:73 -#: assets/models/asset.py:110 assets/models/cluster.py:26 -#: assets/models/domain.py:23 assets/models/group.py:22 -#: assets/models/label.py:25 assets/serializers/admin_user.py:37 +#: assets/models/asset.py:133 assets/models/base.py:33 +#: assets/models/cluster.py:26 assets/models/domain.py:23 +#: assets/models/group.py:22 assets/models/label.py:25 +#: assets/serializers/admin_user.py:38 #: assets/templates/assets/admin_user_detail.html:64 #: assets/templates/assets/cmd_filter_detail.html:69 #: assets/templates/assets/domain_detail.html:68 @@ -256,7 +259,7 @@ msgstr "创建日期" #: applications/templates/applications/remote_app_detail.html:81 #: applications/templates/applications/remote_app_list.html:24 #: applications/templates/applications/user_remote_app_list.html:20 -#: assets/models/asset.py:111 assets/models/base.py:31 +#: assets/models/asset.py:134 assets/models/base.py:32 #: assets/models/cluster.py:29 assets/models/cmd_filter.py:22 #: assets/models/cmd_filter.py:55 assets/models/domain.py:21 #: assets/models/domain.py:53 assets/models/group.py:23 @@ -278,11 +281,11 @@ msgstr "创建日期" #: perms/templates/perms/remote_app_permission_detail.html:94 #: settings/models.py:34 terminal/models.py:32 #: terminal/templates/terminal/terminal_detail.html:63 users/models/group.py:15 -#: users/models/user.py:94 users/templates/users/user_detail.html:127 +#: users/models/user.py:96 users/templates/users/user_detail.html:127 #: users/templates/users/user_group_detail.html:67 #: users/templates/users/user_group_list.html:37 #: users/templates/users/user_profile.html:134 -#: xpack/plugins/change_auth_plan/models.py:99 +#: xpack/plugins/change_auth_plan/models.py:102 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:117 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:19 #: xpack/plugins/cloud/models.py:54 xpack/plugins/cloud/models.py:125 @@ -302,7 +305,7 @@ msgstr "备注" #: perms/templates/perms/remote_app_permission_list.html:17 #: perms/templates/perms/remote_app_permission_remote_app.html:26 #: perms/templates/perms/remote_app_permission_user.html:26 -#: templates/_nav.html:36 templates/_nav.html:48 templates/_nav_user.html:14 +#: templates/_nav.html:36 templates/_nav.html:48 templates/_nav_user.html:16 msgid "RemoteApp" msgstr "远程应用" @@ -310,8 +313,7 @@ msgstr "远程应用" #: assets/templates/assets/_system_user.html:75 #: assets/templates/assets/admin_user_create_update.html:45 #: assets/templates/assets/asset_bulk_update.html:23 -#: assets/templates/assets/asset_create.html:67 -#: assets/templates/assets/asset_update.html:71 +#: assets/templates/assets/asset_create.html:81 #: assets/templates/assets/cmd_filter_create_update.html:15 #: assets/templates/assets/cmd_filter_rule_create_update.html:40 #: assets/templates/assets/domain_create_update.html:16 @@ -340,6 +342,7 @@ msgstr "远程应用" #: xpack/plugins/cloud/templates/cloud/account_create_update.html:33 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create.html:35 #: xpack/plugins/interface/templates/interface/interface.html:72 +#: xpack/plugins/vault/templates/vault/vault_create.html:45 msgid "Reset" msgstr "重置" @@ -347,9 +350,8 @@ msgstr "重置" #: assets/templates/assets/_system_user.html:76 #: assets/templates/assets/admin_user_create_update.html:46 #: assets/templates/assets/asset_bulk_update.html:24 -#: assets/templates/assets/asset_create.html:68 +#: assets/templates/assets/asset_create.html:82 #: assets/templates/assets/asset_list.html:125 -#: assets/templates/assets/asset_update.html:72 #: assets/templates/assets/cmd_filter_create_update.html:16 #: assets/templates/assets/cmd_filter_rule_create_update.html:41 #: assets/templates/assets/domain_create_update.html:17 @@ -367,7 +369,7 @@ msgstr "重置" #: settings/templates/settings/security_setting.html:74 #: settings/templates/settings/terminal_setting.html:73 #: terminal/templates/terminal/command_list.html:103 -#: terminal/templates/terminal/session_list.html:126 +#: terminal/templates/terminal/session_list.html:127 #: terminal/templates/terminal/terminal_update.html:46 #: users/templates/users/_user.html:51 #: users/templates/users/forgot_password.html:42 @@ -378,6 +380,7 @@ msgstr "重置" #: users/templates/users/user_pubkey_update.html:77 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:72 #: xpack/plugins/interface/templates/interface/interface.html:74 +#: xpack/plugins/vault/templates/vault/vault_create.html:46 msgid "Submit" msgstr "提交" @@ -403,12 +406,13 @@ msgstr "提交" #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:20 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:17 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:106 -#: xpack/plugins/change_auth_plan/views.py:83 +#: xpack/plugins/change_auth_plan/views.py:88 msgid "Detail" msgstr "详情" #: applications/templates/applications/remote_app_detail.html:21 #: applications/templates/applications/remote_app_list.html:56 +#: assets/templates/assets/_asset_user_list.html:62 #: assets/templates/assets/admin_user_detail.html:24 #: assets/templates/assets/admin_user_list.html:29 #: assets/templates/assets/admin_user_list.html:112 @@ -505,23 +509,21 @@ msgid "Download application loader" msgstr "下载应用加载器" #: applications/templates/applications/remote_app_list.html:12 -#: applications/views/remote_app.py:47 +#: applications/views/remote_app.py:48 msgid "Create RemoteApp" msgstr "创建远程应用" #: applications/templates/applications/remote_app_list.html:25 #: applications/templates/applications/user_remote_app_list.html:21 #: assets/models/cmd_filter.py:54 -#: assets/templates/assets/admin_user_assets.html:52 +#: assets/templates/assets/_asset_user_list.html:20 #: assets/templates/assets/admin_user_list.html:54 -#: assets/templates/assets/asset_asset_user_list.html:48 #: assets/templates/assets/asset_list.html:108 #: assets/templates/assets/cmd_filter_list.html:28 #: assets/templates/assets/cmd_filter_rule_list.html:63 #: assets/templates/assets/domain_gateway_list.html:73 #: assets/templates/assets/domain_list.html:29 #: assets/templates/assets/label_list.html:17 -#: assets/templates/assets/system_user_asset.html:54 #: assets/templates/assets/system_user_list.html:60 #: assets/templates/assets/user_asset_list.html:48 audits/models.py:38 #: audits/templates/audits/operate_log_list.html:41 @@ -554,48 +556,34 @@ msgstr "动作" msgid "Connect" msgstr "连接" -#: applications/views/remote_app.py:31 applications/views/remote_app.py:46 -#: applications/views/remote_app.py:67 applications/views/remote_app.py:84 -#: assets/models/user.py:134 -#: assets/templates/assets/_asset_group_bulk_update_modal.html:11 -#: assets/templates/assets/system_user_asset.html:22 -#: assets/templates/assets/system_user_detail.html:22 -#: assets/views/admin_user.py:29 assets/views/admin_user.py:47 -#: assets/views/admin_user.py:63 assets/views/admin_user.py:78 -#: assets/views/admin_user.py:102 assets/views/asset.py:53 -#: assets/views/asset.py:69 assets/views/asset.py:107 assets/views/asset.py:148 -#: assets/views/asset.py:165 assets/views/asset.py:189 -#: assets/views/cmd_filter.py:30 assets/views/cmd_filter.py:46 -#: assets/views/cmd_filter.py:62 assets/views/cmd_filter.py:78 -#: assets/views/cmd_filter.py:97 assets/views/cmd_filter.py:130 -#: assets/views/cmd_filter.py:163 assets/views/domain.py:29 -#: assets/views/domain.py:45 assets/views/domain.py:61 -#: assets/views/domain.py:74 assets/views/domain.py:98 -#: assets/views/domain.py:126 assets/views/domain.py:145 -#: assets/views/label.py:26 assets/views/label.py:43 assets/views/label.py:69 -#: assets/views/system_user.py:28 assets/views/system_user.py:44 -#: assets/views/system_user.py:60 assets/views/system_user.py:74 -#: templates/_nav.html:19 xpack/plugins/change_auth_plan/models.py:65 -msgid "Assets" -msgstr "资产管理" +#: applications/views/remote_app.py:31 applications/views/remote_app.py:47 +#: applications/views/remote_app.py:69 applications/views/remote_app.py:87 +#: templates/_nav.html:33 +msgid "Applications" +msgstr "应用管理" #: applications/views/remote_app.py:32 msgid "RemoteApp list" msgstr "远程应用列表" -#: applications/views/remote_app.py:68 +#: applications/views/remote_app.py:70 msgid "Update RemoteApp" msgstr "更新远程应用" -#: applications/views/remote_app.py:85 +#: applications/views/remote_app.py:88 msgid "RemoteApp detail" msgstr "远程应用详情" -#: applications/views/remote_app.py:96 +#: applications/views/remote_app.py:100 msgid "My RemoteApp" msgstr "我的远程应用" -#: assets/api/asset.py:128 +#: assets/api/asset.py:50 +#, python-format +msgid "%(hostname)s was %(action)s successfully" +msgstr "%(hostname)s %(action)s成功" + +#: assets/api/asset.py:129 msgid "Please select assets that need to be updated" msgstr "请选择需要更新的资产" @@ -611,17 +599,17 @@ msgstr "更新节点资产硬件信息: {}" msgid "Test if the assets under the node are connectable: {}" msgstr "测试节点下资产是否可连接: {}" -#: assets/forms/asset.py:27 assets/models/asset.py:80 assets/models/user.py:133 -#: assets/templates/assets/asset_detail.html:194 +#: assets/forms/asset.py:45 assets/models/asset.py:103 +#: assets/models/user.py:134 assets/templates/assets/asset_detail.html:194 #: assets/templates/assets/asset_detail.html:202 -#: assets/templates/assets/system_user_asset.html:95 +#: assets/templates/assets/system_user_asset.html:83 #: perms/models/asset_permission.py:38 -#: xpack/plugins/change_auth_plan/models.py:69 +#: xpack/plugins/change_auth_plan/models.py:72 msgid "Nodes" -msgstr "节点管理" +msgstr "节点" -#: assets/forms/asset.py:30 assets/forms/asset.py:66 assets/models/asset.py:84 -#: assets/models/cluster.py:19 assets/models/user.py:91 +#: assets/forms/asset.py:48 assets/forms/asset.py:83 assets/models/asset.py:107 +#: assets/models/cluster.py:19 assets/models/user.py:92 #: assets/templates/assets/asset_detail.html:80 templates/_nav.html:24 #: xpack/plugins/cloud/models.py:124 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:65 @@ -629,18 +617,16 @@ msgstr "节点管理" msgid "Admin user" msgstr "管理用户" -#: assets/forms/asset.py:33 assets/forms/asset.py:69 assets/forms/asset.py:109 -#: assets/templates/assets/asset_create.html:36 -#: assets/templates/assets/asset_create.html:38 +#: assets/forms/asset.py:51 assets/forms/asset.py:86 assets/forms/asset.py:125 +#: assets/templates/assets/asset_create.html:48 +#: assets/templates/assets/asset_create.html:50 #: assets/templates/assets/asset_list.html:93 -#: assets/templates/assets/asset_update.html:41 -#: assets/templates/assets/asset_update.html:43 #: assets/templates/assets/user_asset_list.html:33 #: xpack/plugins/orgs/templates/orgs/org_list.html:20 msgid "Label" msgstr "标签" -#: assets/forms/asset.py:37 assets/forms/asset.py:73 assets/models/asset.py:79 +#: assets/forms/asset.py:54 assets/forms/asset.py:89 assets/models/asset.py:102 #: assets/models/domain.py:26 assets/models/domain.py:52 #: assets/templates/assets/asset_detail.html:84 #: assets/templates/assets/user_asset_list.html:173 @@ -648,10 +634,9 @@ msgstr "标签" msgid "Domain" msgstr "网域" -#: assets/forms/asset.py:41 assets/forms/asset.py:63 assets/forms/asset.py:77 -#: assets/forms/asset.py:112 assets/models/node.py:31 -#: assets/templates/assets/asset_create.html:30 -#: assets/templates/assets/asset_update.html:35 +#: assets/forms/asset.py:58 assets/forms/asset.py:80 assets/forms/asset.py:93 +#: assets/forms/asset.py:128 assets/models/node.py:31 +#: assets/templates/assets/asset_create.html:42 #: perms/forms/asset_permission.py:49 perms/forms/asset_permission.py:59 #: perms/models/asset_permission.py:57 #: perms/templates/perms/asset_permission_list.html:57 @@ -666,7 +651,7 @@ msgstr "网域" msgid "Node" msgstr "节点" -#: assets/forms/asset.py:45 assets/forms/asset.py:81 +#: assets/forms/asset.py:62 assets/forms/asset.py:97 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" @@ -674,17 +659,17 @@ msgstr "" "root或其他拥有NOPASSWD: ALL权限的用户, 如果是windows或其它硬件可以随意设置一" "个, 更多信息查看左侧 `管理用户` 菜单" -#: assets/forms/asset.py:48 assets/forms/asset.py:84 +#: assets/forms/asset.py:65 assets/forms/asset.py:100 msgid "Windows 2016 RDP protocol is different, If is window 2016, set it" msgstr "Windows 2016的RDP协议与之前不同,如果是请设置" -#: assets/forms/asset.py:49 assets/forms/asset.py:85 +#: assets/forms/asset.py:66 assets/forms/asset.py:101 msgid "" "If your have some network not connect with each other, you can set domain" msgstr "如果有多个的互相隔离的网络,设置资产属于的网域,使用网域网关跳转登录" -#: assets/forms/asset.py:92 assets/forms/asset.py:96 assets/forms/domain.py:17 -#: assets/forms/label.py:15 +#: assets/forms/asset.py:108 assets/forms/asset.py:112 +#: assets/forms/domain.py:17 assets/forms/label.py:15 #: perms/templates/perms/asset_permission_asset.html:88 #: xpack/plugins/change_auth_plan/forms.py:105 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:84 @@ -699,13 +684,13 @@ msgstr "不能包含特殊字符" msgid "SSH gateway support proxy SSH,RDP,VNC" msgstr "SSH网关,支持代理SSH,RDP和VNC" -#: assets/forms/domain.py:74 assets/forms/user.py:85 assets/forms/user.py:147 -#: assets/models/base.py:27 -#: assets/templates/assets/_asset_user_auth_modal.html:15 -#: assets/templates/assets/_asset_user_view_auth_modal.html:31 +#: assets/forms/domain.py:74 assets/forms/user.py:85 assets/forms/user.py:149 +#: assets/models/base.py:28 +#: 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:16 #: assets/templates/assets/admin_user_detail.html:60 #: assets/templates/assets/admin_user_list.html:48 -#: assets/templates/assets/asset_asset_user_list.html:44 #: assets/templates/assets/domain_gateway_list.html:71 #: assets/templates/assets/system_user_detail.html:62 #: assets/templates/assets/system_user_list.html:52 audits/models.py:94 @@ -716,13 +701,13 @@ msgstr "SSH网关,支持代理SSH,RDP和VNC" #: perms/templates/perms/asset_permission_user.html:55 #: perms/templates/perms/remote_app_permission_user.html:54 #: settings/templates/settings/_ldap_list_users_modal.html:37 users/forms.py:13 -#: users/models/user.py:59 users/templates/users/_select_user_modal.html:14 +#: users/models/user.py:61 users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:67 #: users/templates/users/user_list.html:36 #: users/templates/users/user_profile.html:47 #: xpack/plugins/change_auth_plan/forms.py:99 -#: xpack/plugins/change_auth_plan/models.py:60 -#: xpack/plugins/change_auth_plan/models.py:405 +#: xpack/plugins/change_auth_plan/models.py:63 +#: xpack/plugins/change_auth_plan/models.py:409 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:65 #: 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 @@ -734,11 +719,11 @@ msgstr "用户名" msgid "Password or private key passphrase" msgstr "密码或密钥密码" -#: assets/forms/user.py:26 assets/models/base.py:28 -#: assets/serializers/admin_user.py:20 assets/serializers/asset_user.py:19 -#: assets/serializers/system_user.py:16 -#: assets/templates/assets/_asset_user_auth_modal.html:21 -#: assets/templates/assets/_asset_user_view_auth_modal.html:37 +#: assets/forms/user.py:26 assets/models/base.py:29 +#: assets/serializers/admin_user.py:21 assets/serializers/asset_user.py:33 +#: assets/serializers/asset_user.py:86 assets/serializers/system_user.py:16 +#: assets/templates/assets/_asset_user_auth_update_modal.html:21 +#: assets/templates/assets/_asset_user_auth_view_modal.html:27 #: authentication/forms.py:13 #: authentication/templates/authentication/login.html:67 #: authentication/templates/authentication/new_login.html:93 @@ -749,13 +734,15 @@ msgstr "密码或密钥密码" #: users/templates/users/user_profile_update.html:40 #: users/templates/users/user_pubkey_update.html:40 #: users/templates/users/user_update.html:20 -#: xpack/plugins/change_auth_plan/models.py:90 -#: xpack/plugins/change_auth_plan/models.py:260 +#: xpack/plugins/change_auth_plan/models.py:93 +#: xpack/plugins/change_auth_plan/models.py:264 msgid "Password" msgstr "密码" -#: assets/forms/user.py:29 assets/serializers/asset_user.py:27 -#: users/models/user.py:88 +#: assets/forms/user.py:29 assets/serializers/asset_user.py:41 +#: assets/serializers/asset_user.py:94 +#: assets/templates/assets/_asset_user_auth_update_modal.html:27 +#: users/models/user.py:90 msgid "Private key" msgstr "ssh私钥" @@ -767,21 +754,21 @@ msgstr "不合法的密钥,仅支持RSA/DSA格式的密钥" msgid "Password and private key file must be input one" msgstr "密码和私钥, 必须输入一个" -#: assets/forms/user.py:134 +#: assets/forms/user.py:136 msgid "* Automatic login mode must fill in the username." msgstr "自动登录模式,必须填写用户名" -#: assets/forms/user.py:149 assets/models/cmd_filter.py:31 -#: assets/models/user.py:141 assets/templates/assets/_system_user.html:66 +#: assets/forms/user.py:151 assets/models/cmd_filter.py:31 +#: assets/models/user.py:142 assets/templates/assets/_system_user.html:66 #: assets/templates/assets/system_user_detail.html:165 msgid "Command filter" msgstr "命令过滤器" -#: assets/forms/user.py:153 +#: assets/forms/user.py:155 msgid "Auto push system user to asset" msgstr "自动推送系统用户到资产" -#: assets/forms/user.py:154 +#: assets/forms/user.py:156 msgid "" "1-100, High level will be using login asset as default, if user was granted " "more than 2 system user" @@ -789,23 +776,31 @@ msgstr "" "1-100, 1最低优先级,100最高优先级。授权多个用户时,高优先级的系统用户将会作为" "默认登录用户" -#: assets/forms/user.py:156 +#: assets/forms/user.py:158 msgid "" "If you choose manual login mode, you do not need to fill in the username and " "password." msgstr "如果选择手动登录模式,用户名和密码可以不填写" -#: assets/forms/user.py:158 +#: assets/forms/user.py:160 msgid "Use comma split multi command, ex: /bin/whoami,/bin/ifconfig" msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig" -#: assets/models/asset.py:74 assets/models/domain.py:49 +#: assets/models/asset.py:73 assets/models/asset.py:98 +#: assets/models/domain.py:50 +#: assets/templates/assets/domain_gateway_list.html:69 +#: assets/templates/assets/user_asset_list.html:168 +#: settings/templates/settings/replay_storage_create.html:59 +msgid "Port" +msgstr "端口" + +#: assets/models/asset.py:93 assets/models/domain.py:49 +#: assets/serializers/asset_user.py:28 #: assets/templates/assets/_asset_list_modal.html:46 -#: assets/templates/assets/admin_user_assets.html:49 +#: assets/templates/assets/_asset_user_list.html:15 #: assets/templates/assets/asset_detail.html:64 #: assets/templates/assets/asset_list.html:105 #: assets/templates/assets/domain_gateway_list.html:68 -#: assets/templates/assets/system_user_asset.html:51 #: assets/templates/assets/user_asset_list.html:45 #: assets/templates/assets/user_asset_list.html:167 #: audits/templates/audits/login_log_list.html:54 @@ -816,13 +811,13 @@ msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig" msgid "IP" msgstr "IP" -#: assets/models/asset.py:75 assets/templates/assets/_asset_list_modal.html:45 -#: assets/templates/assets/_asset_user_auth_modal.html:9 -#: assets/templates/assets/_asset_user_view_auth_modal.html:25 -#: assets/templates/assets/admin_user_assets.html:48 +#: assets/models/asset.py:94 assets/serializers/asset_user.py:27 +#: assets/templates/assets/_asset_list_modal.html:45 +#: assets/templates/assets/_asset_user_auth_update_modal.html:9 +#: assets/templates/assets/_asset_user_auth_view_modal.html:15 +#: assets/templates/assets/_asset_user_list.html:14 #: assets/templates/assets/asset_detail.html:60 #: assets/templates/assets/asset_list.html:104 -#: assets/templates/assets/system_user_asset.html:50 #: assets/templates/assets/user_asset_list.html:44 #: assets/templates/assets/user_asset_list.html:166 #: perms/templates/perms/asset_permission_asset.html:54 @@ -833,8 +828,9 @@ msgstr "IP" msgid "Hostname" msgstr "主机名" -#: assets/models/asset.py:76 assets/models/domain.py:51 -#: assets/models/user.py:136 assets/templates/assets/asset_detail.html:76 +#: assets/models/asset.py:97 assets/models/asset.py:100 +#: assets/models/domain.py:51 assets/models/user.py:137 +#: assets/templates/assets/asset_detail.html:72 #: assets/templates/assets/domain_gateway_list.html:70 #: assets/templates/assets/system_user_detail.html:70 #: assets/templates/assets/system_user_list.html:53 @@ -843,120 +839,106 @@ msgstr "主机名" msgid "Protocol" msgstr "协议" -#: assets/models/asset.py:77 assets/models/domain.py:50 -#: assets/templates/assets/admin_user_assets.html:50 -#: assets/templates/assets/asset_detail.html:72 -#: assets/templates/assets/domain_gateway_list.html:69 -#: assets/templates/assets/system_user_asset.html:52 -#: assets/templates/assets/user_asset_list.html:168 -#: settings/templates/settings/replay_storage_create.html:59 -msgid "Port" -msgstr "端口" - -#: assets/models/asset.py:78 assets/templates/assets/asset_detail.html:108 +#: assets/models/asset.py:101 assets/templates/assets/asset_detail.html:108 #: assets/templates/assets/user_asset_list.html:170 msgid "Platform" msgstr "系统平台" -#: assets/models/asset.py:81 assets/models/cmd_filter.py:21 +#: assets/models/asset.py:104 assets/models/cmd_filter.py:21 #: assets/models/domain.py:54 assets/models/label.py:22 #: assets/templates/assets/asset_detail.html:116 #: assets/templates/assets/user_asset_list.html:174 msgid "Is active" msgstr "激活" -#: assets/models/asset.py:87 assets/templates/assets/asset_detail.html:68 +#: assets/models/asset.py:110 assets/templates/assets/asset_detail.html:68 msgid "Public IP" msgstr "公网IP" -#: assets/models/asset.py:88 assets/templates/assets/asset_detail.html:124 +#: assets/models/asset.py:111 assets/templates/assets/asset_detail.html:124 msgid "Asset number" msgstr "资产编号" -#: assets/models/asset.py:91 assets/templates/assets/asset_detail.html:88 +#: assets/models/asset.py:114 assets/templates/assets/asset_detail.html:88 msgid "Vendor" msgstr "制造商" -#: assets/models/asset.py:92 assets/templates/assets/asset_detail.html:92 +#: assets/models/asset.py:115 assets/templates/assets/asset_detail.html:92 msgid "Model" msgstr "型号" -#: assets/models/asset.py:93 assets/templates/assets/asset_detail.html:120 +#: assets/models/asset.py:116 assets/templates/assets/asset_detail.html:120 msgid "Serial number" msgstr "序列号" -#: assets/models/asset.py:95 +#: assets/models/asset.py:118 msgid "CPU model" msgstr "CPU型号" -#: assets/models/asset.py:96 +#: assets/models/asset.py:119 #: xpack/plugins/license/templates/license/license_detail.html:80 msgid "CPU count" msgstr "CPU数量" -#: assets/models/asset.py:97 +#: assets/models/asset.py:120 msgid "CPU cores" msgstr "CPU核数" -#: assets/models/asset.py:98 +#: assets/models/asset.py:121 msgid "CPU vcpus" msgstr "CPU总数" -#: assets/models/asset.py:99 assets/templates/assets/asset_detail.html:100 +#: assets/models/asset.py:122 assets/templates/assets/asset_detail.html:100 msgid "Memory" msgstr "内存" -#: assets/models/asset.py:100 +#: assets/models/asset.py:123 msgid "Disk total" msgstr "硬盘大小" -#: assets/models/asset.py:101 +#: assets/models/asset.py:124 msgid "Disk info" msgstr "硬盘信息" -#: assets/models/asset.py:103 assets/templates/assets/asset_detail.html:112 +#: assets/models/asset.py:126 assets/templates/assets/asset_detail.html:112 #: assets/templates/assets/user_asset_list.html:171 msgid "OS" msgstr "操作系统" -#: assets/models/asset.py:104 +#: assets/models/asset.py:127 msgid "OS version" msgstr "系统版本" -#: assets/models/asset.py:105 +#: assets/models/asset.py:128 msgid "OS arch" msgstr "系统架构" -#: assets/models/asset.py:106 +#: assets/models/asset.py:129 msgid "Hostname raw" msgstr "主机名原始" -#: assets/models/asset.py:108 assets/templates/assets/asset_create.html:34 -#: assets/templates/assets/asset_detail.html:231 -#: assets/templates/assets/asset_update.html:39 templates/_nav.html:26 +#: assets/models/asset.py:131 assets/templates/assets/asset_create.html:46 +#: assets/templates/assets/asset_detail.html:231 templates/_nav.html:26 msgid "Labels" msgstr "标签管理" -#: assets/models/asset.py:117 assets/models/base.py:38 -#: assets/serializers/admin_user.py:22 assets/serializers/system_user.py:19 +#: assets/models/asset.py:140 assets/models/base.py:39 +#: assets/serializers/admin_user.py:23 assets/serializers/system_user.py:19 #: assets/templates/assets/admin_user_list.html:51 #: assets/templates/assets/system_user_list.html:57 msgid "Unreachable" msgstr "不可达" -#: assets/models/asset.py:118 assets/models/base.py:39 -#: assets/serializers/admin_user.py:24 assets/serializers/system_user.py:27 -#: assets/templates/assets/admin_user_assets.html:51 +#: assets/models/asset.py:141 assets/models/base.py:40 +#: assets/serializers/admin_user.py:25 assets/serializers/system_user.py:27 #: assets/templates/assets/admin_user_list.html:50 -#: assets/templates/assets/asset_asset_user_list.html:46 #: assets/templates/assets/asset_list.html:107 -#: assets/templates/assets/system_user_asset.html:53 #: assets/templates/assets/system_user_list.html:56 #: users/templates/users/user_group_granted_asset.html:47 msgid "Reachable" msgstr "可连接" -#: assets/models/asset.py:119 assets/models/base.py:40 +#: assets/models/asset.py:142 assets/models/base.py:41 #: authentication/utils.py:9 xpack/plugins/license/models.py:78 msgid "Unknown" msgstr "未知" @@ -965,26 +947,34 @@ msgstr "未知" msgid "Latest version" msgstr "最新版本" -#: assets/models/authbook.py:29 ops/templates/ops/adhoc_history.html:58 +#: assets/models/authbook.py:29 +#: assets/templates/assets/_asset_user_list.html:17 +#: ops/templates/ops/adhoc_history.html:58 #: ops/templates/ops/adhoc_history_detail.html:57 #: ops/templates/ops/task_adhoc.html:58 ops/templates/ops/task_history.html:64 msgid "Version" msgstr "版本" -#: assets/models/authbook.py:34 +#: assets/models/authbook.py:37 msgid "AuthBook" msgstr "" -#: assets/models/base.py:29 xpack/plugins/change_auth_plan/models.py:94 -#: xpack/plugins/change_auth_plan/models.py:267 +#: assets/models/base.py:30 xpack/plugins/change_auth_plan/models.py:97 +#: xpack/plugins/change_auth_plan/models.py:271 msgid "SSH private key" msgstr "ssh密钥" -#: assets/models/base.py:30 xpack/plugins/change_auth_plan/models.py:97 -#: xpack/plugins/change_auth_plan/models.py:263 +#: assets/models/base.py:31 xpack/plugins/change_auth_plan/models.py:100 +#: xpack/plugins/change_auth_plan/models.py:267 msgid "SSH public key" msgstr "ssh公钥" +#: assets/models/base.py:34 assets/serializers/admin_user.py:39 +#: assets/templates/assets/cmd_filter_detail.html:73 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:109 +msgid "Date updated" +msgstr "更新日期" + #: assets/models/cluster.py:20 msgid "Bandwidth" msgstr "带宽" @@ -993,7 +983,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:80 +#: assets/models/cluster.py:22 users/models/user.py:82 #: users/templates/users/user_detail.html:76 msgid "Phone" msgstr "手机" @@ -1015,12 +1005,12 @@ msgid "Operator" msgstr "运营商" #: assets/models/cluster.py:36 assets/models/group.py:34 -#: perms/utils/asset_permission.py:63 +#: perms/utils/asset_permission.py:64 msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:479 +#: users/models/user.py:485 msgid "System" msgstr "系统" @@ -1080,7 +1070,7 @@ msgstr "过滤器" msgid "Type" msgstr "类型" -#: assets/models/cmd_filter.py:51 assets/models/user.py:135 +#: assets/models/cmd_filter.py:51 assets/models/user.py:136 #: assets/templates/assets/cmd_filter_rule_list.html:60 msgid "Priority" msgstr "优先级" @@ -1138,10 +1128,10 @@ msgstr "默认资产组" #: terminal/models.py:154 terminal/templates/terminal/command_list.html:32 #: terminal/templates/terminal/command_list.html:72 #: terminal/templates/terminal/session_list.html:33 -#: terminal/templates/terminal/session_list.html:71 users/forms.py:300 -#: users/models/user.py:36 users/models/user.py:467 users/serializers/v1.py:61 +#: terminal/templates/terminal/session_list.html:71 users/forms.py:301 +#: users/models/user.py:37 users/models/user.py:473 users/serializers/v1.py:62 #: users/templates/users/user_group_detail.html:78 -#: users/templates/users/user_group_list.html:36 users/views/user.py:399 +#: users/templates/users/user_group_list.html:36 users/views/user.py:407 #: xpack/plugins/orgs/forms.py:26 #: xpack/plugins/orgs/templates/orgs/org_detail.html:113 #: xpack/plugins/orgs/templates/orgs/org_list.html:14 @@ -1165,29 +1155,52 @@ msgstr "键" msgid "New node" msgstr "新节点" -#: assets/models/user.py:129 +#: assets/models/user.py:130 msgid "Automatic login" msgstr "自动登录" -#: assets/models/user.py:130 +#: assets/models/user.py:131 msgid "Manually login" msgstr "手动登录" -#: assets/models/user.py:137 assets/templates/assets/_system_user.html:59 +#: assets/models/user.py:135 +#: assets/templates/assets/_asset_group_bulk_update_modal.html:11 +#: assets/templates/assets/system_user_asset.html:22 +#: assets/templates/assets/system_user_detail.html:22 +#: assets/views/admin_user.py:30 assets/views/admin_user.py:49 +#: assets/views/admin_user.py:66 assets/views/admin_user.py:82 +#: assets/views/admin_user.py:107 assets/views/asset.py:52 +#: assets/views/asset.py:69 assets/views/asset.py:128 assets/views/asset.py:171 +#: assets/views/asset.py:199 assets/views/asset.py:226 +#: assets/views/cmd_filter.py:31 assets/views/cmd_filter.py:48 +#: assets/views/cmd_filter.py:65 assets/views/cmd_filter.py:82 +#: assets/views/cmd_filter.py:102 assets/views/cmd_filter.py:136 +#: assets/views/cmd_filter.py:170 assets/views/domain.py:30 +#: assets/views/domain.py:47 assets/views/domain.py:64 +#: assets/views/domain.py:78 assets/views/domain.py:104 +#: assets/views/domain.py:133 assets/views/domain.py:153 +#: assets/views/label.py:27 assets/views/label.py:45 assets/views/label.py:72 +#: assets/views/system_user.py:29 assets/views/system_user.py:46 +#: assets/views/system_user.py:63 assets/views/system_user.py:78 +#: templates/_nav.html:19 xpack/plugins/change_auth_plan/models.py:68 +msgid "Assets" +msgstr "资产管理" + +#: assets/models/user.py:138 assets/templates/assets/_system_user.html:59 #: assets/templates/assets/system_user_detail.html:122 #: assets/templates/assets/system_user_update.html:10 msgid "Auto push" msgstr "自动推送" -#: assets/models/user.py:138 assets/templates/assets/system_user_detail.html:74 +#: assets/models/user.py:139 assets/templates/assets/system_user_detail.html:74 msgid "Sudo" msgstr "Sudo" -#: assets/models/user.py:139 assets/templates/assets/system_user_detail.html:79 +#: assets/models/user.py:140 assets/templates/assets/system_user_detail.html:79 msgid "Shell" msgstr "Shell" -#: assets/models/user.py:140 assets/templates/assets/system_user_detail.html:66 +#: assets/models/user.py:141 assets/templates/assets/system_user_detail.html:66 #: assets/templates/assets/system_user_list.html:54 msgid "Login mode" msgstr "登录模式" @@ -1197,27 +1210,31 @@ msgstr "登录模式" msgid "%(value)s is not an even number" msgstr "%(value)s is not an even number" -#: assets/serializers/admin_user.py:38 -#: assets/templates/assets/asset_asset_user_list.html:47 -#: assets/templates/assets/cmd_filter_detail.html:73 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:109 -msgid "Date updated" -msgstr "更新日期" +#: assets/serializers/asset.py:44 assets/serializers/asset.py:154 +#: assets/templates/assets/asset_create.html:24 +msgid "Protocols" +msgstr "协议组" -#: assets/serializers/asset.py:43 +#: assets/serializers/asset.py:71 msgid "Hardware info" msgstr "硬件信息" -#: assets/serializers/asset.py:44 +#: assets/serializers/asset.py:72 assets/serializers/asset_user.py:29 +#: assets/templates/assets/_asset_user_list.html:18 msgid "Connectivity" msgstr "连接" -#: assets/serializers/asset.py:45 +#: assets/serializers/asset.py:73 orgs/mixins.py:223 msgid "Org name" -msgstr "组织名" +msgstr "组织名称" -#: assets/serializers/asset_user.py:23 users/forms.py:247 -#: users/models/user.py:91 users/templates/users/first_login.html:42 +#: assets/serializers/asset.py:91 +msgid "Protocol duplicate: {}" +msgstr "协议重复: {}" + +#: assets/serializers/asset_user.py:37 assets/serializers/asset_user.py:90 +#: users/forms.py:248 users/models/user.py:93 +#: users/templates/users/first_login.html:42 #: users/templates/users/user_password_update.html:46 #: users/templates/users/user_profile.html:68 #: users/templates/users/user_profile_update.html:43 @@ -1225,6 +1242,14 @@ msgstr "组织名" msgid "Public key" msgstr "ssh公钥" +#: assets/serializers/asset_user.py:43 +msgid "Backend" +msgstr "后端" + +#: assets/serializers/asset_user.py:65 +msgid "private key invalid" +msgstr "密钥不合法" + #: assets/serializers/system_user.py:22 msgid "Unreachable assets" msgstr "不可达资产" @@ -1237,72 +1262,86 @@ msgstr "可连接资产" msgid "Login mode display" msgstr "登录模式显示" -#: assets/tasks.py:31 +#: assets/tasks.py:32 msgid "Asset has been disabled, skipped: {}" msgstr "资产或许不支持ansible, 跳过: {}" -#: assets/tasks.py:35 +#: assets/tasks.py:36 msgid "Asset may not be support ansible, skipped: {}" msgstr "资产或许不支持ansible, 跳过: {}" -#: assets/tasks.py:48 +#: assets/tasks.py:49 msgid "No assets matched, stop task" msgstr "没有匹配到资产,结束任务" -#: assets/tasks.py:73 +#: assets/tasks.py:59 +msgid "No assets matched related system user protocol, stop task" +msgstr "没有匹配到与系统用户协议相关的资产,结束任务" + +#: assets/tasks.py:85 msgid "Get asset info failed: {}" msgstr "获取资产信息失败:{}" -#: assets/tasks.py:123 +#: assets/tasks.py:135 msgid "Update some assets hardware info" msgstr "更新资产硬件信息" -#: assets/tasks.py:140 +#: assets/tasks.py:152 msgid "Update asset hardware info: {}" msgstr "更新资产硬件信息: {}" -#: assets/tasks.py:165 +#: assets/tasks.py:177 msgid "Test assets connectivity" msgstr "测试资产可连接性" -#: assets/tasks.py:189 +#: assets/tasks.py:233 msgid "Test assets connectivity: {}" msgstr "测试资产可连接性: {}" -#: assets/tasks.py:231 +#: assets/tasks.py:275 msgid "Test admin user connectivity period: {}" msgstr "定期测试管理账号可连接性: {}" -#: assets/tasks.py:238 +#: assets/tasks.py:282 msgid "Test admin user connectivity: {}" msgstr "测试管理行号可连接性: {}" -#: assets/tasks.py:277 +#: assets/tasks.py:355 msgid "Test system user connectivity: {}" msgstr "测试系统用户可连接性: {}" -#: assets/tasks.py:284 +#: assets/tasks.py:362 msgid "Test system user connectivity: {} => {}" msgstr "测试系统用户可连接性: {} => {}" -#: assets/tasks.py:297 +#: assets/tasks.py:375 msgid "Test system user connectivity period: {}" msgstr "定期测试系统用户可连接性: {}" -#: assets/tasks.py:374 -msgid "" -"Push system user task skip, auto push not enable or protocol is not ssh: {}" -msgstr "推送系统用户任务跳过,自动推送没有打开,或协议不是ssh: {}" +#: assets/tasks.py:476 assets/tasks.py:562 +#: xpack/plugins/change_auth_plan/models.py:522 +msgid "The asset {} system platform {} does not support run Ansible tasks" +msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务" -#: assets/tasks.py:396 assets/tasks.py:410 +#: assets/tasks.py:488 +msgid "" +"Push system user task skip, auto push not enable or protocol is not ssh or " +"rdp: {}" +msgstr "推送系统用户任务跳过,自动推送没有打开,或协议不是ssh或rdp: {}" + +#: assets/tasks.py:495 +msgid "For security, do not push user {}" +msgstr "为了安全,禁止推送用户 {}" + +#: assets/tasks.py:523 assets/tasks.py:537 msgid "Push system users to assets: {}" msgstr "推送系统用户到入资产: {}" -#: assets/tasks.py:402 +#: assets/tasks.py:529 msgid "Push system users to asset: {} => {}" msgstr "推送系统用户到入资产: {} => {}" -#: assets/tasks.py:459 +#: assets/tasks.py:619 msgid "Test asset user connectivity: {}" msgstr "测试资产用户可连接性: {}" @@ -1311,7 +1350,7 @@ msgid "Import admin user" msgstr "导入管理用户" #: assets/templates/assets/_admin_user_update_modal.html:4 -#: assets/views/admin_user.py:64 +#: assets/views/admin_user.py:67 msgid "Update admin user" msgstr "更新管理用户" @@ -1347,8 +1386,8 @@ msgstr "启用MFA" msgid "Import assets" msgstr "导入资产" -#: assets/templates/assets/_asset_list_modal.html:7 assets/views/asset.py:54 -#: templates/_nav.html:22 xpack/plugins/change_auth_plan/views.py:110 +#: assets/templates/assets/_asset_list_modal.html:7 assets/views/asset.py:53 +#: templates/_nav.html:22 xpack/plugins/change_auth_plan/views.py:116 msgid "Asset list" msgstr "资产列表" @@ -1356,38 +1395,172 @@ msgstr "资产列表" msgid "Update assets" msgstr "更新资产" -#: assets/templates/assets/_asset_user_auth_modal.html:4 +#: assets/templates/assets/_asset_user_auth_update_modal.html:4 msgid "Update asset user auth" msgstr "更新资产用户认证信息" -#: assets/templates/assets/_asset_user_auth_modal.html:23 +#: assets/templates/assets/_asset_user_auth_update_modal.html:23 #: xpack/plugins/change_auth_plan/forms.py:101 msgid "Please input password" msgstr "请输入密码" -#: assets/templates/assets/_asset_user_view_auth_modal.html:5 +#: assets/templates/assets/_asset_user_auth_update_modal.html:68 +#: assets/templates/assets/asset_detail.html:311 +#: users/templates/users/user_detail.html:307 +#: users/templates/users/user_detail.html:334 +#: xpack/plugins/interface/views.py:35 +msgid "Update successfully!" +msgstr "更新成功" + +#: assets/templates/assets/_asset_user_auth_view_modal.html:5 msgid "Asset user auth" msgstr "资产用户信息" -#: assets/templates/assets/_asset_user_view_auth_modal.html:14 -#: audits/models.py:99 audits/templates/audits/login_log_list.html:56 -#: users/forms.py:159 users/models/user.py:83 -#: users/templates/users/first_login.html:45 -msgid "MFA" -msgstr "MFA" +#: assets/templates/assets/_asset_user_auth_view_modal.html:54 +msgid "Copy success" +msgstr "复制成功" -#: assets/templates/assets/_asset_user_view_auth_modal.html:17 -msgid "Need otp auth for view auth" -msgstr "需要二次认证来查看账号信息" +#: assets/templates/assets/_asset_user_auth_view_modal.html:70 +msgid "Get auth info error" +msgstr "获取认证信息错误" + +#: assets/templates/assets/_asset_user_auth_view_modal.html:97 +#: assets/templates/assets/_user_asset_detail_modal.html:23 +#: authentication/templates/authentication/_mfa_confirm_modal.html:53 +#: settings/templates/settings/_ldap_list_users_modal.html:99 +#: templates/_modal.html:22 +msgid "Close" +msgstr "关闭" + +#: assets/templates/assets/_asset_user_list.html:19 +#: audits/templates/audits/operate_log_list.html:71 +#: audits/templates/audits/password_change_log_list.html:53 +#: ops/templates/ops/task_adhoc.html:63 +#: terminal/templates/terminal/command_list.html:76 +#: terminal/templates/terminal/session_detail.html:50 +msgid "Datetime" +msgstr "日期" + +#: assets/templates/assets/_asset_user_list.html:61 +msgid "View" +msgstr "查看" + +#: assets/templates/assets/_asset_user_list.html:63 +#: assets/templates/assets/admin_user_assets.html:61 +#: assets/templates/assets/asset_asset_user_list.html:57 +#: assets/templates/assets/asset_detail.html:182 +#: assets/templates/assets/system_user_asset.html:63 +#: assets/templates/assets/system_user_detail.html:151 +msgid "Test" +msgstr "测试" + +#: assets/templates/assets/_asset_user_list.html:64 +#: assets/templates/assets/system_user_asset.html:72 +#: assets/templates/assets/system_user_detail.html:142 +msgid "Push" +msgstr "推送" + +#: assets/templates/assets/_gateway_test_modal.html:4 +msgid "Test gateway test connection" +msgstr "测试连接网关" + +#: assets/templates/assets/_gateway_test_modal.html:10 terminal/models.py:24 +msgid "SSH Port" +msgstr "SSH端口" + +#: assets/templates/assets/_gateway_test_modal.html:13 +msgid "If use nat, set the ssh real port" +msgstr "如果使用了nat端口映射,请设置为ssh真实监听的端口" + +#: assets/templates/assets/_system_user.html:37 +#: assets/templates/assets/asset_create.html:16 +#: assets/templates/assets/gateway_create_update.html:37 +#: perms/templates/perms/asset_permission_create_update.html:38 +#: perms/templates/perms/remote_app_permission_create_update.html:39 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:43 +msgid "Basic" +msgstr "基本" + +#: assets/templates/assets/_system_user.html:44 +#: assets/templates/assets/asset_create.html:38 +#: assets/templates/assets/gateway_create_update.html:45 +#: users/templates/users/_user.html:21 +msgid "Auth" +msgstr "认证" + +#: assets/templates/assets/_system_user.html:48 +msgid "Auto generate key" +msgstr "自动生成密钥" + +#: assets/templates/assets/_system_user.html:69 +#: assets/templates/assets/asset_create.html:74 +#: assets/templates/assets/gateway_create_update.html:53 +#: perms/templates/perms/asset_permission_create_update.html:53 +#: perms/templates/perms/remote_app_permission_create_update.html:52 +#: terminal/templates/terminal/terminal_update.html:40 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:67 +msgid "Other" +msgstr "其它" + +#: assets/templates/assets/_system_user_import_modal.html:4 +msgid "Import system user" +msgstr "导入系统用户" + +#: assets/templates/assets/_system_user_update_modal.html:4 +#: assets/views/system_user.py:64 +msgid "Update system user" +msgstr "更新系统用户" + +#: assets/templates/assets/_user_asset_detail_modal.html:11 +#: assets/templates/assets/asset_asset_user_list.html:13 +#: assets/templates/assets/asset_detail.html:20 assets/views/asset.py:227 +msgid "Asset detail" +msgstr "资产详情" + +#: assets/templates/assets/admin_user_assets.html:21 +#: assets/templates/assets/admin_user_detail.html:21 +msgid "Assets list" +msgstr "资产列表" + +#: assets/templates/assets/admin_user_assets.html:29 +#: perms/templates/perms/asset_permission_asset.html:35 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:31 +msgid "Asset list of " +msgstr "资产列表" + +#: assets/templates/assets/admin_user_assets.html:52 +#: assets/templates/assets/system_user_asset.html:54 +#: assets/templates/assets/system_user_detail.html:116 +#: perms/templates/perms/asset_permission_detail.html:114 +#: perms/templates/perms/remote_app_permission_detail.html:106 +msgid "Quick update" +msgstr "快速更新" + +#: assets/templates/assets/admin_user_assets.html:58 +#: assets/templates/assets/asset_asset_user_list.html:54 +#: assets/templates/assets/asset_detail.html:179 +msgid "Test connective" +msgstr "测试可连接性" + +#: assets/templates/assets/admin_user_detail.html:83 +msgid "Replace node assets admin user with this" +msgstr "替换资产的管理员" + +#: assets/templates/assets/admin_user_detail.html:91 +#: perms/templates/perms/asset_permission_asset.html:116 +#: xpack/plugins/change_auth_plan/forms.py:109 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:112 +msgid "Select nodes" +msgstr "选择节点" -#: assets/templates/assets/_asset_user_view_auth_modal.html:20 #: assets/templates/assets/admin_user_detail.html:100 #: assets/templates/assets/asset_detail.html:211 #: assets/templates/assets/asset_list.html:682 #: assets/templates/assets/cmd_filter_detail.html:106 -#: assets/templates/assets/system_user_asset.html:112 +#: assets/templates/assets/system_user_asset.html:100 #: assets/templates/assets/system_user_detail.html:182 #: assets/templates/assets/system_user_list.html:170 +#: authentication/templates/authentication/_mfa_confirm_modal.html:20 #: settings/templates/settings/terminal_setting.html:168 #: templates/_modal.html:23 terminal/templates/terminal/session_detail.html:108 #: users/templates/users/user_detail.html:388 @@ -1405,156 +1578,6 @@ msgstr "需要二次认证来查看账号信息" msgid "Confirm" msgstr "确认" -#: assets/templates/assets/_asset_user_view_auth_modal.html:63 -msgid "Copy success" -msgstr "复制成功" - -#: assets/templates/assets/_asset_user_view_auth_modal.html:79 -msgid "Get auth info error" -msgstr "获取认证信息错误" - -#: assets/templates/assets/_asset_user_view_auth_modal.html:139 -#: assets/templates/assets/_user_asset_detail_modal.html:23 -#: settings/templates/settings/_ldap_list_users_modal.html:99 -#: templates/_modal.html:22 -msgid "Close" -msgstr "关闭" - -#: assets/templates/assets/_gateway_test_modal.html:4 -msgid "Test gateway test connection" -msgstr "测试连接网关" - -#: assets/templates/assets/_gateway_test_modal.html:10 terminal/models.py:24 -msgid "SSH Port" -msgstr "SSH端口" - -#: assets/templates/assets/_gateway_test_modal.html:13 -msgid "If use nat, set the ssh real port" -msgstr "如果使用了nat端口映射,请设置为ssh真实监听的端口" - -#: assets/templates/assets/_system_user.html:37 -#: assets/templates/assets/asset_create.html:16 -#: assets/templates/assets/asset_update.html:21 -#: assets/templates/assets/gateway_create_update.html:37 -#: perms/templates/perms/asset_permission_create_update.html:38 -#: perms/templates/perms/remote_app_permission_create_update.html:39 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:43 -msgid "Basic" -msgstr "基本" - -#: assets/templates/assets/_system_user.html:44 -#: assets/templates/assets/asset_create.html:26 -#: assets/templates/assets/asset_update.html:31 -#: assets/templates/assets/gateway_create_update.html:45 -#: users/templates/users/_user.html:21 -msgid "Auth" -msgstr "认证" - -#: assets/templates/assets/_system_user.html:48 -msgid "Auto generate key" -msgstr "自动生成密钥" - -#: assets/templates/assets/_system_user.html:69 -#: assets/templates/assets/asset_create.html:60 -#: assets/templates/assets/asset_update.html:64 -#: assets/templates/assets/gateway_create_update.html:53 -#: perms/templates/perms/asset_permission_create_update.html:53 -#: perms/templates/perms/remote_app_permission_create_update.html:52 -#: terminal/templates/terminal/terminal_update.html:40 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:67 -msgid "Other" -msgstr "其它" - -#: assets/templates/assets/_system_user_import_modal.html:4 -msgid "Import system user" -msgstr "导入系统用户" - -#: assets/templates/assets/_system_user_update_modal.html:4 -#: assets/views/system_user.py:61 -msgid "Update system user" -msgstr "更新系统用户" - -#: assets/templates/assets/_user_asset_detail_modal.html:11 -#: assets/templates/assets/asset_asset_user_list.html:13 -#: assets/templates/assets/asset_detail.html:20 assets/views/asset.py:190 -msgid "Asset detail" -msgstr "资产详情" - -#: assets/templates/assets/admin_user_assets.html:21 -#: assets/templates/assets/admin_user_detail.html:21 -msgid "Assets list" -msgstr "资产列表" - -#: assets/templates/assets/admin_user_assets.html:29 -#: perms/templates/perms/asset_permission_asset.html:35 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:31 -msgid "Asset list of " -msgstr "资产列表" - -#: assets/templates/assets/admin_user_assets.html:64 -#: assets/templates/assets/system_user_asset.html:66 -#: assets/templates/assets/system_user_detail.html:116 -#: perms/templates/perms/asset_permission_detail.html:114 -#: perms/templates/perms/remote_app_permission_detail.html:106 -msgid "Quick update" -msgstr "快速更新" - -#: assets/templates/assets/admin_user_assets.html:70 -#: assets/templates/assets/asset_asset_user_list.html:67 -#: assets/templates/assets/asset_detail.html:179 -msgid "Test connective" -msgstr "测试可连接性" - -#: assets/templates/assets/admin_user_assets.html:73 -#: assets/templates/assets/admin_user_assets.html:118 -#: assets/templates/assets/asset_asset_user_list.html:70 -#: assets/templates/assets/asset_asset_user_list.html:119 -#: assets/templates/assets/asset_detail.html:182 -#: assets/templates/assets/system_user_asset.html:75 -#: assets/templates/assets/system_user_asset.html:166 -#: assets/templates/assets/system_user_detail.html:151 -msgid "Test" -msgstr "测试" - -#: assets/templates/assets/admin_user_assets.html:116 -#: assets/templates/assets/asset_asset_user_list.html:117 -#: assets/templates/assets/system_user_asset.html:169 -msgid "Update auth" -msgstr "更新认证" - -#: assets/templates/assets/admin_user_assets.html:117 -#: assets/templates/assets/asset_asset_user_list.html:118 -#: assets/templates/assets/system_user_asset.html:167 -msgid "View auth" -msgstr "查看认证" - -#: assets/templates/assets/admin_user_assets.html:196 -#: assets/templates/assets/asset_asset_user_list.html:162 -#: assets/templates/assets/asset_detail.html:311 -#: assets/templates/assets/system_user_asset.html:353 -#: users/templates/users/user_detail.html:307 -#: users/templates/users/user_detail.html:334 -#: xpack/plugins/interface/views.py:34 -msgid "Update successfully!" -msgstr "更新成功" - -#: assets/templates/assets/admin_user_assets.html:199 -#: assets/templates/assets/asset_asset_user_list.html:165 -#: assets/templates/assets/system_user_asset.html:356 -msgid "Update failed!" -msgstr "更新失败" - -#: assets/templates/assets/admin_user_detail.html:83 -msgid "Replace node assets admin user with this" -msgstr "替换资产的管理员" - -#: assets/templates/assets/admin_user_detail.html:91 -#: perms/templates/perms/asset_permission_asset.html:116 -#: xpack/plugins/change_auth_plan/forms.py:109 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:112 -msgid "Select nodes" -msgstr "选择节点" - #: assets/templates/assets/admin_user_list.html:7 msgid "" "Admin users are asset (charged server) on the root, or have NOPASSWD: ALL " @@ -1578,6 +1601,7 @@ msgstr "Windows或其它硬件可以随意设置一个" #: audits/templates/audits/login_log_list.html:85 #: users/templates/users/user_group_list.html:10 #: users/templates/users/user_list.html:10 +#: xpack/plugins/vault/templates/vault/vault.html:55 msgid "Export" msgstr "导出" @@ -1588,11 +1612,12 @@ msgstr "导出" #: users/templates/users/user_group_list.html:15 #: users/templates/users/user_list.html:15 #: xpack/plugins/license/templates/license/license_detail.html:110 +#: xpack/plugins/vault/templates/vault/vault.html:60 msgid "Import" msgstr "导入" #: assets/templates/assets/admin_user_list.html:39 -#: assets/views/admin_user.py:48 +#: assets/views/admin_user.py:50 msgid "Create admin user" msgstr "创建管理用户" @@ -1613,6 +1638,7 @@ msgstr "比例" #: users/templates/users/user_group_list.html:194 #: users/templates/users/user_list.html:158 #: users/templates/users/user_list.html:190 +#: xpack/plugins/vault/templates/vault/vault.html:223 msgid "Please select file" msgstr "选择文件" @@ -1625,11 +1651,7 @@ msgstr "资产用户列表" msgid "Asset users of" msgstr "资产用户" -#: assets/templates/assets/asset_asset_user_list.html:45 -msgid "Password version" -msgstr "密码版本" - -#: assets/templates/assets/asset_asset_user_list.html:60 +#: assets/templates/assets/asset_asset_user_list.html:47 #: assets/templates/assets/asset_detail.html:148 #: terminal/templates/terminal/session_detail.html:81 #: users/templates/users/user_detail.html:138 @@ -1696,7 +1718,7 @@ msgstr "" "左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的," "右侧是属于该节点下的资产" -#: assets/templates/assets/asset_list.html:69 assets/views/asset.py:108 +#: assets/templates/assets/asset_list.html:69 assets/views/asset.py:129 msgid "Create asset" msgstr "创建资产" @@ -1830,7 +1852,7 @@ msgstr "删除" msgid "Asset Deleting failed." msgstr "删除失败" -#: assets/templates/assets/asset_update.html:60 +#: assets/templates/assets/asset_update.html:7 msgid "Configuration" msgstr "配置" @@ -1871,12 +1893,12 @@ msgid "else match next rule, if none matched, allowed" msgstr "否则就匹配下一个规则,如果最后没有匹配到规则,则允许执行" #: assets/templates/assets/cmd_filter_list.html:16 -#: assets/views/cmd_filter.py:47 +#: assets/views/cmd_filter.py:49 msgid "Create command filter" msgstr "创建命令过滤器" #: assets/templates/assets/cmd_filter_rule_list.html:33 -#: assets/views/cmd_filter.py:98 +#: assets/views/cmd_filter.py:103 msgid "Command filter rule list" msgstr "命令过滤器规则列表" @@ -1903,7 +1925,7 @@ msgid "Gateway list" msgstr "网关列表" #: assets/templates/assets/domain_gateway_list.html:56 -#: assets/views/domain.py:127 +#: assets/views/domain.py:134 msgid "Create gateway" msgstr "创建网关" @@ -1935,11 +1957,11 @@ msgstr "" msgid "JMS => Domain gateway => Target assets" msgstr "JMS => 网域网关 => 目标资产" -#: assets/templates/assets/domain_list.html:17 assets/views/domain.py:46 +#: assets/templates/assets/domain_list.html:17 assets/views/domain.py:48 msgid "Create domain" msgstr "创建网域" -#: assets/templates/assets/label_list.html:6 assets/views/label.py:44 +#: assets/templates/assets/label_list.html:6 assets/views/label.py:46 msgid "Create label" msgstr "创建标签" @@ -1947,23 +1969,17 @@ msgstr "创建标签" msgid "Assets of " msgstr "资产" -#: assets/templates/assets/system_user_asset.html:72 +#: assets/templates/assets/system_user_asset.html:60 #: assets/templates/assets/system_user_detail.html:148 msgid "Test assets connective" msgstr "测试资产可连接性" -#: assets/templates/assets/system_user_asset.html:81 +#: assets/templates/assets/system_user_asset.html:69 #: assets/templates/assets/system_user_detail.html:139 msgid "Push system user now" msgstr "立刻推送系统" -#: assets/templates/assets/system_user_asset.html:84 -#: assets/templates/assets/system_user_asset.html:164 -#: assets/templates/assets/system_user_detail.html:142 -msgid "Push" -msgstr "推送" - -#: assets/templates/assets/system_user_asset.html:103 +#: assets/templates/assets/system_user_asset.html:91 msgid "Add to node" msgstr "添加到节点" @@ -2001,16 +2017,14 @@ msgstr "" #: assets/templates/assets/system_user_list.html:12 msgid "" "When system users are created, if you choose auto push Jumpserver to use " -"ansible push system users into the asset, if the asset (Switch, Windows) " -"does not support ansible, please manually fill in the account password. " -"Automatic push for Windows is not currently supported." +"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自动推送系统用户到" -"资产中,如果资产(交换机、windows)不支持ansible, 请手动填写账号密码。目前还不" -"支持Windows的自动推送" +"资产中,如果资产(交换机)不支持ansible, 请手动填写账号密码。" #: assets/templates/assets/system_user_list.html:43 -#: assets/views/system_user.py:45 +#: assets/views/system_user.py:47 msgid "Create system user" msgstr "创建系统用户" @@ -2031,99 +2045,99 @@ msgstr "删除系统用户" msgid "System Users Deleting failed." msgstr "系统用户删除失败" -#: assets/views/admin_user.py:30 +#: assets/views/admin_user.py:31 msgid "Admin user list" msgstr "管理用户列表" -#: assets/views/admin_user.py:79 assets/views/admin_user.py:103 +#: assets/views/admin_user.py:83 assets/views/admin_user.py:108 msgid "Admin user detail" msgstr "管理用户详情" -#: assets/views/asset.py:81 templates/_nav_user.html:4 +#: assets/views/asset.py:82 templates/_nav_user.html:4 msgid "My assets" msgstr "我的资产" -#: assets/views/asset.py:122 +#: assets/views/asset.py:144 msgid "Bulk update asset success" msgstr "批量更新资产成功" -#: assets/views/asset.py:149 +#: assets/views/asset.py:172 msgid "Bulk update asset" msgstr "批量更新资产" -#: assets/views/asset.py:166 +#: assets/views/asset.py:200 msgid "Update asset" msgstr "更新资产" -#: assets/views/asset.py:307 +#: assets/views/asset.py:347 msgid "already exists" msgstr "已经存在" -#: assets/views/cmd_filter.py:31 +#: assets/views/cmd_filter.py:32 msgid "Command filter list" msgstr "命令过滤器列表" -#: assets/views/cmd_filter.py:63 +#: assets/views/cmd_filter.py:66 msgid "Update command filter" msgstr "更新命令过滤器" -#: assets/views/cmd_filter.py:79 +#: assets/views/cmd_filter.py:83 msgid "Command filter detail" msgstr "命令过滤器详情" -#: assets/views/cmd_filter.py:131 +#: assets/views/cmd_filter.py:137 msgid "Create command filter rule" msgstr "创建命令过滤器规则" -#: assets/views/cmd_filter.py:164 +#: assets/views/cmd_filter.py:171 msgid "Update command filter rule" msgstr "更新命令过滤器规则" -#: assets/views/domain.py:30 templates/_nav.html:23 +#: assets/views/domain.py:31 templates/_nav.html:23 msgid "Domain list" msgstr "网域列表" -#: assets/views/domain.py:62 +#: assets/views/domain.py:65 msgid "Update domain" msgstr "更新网域" -#: assets/views/domain.py:75 +#: assets/views/domain.py:79 msgid "Domain detail" msgstr "网域详情" -#: assets/views/domain.py:99 +#: assets/views/domain.py:105 msgid "Domain gateway list" msgstr "域网关列表" -#: assets/views/domain.py:146 +#: assets/views/domain.py:154 msgid "Update gateway" msgstr "创建网关" -#: assets/views/label.py:27 +#: assets/views/label.py:28 msgid "Label list" msgstr "标签列表" -#: assets/views/label.py:53 +#: assets/views/label.py:55 msgid "Tips: Avoid using label names reserved internally: {}" msgstr "提示: 请避免使用内部预留标签名: {}" -#: assets/views/label.py:70 +#: assets/views/label.py:73 msgid "Update label" msgstr "更新标签" -#: assets/views/system_user.py:29 +#: assets/views/system_user.py:30 msgid "System user list" msgstr "系统用户列表" -#: assets/views/system_user.py:75 +#: assets/views/system_user.py:79 msgid "System user detail" msgstr "系统用户详情" -#: assets/views/system_user.py:96 +#: assets/views/system_user.py:102 msgid "assets" msgstr "资产管理" -#: assets/views/system_user.py:97 +#: assets/views/system_user.py:103 msgid "System user asset" msgstr "系统用户资产" @@ -2155,7 +2169,7 @@ msgstr "文件名" msgid "Success" msgstr "成功" -#: audits/models.py:32 +#: audits/models.py:32 xpack/plugins/vault/templates/vault/vault.html:46 msgid "Create" msgstr "创建" @@ -2222,8 +2236,15 @@ msgstr "登录城市" msgid "User agent" msgstr "Agent" +#: audits/models.py:99 audits/templates/audits/login_log_list.html:56 +#: authentication/templates/authentication/_mfa_confirm_modal.html:14 +#: users/forms.py:160 users/models/user.py:85 +#: users/templates/users/first_login.html:45 +msgid "MFA" +msgstr "MFA" + #: audits/models.py:100 audits/templates/audits/login_log_list.html:57 -#: xpack/plugins/change_auth_plan/models.py:413 +#: xpack/plugins/change_auth_plan/models.py:417 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:15 #: xpack/plugins/cloud/models.py:172 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:69 @@ -2249,8 +2270,8 @@ msgstr "登录日期" #: perms/templates/perms/asset_permission_detail.html:86 #: perms/templates/perms/remote_app_permission_detail.html:78 #: terminal/models.py:165 terminal/templates/terminal/session_list.html:78 -#: xpack/plugins/change_auth_plan/models.py:246 -#: xpack/plugins/change_auth_plan/models.py:416 +#: xpack/plugins/change_auth_plan/models.py:250 +#: xpack/plugins/change_auth_plan/models.py:420 #: 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 msgid "Date start" @@ -2300,36 +2321,29 @@ msgstr "城市" msgid "Date" msgstr "日期" -#: audits/templates/audits/operate_log_list.html:71 -#: audits/templates/audits/password_change_log_list.html:53 -#: ops/templates/ops/task_adhoc.html:63 -#: terminal/templates/terminal/command_list.html:76 -#: terminal/templates/terminal/session_detail.html:50 -msgid "Datetime" -msgstr "日期" - -#: audits/views.py:85 audits/views.py:129 audits/views.py:165 -#: audits/views.py:209 audits/views.py:241 templates/_nav.html:87 +#: audits/views.py:85 audits/views.py:129 audits/views.py:166 +#: audits/views.py:211 audits/views.py:243 templates/_nav.html:87 +#: templates/_nav_audits.html:22 msgid "Audits" msgstr "日志审计" -#: audits/views.py:86 templates/_nav.html:91 +#: audits/views.py:86 templates/_nav.html:91 templates/_nav_audits.html:26 msgid "FTP log" msgstr "FTP日志" -#: audits/views.py:130 templates/_nav.html:92 +#: audits/views.py:130 templates/_nav.html:92 templates/_nav_audits.html:27 msgid "Operate log" msgstr "操作日志" -#: audits/views.py:166 templates/_nav.html:93 +#: audits/views.py:167 templates/_nav.html:93 templates/_nav_audits.html:28 msgid "Password change log" msgstr "改密日志" -#: audits/views.py:210 templates/_nav.html:90 +#: audits/views.py:212 templates/_nav.html:90 templates/_nav_audits.html:25 msgid "Login log" msgstr "登录日志" -#: audits/views.py:242 +#: audits/views.py:244 msgid "Command execution log" msgstr "命令执行" @@ -2424,6 +2438,18 @@ msgstr "MFA 验证码" msgid "Private Token" msgstr "ssh密钥" +#: authentication/templates/authentication/_mfa_confirm_modal.html:5 +msgid "MFA confirm" +msgstr "MFA确认" + +#: authentication/templates/authentication/_mfa_confirm_modal.html:17 +msgid "Need otp auth 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 #: users/templates/users/reset_password.html:25 @@ -2544,8 +2570,8 @@ msgstr "欢迎回来,请输入用户名和密码登录" msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:172 users/views/user.py:545 -#: users/views/user.py:570 +#: authentication/views/login.py:172 users/views/user.py:555 +#: users/views/user.py:580 msgid "MFA code invalid, or ntp sync server time" msgstr "MFA验证码不正确,或者服务器端时间不对" @@ -2603,19 +2629,36 @@ msgstr "" msgid "Encrypt field using Secret Key" msgstr "" -#: common/mixins.py:35 +#: common/mixins.py:36 msgid "is discard" msgstr "" -#: common/mixins.py:36 +#: common/mixins.py:37 msgid "discard time" msgstr "" -#: common/validators.py:7 +#: common/mixins.py:210 +#, python-format +msgid "%(name)s was %(action)s successfully" +msgstr "%(name)s %(action)s成功" + +#: common/mixins.py:211 +msgid "create" +msgstr "创建" + +#: common/mixins.py:211 +msgid "update" +msgstr "更新" + +#: common/validators.py:11 msgid "Special char not allowed" msgstr "不能包含特殊字符" -#: jumpserver/views.py:185 +#: common/validators.py:23 +msgid "This field must be unique." +msgstr "字段必须唯一" + +#: jumpserver/views.py:188 msgid "" "
Luna is a separately deployed program, you need to deploy Luna, coco, " "configure nginx for url distribution,
If you see this page, " @@ -2690,48 +2733,48 @@ msgstr "Become" msgid "Create by" msgstr "创建者" -#: ops/models/adhoc.py:223 +#: ops/models/adhoc.py:224 msgid "{} Start task: {}" msgstr "{} 任务开始: {}" -#: ops/models/adhoc.py:226 +#: ops/models/adhoc.py:227 msgid "{} Task finish" msgstr "{} 任务结束" -#: ops/models/adhoc.py:324 +#: ops/models/adhoc.py:325 msgid "Start time" msgstr "开始时间" -#: ops/models/adhoc.py:325 +#: ops/models/adhoc.py:326 msgid "End time" msgstr "完成时间" -#: ops/models/adhoc.py:326 ops/templates/ops/adhoc_history.html:57 +#: ops/models/adhoc.py:327 ops/templates/ops/adhoc_history.html:57 #: ops/templates/ops/task_history.html:63 ops/templates/ops/task_list.html:33 -#: xpack/plugins/change_auth_plan/models.py:249 -#: xpack/plugins/change_auth_plan/models.py:419 +#: xpack/plugins/change_auth_plan/models.py:253 +#: xpack/plugins/change_auth_plan/models.py:423 #: 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 msgid "Time" msgstr "时间" -#: ops/models/adhoc.py:327 ops/templates/ops/adhoc_detail.html:106 +#: ops/models/adhoc.py:328 ops/templates/ops/adhoc_detail.html:106 #: ops/templates/ops/adhoc_history.html:55 #: ops/templates/ops/adhoc_history_detail.html:69 #: ops/templates/ops/task_detail.html:84 ops/templates/ops/task_history.html:61 msgid "Is finished" msgstr "是否完成" -#: ops/models/adhoc.py:328 ops/templates/ops/adhoc_history.html:56 +#: ops/models/adhoc.py:329 ops/templates/ops/adhoc_history.html:56 #: ops/templates/ops/task_history.html:62 msgid "Is success" msgstr "是否成功" -#: ops/models/adhoc.py:329 +#: ops/models/adhoc.py:330 msgid "Adhoc raw result" msgstr "结果" -#: ops/models/adhoc.py:330 +#: ops/models/adhoc.py:331 msgid "Adhoc result summary" msgstr "汇总" @@ -2759,7 +2802,7 @@ msgid "Version detail" msgstr "版本详情" #: ops/templates/ops/adhoc_detail.html:22 -#: ops/templates/ops/adhoc_history.html:22 ops/views/adhoc.py:122 +#: ops/templates/ops/adhoc_history.html:22 ops/views/adhoc.py:128 msgid "Version run history" msgstr "执行历史" @@ -2818,7 +2861,7 @@ msgstr "执行历史" msgid "F/S/T" msgstr "失败/成功/总" -#: ops/templates/ops/adhoc_history_detail.html:19 ops/views/adhoc.py:135 +#: ops/templates/ops/adhoc_history_detail.html:19 ops/views/adhoc.py:142 msgid "Run history detail" msgstr "执行历史详情" @@ -2859,33 +2902,33 @@ msgstr "任务列表" msgid "Go" msgstr "" -#: ops/templates/ops/command_execution_create.html:152 +#: ops/templates/ops/command_execution_create.html:155 msgid "Selected assets" msgstr "已选择资产" -#: ops/templates/ops/command_execution_create.html:155 +#: ops/templates/ops/command_execution_create.html:158 msgid "In total" msgstr "总共" -#: ops/templates/ops/command_execution_create.html:190 +#: ops/templates/ops/command_execution_create.html:193 msgid "" "Select the left asset, select the running system user, execute command in " "batch" msgstr "选择左侧资产, 选择运行的系统用户,批量执行命令" -#: ops/templates/ops/command_execution_create.html:208 +#: ops/templates/ops/command_execution_create.html:211 msgid "Unselected assets" msgstr "没有选中资产" -#: ops/templates/ops/command_execution_create.html:212 +#: ops/templates/ops/command_execution_create.html:215 msgid "No input command" msgstr "没有输入命令" -#: ops/templates/ops/command_execution_create.html:216 +#: ops/templates/ops/command_execution_create.html:219 msgid "No system user was selected" msgstr "没有选择系统用户" -#: ops/templates/ops/command_execution_create.html:261 +#: ops/templates/ops/command_execution_create.html:264 msgid "Pending" msgstr "等待" @@ -2894,12 +2937,12 @@ msgid "Finished" msgstr "结束" #: ops/templates/ops/task_adhoc.html:19 ops/templates/ops/task_detail.html:20 -#: ops/templates/ops/task_history.html:19 ops/views/adhoc.py:70 +#: ops/templates/ops/task_history.html:19 ops/views/adhoc.py:72 msgid "Task detail" msgstr "任务详情" #: ops/templates/ops/task_adhoc.html:22 ops/templates/ops/task_detail.html:23 -#: ops/templates/ops/task_history.html:22 ops/views/adhoc.py:83 +#: ops/templates/ops/task_history.html:22 ops/views/adhoc.py:86 msgid "Task versions" msgstr "任务各版本" @@ -2944,31 +2987,31 @@ msgstr "任务开始: " msgid "Update task content: {}" msgstr "更新任务内容: {}" -#: ops/views/adhoc.py:44 ops/views/adhoc.py:69 ops/views/adhoc.py:82 -#: ops/views/adhoc.py:95 ops/views/adhoc.py:108 ops/views/adhoc.py:121 -#: ops/views/adhoc.py:134 ops/views/command.py:44 ops/views/command.py:68 +#: ops/views/adhoc.py:45 ops/views/adhoc.py:71 ops/views/adhoc.py:85 +#: ops/views/adhoc.py:99 ops/views/adhoc.py:113 ops/views/adhoc.py:127 +#: ops/views/adhoc.py:141 ops/views/command.py:47 ops/views/command.py:72 msgid "Ops" msgstr "作业中心" -#: ops/views/adhoc.py:45 templates/_nav.html:81 +#: ops/views/adhoc.py:46 templates/_nav.html:81 msgid "Task list" msgstr "任务列表" -#: ops/views/adhoc.py:96 +#: ops/views/adhoc.py:100 msgid "Task run history" msgstr "执行历史" -#: ops/views/command.py:45 +#: ops/views/command.py:48 msgid "Command execution list" msgstr "命令执行列表" -#: ops/views/command.py:69 templates/_nav_user.html:22 +#: ops/views/command.py:73 templates/_nav_user.html:26 msgid "Command execution" msgstr "命令执行" -#: orgs/mixins.py:81 orgs/models.py:24 +#: orgs/mixins.py:85 orgs/mixins.py:222 orgs/models.py:24 msgid "Organization" -msgstr "组织管理" +msgstr "组织" #: perms/const.py:18 settings/forms.py:143 msgid "All" @@ -2988,8 +3031,8 @@ msgstr "下载文件" #: perms/templates/perms/asset_permission_list.html:75 #: perms/templates/perms/asset_permission_list.html:122 #: perms/templates/perms/remote_app_permission_list.html:16 -#: templates/_nav.html:14 users/forms.py:270 users/models/group.py:26 -#: users/models/user.py:67 users/templates/users/_select_user_modal.html:16 +#: templates/_nav.html:14 users/forms.py:271 users/models/group.py:26 +#: users/models/user.py:69 users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_detail.html:213 #: users/templates/users/user_list.html:38 #: xpack/plugins/orgs/templates/orgs/org_list.html:15 @@ -3018,7 +3061,7 @@ msgstr "资产授权" #: perms/models/asset_permission.py:61 perms/models/base.py:40 #: perms/templates/perms/asset_permission_detail.html:90 #: perms/templates/perms/remote_app_permission_detail.html:82 -#: users/models/user.py:99 users/templates/users/user_detail.html:107 +#: users/models/user.py:101 users/templates/users/user_detail.html:107 #: users/templates/users/user_profile.html:116 msgid "Date expired" msgstr "失效日期" @@ -3164,40 +3207,40 @@ msgstr "添加用户" msgid "Add user group to this permission" msgstr "添加用户组" -#: perms/views/asset_permission.py:33 perms/views/asset_permission.py:65 -#: perms/views/asset_permission.py:80 perms/views/asset_permission.py:95 -#: perms/views/asset_permission.py:130 perms/views/asset_permission.py:162 +#: perms/views/asset_permission.py:34 perms/views/asset_permission.py:67 +#: perms/views/asset_permission.py:83 perms/views/asset_permission.py:99 +#: perms/views/asset_permission.py:136 perms/views/asset_permission.py:169 #: perms/views/remote_app_permission.py:33 -#: perms/views/remote_app_permission.py:48 -#: perms/views/remote_app_permission.py:63 -#: perms/views/remote_app_permission.py:76 -#: perms/views/remote_app_permission.py:102 -#: perms/views/remote_app_permission.py:138 templates/_nav.html:41 +#: perms/views/remote_app_permission.py:49 +#: perms/views/remote_app_permission.py:65 +#: perms/views/remote_app_permission.py:79 +#: perms/views/remote_app_permission.py:106 +#: perms/views/remote_app_permission.py:143 templates/_nav.html:41 #: xpack/plugins/orgs/templates/orgs/org_list.html:21 msgid "Perms" msgstr "权限管理" -#: perms/views/asset_permission.py:34 +#: perms/views/asset_permission.py:35 msgid "Asset permission list" msgstr "资产授权列表" -#: perms/views/asset_permission.py:66 +#: perms/views/asset_permission.py:68 msgid "Create asset permission" msgstr "创建权限规则" -#: perms/views/asset_permission.py:81 +#: perms/views/asset_permission.py:84 msgid "Update asset permission" msgstr "更新资产授权" -#: perms/views/asset_permission.py:96 +#: perms/views/asset_permission.py:100 msgid "Asset permission detail" msgstr "资产授权详情" -#: perms/views/asset_permission.py:131 +#: perms/views/asset_permission.py:137 msgid "Asset permission user list" msgstr "资产授权用户列表" -#: perms/views/asset_permission.py:163 +#: perms/views/asset_permission.py:170 msgid "Asset permission asset list" msgstr "资产授权资产列表" @@ -3205,23 +3248,23 @@ msgstr "资产授权资产列表" msgid "RemoteApp permission list" msgstr "远程应用授权列表" -#: perms/views/remote_app_permission.py:49 +#: perms/views/remote_app_permission.py:50 msgid "Create RemoteApp permission" msgstr "创建远程应用授权规则" -#: perms/views/remote_app_permission.py:64 +#: perms/views/remote_app_permission.py:66 msgid "Update RemoteApp permission" msgstr "更新远程应用授权规则" -#: perms/views/remote_app_permission.py:77 +#: perms/views/remote_app_permission.py:80 msgid "RemoteApp permission detail" msgstr "远程应用授权详情" -#: perms/views/remote_app_permission.py:103 +#: perms/views/remote_app_permission.py:107 msgid "RemoteApp permission user list" msgstr "远程应用授权用户列表" -#: perms/views/remote_app_permission.py:139 +#: perms/views/remote_app_permission.py:144 msgid "RemoteApp permission RemoteApp list" msgstr "远程应用授权远程应用列表" @@ -3302,7 +3345,7 @@ msgstr "发送账号" #: settings/forms.py:89 msgid "Tips: Send mail account, default SMTP account as the send account" -msgstr "提示:发送邮件账号,默认使用 SMTP账号 作为发送账号" +msgstr "提示:发送邮件账号,默认使用SMTP账号作为发送账号" #: settings/forms.py:93 msgid "Use SSL" @@ -3554,7 +3597,7 @@ msgid "Please submit the LDAP configuration before import" msgstr "请先提交LDAP配置再进行导入" #: settings/templates/settings/_ldap_list_users_modal.html:39 -#: users/models/user.py:63 users/templates/users/user_detail.html:71 +#: users/models/user.py:65 users/templates/users/user_detail.html:71 #: users/templates/users/user_profile.html:59 msgid "Email" msgstr "邮件" @@ -3569,7 +3612,7 @@ msgstr "已存在" #: settings/templates/settings/ldap_setting.html:15 #: settings/templates/settings/security_setting.html:15 #: settings/templates/settings/terminal_setting.html:16 -#: settings/templates/settings/terminal_setting.html:49 settings/views.py:19 +#: settings/templates/settings/terminal_setting.html:49 settings/views.py:20 msgid "Basic setting" msgstr "基本设置" @@ -3578,7 +3621,7 @@ msgstr "基本设置" #: settings/templates/settings/email_setting.html:18 #: settings/templates/settings/ldap_setting.html:18 #: settings/templates/settings/security_setting.html:18 -#: settings/templates/settings/terminal_setting.html:20 settings/views.py:45 +#: settings/templates/settings/terminal_setting.html:20 settings/views.py:47 msgid "Email setting" msgstr "邮件设置" @@ -3587,7 +3630,7 @@ msgstr "邮件设置" #: settings/templates/settings/email_setting.html:21 #: settings/templates/settings/ldap_setting.html:21 #: settings/templates/settings/security_setting.html:21 -#: settings/templates/settings/terminal_setting.html:23 settings/views.py:178 +#: settings/templates/settings/terminal_setting.html:23 settings/views.py:186 msgid "Email content setting" msgstr "邮件内容设置" @@ -3596,7 +3639,7 @@ msgstr "邮件内容设置" #: settings/templates/settings/email_setting.html:24 #: settings/templates/settings/ldap_setting.html:24 #: settings/templates/settings/security_setting.html:24 -#: settings/templates/settings/terminal_setting.html:27 settings/views.py:71 +#: settings/templates/settings/terminal_setting.html:27 settings/views.py:74 msgid "LDAP setting" msgstr "LDAP设置" @@ -3605,7 +3648,7 @@ msgstr "LDAP设置" #: settings/templates/settings/email_setting.html:27 #: settings/templates/settings/ldap_setting.html:27 #: settings/templates/settings/security_setting.html:27 -#: settings/templates/settings/terminal_setting.html:31 settings/views.py:100 +#: settings/templates/settings/terminal_setting.html:31 settings/views.py:104 msgid "Terminal setting" msgstr "终端设置" @@ -3615,7 +3658,7 @@ msgstr "终端设置" #: settings/templates/settings/ldap_setting.html:30 #: settings/templates/settings/security_setting.html:30 #: settings/templates/settings/security_setting.html:45 -#: settings/templates/settings/terminal_setting.html:34 settings/views.py:152 +#: settings/templates/settings/terminal_setting.html:34 settings/views.py:159 msgid "Security setting" msgstr "安全设置" @@ -3756,22 +3799,22 @@ msgstr "在ou:{}中没有匹配条目" msgid "The user source is not LDAP" msgstr "用户来源不是LDAP" -#: settings/views.py:18 settings/views.py:44 settings/views.py:70 -#: settings/views.py:99 settings/views.py:126 settings/views.py:138 -#: settings/views.py:151 settings/views.py:177 templates/_nav.html:122 +#: settings/views.py:19 settings/views.py:46 settings/views.py:73 +#: settings/views.py:103 settings/views.py:131 settings/views.py:144 +#: settings/views.py:158 settings/views.py:185 templates/_nav.html:122 msgid "Settings" msgstr "系统设置" -#: settings/views.py:29 settings/views.py:55 settings/views.py:81 -#: settings/views.py:112 settings/views.py:162 settings/views.py:188 +#: settings/views.py:30 settings/views.py:57 settings/views.py:84 +#: settings/views.py:116 settings/views.py:169 settings/views.py:196 msgid "Update setting successfully" msgstr "更新设置成功" -#: settings/views.py:127 +#: settings/views.py:132 msgid "Create replay storage" msgstr "创建录像存储" -#: settings/views.py:139 +#: settings/views.py:145 msgid "Create command storage" msgstr "创建命令存储" @@ -3787,14 +3830,14 @@ msgstr "文档" msgid "Commercial support" msgstr "商业支持" -#: templates/_header_bar.html:89 templates/_nav_user.html:28 users/forms.py:138 +#: templates/_header_bar.html:89 templates/_nav_user.html:32 users/forms.py:139 #: users/templates/users/_user.html:43 #: users/templates/users/first_login.html:39 #: users/templates/users/user_password_update.html:40 #: users/templates/users/user_profile.html:17 #: users/templates/users/user_profile_update.html:37 #: users/templates/users/user_profile_update.html:57 -#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:381 +#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:388 msgid "Profile" msgstr "个人信息" @@ -3811,6 +3854,7 @@ msgid "Logout" msgstr "注销登录" #: templates/_header_bar.html:114 templates/_nav.html:4 +#: templates/_nav_audits.html:4 msgid "Dashboard" msgstr "仪表盘" @@ -3886,11 +3930,11 @@ msgstr "" "\"%(user_pubkey_update)s\"> 链接 更新\n" " " -#: templates/_nav.html:10 users/views/group.py:27 users/views/group.py:43 -#: users/views/group.py:59 users/views/group.py:75 users/views/group.py:91 -#: users/views/login.py:154 users/views/user.py:70 users/views/user.py:86 -#: users/views/user.py:129 users/views/user.py:207 users/views/user.py:368 -#: users/views/user.py:418 users/views/user.py:458 +#: templates/_nav.html:10 users/views/group.py:28 users/views/group.py:45 +#: users/views/group.py:62 users/views/group.py:79 users/views/group.py:96 +#: users/views/login.py:154 users/views/user.py:70 users/views/user.py:87 +#: users/views/user.py:131 users/views/user.py:211 users/views/user.py:374 +#: users/views/user.py:426 users/views/user.py:467 msgid "Users" msgstr "用户管理" @@ -3902,38 +3946,36 @@ msgstr "用户列表" msgid "Command filters" msgstr "命令过滤" -#: templates/_nav.html:33 -msgid "Applications" -msgstr "应用管理" - -#: templates/_nav.html:55 +#: templates/_nav.html:55 templates/_nav_audits.html:11 +#: terminal/views/command.py:51 terminal/views/session.py:74 +#: terminal/views/session.py:92 terminal/views/session.py:116 +#: terminal/views/terminal.py:31 terminal/views/terminal.py:47 +#: terminal/views/terminal.py:60 msgid "Sessions" msgstr "会话管理" -#: templates/_nav.html:58 +#: templates/_nav.html:58 templates/_nav_audits.html:14 msgid "Session online" msgstr "在线会话" -#: templates/_nav.html:59 +#: templates/_nav.html:59 templates/_nav_audits.html:15 +#: terminal/views/session.py:93 msgid "Session offline" msgstr "历史会话" -#: templates/_nav.html:60 +#: templates/_nav.html:60 templates/_nav_audits.html:16 msgid "Commands" msgstr "命令记录" -#: templates/_nav.html:63 templates/_nav_user.html:33 +#: templates/_nav.html:63 templates/_nav_user.html:37 msgid "Web terminal" msgstr "Web终端" -#: templates/_nav.html:68 templates/_nav_user.html:38 +#: templates/_nav.html:68 templates/_nav_user.html:42 msgid "File manager" msgstr "文件管理" -#: templates/_nav.html:72 terminal/views/command.py:50 -#: terminal/views/session.py:74 terminal/views/session.py:92 -#: terminal/views/session.py:115 terminal/views/terminal.py:31 -#: terminal/views/terminal.py:46 terminal/views/terminal.py:58 +#: templates/_nav.html:72 msgid "Terminal" msgstr "终端管理" @@ -3941,7 +3983,7 @@ msgstr "终端管理" msgid "Job Center" msgstr "作业中心" -#: templates/_nav.html:82 templates/_nav.html:94 +#: templates/_nav.html:82 templates/_nav.html:94 templates/_nav_audits.html:29 msgid "Batch command" msgstr "批量命令" @@ -3949,7 +3991,7 @@ msgstr "批量命令" msgid "XPack" msgstr "" -#: templates/_nav.html:108 xpack/plugins/cloud/views.py:26 +#: templates/_nav.html:108 xpack/plugins/cloud/views.py:28 msgid "Account list" msgstr "账户列表" @@ -3957,7 +3999,7 @@ msgstr "账户列表" msgid "Sync instance" msgstr "同步实例" -#: templates/_nav_user.html:9 +#: templates/_nav_user.html:11 msgid "My Applications" msgstr "我的应用" @@ -4220,12 +4262,12 @@ msgid "Export command" msgstr "导出命令" #: terminal/templates/terminal/session_detail.html:17 -#: terminal/views/session.py:116 +#: terminal/views/session.py:117 msgid "Session detail" msgstr "会话详情" #: terminal/templates/terminal/session_detail.html:28 -#: terminal/views/command.py:51 +#: terminal/views/command.py:52 msgid "Command list" msgstr "命令记录列表" @@ -4258,24 +4300,24 @@ msgstr "时长" msgid "Terminate" msgstr "终断" -#: terminal/templates/terminal/session_list.html:121 +#: terminal/templates/terminal/session_list.html:122 msgid "Terminate selected" msgstr "终断所选" -#: terminal/templates/terminal/session_list.html:122 +#: terminal/templates/terminal/session_list.html:123 msgid "Confirm finished" msgstr "确认已完成" -#: terminal/templates/terminal/session_list.html:142 +#: terminal/templates/terminal/session_list.html:144 msgid "Terminate task send, waiting ..." msgstr "终断任务已发送,请等待" -#: terminal/templates/terminal/session_list.html:155 +#: terminal/templates/terminal/session_list.html:157 msgid "Finish session success" msgstr "标记会话完成成功" #: terminal/templates/terminal/terminal_detail.html:13 -#: terminal/views/terminal.py:59 +#: terminal/views/terminal.py:61 msgid "Terminal detail" msgstr "终端详情" @@ -4315,27 +4357,23 @@ msgstr "信息" msgid "Session online list" msgstr "在线会话" -#: terminal/views/session.py:93 -msgid "Session offline list" -msgstr "离线会话" - #: terminal/views/terminal.py:32 msgid "Terminal list" msgstr "终端列表" -#: terminal/views/terminal.py:46 +#: terminal/views/terminal.py:47 msgid "Update terminal" msgstr "更新终端" -#: terminal/views/terminal.py:105 terminal/views/terminal.py:106 +#: terminal/views/terminal.py:110 terminal/views/terminal.py:111 msgid "Redirect to web terminal" msgstr "重定向到web terminal" -#: terminal/views/terminal.py:113 +#: terminal/views/terminal.py:118 msgid "Connect ssh terminal" msgstr "连接ssh终端" -#: terminal/views/terminal.py:114 +#: terminal/views/terminal.py:119 msgid "" "You should use your ssh client tools connect terminal: {}

{}" msgstr "你可以使用ssh客户端工具连接终端" @@ -4348,7 +4386,7 @@ msgstr "你没有权限" msgid "Could not reset self otp, use profile reset instead" msgstr "不能再该页面重置MFA, 请去个人信息页面重置" -#: users/forms.py:32 users/models/user.py:71 +#: users/forms.py:32 users/models/user.py:73 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:87 #: users/templates/users/user_list.html:37 @@ -4356,11 +4394,11 @@ msgstr "不能再该页面重置MFA, 请去个人信息页面重置" msgid "Role" msgstr "角色" -#: users/forms.py:35 users/forms.py:217 +#: users/forms.py:35 users/forms.py:218 msgid "ssh public key" msgstr "ssh公钥" -#: users/forms.py:36 users/forms.py:218 +#: users/forms.py:36 users/forms.py:219 msgid "ssh-rsa AAAA..." msgstr "" @@ -4372,23 +4410,23 @@ msgstr "复制用户公钥到这里" msgid "Join user groups" msgstr "添加到用户组" -#: users/forms.py:85 users/forms.py:232 +#: users/forms.py:86 users/forms.py:233 msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" -#: users/forms.py:89 users/forms.py:236 users/serializers/v1.py:47 +#: users/forms.py:90 users/forms.py:237 users/serializers/v1.py:48 msgid "Not a valid ssh public key" msgstr "ssh密钥不合法" -#: users/forms.py:109 +#: users/forms.py:110 msgid "Reset link will be generated and sent to the user" msgstr "生成重置密码链接,通过邮件发送给用户" -#: users/forms.py:110 +#: users/forms.py:111 msgid "Set password" msgstr "设置密码" -#: users/forms.py:117 xpack/plugins/change_auth_plan/models.py:83 +#: users/forms.py:118 xpack/plugins/change_auth_plan/models.py:86 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:51 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:69 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:57 @@ -4396,7 +4434,7 @@ msgstr "设置密码" msgid "Password strategy" msgstr "密码策略" -#: users/forms.py:144 +#: users/forms.py:145 msgid "" "Tip: when enabled, you will enter the MFA binding process the next time you " "log in. you can also directly bind in \"personal information -> quick " @@ -4405,11 +4443,11 @@ msgstr "" "提示:启用之后您将会在下次登录时进入MFA绑定流程;您也可以在(个人信息->快速修" "改->更改MFA设置)中直接绑定!" -#: users/forms.py:154 +#: users/forms.py:155 msgid "* Enable MFA authentication to make the account more secure." msgstr "* 启用MFA认证,使账号更加安全." -#: users/forms.py:164 +#: users/forms.py:165 msgid "" "In order to protect you and your company, please keep your account, password " "and key sensitive information properly. (for example: setting complex " @@ -4418,120 +4456,124 @@ msgstr "" "为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:" "设置复杂密码,启用MFA认证)" -#: users/forms.py:171 users/templates/users/first_login.html:48 +#: users/forms.py:172 users/templates/users/first_login.html:48 #: users/templates/users/first_login.html:107 #: users/templates/users/first_login.html:130 msgid "Finish" msgstr "完成" -#: users/forms.py:177 +#: users/forms.py:178 msgid "Old password" msgstr "原来密码" -#: users/forms.py:182 +#: users/forms.py:183 msgid "New password" msgstr "新密码" -#: users/forms.py:187 +#: users/forms.py:188 msgid "Confirm password" msgstr "确认密码" -#: users/forms.py:197 +#: users/forms.py:198 msgid "Old password error" msgstr "原来密码错误" -#: users/forms.py:205 +#: users/forms.py:206 msgid "Password does not match" msgstr "密码不一致" -#: users/forms.py:215 +#: users/forms.py:216 msgid "Automatically configure and download the SSH key" msgstr "自动配置并下载SSH密钥" -#: users/forms.py:219 +#: users/forms.py:220 msgid "Paste your id_rsa.pub here." msgstr "复制你的公钥到这里" -#: users/forms.py:253 users/forms.py:258 users/forms.py:304 +#: users/forms.py:254 users/forms.py:259 users/forms.py:305 #: xpack/plugins/orgs/forms.py:30 msgid "Select users" msgstr "选择用户" -#: users/models/user.py:35 users/models/user.py:475 +#: users/models/user.py:36 users/models/user.py:481 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:37 +#: users/models/user.py:38 msgid "Application" msgstr "应用程序" -#: users/models/user.py:40 users/templates/users/user_profile.html:92 +#: users/models/user.py:39 +msgid "Auditor" +msgstr "审计员" + +#: users/models/user.py:42 users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:159 #: users/templates/users/user_profile.html:162 msgid "Disable" msgstr "禁用" -#: users/models/user.py:41 users/templates/users/user_profile.html:90 +#: users/models/user.py:43 users/templates/users/user_profile.html:90 #: users/templates/users/user_profile.html:166 msgid "Enable" msgstr "启用" -#: users/models/user.py:42 users/templates/users/user_profile.html:88 +#: users/models/user.py:44 users/templates/users/user_profile.html:88 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:74 +#: users/models/user.py:76 msgid "Avatar" msgstr "头像" -#: users/models/user.py:77 users/templates/users/user_detail.html:82 +#: users/models/user.py:79 users/templates/users/user_detail.html:82 msgid "Wechat" msgstr "微信" -#: users/models/user.py:106 users/templates/users/user_detail.html:103 +#: users/models/user.py:108 users/templates/users/user_detail.html:103 #: users/templates/users/user_list.html:39 #: users/templates/users/user_profile.html:100 msgid "Source" msgstr "用户来源" -#: users/models/user.py:110 +#: users/models/user.py:112 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:136 users/templates/users/user_update.html:22 -#: users/views/login.py:47 users/views/login.py:108 users/views/user.py:431 +#: users/models/user.py:138 users/templates/users/user_update.html:22 +#: users/views/login.py:46 users/views/login.py:107 users/views/user.py:439 msgid "User auth from {}, go there change password" msgstr "用户认证源来自 {}, 请去相应系统修改密码" -#: users/models/user.py:478 +#: users/models/user.py:484 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/serializers/v1.py:28 +#: users/serializers/v1.py:29 msgid "Groups name" msgstr "用户组名" -#: users/serializers/v1.py:29 +#: users/serializers/v1.py:30 msgid "Source name" msgstr "用户来源名" -#: users/serializers/v1.py:30 +#: users/serializers/v1.py:31 msgid "Is first login" msgstr "首次登录" -#: users/serializers/v1.py:31 +#: users/serializers/v1.py:32 msgid "Role name" msgstr "角色名" -#: users/serializers/v1.py:32 +#: users/serializers/v1.py:33 msgid "Is valid" msgstr "账户是否有效" -#: users/serializers/v1.py:33 +#: users/serializers/v1.py:34 msgid "Is expired" msgstr " 是否过期" -#: users/serializers/v1.py:34 +#: users/serializers/v1.py:35 msgid "Avatar url" msgstr "头像路径" @@ -4576,7 +4618,7 @@ msgid "Import user groups" msgstr "导入用户组" #: users/templates/users/_user_groups_update_modal.html:4 -#: users/views/group.py:60 +#: users/views/group.py:63 msgid "Update user group" msgstr "更新用户组" @@ -4585,7 +4627,7 @@ msgid "Import users" msgstr "导入用户" #: users/templates/users/_user_update_modal.html:4 -#: users/templates/users/user_update.html:4 users/views/user.py:130 +#: users/templates/users/user_update.html:4 users/views/user.py:132 msgid "Update user" msgstr "更新用户" @@ -4658,7 +4700,7 @@ msgid "Always young, always with tears in my eyes. Stay foolish Stay hungry" msgstr "永远年轻,永远热泪盈眶 stay foolish stay hungry" #: users/templates/users/reset_password.html:46 -#: users/templates/users/user_detail.html:373 users/utils.py:98 +#: users/templates/users/user_detail.html:373 users/utils.py:88 msgid "Reset password" msgstr "重置密码" @@ -4723,12 +4765,12 @@ msgid "Very strong" msgstr "很强" #: users/templates/users/user_create.html:4 -#: users/templates/users/user_list.html:28 users/views/user.py:87 +#: users/templates/users/user_list.html:28 users/views/user.py:88 msgid "Create user" msgstr "创建用户" #: users/templates/users/user_detail.html:19 -#: users/templates/users/user_granted_asset.html:18 users/views/user.py:208 +#: users/templates/users/user_granted_asset.html:18 users/views/user.py:212 msgid "User detail" msgstr "用户详情" @@ -4831,7 +4873,7 @@ msgstr "重置用户MFA成功" #: users/templates/users/user_group_detail.html:22 #: users/templates/users/user_group_granted_asset.html:18 -#: users/views/group.py:76 +#: users/views/group.py:80 msgid "User group detail" msgstr "用户组详情" @@ -4840,7 +4882,7 @@ msgstr "用户组详情" msgid "Add user" msgstr "添加用户" -#: users/templates/users/user_group_list.html:28 users/views/group.py:44 +#: users/templates/users/user_group_list.html:28 users/views/group.py:46 msgid "Create user group" msgstr "创建用户组" @@ -4931,8 +4973,8 @@ msgstr "安装完成后点击下一步进入绑定页面(如已安装,直接 msgid "Administrator Settings force MFA login" msgstr "管理员设置强制使用MFA登录" -#: users/templates/users/user_profile.html:120 users/views/user.py:244 -#: users/views/user.py:298 +#: users/templates/users/user_profile.html:120 users/views/user.py:248 +#: users/views/user.py:303 msgid "User groups" msgstr "用户组" @@ -4982,7 +5024,7 @@ msgid "" "corresponding private key." msgstr "新的公钥已设置成功,请下载对应的私钥" -#: users/utils.py:38 +#: users/utils.py:28 #, python-format msgid "" "\n" @@ -5027,16 +5069,16 @@ msgstr "" "

\n" " " -#: users/utils.py:73 +#: users/utils.py:63 msgid "Create account successfully" msgstr "创建账户成功" -#: users/utils.py:77 +#: users/utils.py:67 #, python-format msgid "Hello %(name)s" msgstr "您好 %(name)s" -#: users/utils.py:100 +#: users/utils.py:90 #, python-format msgid "" "\n" @@ -5080,11 +5122,11 @@ msgstr "" "
\n" " " -#: users/utils.py:131 +#: users/utils.py:121 msgid "Security notice" msgstr "安全通知" -#: users/utils.py:133 +#: users/utils.py:123 #, python-format msgid "" "\n" @@ -5133,11 +5175,11 @@ msgstr "" "
\n" " " -#: users/utils.py:169 +#: users/utils.py:159 msgid "SSH Key Reset" msgstr "重置ssh密钥" -#: users/utils.py:171 +#: users/utils.py:161 #, python-format msgid "" "\n" @@ -5162,56 +5204,56 @@ msgstr "" "
\n" " " -#: users/utils.py:204 +#: users/utils.py:194 msgid "User not exist" msgstr "用户不存在" -#: users/utils.py:206 +#: users/utils.py:196 msgid "Disabled or expired" msgstr "禁用或失效" -#: users/utils.py:219 +#: users/utils.py:209 msgid "Password or SSH public key invalid" msgstr "密码或密钥不合法" -#: users/views/group.py:28 +#: users/views/group.py:29 msgid "User group list" msgstr "用户组列表" -#: users/views/group.py:92 +#: users/views/group.py:97 msgid "User group granted asset" msgstr "用户组授权资产" -#: users/views/login.py:44 +#: users/views/login.py:43 msgid "Email address invalid, please input again" msgstr "邮箱地址错误,重新输入" -#: users/views/login.py:60 +#: users/views/login.py:59 msgid "Send reset password message" msgstr "发送重置密码邮件" -#: users/views/login.py:61 +#: users/views/login.py:60 msgid "Send reset password mail success, login your mail box and follow it " msgstr "" "发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)" -#: users/views/login.py:74 +#: users/views/login.py:73 msgid "Reset password success" msgstr "重置密码成功" -#: users/views/login.py:75 +#: users/views/login.py:74 msgid "Reset password success, return to login page" msgstr "重置密码成功,返回到登录页面" -#: users/views/login.py:90 users/views/login.py:106 +#: users/views/login.py:89 users/views/login.py:105 msgid "Token invalid or expired" msgstr "Token错误或失效" -#: users/views/login.py:102 +#: users/views/login.py:101 msgid "Password not same" msgstr "密码不一致" -#: users/views/login.py:115 users/views/user.py:144 users/views/user.py:441 +#: users/views/login.py:114 users/views/user.py:146 users/views/user.py:449 msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" @@ -5219,51 +5261,51 @@ msgstr "* 您的密码不符合要求" msgid "First login" msgstr "首次登录" -#: users/views/user.py:161 +#: users/views/user.py:163 msgid "Bulk update user success" msgstr "批量更新用户成功" -#: users/views/user.py:188 +#: users/views/user.py:191 msgid "Bulk update user" msgstr "批量更新用户" -#: users/views/user.py:273 +#: users/views/user.py:278 msgid "Invalid file." msgstr "文件不合法" -#: users/views/user.py:369 +#: users/views/user.py:375 msgid "User granted assets" msgstr "用户授权资产" -#: users/views/user.py:400 +#: users/views/user.py:408 msgid "Profile setting" msgstr "个人信息设置" -#: users/views/user.py:419 +#: users/views/user.py:427 msgid "Password update" msgstr "密码更新" -#: users/views/user.py:459 +#: users/views/user.py:468 msgid "Public key update" msgstr "密钥更新" -#: users/views/user.py:500 +#: users/views/user.py:510 msgid "Password invalid" msgstr "用户名或密码无效" -#: users/views/user.py:600 +#: users/views/user.py:610 msgid "MFA enable success" msgstr "MFA 绑定成功" -#: users/views/user.py:601 +#: users/views/user.py:611 msgid "MFA enable success, return login page" msgstr "MFA 绑定成功,返回到登录页面" -#: users/views/user.py:603 +#: users/views/user.py:613 msgid "MFA disable success" msgstr "MFA 解绑成功" -#: users/views/user.py:604 +#: users/views/user.py:614 msgid "MFA disable success, return login page" msgstr "MFA 解绑成功,返回登录页面" @@ -5292,23 +5334,17 @@ msgstr "定时执行" #: xpack/plugins/change_auth_plan/forms.py:120 msgid "" -"Tips: Currently only unix-like assets are supported, while Windows assets " -"are not" -msgstr "提示:目前仅支持类Unix资产,暂不支持Windows资产" - -#: xpack/plugins/change_auth_plan/forms.py:122 -msgid "" "Tips: The username of the user on the asset to be modified. if the user " "exists, change the password; If the user does not exist, create the user." msgstr "" "提示:用户名为将要修改的资产上的用户的用户名。如果用户存在,则修改密码;如果" "用户不存在,则创建用户。" -#: xpack/plugins/change_auth_plan/forms.py:126 +#: xpack/plugins/change_auth_plan/forms.py:124 msgid "Tips: (Units: hour)" msgstr "提示:(单位: 时)" -#: xpack/plugins/change_auth_plan/forms.py:127 +#: xpack/plugins/change_auth_plan/forms.py:125 msgid "" "eg: Every Sunday 03:05 run <5 3 * * 0>
Tips: Using 5 digits linux " "crontab expressions (gfpixJ z34?`%M8rn;8<2Q^zCD**Uw7Z@-969U?I%tWnYUO$)+kNXdTQKf)`k&3Ka_J!;RWhH zuh@@27{cCwSs@N#5JxeF<2ZqHxP&Keyu83HO1}zA%`(>NKBTdPfe}<7Q`m?rsEzlW zr&vV)4vX;toADLP@E2z=N;C>^9?NkNr*R9}*lsb({`O2G!H0KLq~AEqDJ(F%j{Y&} z#qk=`_=H+GOcb)hS%VS!O*nwWU6+oyfvT(eFA7S9WxarEy`EV$>Wy%!DeuwnHmrimoAU0w3UwtadR)pfdYJVe q1xMF5S2uD!f%QPRucN1{xw~&L-IY5F&H6$g+4Ha6gPgBu&;JMKs5}q= delta 701 zcmXxi%`1da6vy#1FBoII%rJvtu&_ZA(=@SRLye?SvbSPqp=38(*+>&hkwn?ZQ&tof zHkALsij*wXl$Y}TJ>9u=`rP}>JolV?&vTmGPL^I0;XPyY5Zy#9U^alisBsuH8^j~* z#V5?;C)Q$r$gB>BFo9W|#Ti`1U97=xY(xv2H9-@yE?I_20}qB!1>{l1ETR&wJIfeh z?O{D$V;??XGrr>jR#1f%$l8KMEaEaM@g=H|J4}+_o|&l9H=N?s^Ss@~dWU1R;W=jU z7Pax0^9N(BgH$z+%{Yw()c\n" "Language-Team: LANGUAGE \n" @@ -17,58 +17,58 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: static/js/jumpserver.js:249 +#: static/js/jumpserver.js:263 msgid "Update is successful!" msgstr "更新成功" -#: static/js/jumpserver.js:251 +#: static/js/jumpserver.js:265 msgid "An unknown error occurred while updating.." msgstr "更新时发生未知错误" -#: static/js/jumpserver.js:315 static/js/jumpserver.js:352 -#: static/js/jumpserver.js:355 +#: static/js/jumpserver.js:329 static/js/jumpserver.js:366 +#: static/js/jumpserver.js:369 msgid "Error" msgstr "错误" -#: static/js/jumpserver.js:315 +#: static/js/jumpserver.js:329 msgid "Being used by the asset, please unbind the asset first." msgstr "正在被资产使用中,请先解除资产绑定" -#: static/js/jumpserver.js:321 static/js/jumpserver.js:362 +#: static/js/jumpserver.js:335 static/js/jumpserver.js:376 msgid "Delete the success" msgstr "删除成功" -#: static/js/jumpserver.js:327 +#: static/js/jumpserver.js:341 msgid "Are you sure about deleting it?" msgstr "你确定删除吗 ?" -#: static/js/jumpserver.js:331 static/js/jumpserver.js:372 +#: static/js/jumpserver.js:345 static/js/jumpserver.js:386 msgid "Cancel" msgstr "取消" -#: static/js/jumpserver.js:333 static/js/jumpserver.js:374 +#: static/js/jumpserver.js:347 static/js/jumpserver.js:388 msgid "Confirm" msgstr "确认" -#: static/js/jumpserver.js:352 +#: static/js/jumpserver.js:366 msgid "" "The organization contains undeleted information. Please try again after " "deleting" msgstr "组织中包含未删除信息,请删除后重试" -#: static/js/jumpserver.js:355 +#: static/js/jumpserver.js:369 msgid "" "Do not perform this operation under this organization. Try again after " "switching to another organization" msgstr "请勿在此组织下执行此操作,切换到其他组织后重试" -#: static/js/jumpserver.js:368 +#: static/js/jumpserver.js:382 msgid "" "Please ensure that the following information in the organization has been " "deleted" msgstr "请确保组织内的以下信息已删除" -#: static/js/jumpserver.js:369 +#: static/js/jumpserver.js:383 msgid "" "User list、User group、Asset list、Domain list、Admin user、System user、" "Labels、Asset permission" @@ -76,76 +76,80 @@ msgstr "" "用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权" "规则" -#: static/js/jumpserver.js:408 +#: static/js/jumpserver.js:422 msgid "Loading ..." msgstr "加载中 ..." -#: static/js/jumpserver.js:409 +#: static/js/jumpserver.js:423 msgid "Search" msgstr "搜索" -#: static/js/jumpserver.js:412 +#: static/js/jumpserver.js:426 #, javascript-format msgid "Selected item %d" msgstr "选中 %d 项" -#: static/js/jumpserver.js:416 +#: static/js/jumpserver.js:430 msgid "Per page _MENU_" msgstr "每页 _MENU_" -#: static/js/jumpserver.js:417 +#: static/js/jumpserver.js:431 msgid "" "Displays the results of items _START_ to _END_; A total of _TOTAL_ entries" msgstr "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项" -#: static/js/jumpserver.js:420 +#: static/js/jumpserver.js:434 msgid "No match" msgstr "没有匹配项" -#: static/js/jumpserver.js:421 +#: static/js/jumpserver.js:435 msgid "No record" msgstr "没有记录" -#: static/js/jumpserver.js:563 +#: static/js/jumpserver.js:577 msgid "Unknown error occur" msgstr "" -#: static/js/jumpserver.js:800 +#: static/js/jumpserver.js:816 msgid "Password minimum length {N} bits" msgstr "密码最小长度 {N} 位" -#: static/js/jumpserver.js:801 +#: static/js/jumpserver.js:817 msgid "Must contain capital letters" msgstr "必须包含大写字母" -#: static/js/jumpserver.js:802 +#: static/js/jumpserver.js:818 msgid "Must contain lowercase letters" msgstr "必须包含小写字母" -#: static/js/jumpserver.js:803 +#: static/js/jumpserver.js:819 msgid "Must contain numeric characters" msgstr "必须包含数字字符" -#: static/js/jumpserver.js:804 +#: static/js/jumpserver.js:820 msgid "Must contain special characters" msgstr "必须包含特殊字符" -#: static/js/jumpserver.js:976 +#: static/js/jumpserver.js:995 msgid "Export failed" msgstr "导出失败" -#: static/js/jumpserver.js:993 +#: static/js/jumpserver.js:1012 msgid "Import Success" msgstr "导入成功" -#: static/js/jumpserver.js:998 +#: static/js/jumpserver.js:1017 msgid "Update Success" msgstr "更新成功" -#: static/js/jumpserver.js:1028 +#: static/js/jumpserver.js:1018 +msgid "Count" +msgstr "数量" + +#: static/js/jumpserver.js:1047 msgid "Import failed" msgstr "导入失败" -#: static/js/jumpserver.js:1033 +#: static/js/jumpserver.js:1052 msgid "Update failed" msgstr "更新失败" diff --git a/apps/ops/inventory.py b/apps/ops/inventory.py index 002b6fbda..ed3b9a057 100644 --- a/apps/ops/inventory.py +++ b/apps/ops/inventory.py @@ -21,7 +21,7 @@ class JMSBaseInventory(BaseInventory): 'id': asset.id, 'hostname': asset.hostname, 'ip': asset.ip, - 'port': asset.port, + 'port': asset.ssh_port, 'vars': dict(), 'groups': [], } @@ -29,8 +29,15 @@ class JMSBaseInventory(BaseInventory): info["vars"].update(self.make_proxy_command(asset)) if run_as_admin: info.update(asset.get_auth_info()) + if asset.is_unixlike(): + info["become"] = asset.admin_user.become_info for node in asset.nodes.all(): info["groups"].append(node.value) + if asset.is_windows(): + info["vars"].update({ + "ansible_connection": "ssh", + "ansible_shell_type": "cmd", + }) for label in asset.labels.all(): info["vars"].update({ label.name: label.value @@ -73,7 +80,7 @@ class JMSInventory(JMSBaseInventory): """ def __init__(self, assets, run_as_admin=False, run_as=None, become_info=None): """ - :param host_id_list: ["test1", ] + :param assets: assets :param run_as_admin: True 是否使用管理用户去执行, 每台服务器的管理用户可能不同 :param run_as: 用户名(添加了统一的资产用户管理器之后AssetUserManager加上之后修改为username) :param become_info: 是否become成某个用户去执行 @@ -86,28 +93,26 @@ class JMSInventory(JMSBaseInventory): host_list = [] for asset in assets: - info = self.convert_to_ansible(asset, run_as_admin=run_as_admin) - host_list.append(info) - - if run_as: - for host in host_list: + host = self.convert_to_ansible(asset, run_as_admin=run_as_admin) + if run_as: run_user_info = self.get_run_user_info(host) host.update(run_user_info) - - if become_info: - for host in host_list: + if become_info and asset.is_unixlike(): host.update(become_info) + host_list.append(host) + super().__init__(host_list=host_list) def get_run_user_info(self, host): - from assets.backends.multi import AssetUserManager + from assets.backends import AssetUserManager if not self.run_as: return {} try: asset = self.assets.get(id=host.get('id')) - run_user = AssetUserManager.get(self.run_as, asset) + manager = AssetUserManager() + run_user = manager.get(self.run_as, asset) except Exception as e: logger.error(e, exc_info=True) return {} @@ -133,12 +138,10 @@ class JMSCustomInventory(JMSBaseInventory): host_list = [] for asset in assets: - info = self.convert_to_ansible(asset) - host_list.append(info) - - for host in host_list: + host = self.convert_to_ansible(asset) run_user_info = self.get_run_user_info() host.update(run_user_info) + host_list.append(host) super().__init__(host_list=host_list) diff --git a/apps/ops/models/adhoc.py b/apps/ops/models/adhoc.py index 2f1adc355..1f76283c7 100644 --- a/apps/ops/models/adhoc.py +++ b/apps/ops/models/adhoc.py @@ -220,6 +220,7 @@ class AdHoc(models.Model): time_start = time.time() try: date_start = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + history.date_start = date_start print(_("{} Start task: {}").format(date_start, self.task.name)) raw, summary = self._run_only() date_end = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index aef8ea055..a8afee59b 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -1,6 +1,7 @@ # coding: utf-8 import os import subprocess +import datetime from django.conf import settings from celery import shared_task, subtask @@ -104,6 +105,13 @@ def hello(name, callback=None): print("Hello {}".format(name)) +@shared_task +# @after_app_shutdown_clean_periodic +# @register_as_period_task(interval=30) +def hello123(): + print("{} Hello world".format(datetime.datetime.now().strftime("%H:%M:%S"))) + + @shared_task def hello_callback(result): print(result) diff --git a/apps/ops/templates/ops/command_execution_create.html b/apps/ops/templates/ops/command_execution_create.html index 4aaee0406..230b4e6a1 100644 --- a/apps/ops/templates/ops/command_execution_create.html +++ b/apps/ops/templates/ops/command_execution_create.html @@ -134,8 +134,11 @@ function getSelectedAssetsNode() { var assetsNodeId = []; var assetsNode = []; nodes.forEach(function (node) { - if (node.meta.type === 'asset' && !node.isHidden && node.meta.asset.protocol === 'ssh') { - if (assetsNodeId.indexOf(node.id) === -1) { + if (node.meta.type === 'asset' && !node.isHidden) { + var protocols = $.map(node.meta.asset.protocols, function (v) { + return v.name + }); + if (assetsNodeId.indexOf(node.id) === -1 && protocols.indexOf("ssh") > -1) { assetsNodeId.push(node.id); assetsNode.push(node) } diff --git a/apps/ops/utils.py b/apps/ops/utils.py index 4a7def4fa..70cba27ba 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -18,7 +18,7 @@ def update_or_create_ansible_task( run_as_admin=False, run_as=None, become_info=None, ): if not hosts or not tasks or not task_name: - return + return None, None set_to_root_org() defaults = { 'name': task_name, diff --git a/apps/ops/views/adhoc.py b/apps/ops/views/adhoc.py index 70b9b3f45..06a9f1932 100644 --- a/apps/ops/views/adhoc.py +++ b/apps/ops/views/adhoc.py @@ -5,7 +5,7 @@ from django.conf import settings from django.views.generic import ListView, DetailView from common.mixins import DatetimeSearchMixin -from common.permissions import AdminUserRequiredMixin +from common.permissions import PermissionsMixin, IsOrgAdmin from orgs.utils import current_org from ..models import Task, AdHoc, AdHocRunHistory @@ -17,13 +17,14 @@ __all__ = [ ] -class TaskListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): +class TaskListView(PermissionsMixin, DatetimeSearchMixin, ListView): paginate_by = settings.DISPLAY_PER_PAGE model = Task ordering = ('-date_created',) context_object_name = 'task_list' template_name = 'ops/task_list.html' keyword = '' + permission_classes = [IsOrgAdmin] def get_queryset(self): queryset = super().get_queryset() @@ -51,9 +52,10 @@ class TaskListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): return super().get_context_data(**kwargs) -class TaskDetailView(AdminUserRequiredMixin, DetailView): +class TaskDetailView(PermissionsMixin, DetailView): model = Task template_name = 'ops/task_detail.html' + permission_classes = [IsOrgAdmin] def get_queryset(self): queryset = super().get_queryset() @@ -73,9 +75,10 @@ class TaskDetailView(AdminUserRequiredMixin, DetailView): return super().get_context_data(**kwargs) -class TaskAdhocView(AdminUserRequiredMixin, DetailView): +class TaskAdhocView(PermissionsMixin, DetailView): model = Task template_name = 'ops/task_adhoc.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -86,9 +89,10 @@ class TaskAdhocView(AdminUserRequiredMixin, DetailView): return super().get_context_data(**kwargs) -class TaskHistoryView(AdminUserRequiredMixin, DetailView): +class TaskHistoryView(PermissionsMixin, DetailView): model = Task template_name = 'ops/task_history.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -99,9 +103,10 @@ class TaskHistoryView(AdminUserRequiredMixin, DetailView): return super().get_context_data(**kwargs) -class AdHocDetailView(AdminUserRequiredMixin, DetailView): +class AdHocDetailView(PermissionsMixin, DetailView): model = AdHoc template_name = 'ops/adhoc_detail.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -112,9 +117,10 @@ class AdHocDetailView(AdminUserRequiredMixin, DetailView): return super().get_context_data(**kwargs) -class AdHocHistoryView(AdminUserRequiredMixin, DetailView): +class AdHocHistoryView(PermissionsMixin, DetailView): model = AdHoc template_name = 'ops/adhoc_history.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -125,9 +131,10 @@ class AdHocHistoryView(AdminUserRequiredMixin, DetailView): return super().get_context_data(**kwargs) -class AdHocHistoryDetailView(AdminUserRequiredMixin, DetailView): +class AdHocHistoryDetailView(PermissionsMixin, DetailView): model = AdHocRunHistory template_name = 'ops/adhoc_history_detail.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { diff --git a/apps/ops/views/celery.py b/apps/ops/views/celery.py index e549f62c0..f2f0124bd 100644 --- a/apps/ops/views/celery.py +++ b/apps/ops/views/celery.py @@ -2,14 +2,15 @@ # from django.views.generic import TemplateView -from common.permissions import AdminUserRequiredMixin +from common.permissions import PermissionsMixin, IsOrgAdmin, IsAuditor __all__ = ['CeleryTaskLogView'] -class CeleryTaskLogView(AdminUserRequiredMixin, TemplateView): +class CeleryTaskLogView(PermissionsMixin, TemplateView): template_name = 'ops/celery_task_log.html' + permission_classes = [IsOrgAdmin | IsAuditor] def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) diff --git a/apps/ops/views/command.py b/apps/ops/views/command.py index 3846aa766..6275f0f3f 100644 --- a/apps/ops/views/command.py +++ b/apps/ops/views/command.py @@ -5,7 +5,9 @@ from django.utils.translation import ugettext as _ from django.conf import settings from django.views.generic import ListView, TemplateView -from common.permissions import AdminUserRequiredMixin, LoginRequiredMixin +from common.permissions import ( + PermissionsMixin, IsOrgAdmin, IsAuditor, IsValidUser +) from common.mixins import DatetimeSearchMixin from ..models import CommandExecution from ..forms import CommandExecutionForm @@ -16,13 +18,14 @@ __all__ = [ ] -class CommandExecutionListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): +class CommandExecutionListView(PermissionsMixin, DatetimeSearchMixin, ListView): template_name = 'ops/command_execution_list.html' model = CommandExecution paginate_by = settings.DISPLAY_PER_PAGE ordering = ('-date_created',) context_object_name = 'task_list' keyword = '' + permission_classes = [IsOrgAdmin | IsAuditor] def _get_queryset(self): self.keyword = self.request.GET.get('keyword', '') @@ -51,9 +54,10 @@ class CommandExecutionListView(AdminUserRequiredMixin, DatetimeSearchMixin, List return super().get_context_data(**kwargs) -class CommandExecutionStartView(LoginRequiredMixin, TemplateView): +class CommandExecutionStartView(PermissionsMixin, TemplateView): template_name = 'ops/command_execution_create.html' form_class = CommandExecutionForm + permission_classes = [IsValidUser] def get_user_system_users(self): from perms.utils import AssetPermissionUtil diff --git a/apps/orgs/context_processor.py b/apps/orgs/context_processor.py index 8a9111d05..493031bc0 100644 --- a/apps/orgs/context_processor.py +++ b/apps/orgs/context_processor.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- # -from .utils import current_org, get_current_org +from .utils import current_org, get_org_from_request from .models import Organization def org_processor(request): context = { 'ADMIN_ORGS': Organization.get_user_admin_orgs(request.user), - 'CURRENT_ORG': get_current_org(), + 'CURRENT_ORG': get_org_from_request(request), 'HAS_ORG_PERM': current_org.can_admin_by(request.user), } return context diff --git a/apps/orgs/middleware.py b/apps/orgs/middleware.py index 04a2d6f7a..cda25bedc 100644 --- a/apps/orgs/middleware.py +++ b/apps/orgs/middleware.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # +from .models import Organization from .utils import get_org_from_request, set_current_org @@ -8,7 +9,21 @@ class OrgMiddleware: def __init__(self, get_response): self.get_response = get_response + @staticmethod + def set_permed_org_if_need(request): + if request.path.startswith('/api'): + return + if not (request.user.is_authenticated and request.user.is_org_admin): + return + org = get_org_from_request(request) + if org.can_admin_by(request.user): + return + admin_orgs = Organization.get_user_admin_orgs(request.user) + if admin_orgs: + request.session['oid'] = str(admin_orgs[0].id) + def __call__(self, request): + self.set_permed_org_if_need(request) org = get_org_from_request(request) request.current_org = org set_current_org(org) diff --git a/apps/orgs/mixins.py b/apps/orgs/mixins.py index f41ffccbe..e683eb90e 100644 --- a/apps/orgs/mixins.py +++ b/apps/orgs/mixins.py @@ -9,8 +9,11 @@ from django.forms import ModelForm from django.http.response import HttpResponseForbidden from django.core.exceptions import ValidationError from rest_framework import serializers +from rest_framework.validators import UniqueTogetherValidator from common.utils import get_logger +from common.validators import ProjectUniqueValidator +from common.mixins import BulkSerializerMixin from .utils import ( current_org, set_current_org, set_to_root_org, get_current_org_id ) @@ -23,6 +26,7 @@ __all__ = [ 'OrgManager', 'OrgViewGenericMixin', 'OrgModelMixin', 'OrgModelForm', 'RootOrgViewMixin', 'OrgMembershipSerializerMixin', 'OrgMembershipModelViewSetMixin', 'OrgResourceSerializerMixin', + 'BulkOrgResourceSerializerMixin', 'BulkOrgResourceModelSerializer', ] @@ -215,4 +219,29 @@ class OrgResourceSerializerMixin(serializers.Serializer): 由于HiddenField字段不可读,API获取资产信息时获取不到org_id, 但是coco需要资产的org_id字段,所以修改为CharField类型 """ - org_id = serializers.CharField(default=get_current_org_id) + org_id = serializers.ReadOnlyField(default=get_current_org_id, label=_("Organization")) + org_name = serializers.ReadOnlyField(label=_("Org name")) + + def get_validators(self): + _validators = super().get_validators() + validators = [] + + for v in _validators: + if isinstance(v, UniqueTogetherValidator) \ + and "org_id" in v.fields: + v = ProjectUniqueValidator(v.queryset, v.fields) + validators.append(v) + return validators + + def get_field_names(self, declared_fields, info): + fields = super().get_field_names(declared_fields, info) + fields.extend(["org_id", "org_name"]) + return fields + + +class BulkOrgResourceSerializerMixin(BulkSerializerMixin, OrgResourceSerializerMixin): + pass + + +class BulkOrgResourceModelSerializer(BulkOrgResourceSerializerMixin, serializers.ModelSerializer): + pass diff --git a/apps/orgs/models.py b/apps/orgs/models.py index d7373339b..e00270f72 100644 --- a/apps/orgs/models.py +++ b/apps/orgs/models.py @@ -96,7 +96,7 @@ class Organization(models.Model): admin_orgs = [] if user.is_anonymous: return admin_orgs - elif user.is_superuser: + elif user.is_superuser or user.is_auditor: admin_orgs = list(cls.objects.all()) admin_orgs.append(cls.default()) elif user.is_org_admin: diff --git a/apps/perms/api/user_permission.py b/apps/perms/api/user_permission.py index c4e86f4bd..8cf5dc3f3 100644 --- a/apps/perms/api/user_permission.py +++ b/apps/perms/api/user_permission.py @@ -14,7 +14,6 @@ from rest_framework.pagination import LimitOffsetPagination from common.permissions import IsValidUser, IsOrgAdminOrAppUser from common.tree import TreeNodeSerializer from common.utils import get_logger -from orgs.utils import set_to_root_org from ..utils import ( AssetPermissionUtil, parse_asset_to_tree_node, parse_node_to_tree_node, check_system_user_action, RemoteAppPermissionUtil, @@ -26,7 +25,7 @@ from ..hands import ( ) from .. import serializers, const from ..mixins import ( - AssetsFilterMixin, RemoteAppFilterMixin, ChangeOrgIfNeedMixin + AssetsFilterMixin, RemoteAppFilterMixin ) from ..models import Action @@ -48,14 +47,6 @@ class UserPermissionCacheMixin: CACHE_TIME = settings.ASSETS_PERM_CACHE_TIME _object = None - @staticmethod - def change_org_if_need(request, kwargs): - if request.user.is_authenticated and \ - request.user.is_superuser or \ - request.user.is_app or \ - kwargs.get('pk') is None: - set_to_root_org() - def get_object(self): return None @@ -115,7 +106,6 @@ class UserPermissionCacheMixin: cache.set(key, response.data, self.CACHE_TIME) def get(self, request, *args, **kwargs): - self.change_org_if_need(request, kwargs) self.cache_policy = request.GET.get('cache_policy', '0') obj = self._get_object() @@ -156,7 +146,7 @@ class UserGrantedAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, ListAPIV util = AssetPermissionUtil(user, cache_policy=self.cache_policy) assets = util.get_assets() for k, v in assets.items(): - system_users_granted = [s for s in v if s.protocol == k.protocol] + system_users_granted = [s for s in v if k.has_protocol(s.protocol)] k.system_users_granted = system_users_granted queryset.append(k) return queryset @@ -217,8 +207,7 @@ class UserGrantedNodesWithAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, for node, _assets in nodes.items(): assets = _assets.keys() for k, v in _assets.items(): - system_users_granted = [s for s in v if - s.protocol == k.protocol] + system_users_granted = [s for s in v if k.has_protocol(s.protocol)] k.system_users_granted = system_users_granted node.assets_granted = assets queryset.append(node) @@ -366,7 +355,7 @@ class UserGrantedNodeChildrenApi(UserPermissionCacheMixin, ListAPIView): for asset, system_users in nodes_granted[node].items(): fake_node = asset.as_node() fake_node.assets_amount = 0 - system_users = [s for s in system_users if s.protocol == asset.protocol] + system_users = [s for s in system_users if asset.has_protocol(s.protocol)] fake_node.asset.system_users_granted = system_users fake_node.key = node.key + ':0' fake_nodes.append(fake_node) @@ -391,7 +380,7 @@ class UserGrantedNodeChildrenApi(UserPermissionCacheMixin, ListAPIView): fake_node = asset.as_node() fake_node.assets_amount = 0 system_users = [s for s in system_users if - s.protocol == asset.protocol] + asset.has_protocol(s.protocol)] fake_node.asset.system_users_granted = system_users fake_node.key = node.key + ':0' matched_assets.append(fake_node) @@ -462,7 +451,7 @@ class GetUserAssetPermissionActionsApi(UserPermissionCacheMixin, APIView): # RemoteApp permission -class UserGrantedRemoteAppsApi(ChangeOrgIfNeedMixin, RemoteAppFilterMixin, ListAPIView): +class UserGrantedRemoteAppsApi(RemoteAppFilterMixin, ListAPIView): permission_classes = (IsOrgAdminOrAppUser,) serializer_class = RemoteAppSerializer pagination_class = LimitOffsetPagination @@ -487,7 +476,7 @@ class UserGrantedRemoteAppsApi(ChangeOrgIfNeedMixin, RemoteAppFilterMixin, ListA return super().get_permissions() -class UserGrantedRemoteAppsAsTreeApi(ChangeOrgIfNeedMixin, ListAPIView): +class UserGrantedRemoteAppsAsTreeApi(ListAPIView): serializer_class = TreeNodeSerializer permission_classes = (IsOrgAdminOrAppUser,) @@ -519,7 +508,7 @@ class UserGrantedRemoteAppsAsTreeApi(ChangeOrgIfNeedMixin, ListAPIView): return super().get_permissions() -class ValidateUserRemoteAppPermissionApi(ChangeOrgIfNeedMixin, APIView): +class ValidateUserRemoteAppPermissionApi(APIView): permission_classes = (IsOrgAdminOrAppUser,) def get(self, request, *args, **kwargs): @@ -533,5 +522,4 @@ class ValidateUserRemoteAppPermissionApi(ChangeOrgIfNeedMixin, APIView): remote_apps = util.get_remote_apps() if remote_app not in remote_apps: return Response({'msg': False}, status=403) - return Response({'msg': True}, status=200) diff --git a/apps/perms/hands.py b/apps/perms/hands.py index 2a208fefa..1195abf35 100644 --- a/apps/perms/hands.py +++ b/apps/perms/hands.py @@ -1,13 +1,13 @@ # ~*~ coding: utf-8 ~*~ # -from common.permissions import AdminUserRequiredMixin from users.models import User, UserGroup -from assets.models import Asset, SystemUser, Node, RemoteApp +from assets.models import Asset, SystemUser, Node from assets.serializers import ( AssetGrantedSerializer, NodeSerializer ) from applications.serializers import RemoteAppSerializer +from applications.models import RemoteApp diff --git a/apps/perms/serializers/asset_permission.py b/apps/perms/serializers/asset_permission.py index a1eec6710..842c719e8 100644 --- a/apps/perms/serializers/asset_permission.py +++ b/apps/perms/serializers/asset_permission.py @@ -4,15 +4,15 @@ from rest_framework import serializers from common.fields import StringManyToManyField +from orgs.mixins import BulkOrgResourceModelSerializer from perms.models import AssetPermission, Action -from assets.models import Node, Asset, SystemUser +from assets.models import Node from assets.serializers import AssetGrantedSerializer __all__ = [ 'AssetPermissionCreateUpdateSerializer', 'AssetPermissionListSerializer', 'AssetPermissionUpdateUserSerializer', 'AssetPermissionUpdateAssetSerializer', 'AssetPermissionNodeSerializer', 'GrantedNodeSerializer', - 'GrantedAssetSerializer', 'GrantedSystemUserSerializer', 'ActionSerializer', 'NodeGrantedSerializer', ] @@ -23,13 +23,13 @@ class ActionSerializer(serializers.ModelSerializer): fields = '__all__' -class AssetPermissionCreateUpdateSerializer(serializers.ModelSerializer): +class AssetPermissionCreateUpdateSerializer(BulkOrgResourceModelSerializer): class Meta: model = AssetPermission exclude = ('created_by', 'date_created') -class AssetPermissionListSerializer(serializers.ModelSerializer): +class AssetPermissionListSerializer(BulkOrgResourceModelSerializer): users = StringManyToManyField(many=True, read_only=True) user_groups = StringManyToManyField(many=True, read_only=True) assets = StringManyToManyField(many=True, read_only=True) @@ -122,19 +122,21 @@ class GrantedNodeSerializer(serializers.ModelSerializer): ] -class GrantedAssetSerializer(serializers.ModelSerializer): - class Meta: - model = Asset - fields = [ - 'id', 'hostname', 'ip', 'port', 'protocol', 'platform', - 'domain', 'is_active', 'comment' - ] +# class GrantedAssetSerializer(serializers.ModelSerializer): +# protocols = ProtocolSerializer(many=True) +# +# class Meta: +# model = Asset +# fields = [ +# 'id', 'hostname', 'ip', 'protocols', 'port', 'protocol', +# 'platform', 'domain', 'is_active', 'comment' +# ] -class GrantedSystemUserSerializer(serializers.ModelSerializer): - class Meta: - model = SystemUser - fields = [ - 'id', 'name', 'username', 'protocol', 'priority', - 'login_mode', 'comment' - ] +# class GrantedSystemUserSerializer(serializers.ModelSerializer): +# class Meta: +# model = SystemUser +# fields = [ +# 'id', 'name', 'username', 'protocol', 'priority', +# 'login_mode', 'comment' +# ] diff --git a/apps/perms/serializers/remote_app_permission.py b/apps/perms/serializers/remote_app_permission.py index bb95d910c..727076e77 100644 --- a/apps/perms/serializers/remote_app_permission.py +++ b/apps/perms/serializers/remote_app_permission.py @@ -3,6 +3,8 @@ from rest_framework import serializers +from common.serializers import AdaptedBulkListSerializer +from orgs.mixins import BulkOrgResourceModelSerializer from ..models import RemoteAppPermission @@ -13,13 +15,14 @@ __all__ = [ ] -class RemoteAppPermissionSerializer(serializers.ModelSerializer): +class RemoteAppPermissionSerializer(BulkOrgResourceModelSerializer): class Meta: model = RemoteAppPermission + list_serializer_class = AdaptedBulkListSerializer fields = [ 'id', 'name', 'users', 'user_groups', 'remote_apps', 'comment', 'is_active', 'date_start', 'date_expired', 'is_valid', - 'created_by', 'date_created', 'org_id' + 'created_by', 'date_created', ] read_only_fields = ['created_by', 'date_created'] diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py index ed9f8f1a3..4fbb136d7 100644 --- a/apps/perms/utils/asset_permission.py +++ b/apps/perms/utils/asset_permission.py @@ -11,6 +11,7 @@ from django.core.cache import cache from django.conf import settings from django.utils.translation import ugettext as _ +from orgs.utils import set_to_root_org from common.utils import get_logger from common.tree import TreeNode from .. import const @@ -162,6 +163,11 @@ class AssetPermissionUtil: self._filter_id = 'None' # 当通过filter更改 permission是标记 self.cache_policy = cache_policy self.tree = GenerateTree() + self.change_org_if_need() + + @staticmethod + def change_org_if_need(): + set_to_root_org() @classmethod def is_not_using_cache(cls, cache_policy): @@ -238,7 +244,7 @@ class AssetPermissionUtil: for perm in permissions: actions = perm.actions.all() for asset in perm.assets.all().valid().prefetch_related('nodes'): - system_users = perm.system_users.filter(protocol=asset.protocol) + system_users = perm.system_users.filter(protocol__in=asset.protocols_name) system_users = self._structured_system_user(system_users, actions) assets[asset].update(system_users) return assets @@ -255,7 +261,7 @@ class AssetPermissionUtil: _assets = node.get_all_assets().valid().prefetch_related('nodes') for asset in _assets: for system_user, attr_dict in system_users.items(): - if system_user.protocol != asset.protocol: + if not asset.has_protocol(system_user.protocol): continue if system_user in assets[asset]: actions = assets[asset][system_user]['actions'] @@ -279,15 +285,12 @@ class AssetPermissionUtil: resource=resource ) - @property def node_key(self): return self.get_cache_key('NODES_WITH_ASSETS') - @property def asset_key(self): return self.get_cache_key('ASSETS') - @property def system_key(self): return self.get_cache_key('SYSTEM_USER') @@ -457,7 +460,7 @@ def parse_node_to_tree_node(node): def parse_asset_to_tree_node(node, asset, system_users): - system_users_protocol_matched = [s for s in system_users if s.protocol == asset.protocol] + system_users_protocol_matched = [s for s in system_users if asset.has_protocol(s.protocol)] icon_skin = 'file' if asset.platform.lower() == 'windows': icon_skin = 'windows' @@ -490,8 +493,8 @@ def parse_asset_to_tree_node(node, asset, system_users): 'id': asset.id, 'hostname': asset.hostname, 'ip': asset.ip, - 'port': asset.port, - 'protocol': asset.protocol, + 'protocols': [{"name": p.name, "port": p.port} + for p in asset.protocols.all()], 'platform': asset.platform, 'domain': None if not asset.domain else asset.domain.id, 'is_active': asset.is_active, diff --git a/apps/perms/utils/remote_app_permission.py b/apps/perms/utils/remote_app_permission.py index 0f67e32dc..a612b9ffb 100644 --- a/apps/perms/utils/remote_app_permission.py +++ b/apps/perms/utils/remote_app_permission.py @@ -4,6 +4,7 @@ from django.db.models import Q from common.tree import TreeNode +from orgs.utils import set_to_root_org from ..models import RemoteAppPermission @@ -38,6 +39,11 @@ class RemoteAppPermissionUtil: def __init__(self, obj): self.object = obj + self.change_org_if_need() + + @staticmethod + def change_org_if_need(): + set_to_root_org() @property def permissions(self): diff --git a/apps/perms/views/asset_permission.py b/apps/perms/views/asset_permission.py index e85acaf39..26ecfc9f2 100644 --- a/apps/perms/views/asset_permission.py +++ b/apps/perms/views/asset_permission.py @@ -8,7 +8,7 @@ from django.views.generic.edit import DeleteView, SingleObjectMixin from django.urls import reverse_lazy from django.conf import settings -from common.permissions import AdminUserRequiredMixin +from common.permissions import PermissionsMixin, IsOrgAdmin from orgs.utils import current_org from perms.hands import Node, Asset, SystemUser, User, UserGroup from perms.models import AssetPermission, Action @@ -25,8 +25,9 @@ __all__ = [ ] -class AssetPermissionListView(AdminUserRequiredMixin, TemplateView): +class AssetPermissionListView(PermissionsMixin, TemplateView): template_name = 'perms/asset_permission_list.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -37,11 +38,12 @@ class AssetPermissionListView(AdminUserRequiredMixin, TemplateView): return super().get_context_data(**kwargs) -class AssetPermissionCreateView(AdminUserRequiredMixin, CreateView): +class AssetPermissionCreateView(PermissionsMixin, CreateView): model = AssetPermission form_class = AssetPermissionForm template_name = 'perms/asset_permission_create_update.html' success_url = reverse_lazy('perms:asset-permission-list') + permission_classes = [IsOrgAdmin] def get_form(self, form_class=None): form = super().get_form(form_class=form_class) @@ -69,11 +71,12 @@ class AssetPermissionCreateView(AdminUserRequiredMixin, CreateView): return super().get_context_data(**kwargs) -class AssetPermissionUpdateView(AdminUserRequiredMixin, UpdateView): +class AssetPermissionUpdateView(PermissionsMixin, UpdateView): model = AssetPermission form_class = AssetPermissionForm template_name = 'perms/asset_permission_create_update.html' success_url = reverse_lazy("perms:asset-permission-list") + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -84,11 +87,12 @@ class AssetPermissionUpdateView(AdminUserRequiredMixin, UpdateView): return super().get_context_data(**kwargs) -class AssetPermissionDetailView(AdminUserRequiredMixin, DetailView): +class AssetPermissionDetailView(PermissionsMixin, DetailView): model = AssetPermission form_class = AssetPermissionForm template_name = 'perms/asset_permission_detail.html' success_url = reverse_lazy("perms:asset-permission-list") + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -102,19 +106,21 @@ class AssetPermissionDetailView(AdminUserRequiredMixin, DetailView): return super().get_context_data(**kwargs) -class AssetPermissionDeleteView(AdminUserRequiredMixin, DeleteView): +class AssetPermissionDeleteView(PermissionsMixin, DeleteView): model = AssetPermission template_name = 'delete_confirm.html' success_url = reverse_lazy('perms:asset-permission-list') + permission_classes = [IsOrgAdmin] -class AssetPermissionUserView(AdminUserRequiredMixin, +class AssetPermissionUserView(PermissionsMixin, SingleObjectMixin, ListView): template_name = 'perms/asset_permission_user.html' context_object_name = 'asset_permission' paginate_by = settings.DISPLAY_PER_PAGE object = None + permission_classes = [IsOrgAdmin] def get(self, request, *args, **kwargs): self.object = self.get_object(queryset=AssetPermission.objects.all()) @@ -140,13 +146,14 @@ class AssetPermissionUserView(AdminUserRequiredMixin, return super().get_context_data(**kwargs) -class AssetPermissionAssetView(AdminUserRequiredMixin, +class AssetPermissionAssetView(PermissionsMixin, SingleObjectMixin, ListView): template_name = 'perms/asset_permission_asset.html' context_object_name = 'asset_permission' paginate_by = settings.DISPLAY_PER_PAGE object = None + permission_classes = [IsOrgAdmin] def get(self, request, *args, **kwargs): self.object = self.get_object(queryset = AssetPermission.objects.all()) diff --git a/apps/perms/views/remote_app_permission.py b/apps/perms/views/remote_app_permission.py index 660d43ccd..3e8a6bff4 100644 --- a/apps/perms/views/remote_app_permission.py +++ b/apps/perms/views/remote_app_permission.py @@ -9,11 +9,10 @@ from django.views.generic import ( from django.views.generic.edit import SingleObjectMixin from django.conf import settings -from common.permissions import AdminUserRequiredMixin +from common.permissions import PermissionsMixin, IsOrgAdmin from orgs.utils import current_org -from users.models import UserGroup -from assets.models import RemoteApp +from ..hands import RemoteApp, UserGroup from ..models import RemoteAppPermission from ..forms import RemoteAppPermissionCreateUpdateForm @@ -25,8 +24,9 @@ __all__ = [ ] -class RemoteAppPermissionListView(AdminUserRequiredMixin, TemplateView): +class RemoteAppPermissionListView(PermissionsMixin, TemplateView): template_name = 'perms/remote_app_permission_list.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -37,11 +37,12 @@ class RemoteAppPermissionListView(AdminUserRequiredMixin, TemplateView): return super().get_context_data(**kwargs) -class RemoteAppPermissionCreateView(AdminUserRequiredMixin, CreateView): +class RemoteAppPermissionCreateView(PermissionsMixin, CreateView): template_name = 'perms/remote_app_permission_create_update.html' model = RemoteAppPermission form_class = RemoteAppPermissionCreateUpdateForm success_url = reverse_lazy('perms:remote-app-permission-list') + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -52,11 +53,12 @@ class RemoteAppPermissionCreateView(AdminUserRequiredMixin, CreateView): return super().get_context_data(**kwargs) -class RemoteAppPermissionUpdateView(AdminUserRequiredMixin, UpdateView): +class RemoteAppPermissionUpdateView(PermissionsMixin, UpdateView): template_name = 'perms/remote_app_permission_create_update.html' model = RemoteAppPermission form_class = RemoteAppPermissionCreateUpdateForm success_url = reverse_lazy('perms:remote-app-permission-list') + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -67,9 +69,10 @@ class RemoteAppPermissionUpdateView(AdminUserRequiredMixin, UpdateView): return super().get_context_data(**kwargs) -class RemoteAppPermissionDetailView(AdminUserRequiredMixin, DetailView): +class RemoteAppPermissionDetailView(PermissionsMixin, DetailView): template_name = 'perms/remote_app_permission_detail.html' model = RemoteAppPermission + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { @@ -80,13 +83,14 @@ class RemoteAppPermissionDetailView(AdminUserRequiredMixin, DetailView): return super().get_context_data(**kwargs) -class RemoteAppPermissionUserView(AdminUserRequiredMixin, +class RemoteAppPermissionUserView(PermissionsMixin, SingleObjectMixin, ListView): template_name = 'perms/remote_app_permission_user.html' context_object_name = 'remote_app_permission' paginate_by = settings.DISPLAY_PER_PAGE object = None + permission_classes = [IsOrgAdmin] def get(self, request, *args, **kwargs): self.object = self.get_object( @@ -112,13 +116,14 @@ class RemoteAppPermissionUserView(AdminUserRequiredMixin, return super().get_context_data(**kwargs) -class RemoteAppPermissionRemoteAppView(AdminUserRequiredMixin, +class RemoteAppPermissionRemoteAppView(PermissionsMixin, SingleObjectMixin, ListView): template_name = 'perms/remote_app_permission_remote_app.html' context_object_name = 'remote_app_permission' paginate_by = settings.DISPLAY_PER_PAGE object = None + permission_classes = [IsOrgAdmin] def get(self, request, *args, **kwargs): self.object = self.get_object( diff --git a/apps/settings/views.py b/apps/settings/views.py index f6d70e192..a9df717d7 100644 --- a/apps/settings/views.py +++ b/apps/settings/views.py @@ -3,15 +3,16 @@ from django.shortcuts import render, redirect from django.contrib import messages from django.utils.translation import ugettext as _ -from common.permissions import SuperUserRequiredMixin +from common.permissions import PermissionsMixin, IsSuperUser from common import utils from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \ TerminalSettingForm, SecuritySettingForm, EmailContentSettingForm -class BasicSettingView(SuperUserRequiredMixin, TemplateView): +class BasicSettingView(PermissionsMixin, TemplateView): form_class = BasicSettingForm template_name = "settings/basic_setting.html" + permission_classes = [IsSuperUser] def get_context_data(self, **kwargs): context = { @@ -35,9 +36,10 @@ class BasicSettingView(SuperUserRequiredMixin, TemplateView): return render(request, self.template_name, context) -class EmailSettingView(SuperUserRequiredMixin, TemplateView): +class EmailSettingView(PermissionsMixin, TemplateView): form_class = EmailSettingForm template_name = "settings/email_setting.html" + permission_classes = [IsSuperUser] def get_context_data(self, **kwargs): context = { @@ -61,9 +63,10 @@ class EmailSettingView(SuperUserRequiredMixin, TemplateView): return render(request, self.template_name, context) -class LDAPSettingView(SuperUserRequiredMixin, TemplateView): +class LDAPSettingView(PermissionsMixin, TemplateView): form_class = LDAPSettingForm template_name = "settings/ldap_setting.html" + permission_classes = [IsSuperUser] def get_context_data(self, **kwargs): context = { @@ -87,9 +90,10 @@ class LDAPSettingView(SuperUserRequiredMixin, TemplateView): return render(request, self.template_name, context) -class TerminalSettingView(SuperUserRequiredMixin, TemplateView): +class TerminalSettingView(PermissionsMixin, TemplateView): form_class = TerminalSettingForm template_name = "settings/terminal_setting.html" + permission_classes = [IsSuperUser] def get_context_data(self, **kwargs): command_storage = utils.get_command_storage_setting() @@ -118,8 +122,9 @@ class TerminalSettingView(SuperUserRequiredMixin, TemplateView): return render(request, self.template_name, context) -class ReplayStorageCreateView(SuperUserRequiredMixin, TemplateView): +class ReplayStorageCreateView(PermissionsMixin, TemplateView): template_name = 'settings/replay_storage_create.html' + permission_classes = [IsSuperUser] def get_context_data(self, **kwargs): context = { @@ -130,8 +135,9 @@ class ReplayStorageCreateView(SuperUserRequiredMixin, TemplateView): return super().get_context_data(**kwargs) -class CommandStorageCreateView(SuperUserRequiredMixin, TemplateView): +class CommandStorageCreateView(PermissionsMixin, TemplateView): template_name = 'settings/command_storage_create.html' + permission_classes = [IsSuperUser] def get_context_data(self, **kwargs): context = { @@ -142,9 +148,10 @@ class CommandStorageCreateView(SuperUserRequiredMixin, TemplateView): return super().get_context_data(**kwargs) -class SecuritySettingView(SuperUserRequiredMixin, TemplateView): +class SecuritySettingView(PermissionsMixin, TemplateView): form_class = SecuritySettingForm template_name = "settings/security_setting.html" + permission_classes = [IsSuperUser] def get_context_data(self, **kwargs): context = { @@ -168,9 +175,10 @@ class SecuritySettingView(SuperUserRequiredMixin, TemplateView): return render(request, self.template_name, context) -class EmailContentSettingView(SuperUserRequiredMixin, TemplateView): +class EmailContentSettingView(PermissionsMixin, TemplateView): template_name = "settings/email_content_setting.html" form_class = EmailContentSettingForm + permission_classes = [IsSuperUser] def get_context_data(self, **kwargs): context = { diff --git a/apps/static/css/jumpserver.css b/apps/static/css/jumpserver.css index c3dcfb386..b1f2633d8 100644 --- a/apps/static/css/jumpserver.css +++ b/apps/static/css/jumpserver.css @@ -453,4 +453,16 @@ div.dataTables_wrapper div.dataTables_filter { #tree-refresh .fa-refresh { font: normal normal normal 14px/1 FontAwesome !important; -} \ No newline at end of file +} + +.select2-selection__rendered span.select2-selection, .select2-container .select2-selection--single, .select2-selection__arrow { + height: 34px !important; +} + +.select2-selection { + border-radius: 0 !important; +} + +span.select2-selection__placeholder { + line-height: 34px !important; +} diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js index 0197312fd..5eac258f8 100644 --- a/apps/static/js/jumpserver.js +++ b/apps/static/js/jumpserver.js @@ -165,11 +165,13 @@ function formSubmit(props) { /* { "form": $("form"), + "data": {}, "url": "", "method": "POST", "redirect_to": "", "success": function(data, textStatue, jqXHR){}, - "error": function(jqXHR, textStatus, errorThrown) {} + "error": function(jqXHR, textStatus, errorThrown) {}, + "message": "", } */ props = props || {}; @@ -183,6 +185,10 @@ function formSubmit(props) { dataType: props.data_type || "json" }).done(function (data, textState, jqXHR) { if (redirect_to) { + if (props.message) { + var messages="ed65330a45559c87345a0eb6ac7812d18d0d8976$[[\"__json_message\"\0540\05425\054\"asdfasdf \\u521b\\u5efa\\u6210\\u529f\"]]" + setCookie("messages", messages) + } location.href = redirect_to; } else if (typeof props.success === 'function') { return props.success(data, textState, jqXHR); @@ -230,7 +236,15 @@ function formSubmit(props) { var help_msg = v.join("
") ; helpBlockRef.html(help_msg); } else { - noneFieldErrorMsg += v + '
'; + $.each(v, function (kk, vv) { + if (typeof errors === "object") { + $.each(vv, function (kkk, vvv) { + noneFieldErrorMsg += " " + vvv + '
'; + }) + } else{ + noneFieldErrorMsg += vv + '
'; + } + }) } }); if (noneFieldErrorRef.length === 1 && noneFieldErrorMsg !== '') { @@ -632,6 +646,8 @@ jumpserver.initServerSideDataTable = function (options) { $.each(rows, function (id, row) { table.selected_rows.push(row); if (row.id && $.inArray(row.id, table.selected) === -1){ + console.log(table) + console.log(table.selected); table.selected.push(row.id) } }) @@ -913,8 +929,11 @@ function initPopover($container, $progress, $idPassword, $el, password_check_rul } // 解决input框中的资产和弹出表格中资产的显示不一致 -function initSelectedAssets2Table(){ - var inputAssets = $('#id_assets').val(); +function initSelectedAssets2Table(id){ + if (!id) { + id = "#id_assets" + } + var inputAssets = $(id).val(); var selectedAssets = asset_table2.selected.concat(); // input assets无,table assets选中,则取消勾选(再次click) @@ -996,7 +1015,7 @@ function APIImportData(props){ $('#updated_failed').html(''); $('#updated_failed_detail').html(''); $('#success_updated').html(gettext("Update Success")); - $('#success_updated_detail').html("Count" + ": " + data.length); + $('#success_updated_detail').html(gettext("Count") + ": " + data.length); } props.data_table.ajax.reload() diff --git a/apps/templates/_left_side_bar.html b/apps/templates/_left_side_bar.html index 2704069af..3c38d6d12 100644 --- a/apps/templates/_left_side_bar.html +++ b/apps/templates/_left_side_bar.html @@ -4,6 +4,8 @@ {% include '_user_profile.html' %} {% if request.user.is_org_admin and request.COOKIES.IN_ADMIN_PAGE != "No" %} {% include '_nav.html' %} + {% elif request.user.is_auditor %} + {% include '_nav_audits.html' %} {% else %} {% include '_nav_user.html' %} {% endif %} diff --git a/apps/templates/_nav_audits.html b/apps/templates/_nav_audits.html new file mode 100644 index 000000000..853bb24b8 --- /dev/null +++ b/apps/templates/_nav_audits.html @@ -0,0 +1,31 @@ +{% load i18n %} +
  • + + {% trans 'Dashboard' %} + + +
  • + +
  • + + {% trans 'Sessions' %} + + +
  • + +
  • + + {% trans 'Audits' %} + + +
  • \ No newline at end of file diff --git a/apps/templates/_nav_user.html b/apps/templates/_nav_user.html index 5f77fb99e..c745c0cdc 100644 --- a/apps/templates/_nav_user.html +++ b/apps/templates/_nav_user.html @@ -4,6 +4,8 @@ {% trans 'My assets' %} + +{% if LICENSE_VALID %}
  • {% trans 'My Applications' %} @@ -16,6 +18,8 @@
  • +{% endif %} + {% if SECURITY_COMMAND_EXECUTION %}
  • diff --git a/apps/terminal/api/session.py b/apps/terminal/api/session.py index f52d7b2b7..2943641a1 100644 --- a/apps/terminal/api/session.py +++ b/apps/terminal/api/session.py @@ -15,7 +15,7 @@ import jms_storage from common.utils import is_uuid -from common.permissions import IsOrgAdminOrAppUser +from common.permissions import IsOrgAdminOrAppUser, IsAuditor from ..hands import SystemUser from ..models import Terminal, Session from .. import serializers @@ -30,7 +30,7 @@ class SessionViewSet(BulkModelViewSet): queryset = Session.objects.all() serializer_class = serializers.SessionSerializer pagination_class = LimitOffsetPagination - permission_classes = (IsOrgAdminOrAppUser,) + permission_classes = (IsOrgAdminOrAppUser | IsAuditor, ) def get_queryset(self): queryset = super().get_queryset() @@ -68,7 +68,7 @@ class CommandViewSet(viewsets.ViewSet): """ command_store = get_command_storage() serializer_class = SessionCommandSerializer - permission_classes = (IsOrgAdminOrAppUser,) + permission_classes = (IsOrgAdminOrAppUser | IsAuditor,) def get_queryset(self): self.command_store.filter(**dict(self.request.query_params)) diff --git a/apps/terminal/templates/terminal/session_list.html b/apps/terminal/templates/terminal/session_list.html index 355c8226d..44bbaa32b 100644 --- a/apps/terminal/templates/terminal/session_list.html +++ b/apps/terminal/templates/terminal/session_list.html @@ -103,7 +103,7 @@ {% if session.is_finished %} {% trans "Replay" %} {% else %} - {% if session.protocol == 'ssh' %} + {% if session.protocol == 'ssh' and request.user.is_org_admin%} {% trans "Terminate" %} {% else %} {% trans "Terminate" %} @@ -115,6 +115,7 @@ {% endblock %} {% block content_bottom_left %} + {% if request.user.is_org_admin %}