From 4e705a52eb33abc34e5fa8caec610c3e5ecdae22 Mon Sep 17 00:00:00 2001 From: BaiJiangJie <32935519+BaiJiangJie@users.noreply.github.com> Date: Mon, 18 Mar 2019 10:15:33 +0800 Subject: [PATCH] =?UTF-8?q?[Feature]=20=E6=B7=BB=E5=8A=A0=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86=E5=99=A8=20(#2489?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Feature] 1. 资产用户管理器 * [Feature] 2. 资产用户管理器: 更新AuthBook * [Feature] 3. 资产用户管理器: 添加 AssetUser API * [Feature] 4. AssetUser Model: 添加方法 load_related_asset_auth * [Feature] 5. AdminUser: 更新管理用户获取认证信息时,先加载相关资产的认证 * [Feature] 6. SystemUser: 更新系统用户获取认证信息时,先加载相关资产的认证 * [Feature] 前端页面: 添加资产用户列表页面 * [Feature] 前端页面: 管理用户的资产管理页面添加按钮: 修改资产用户认证信息 * [Feature] 前端页面: 系统用户的资产管理页面添加按钮: 修改资产用户认证信息 * [Feature] 优化: 从管理用户和系统用户的backend中获取相关资产用户的逻辑 * [Update] Fix 1 * [Feature] 优化: SystemUserBackend之filter功能 * [Feature] 优化: AdminUserBackend之filter功能 * [Feature] 优化: AdminUserBackend和SystemUserBackend功能 * [Feature] 更新翻译: 资产用户管理器 * [Update] 更新资产用户列表页名称为: asset_asset_user_list.html * [Bugfix] 修改bug: SystemUserBackend 根据用户名过滤系统用户 * [Feature] 添加: 资产用户列表中可测试资产用户的连接性 * [Update] 修改: AdHoc model的run_as字段从SystemUser外键修改为username字符串 * [Feature] 添加: 获取系统用户认证信息(对应某个资产)API * [Update] 更新: API获取asset user时进行排序 * [Bugfix] 修改: 资产用户可连接性CACHE_KEY * [Update] 更新翻译信息 * [Update] 修改获取资产用户认证信息API的返回响应(200/400) * [Update] 修改BaseUser获取特定资产的方法名 * [Update] 修改logger输出,AuthBook set_version_and_latest * [Update] 修改日志输出添加exc_info参数 * [Update] 移除AuthBook迁移文件0026 * [Bugfix] 修复AdminUserBackend获取instances为空的bug --- apps/assets/api/__init__.py | 1 + apps/assets/api/asset_user.py | 101 ++ apps/assets/api/system_user.py | 18 +- apps/assets/backends/__init__.py | 0 apps/assets/backends/base.py | 60 ++ apps/assets/backends/external/__init__.py | 2 + apps/assets/backends/external/db.py | 31 + apps/assets/backends/external/utils.py | 16 + 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/multi.py | 40 + apps/assets/const.py | 12 + apps/assets/models/__init__.py | 1 + apps/assets/models/asset.py | 2 + apps/assets/models/authbook.py | 93 ++ apps/assets/models/base.py | 38 +- apps/assets/serializers/__init__.py | 1 + apps/assets/serializers/asset_user.py | 65 ++ apps/assets/signals_handler.py | 16 +- apps/assets/tasks.py | 75 +- .../assets/_asset_user_auth_modal.html | 28 + .../templates/assets/admin_user_assets.html | 48 +- .../assets/asset_asset_user_list.html | 218 ++++ .../assets/templates/assets/asset_detail.html | 7 +- .../templates/assets/system_user_asset.html | 49 +- apps/assets/urls/api_urls.py | 10 + apps/assets/urls/views_urls.py | 2 + apps/assets/views/asset.py | 16 +- apps/jumpserver/settings.py | 3 + apps/locale/zh/LC_MESSAGES/django.mo | Bin 64813 -> 65375 bytes apps/locale/zh/LC_MESSAGES/django.po | 954 ++++++++++-------- apps/ops/inventory.py | 25 +- apps/ops/models/adhoc.py | 4 +- 37 files changed, 1647 insertions(+), 483 deletions(-) create mode 100644 apps/assets/api/asset_user.py create mode 100644 apps/assets/backends/__init__.py create mode 100644 apps/assets/backends/base.py create mode 100644 apps/assets/backends/external/__init__.py create mode 100644 apps/assets/backends/external/db.py create mode 100644 apps/assets/backends/external/utils.py create mode 100644 apps/assets/backends/external/vault.py create mode 100644 apps/assets/backends/internal/__init__.py create mode 100644 apps/assets/backends/internal/admin_user.py create mode 100644 apps/assets/backends/internal/asset_user.py create mode 100644 apps/assets/backends/internal/system_user.py create mode 100644 apps/assets/backends/internal/utils.py create mode 100644 apps/assets/backends/multi.py create mode 100644 apps/assets/models/authbook.py create mode 100644 apps/assets/serializers/asset_user.py create mode 100644 apps/assets/templates/assets/_asset_user_auth_modal.html create mode 100644 apps/assets/templates/assets/asset_asset_user_list.html diff --git a/apps/assets/api/__init__.py b/apps/assets/api/__init__.py index 1d7dbca00..b0efb6af8 100644 --- a/apps/assets/api/__init__.py +++ b/apps/assets/api/__init__.py @@ -5,3 +5,4 @@ from .system_user import * from .node import * from .domain import * from .cmd_filter import * +from .asset_user import * diff --git a/apps/assets/api/asset_user.py b/apps/assets/api/asset_user.py new file mode 100644 index 000000000..7000a95a5 --- /dev/null +++ b/apps/assets/api/asset_user.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# + + +from rest_framework.response import Response +from rest_framework import viewsets, status, generics +from rest_framework.pagination import LimitOffsetPagination + +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 .. import serializers +from ..tasks import test_asset_users_connectivity_manual + + +__all__ = [ + 'AssetUserViewSet', 'AssetUserAuthInfoApi', 'AssetUserTestConnectiveApi', +] + + +logger = get_logger(__name__) + + +class AssetUserViewSet(viewsets.GenericViewSet): + 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) + + 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) + return queryset + + def filter_queryset(self, queryset): + queryset = sorted( + queryset, + key=lambda q: (q.asset.hostname, q.connectivity, q.username) + ) + return queryset + + +class AssetUserAuthInfoApi(generics.RetrieveAPIView): + serializer_class = serializers.AssetUserAuthInfoSerializer + permission_classes = (IsOrgAdminOrAppUser,) + + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance) + status_code = status.HTTP_200_OK + if not instance: + status_code = status.HTTP_400_BAD_REQUEST + return Response(serializer.data, status=status_code) + + def get_object(self): + username = self.request.GET.get('username') + asset_id = self.request.GET.get('asset_id') + asset = get_object_or_none(Asset, pk=asset_id) + try: + instance = AssetUserManager.get(username, asset) + except Exception as e: + logger.error(e, exc_info=True) + return None + else: + return instance + + +class AssetUserTestConnectiveApi(generics.RetrieveAPIView): + """ + Test asset users connective + """ + + 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) + return asset_users + + def retrieve(self, request, *args, **kwargs): + asset_users = self.get_asset_users() + task = test_asset_users_connectivity_manual.delay(asset_users) + return Response({"task": task.id}) + + + diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index 5c131f400..9805872e7 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -30,7 +30,7 @@ from ..tasks import push_system_user_to_assets_manual, \ logger = get_logger(__file__) __all__ = [ - 'SystemUserViewSet', 'SystemUserAuthInfoApi', + 'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi', 'SystemUserPushApi', 'SystemUserTestConnectiveApi', 'SystemUserAssetsListView', 'SystemUserPushToAssetApi', 'SystemUserTestAssetConnectivityApi', 'SystemUserCommandFilterRuleListApi', @@ -68,6 +68,22 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): return Response(status=204) +class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView): + """ + Get system user with asset auth info + """ + queryset = SystemUser.objects.all() + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = serializers.SystemUserAuthSerializer + + def get_object(self): + instance = super().get_object() + aid = self.kwargs.get('aid') + asset = get_object_or_404(Asset, pk=aid) + instance.load_specific_asset_auth(asset) + return instance + + class SystemUserPushApi(generics.RetrieveAPIView): """ Push system user to cluster assets api diff --git a/apps/assets/backends/__init__.py b/apps/assets/backends/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/assets/backends/base.py b/apps/assets/backends/base.py new file mode 100644 index 000000000..c93ea6a31 --- /dev/null +++ b/apps/assets/backends/base.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# + +from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist +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): + """ + :param username: 用户名 + :param asset: 对象 + :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)) + + @classmethod + def raise_multiple_return(cls, name, length): + raise cls.MultipleObjectsReturned(cls.MSG_MULTIPLE.format(name, length)) diff --git a/apps/assets/backends/external/__init__.py b/apps/assets/backends/external/__init__.py new file mode 100644 index 000000000..ec51c5a2b --- /dev/null +++ b/apps/assets/backends/external/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# diff --git a/apps/assets/backends/external/db.py b/apps/assets/backends/external/db.py new file mode 100644 index 000000000..f3f6c16d6 --- /dev/null +++ b/apps/assets/backends/external/db.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# + +from assets.models import AuthBook + +from ..base import BaseBackend + + +class AuthBookBackend(BaseBackend): + + @classmethod + def filter(cls, username=None, asset=None, latest=True): + queryset = AuthBook.objects.all() + if username: + queryset = queryset.filter(username=username) + if asset: + queryset = queryset.filter(asset=asset) + if latest: + queryset = queryset.latest_version() + return queryset + + @classmethod + def create(cls, **kwargs): + auth_info = { + 'password': kwargs.pop('password', ''), + 'public_key': kwargs.pop('public_key', ''), + 'private_key': kwargs.pop('private_key', '') + } + obj = AuthBook.objects.create(**kwargs) + obj.set_auth(**auth_info) + return obj diff --git a/apps/assets/backends/external/utils.py b/apps/assets/backends/external/utils.py new file mode 100644 index 000000000..62be16c1d --- /dev/null +++ b/apps/assets/backends/external/utils.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# + +# from django.conf import settings + +from .db import AuthBookBackend +# from .vault import VaultBackend + + +def get_backend(): + default_backend = AuthBookBackend + + # if settings.BACKEND_ASSET_USER_AUTH_VAULT: + # return VaultBackend + + return default_backend diff --git a/apps/assets/backends/external/vault.py b/apps/assets/backends/external/vault.py new file mode 100644 index 000000000..da7583458 --- /dev/null +++ b/apps/assets/backends/external/vault.py @@ -0,0 +1,19 @@ +# -*- 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 new file mode 100644 index 000000000..f19a64d9a --- /dev/null +++ b/apps/assets/backends/internal/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# + + diff --git a/apps/assets/backends/internal/admin_user.py b/apps/assets/backends/internal/admin_user.py new file mode 100644 index 000000000..e67b41fc9 --- /dev/null +++ b/apps/assets/backends/internal/admin_user.py @@ -0,0 +1,38 @@ +# -*- 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 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 new file mode 100644 index 000000000..8502cf5d9 --- /dev/null +++ b/apps/assets/backends/internal/asset_user.py @@ -0,0 +1,32 @@ +# -*- 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 new file mode 100644 index 000000000..52b22215c --- /dev/null +++ b/apps/assets/backends/internal/system_user.py @@ -0,0 +1,75 @@ +# -*- 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: + _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 new file mode 100644 index 000000000..65b4fa821 --- /dev/null +++ b/apps/assets/backends/internal/utils.py @@ -0,0 +1,26 @@ +# -*- 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/multi.py b/apps/assets/backends/multi.py new file mode 100644 index 000000000..785176885 --- /dev/null +++ b/apps/assets/backends/multi.py @@ -0,0 +1,40 @@ +# -*- 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/const.py b/apps/assets/const.py index 7cee7aedd..901ade1ff 100644 --- a/apps/assets/const.py +++ b/apps/assets/const.py @@ -32,6 +32,18 @@ TEST_SYSTEM_USER_CONN_TASKS = [ } ] + +ASSET_USER_CONN_CACHE_KEY = 'ASSET_USER_CONN_{}_{}' +TEST_ASSET_USER_CONN_TASKS = [ + { + "name": "ping", + "action": { + "module": "ping", + } + } +] + + TASK_OPTIONS = { 'timeout': 10, 'forks': 10, diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index c60830fba..b87a18796 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -7,3 +7,4 @@ from .node import * from .asset import * from .cmd_filter import * from .utils import * +from .authbook import * diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index 244c2b7e7..5ad9830f3 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -197,6 +197,7 @@ class Asset(OrgModelMixin): 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, @@ -232,6 +233,7 @@ class Asset(OrgModelMixin): """ 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, diff --git a/apps/assets/models/authbook.py b/apps/assets/models/authbook.py new file mode 100644 index 000000000..713c8b41c --- /dev/null +++ b/apps/assets/models/authbook.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# + +from django.db import models +from django.utils.translation import ugettext as _ +from django.core.cache import cache + +from orgs.mixins import OrgManager + +from .base import AssetUser +from ..const import ASSET_USER_CONN_CACHE_KEY + +__all__ = ['AuthBook'] + + +class AuthBookQuerySet(models.QuerySet): + + def latest_version(self): + return self.filter(is_latest=True) + + +class AuthBookManager(OrgManager): + pass + + +class AuthBook(AssetUser): + asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset')) + is_latest = models.BooleanField(default=False, verbose_name=_('Latest version')) + version = models.IntegerField(default=1, verbose_name=_('Version')) + + objects = AuthBookManager.from_queryset(AuthBookQuerySet)() + + class Meta: + verbose_name = _('AuthBook') + + def _set_latest(self): + self._remove_pre_obj_latest() + self.is_latest = True + self.save() + + def _get_pre_obj(self): + pre_obj = self.__class__.objects.filter( + username=self.username, asset=self.asset).latest_version().first() + return pre_obj + + def _remove_pre_obj_latest(self): + pre_obj = self._get_pre_obj() + if pre_obj: + pre_obj.is_latest = False + pre_obj.save() + + def _set_version(self): + pre_obj = self._get_pre_obj() + if pre_obj: + self.version = pre_obj.version + 1 + else: + self.version = 1 + self.save() + + def set_version_and_latest(self): + self._set_version() + self._set_latest() + + @property + def _conn_cache_key(self): + return ASSET_USER_CONN_CACHE_KEY.format(self.id, self.asset.id) + + @property + def connectivity(self): + 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', {}).keys(): + if host == self.asset.hostname: + _connectivity = self.REACHABLE + + cache.set(self._conn_cache_key, _connectivity, 3600) + + @property + def keyword(self): + return {'username': self.username, 'asset': self.asset} + + def __str__(self): + return '{}@{}'.format(self.username, self.asset) + diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 37e099e99..30d8ae0f5 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -9,13 +9,17 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from django.conf import settings -from common.utils import get_signer, ssh_key_string_to_obj, ssh_key_gen +from common.utils import ( + get_signer, ssh_key_string_to_obj, ssh_key_gen, get_logger +) from common.validators import alphanumeric from orgs.mixins import OrgModelMixin from .utils import private_key_validator signer = get_signer() +logger = get_logger(__file__) + class AssetUser(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) @@ -45,8 +49,8 @@ class AssetUser(OrgModelMixin): @password.setter def password(self, password_raw): - raise AttributeError("Using set_auth do that") - # self._password = signer.sign(password_raw) + # raise AttributeError("Using set_auth do that") + self._password = signer.sign(password_raw) @property def private_key(self): @@ -55,8 +59,8 @@ class AssetUser(OrgModelMixin): @private_key.setter def private_key(self, private_key_raw): - raise AttributeError("Using set_auth do that") - # self._private_key = signer.sign(private_key_raw) + # raise AttributeError("Using set_auth do that") + self._private_key = signer.sign(private_key_raw) @property def private_key_obj(self): @@ -88,6 +92,11 @@ class AssetUser(OrgModelMixin): else: return None + @public_key.setter + def public_key(self, public_key_raw): + # raise AttributeError("Using set_auth do that") + self._public_key = signer.sign(public_key_raw) + @property def public_key_obj(self): if self.public_key: @@ -115,6 +124,25 @@ class AssetUser(OrgModelMixin): def get_auth(self, asset=None): pass + def load_specific_asset_auth(self, asset): + from ..backends.multi import AssetUserManager + try: + other = AssetUserManager.get(username=self.username, asset=asset) + except Exception as e: + logger.error(e, exc_info=True) + else: + self._merge_auth(other) + + def _merge_auth(self, other): + if not other: + return + if other.password: + self.password = other.password + if other.public_key: + self.public_key = other.public_key + if other.private_key: + self.private_key = other.private_key + def clear_auth(self): self._password = '' self._private_key = '' diff --git a/apps/assets/serializers/__init__.py b/apps/assets/serializers/__init__.py index ad4439be3..f9866688d 100644 --- a/apps/assets/serializers/__init__.py +++ b/apps/assets/serializers/__init__.py @@ -8,3 +8,4 @@ from .system_user import * from .node import * from .domain import * from .cmd_filter import * +from .asset_user import * diff --git a/apps/assets/serializers/asset_user.py b/apps/assets/serializers/asset_user.py new file mode 100644 index 000000000..f7345437e --- /dev/null +++ b/apps/assets/serializers/asset_user.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# + +from django.utils.translation import ugettext as _ +from rest_framework import serializers + +from ..models import AuthBook +from ..backends.multi import AssetUserManager + +__all__ = [ + 'AssetUserSerializer', 'AssetUserAuthInfoSerializer', +] + + +class AssetUserSerializer(serializers.ModelSerializer): + + password = serializers.CharField( + max_length=256, allow_blank=True, allow_null=True, write_only=True, + required=False, help_text=_('Password') + ) + public_key = serializers.CharField( + max_length=4096, allow_blank=True, allow_null=True, write_only=True, + required=False, help_text=_('Public key') + ) + private_key = serializers.CharField( + max_length=4096, allow_blank=True, allow_null=True, write_only=True, + required=False, help_text=_('Private key') + ) + + class Meta: + model = AuthBook + read_only_fields = ( + 'date_created', 'date_updated', 'created_by', + 'is_latest', 'version', 'connectivity', + ) + fields = '__all__' + extra_kwargs = { + '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 create(self, validated_data): + kwargs = { + 'name': validated_data.get('name'), + 'username': validated_data.get('username'), + 'asset': validated_data.get('asset'), + 'comment': validated_data.get('comment', ''), + 'org_id': validated_data.get('org_id', ''), + 'password': validated_data.get('password'), + 'public_key': validated_data.get('public_key'), + 'private_key': validated_data.get('private_key') + } + instance = AssetUserManager.create(**kwargs) + return instance + + +class AssetUserAuthInfoSerializer(serializers.ModelSerializer): + class Meta: + model = AuthBook + fields = ['password', 'private_key', 'public_key'] diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index 7d3b67f6c..4afb97e75 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -5,9 +5,12 @@ from django.db.models.signals import post_save, m2m_changed, post_delete from django.dispatch import receiver from common.utils import get_logger -from .models import Asset, SystemUser, Node -from .tasks import update_assets_hardware_info_util, \ - test_asset_connectivity_util, push_system_user_to_assets +from .models import Asset, SystemUser, Node, AuthBook +from .tasks import ( + update_assets_hardware_info_util, + test_asset_connectivity_util, + push_system_user_to_assets +) logger = get_logger(__file__) @@ -109,3 +112,10 @@ def on_node_assets_changed(sender, instance=None, **kwargs): def on_node_update_or_created(sender, instance=None, created=False, **kwargs): if instance and not created: instance.expire_full_value() + + +@receiver(post_save, sender=AuthBook) +def on_auth_book_created(sender, instance=None, created=False, **kwargs): + if created: + logger.debug('Receive create auth book object signal.') + instance.set_version_and_latest() diff --git a/apps/assets/tasks.py b/apps/assets/tasks.py index 4a166eada..d6dc58a8e 100644 --- a/apps/assets/tasks.py +++ b/apps/assets/tasks.py @@ -26,16 +26,22 @@ disk_pattern = re.compile(r'^hd|sd|xvd|vd') PERIOD_TASK = os.environ.get("PERIOD_TASK", "on") +def check_asset_can_run_ansible(asset): + if not asset.is_active: + msg = _("Asset has been disabled, skipped: {}").format(asset) + logger.info(msg) + return False + if not asset.support_ansible(): + msg = _("Asset may not be support ansible, skipped: {}").format(asset) + logger.info(msg) + return False + return True + + def clean_hosts(assets): clean_assets = [] for asset in assets: - if not asset.is_active: - msg = _("Asset has been disabled, skipped: {}").format(asset) - logger.info(msg) - continue - if not asset.support_ansible(): - msg = _("Asset may not be support ansible, skipped: {}").format(asset) - logger.info(msg) + if not check_asset_can_run_ansible(asset): continue clean_assets.append(asset) if not clean_assets: @@ -259,7 +265,7 @@ def test_system_user_connectivity_util(system_user, assets, task_name): task, created = update_or_create_ansible_task( task_name, hosts=hosts, tasks=tasks, pattern='all', options=const.TASK_OPTIONS, - run_as=system_user, created_by=system_user.org_id, + run_as=system_user.username, created_by=system_user.org_id, ) result = task.run() set_system_user_connectivity_info(system_user, result) @@ -370,16 +376,18 @@ def push_system_user_util(system_user, assets, task_name): logger.info(msg) return - tasks = get_push_system_user_tasks(system_user) hosts = clean_hosts(assets) if not hosts: return {} - 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=system_user.org_id, - ) - return task.run() + for host in hosts: + system_user.load_specific_asset_auth(host) + tasks = get_push_system_user_tasks(system_user) + 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, + created_by=system_user.org_id, + ) + task.run() @shared_task @@ -415,6 +423,43 @@ def test_admin_user_connectability_period(): pass +@shared_task +def set_asset_user_connectivity_info(asset_user, result): + summary = result[1] + asset_user.connectivity = summary + + +@shared_task +def test_asset_user_connectivity_util(asset_user, task_name): + """ + :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 + ) + result = task.run() + set_asset_user_connectivity_info(asset_user, result) + + +@shared_task +def test_asset_users_connectivity_manual(asset_users): + """ + :param asset_users: 对象 + """ + for asset_user in asset_users: + task_name = _("Test asset user connectivity: {}").format(asset_user) + test_asset_user_connectivity_util(asset_user, task_name) + + # @shared_task # @register_as_period_task(interval=3600) # @after_app_ready_start diff --git a/apps/assets/templates/assets/_asset_user_auth_modal.html b/apps/assets/templates/assets/_asset_user_auth_modal.html new file mode 100644 index 000000000..be615ce52 --- /dev/null +++ b/apps/assets/templates/assets/_asset_user_auth_modal.html @@ -0,0 +1,28 @@ +{% 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/admin_user_assets.html b/apps/assets/templates/assets/admin_user_assets.html index 7a9882563..d22c5406f 100644 --- a/apps/assets/templates/assets/admin_user_assets.html +++ b/apps/assets/templates/assets/admin_user_assets.html @@ -84,6 +84,7 @@ + {% include 'assets/_asset_user_auth_modal.html' %} {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/assets/templates/assets/asset_asset_user_list.html b/apps/assets/templates/assets/asset_asset_user_list.html new file mode 100644 index 000000000..540be02d8 --- /dev/null +++ b/apps/assets/templates/assets/asset_asset_user_list.html @@ -0,0 +1,218 @@ +{% extends 'base.html' %} +{% load common_tags %} +{% load static %} +{% load i18n %} + +{% block custom_head_css_js %} +{% endblock %} + +{% block content %} +
+
+
+
+ +
+
+
+
+ {% trans 'Asset users of' %} {{ asset.hostname }} +
+ + + + + + + + + + +
+
+
+ + + + + + + + + + + + + +
{% trans 'Username' %}{% trans 'Version' %}{% trans 'Reachable' %}{% trans 'Date updated' %}{% trans 'Action' %}
+
+
+
+
+
+
+ {% trans 'Quick modify' %} +
+
+ + + {% if asset.protocol == 'ssh' %} + + + + + {% endif %} + +
{% trans 'Test connective' %}: + + + +
+
+
+
+
+
+
+
+
+ {% include 'assets/_asset_user_auth_modal.html' %} +{% endblock %} +{% block custom_foot_js %} + +{% endblock %} \ No newline at end of file diff --git a/apps/assets/templates/assets/asset_detail.html b/apps/assets/templates/assets/asset_detail.html index c766f583c..4ff3bdf75 100644 --- a/apps/assets/templates/assets/asset_detail.html +++ b/apps/assets/templates/assets/asset_detail.html @@ -19,6 +19,9 @@
  • {% trans 'Asset detail' %}
  • +
  • + {% trans 'Asset user list' %} +
  • {% if user.is_superuser %}
  • {% trans 'Update' %} @@ -32,7 +35,7 @@
    -
    +
    {{ asset.hostname }} @@ -139,7 +142,7 @@
    {% if user.is_superuser or user.is_org_admin %} -
    +
    {% trans 'Quick modify' %} diff --git a/apps/assets/templates/assets/system_user_asset.html b/apps/assets/templates/assets/system_user_asset.html index 9bcf6b5aa..4ffdf2a91 100644 --- a/apps/assets/templates/assets/system_user_asset.html +++ b/apps/assets/templates/assets/system_user_asset.html @@ -132,6 +132,7 @@
    + {% include 'assets/_asset_user_auth_modal.html' %} {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 4aceda2e8..6ad54cf7e 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -17,6 +17,7 @@ router.register(r'nodes', api.NodeViewSet, 'node') 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') cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filter', lookup='filter') cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule') @@ -31,6 +32,12 @@ urlpatterns = [ path('assets//gateway/', api.AssetGatewayApi.as_view(), name='asset-gateway'), + path('asset-user/auth-info/', + api.AssetUserAuthInfoApi.as_view(), name='asset-user-auth-info'), + path('asset-user/test-connective/', + api.AssetUserTestConnectiveApi.as_view(), name='asset-user-connective'), + + path('admin-user//nodes/', api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'), path('admin-user//auth/', @@ -42,6 +49,8 @@ urlpatterns = [ path('system-user//auth-info/', api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'), + path('system-user//asset//auth-info/', + api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'), path('system-user//assets/', api.SystemUserAssetsListView.as_view(), name='system-user-assets'), path('system-user//push/', @@ -79,6 +88,7 @@ urlpatterns = [ path('gateway//test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'), + ] urlpatterns += router.urls + cmd_filter_router.urls diff --git a/apps/assets/urls/views_urls.py b/apps/assets/urls/views_urls.py index 76651cfdb..f9f67b849 100644 --- a/apps/assets/urls/views_urls.py +++ b/apps/assets/urls/views_urls.py @@ -15,6 +15,8 @@ urlpatterns = [ path('asset//update/', views.AssetUpdateView.as_view(), name='asset-update'), path('asset//delete/', views.AssetDeleteView.as_view(), name='asset-delete'), path('asset/update/', views.AssetBulkUpdateView.as_view(), name='asset-bulk-update'), + # Asset user view + path('asset//asset-user/', views.AssetUserListView.as_view(), name='asset-user-list'), # User asset view path('user-asset/', views.UserAssetListView.as_view(), name='user-asset-list'), diff --git a/apps/assets/views/asset.py b/apps/assets/views/asset.py index 572772dc5..b7493e047 100644 --- a/apps/assets/views/asset.py +++ b/apps/assets/views/asset.py @@ -34,7 +34,7 @@ from ..models import Asset, AdminUser, SystemUser, Label, Node, Domain __all__ = [ - 'AssetListView', 'AssetCreateView', 'AssetUpdateView', + 'AssetListView', 'AssetCreateView', 'AssetUpdateView', 'AssetUserListView', 'UserAssetListView', 'AssetBulkUpdateView', 'AssetDetailView', 'AssetDeleteView', 'AssetExportView', 'BulkImportAssetView', ] @@ -56,6 +56,20 @@ class AssetListView(AdminUserRequiredMixin, TemplateView): return super().get_context_data(**kwargs) +class AssetUserListView(AdminUserRequiredMixin, DetailView): + model = Asset + context_object_name = 'asset' + template_name = 'assets/asset_asset_user_list.html' + + def get_context_data(self, **kwargs): + context = { + 'app': _('Assets'), + 'action': _('Asset user list'), + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + class UserAssetListView(LoginRequiredMixin, TemplateView): template_name = 'assets/user_asset_list.html' diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 1147e07e8..b7e78e6df 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -572,3 +572,6 @@ LOGIN_LOG_KEEP_DAYS = CONFIG.LOGIN_LOG_KEEP_DAYS # User or user group permission cache time, default 3600 seconds ASSETS_PERM_CACHE_TIME = CONFIG.ASSETS_PERM_CACHE_TIME + +# Asset user auth external backend, default AuthBook backend +BACKEND_ASSET_USER_AUTH_VAULT = False diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index a9d91262dbeb9cd4c47d052dd5b7629e9d448f48..cb390f14de1f7bf97b68df6fa2a169cbdc07a22c 100644 GIT binary patch delta 19751 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 aMUS>mj9W7D|J_xTZt=g{dik`J-~R#qEpasf delta 19322 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\n" "Language-Team: Jumpserver team\n" @@ -30,8 +30,8 @@ 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:191 -#: assets/templates/assets/asset_detail.html:199 +#: assets/templates/assets/asset_detail.html:194 +#: assets/templates/assets/asset_detail.html:202 #: assets/templates/assets/system_user_asset.html:95 perms/models.py:32 msgid "Nodes" msgstr "节点管理" @@ -39,7 +39,7 @@ msgstr "节点管理" #: assets/forms/asset.py:30 assets/forms/asset.py:66 assets/forms/asset.py:105 #: assets/forms/asset.py:109 assets/models/asset.py:84 #: assets/models/cluster.py:19 assets/models/user.py:91 -#: assets/templates/assets/asset_detail.html:77 templates/_nav.html:24 +#: 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:67 #: xpack/plugins/orgs/templates/orgs/org_list.html:18 @@ -52,15 +52,15 @@ msgstr "管理用户" #: assets/templates/assets/asset_list.html:81 #: assets/templates/assets/asset_update.html:41 #: assets/templates/assets/asset_update.html:43 -#: assets/templates/assets/user_asset_list.html:34 +#: 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/models/domain.py:26 assets/models/domain.py:52 -#: assets/templates/assets/asset_detail.html:81 -#: assets/templates/assets/user_asset_list.html:157 +#: assets/templates/assets/asset_detail.html:84 +#: assets/templates/assets/user_asset_list.html:163 #: xpack/plugins/orgs/templates/orgs/org_list.html:17 msgid "Domain" msgstr "网域" @@ -104,16 +104,17 @@ msgstr "选择资产" #: assets/forms/asset.py:101 assets/models/asset.py:77 #: assets/models/domain.py:50 assets/templates/assets/admin_user_assets.html:50 -#: assets/templates/assets/asset_detail.html:69 +#: assets/templates/assets/asset_detail.html:72 #: assets/templates/assets/domain_gateway_list.html:58 #: assets/templates/assets/system_user_asset.html:52 -#: assets/templates/assets/user_asset_list.html:152 +#: assets/templates/assets/user_asset_list.html:158 #: settings/templates/settings/replay_storage_create.html:59 msgid "Port" msgstr "端口" #: assets/forms/domain.py:15 assets/forms/label.py:13 -#: assets/models/asset.py:277 assets/templates/assets/admin_user_list.html:28 +#: assets/models/asset.py:279 assets/models/authbook.py:27 +#: assets/templates/assets/admin_user_list.html:28 #: assets/templates/assets/domain_detail.html:60 #: assets/templates/assets/domain_list.html:26 #: assets/templates/assets/label_list.html:16 @@ -129,6 +130,7 @@ msgstr "端口" #: terminal/templates/terminal/command_list.html:73 #: terminal/templates/terminal/session_list.html:41 #: terminal/templates/terminal/session_list.html:72 +#: xpack/plugins/change_asset_password_plan/models.py:17 #: xpack/plugins/cloud/models.py:187 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:65 #: xpack/plugins/orgs/templates/orgs/org_list.html:16 @@ -153,7 +155,7 @@ msgstr "不能包含特殊字符" #: assets/templates/assets/label_list.html:14 #: assets/templates/assets/system_user_detail.html:58 #: assets/templates/assets/system_user_list.html:29 ops/models/adhoc.py:37 -#: ops/templates/ops/task_detail.html:60 ops/templates/ops/task_list.html:35 +#: ops/templates/ops/task_detail.html:60 ops/templates/ops/task_list.html:27 #: orgs/models.py:12 perms/models.py:28 #: perms/templates/perms/asset_permission_detail.html:62 #: perms/templates/perms/asset_permission_list.html:53 @@ -165,13 +167,14 @@ msgstr "不能包含特殊字符" #: settings/templates/settings/terminal_setting.html:102 terminal/models.py:22 #: terminal/models.py:233 terminal/templates/terminal/terminal_detail.html:43 #: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14 -#: users/models/user.py:55 users/templates/users/_select_user_modal.html:13 +#: users/models/user.py:54 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:12 #: users/templates/users/user_list.html:23 #: users/templates/users/user_profile.html:51 #: users/templates/users/user_pubkey_update.html:53 +#: xpack/plugins/change_asset_password_plan/models.py:15 #: xpack/plugins/cloud/models.py:49 xpack/plugins/cloud/models.py:119 #: xpack/plugins/cloud/templates/cloud/account_detail.html:52 #: xpack/plugins/cloud/templates/cloud/account_list.html:12 @@ -183,20 +186,23 @@ msgid "Name" msgstr "名称" #: assets/forms/domain.py:71 assets/forms/user.py:81 assets/forms/user.py:143 -#: assets/models/base.py:23 assets/templates/assets/admin_user_detail.html:60 +#: assets/models/base.py:23 +#: assets/templates/assets/_asset_user_auth_modal.html:15 +#: assets/templates/assets/admin_user_detail.html:60 #: assets/templates/assets/admin_user_list.html:27 +#: assets/templates/assets/asset_asset_user_list.html:48 #: assets/templates/assets/domain_gateway_list.html:60 #: assets/templates/assets/system_user_detail.html:62 -#: assets/templates/assets/system_user_list.html:30 -#: audits/templates/audits/login_log_list.html:49 -#: perms/templates/perms/asset_permission_list.html:74 -#: perms/templates/perms/asset_permission_user.html:55 users/forms.py:15 -#: users/forms.py:33 users/models/authentication.py:77 users/models/user.py:53 -#: users/templates/users/_select_user_modal.html:14 +#: assets/templates/assets/system_user_list.html:30 audits/models.py:93 +#: audits/templates/audits/login_log_list.html:49 authentication/forms.py:11 +#: ops/models/adhoc.py:164 perms/templates/perms/asset_permission_list.html:74 +#: perms/templates/perms/asset_permission_user.html:55 users/forms.py:13 +#: users/models/user.py:52 users/templates/users/_select_user_modal.html:14 #: users/templates/users/login.html:64 users/templates/users/new_login.html:85 #: users/templates/users/user_detail.html:67 #: users/templates/users/user_list.html:24 #: users/templates/users/user_profile.html:47 +#: xpack/plugins/change_asset_password_plan/models.py:16 msgid "Username" msgstr "用户名" @@ -204,9 +210,12 @@ msgstr "用户名" msgid "Password or private key passphrase" msgstr "密码或密钥密码" -#: assets/forms/user.py:26 assets/models/base.py:24 settings/forms.py:103 -#: users/forms.py:17 users/forms.py:35 users/forms.py:47 -#: users/templates/users/login.html:67 users/templates/users/new_login.html:90 +#: assets/forms/user.py:26 assets/models/base.py:24 +#: assets/serializers/asset_user.py:19 +#: assets/templates/assets/_asset_user_auth_modal.html:21 +#: authentication/forms.py:13 settings/forms.py:103 users/forms.py:15 +#: users/forms.py:27 users/templates/users/login.html:67 +#: users/templates/users/new_login.html:90 #: users/templates/users/reset_password.html:53 #: users/templates/users/user_create.html:10 #: users/templates/users/user_password_authentication.html:18 @@ -214,10 +223,13 @@ 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_asset_password_plan/models.py:19 msgid "Password" msgstr "密码" -#: assets/forms/user.py:29 users/models/user.py:82 +#: assets/forms/user.py:29 assets/serializers/asset_user.py:27 +#: users/models/user.py:81 +#: xpack/plugins/change_asset_password_plan/models.py:20 msgid "Private key" msgstr "ssh私钥" @@ -264,72 +276,73 @@ msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig" #: assets/models/asset.py:74 assets/models/domain.py:49 #: assets/templates/assets/_asset_list_modal.html:46 #: assets/templates/assets/admin_user_assets.html:49 -#: assets/templates/assets/asset_detail.html:61 +#: assets/templates/assets/asset_detail.html:64 #: assets/templates/assets/asset_list.html:93 #: assets/templates/assets/domain_gateway_list.html:57 #: assets/templates/assets/system_user_asset.html:51 -#: assets/templates/assets/user_asset_list.html:46 -#: assets/templates/assets/user_asset_list.html:151 +#: assets/templates/assets/user_asset_list.html:45 +#: assets/templates/assets/user_asset_list.html:157 #: audits/templates/audits/login_log_list.html:52 -#: perms/templates/perms/asset_permission_asset.html:55 settings/forms.py:132 +#: perms/templates/perms/asset_permission_asset.html:55 settings/forms.py:133 #: users/templates/users/user_granted_asset.html:45 #: users/templates/users/user_group_granted_asset.html:45 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/admin_user_assets.html:48 -#: assets/templates/assets/asset_detail.html:57 +#: assets/templates/assets/asset_detail.html:60 #: assets/templates/assets/asset_list.html:92 #: assets/templates/assets/system_user_asset.html:50 -#: assets/templates/assets/user_asset_list.html:45 -#: assets/templates/assets/user_asset_list.html:150 +#: assets/templates/assets/user_asset_list.html:44 +#: assets/templates/assets/user_asset_list.html:156 #: perms/templates/perms/asset_permission_asset.html:54 -#: perms/templates/perms/asset_permission_list.html:77 settings/forms.py:131 +#: perms/templates/perms/asset_permission_list.html:77 settings/forms.py:132 #: users/templates/users/user_granted_asset.html:44 #: users/templates/users/user_group_granted_asset.html:44 msgid "Hostname" msgstr "主机名" #: assets/models/asset.py:76 assets/models/domain.py:51 -#: assets/models/user.py:136 assets/templates/assets/asset_detail.html:73 +#: assets/models/user.py:136 assets/templates/assets/asset_detail.html:76 #: assets/templates/assets/domain_gateway_list.html:59 #: assets/templates/assets/system_user_detail.html:70 #: assets/templates/assets/system_user_list.html:31 -#: assets/templates/assets/user_asset_list.html:153 +#: assets/templates/assets/user_asset_list.html:159 #: terminal/templates/terminal/session_list.html:75 msgid "Protocol" msgstr "协议" -#: assets/models/asset.py:78 assets/templates/assets/asset_detail.html:105 -#: assets/templates/assets/user_asset_list.html:154 +#: assets/models/asset.py:78 assets/templates/assets/asset_detail.html:108 +#: assets/templates/assets/user_asset_list.html:160 msgid "Platform" msgstr "系统平台" #: assets/models/asset.py:81 assets/models/cmd_filter.py:21 #: assets/models/domain.py:54 assets/models/label.py:22 -#: assets/templates/assets/asset_detail.html:113 -#: assets/templates/assets/user_asset_list.html:158 +#: assets/templates/assets/asset_detail.html:116 +#: assets/templates/assets/user_asset_list.html:164 msgid "Is active" msgstr "激活" -#: assets/models/asset.py:87 assets/templates/assets/asset_detail.html:65 +#: assets/models/asset.py:87 assets/templates/assets/asset_detail.html:68 msgid "Public IP" msgstr "公网IP" -#: assets/models/asset.py:88 assets/templates/assets/asset_detail.html:121 +#: assets/models/asset.py:88 assets/templates/assets/asset_detail.html:124 msgid "Asset number" msgstr "资产编号" -#: assets/models/asset.py:91 assets/templates/assets/asset_detail.html:85 +#: assets/models/asset.py:91 assets/templates/assets/asset_detail.html:88 msgid "Vendor" msgstr "制造商" -#: assets/models/asset.py:92 assets/templates/assets/asset_detail.html:89 +#: assets/models/asset.py:92 assets/templates/assets/asset_detail.html:92 msgid "Model" msgstr "型号" -#: assets/models/asset.py:93 assets/templates/assets/asset_detail.html:117 +#: assets/models/asset.py:93 assets/templates/assets/asset_detail.html:120 msgid "Serial number" msgstr "序列号" @@ -350,7 +363,7 @@ msgstr "CPU核数" msgid "CPU vcpus" msgstr "CPU总数" -#: assets/models/asset.py:99 assets/templates/assets/asset_detail.html:97 +#: assets/models/asset.py:99 assets/templates/assets/asset_detail.html:100 msgid "Memory" msgstr "内存" @@ -362,8 +375,8 @@ msgstr "硬盘大小" msgid "Disk info" msgstr "硬盘信息" -#: assets/models/asset.py:103 assets/templates/assets/asset_detail.html:109 -#: assets/templates/assets/user_asset_list.html:155 +#: assets/models/asset.py:103 assets/templates/assets/asset_detail.html:112 +#: assets/templates/assets/user_asset_list.html:161 msgid "OS" msgstr "操作系统" @@ -380,7 +393,7 @@ msgid "Hostname raw" msgstr "主机名原始" #: assets/models/asset.py:108 assets/templates/assets/asset_create.html:34 -#: assets/templates/assets/asset_detail.html:228 +#: assets/templates/assets/asset_detail.html:231 #: assets/templates/assets/asset_update.html:39 templates/_nav.html:26 msgid "Labels" msgstr "标签管理" @@ -389,13 +402,13 @@ msgstr "标签管理" #: 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 -#: assets/templates/assets/asset_detail.html:125 +#: assets/templates/assets/asset_detail.html:128 #: assets/templates/assets/cmd_filter_detail.html:77 #: assets/templates/assets/domain_detail.html:72 #: assets/templates/assets/system_user_detail.html:100 #: ops/templates/ops/adhoc_detail.html:86 orgs/models.py:15 perms/models.py:37 #: perms/models.py:90 perms/templates/perms/asset_permission_detail.html:98 -#: users/models/user.py:96 users/templates/users/user_detail.html:111 +#: users/models/user.py:95 users/templates/users/user_detail.html:111 #: xpack/plugins/cloud/models.py:55 xpack/plugins/cloud/models.py:127 msgid "Created by" msgstr "创建者" @@ -424,7 +437,7 @@ msgstr "创建日期" #: assets/models/domain.py:53 assets/models/group.py:23 #: assets/models/label.py:23 assets/templates/assets/admin_user_detail.html:72 #: assets/templates/assets/admin_user_list.html:32 -#: assets/templates/assets/asset_detail.html:133 +#: assets/templates/assets/asset_detail.html:136 #: assets/templates/assets/cmd_filter_detail.html:65 #: assets/templates/assets/cmd_filter_list.html:27 #: assets/templates/assets/cmd_filter_rule_list.html:62 @@ -433,16 +446,17 @@ msgstr "创建日期" #: assets/templates/assets/domain_list.html:28 #: assets/templates/assets/system_user_detail.html:104 #: assets/templates/assets/system_user_list.html:37 -#: assets/templates/assets/user_asset_list.html:159 ops/models/adhoc.py:43 +#: assets/templates/assets/user_asset_list.html:165 ops/models/adhoc.py:43 #: orgs/models.py:17 perms/models.py:39 perms/models.py:92 #: perms/templates/perms/asset_permission_detail.html:102 settings/models.py:34 #: terminal/models.py:32 terminal/templates/terminal/terminal_detail.html:63 -#: users/models/group.py:15 users/models/user.py:88 +#: users/models/group.py:15 users/models/user.py:87 #: users/templates/users/user_detail.html:127 #: users/templates/users/user_group_detail.html:67 #: users/templates/users/user_group_list.html:14 -#: users/templates/users/user_profile.html:134 xpack/plugins/cloud/models.py:54 -#: xpack/plugins/cloud/models.py:125 +#: users/templates/users/user_profile.html:134 +#: xpack/plugins/change_asset_password_plan/models.py:26 +#: xpack/plugins/cloud/models.py:54 xpack/plugins/cloud/models.py:125 #: xpack/plugins/cloud/templates/cloud/account_detail.html:72 #: xpack/plugins/cloud/templates/cloud/account_list.html:15 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:71 @@ -461,6 +475,7 @@ msgstr "不可达" #: assets/models/asset.py:118 assets/models/base.py:35 #: assets/templates/assets/admin_user_assets.html:51 #: assets/templates/assets/admin_user_list.html:29 +#: assets/templates/assets/asset_asset_user_list.html:50 #: assets/templates/assets/asset_list.html:95 #: assets/templates/assets/system_user_asset.html:53 #: assets/templates/assets/system_user_list.html:34 @@ -469,10 +484,26 @@ msgid "Reachable" msgstr "可连接" #: assets/models/asset.py:119 assets/models/base.py:36 -#: xpack/plugins/license/models.py:78 +#: authentication/utils.py:9 xpack/plugins/license/models.py:78 msgid "Unknown" msgstr "未知" +#: assets/models/authbook.py:28 ops/templates/ops/task_detail.html:72 +msgid "Latest version" +msgstr "最新版本" + +#: assets/models/authbook.py:29 +#: assets/templates/assets/asset_asset_user_list.html:49 +#: 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 +msgid "AuthBook" +msgstr "" + #: assets/models/base.py:25 msgid "SSH private key" msgstr "ssh密钥" @@ -489,7 +520,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:74 +#: assets/models/cluster.py:22 users/models/user.py:73 #: users/templates/users/user_detail.html:76 msgid "Phone" msgstr "手机" @@ -515,7 +546,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:441 +#: users/models/user.py:457 msgid "System" msgstr "系统" @@ -596,6 +627,7 @@ msgstr "每行一个命令" #: assets/models/cmd_filter.py:54 #: assets/templates/assets/admin_user_assets.html:52 #: assets/templates/assets/admin_user_list.html:33 +#: assets/templates/assets/asset_asset_user_list.html:52 #: assets/templates/assets/asset_list.html:96 #: assets/templates/assets/cmd_filter_list.html:28 #: assets/templates/assets/cmd_filter_rule_list.html:63 @@ -607,7 +639,7 @@ msgstr "每行一个命令" #: audits/templates/audits/operate_log_list.html:41 #: audits/templates/audits/operate_log_list.html:67 #: ops/templates/ops/adhoc_history.html:59 ops/templates/ops/task_adhoc.html:64 -#: ops/templates/ops/task_history.html:65 ops/templates/ops/task_list.html:42 +#: ops/templates/ops/task_history.html:65 ops/templates/ops/task_list.html:34 #: perms/templates/perms/asset_permission_list.html:60 #: settings/templates/settings/terminal_setting.html:82 #: settings/templates/settings/terminal_setting.html:104 @@ -657,8 +689,8 @@ msgstr "默认资产组" #: 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:303 -#: users/models/user.py:33 users/models/user.py:429 +#: terminal/templates/terminal/session_list.html:71 users/forms.py:283 +#: users/models/user.py:32 users/models/user.py:445 #: users/templates/users/user_group_detail.html:78 #: users/templates/users/user_group_list.html:13 users/views/user.py:386 #: xpack/plugins/orgs/forms.py:26 @@ -680,7 +712,7 @@ msgstr "分类" msgid "Key" msgstr "键" -#: assets/models/node.py:127 +#: assets/models/node.py:128 msgid "New node" msgstr "新节点" @@ -699,18 +731,19 @@ msgstr "手动登录" #: 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:50 -#: assets/views/asset.py:89 assets/views/asset.py:133 assets/views/asset.py:150 -#: assets/views/asset.py:174 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 +#: assets/views/asset.py:66 assets/views/asset.py:103 assets/views/asset.py:147 +#: assets/views/asset.py:164 assets/views/asset.py:188 +#: 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 msgid "Assets" msgstr "资产管理" @@ -733,7 +766,7 @@ msgstr "Shell" msgid "Login mode" msgstr "登录模式" -#: assets/models/user.py:247 assets/templates/assets/user_asset_list.html:156 +#: assets/models/user.py:247 assets/templates/assets/user_asset_list.html:162 #: audits/models.py:19 audits/templates/audits/ftp_log_list.html:49 #: audits/templates/audits/ftp_log_list.html:72 perms/forms.py:48 #: perms/models.py:33 perms/models.py:87 @@ -755,71 +788,85 @@ msgstr "系统用户" msgid "%(value)s is not an even number" msgstr "%(value)s is not an even number" -#: assets/tasks.py:33 +#: assets/serializers/asset_user.py:23 users/forms.py:230 +#: users/models/user.py:84 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 +#: users/templates/users/user_pubkey_update.html:43 +#: xpack/plugins/change_asset_password_plan/models.py:21 +msgid "Public key" +msgstr "ssh公钥" + +#: assets/tasks.py:31 msgid "Asset has been disabled, skipped: {}" msgstr "资产或许不支持ansible, 跳过: {}" -#: assets/tasks.py:37 +#: assets/tasks.py:35 msgid "Asset may not be support ansible, skipped: {}" msgstr "资产或许不支持ansible, 跳过: {}" -#: assets/tasks.py:42 +#: assets/tasks.py:48 msgid "No assets matched, stop task" msgstr "没有匹配到资产,结束任务" -#: assets/tasks.py:67 +#: assets/tasks.py:73 msgid "Get asset info failed: {}" msgstr "获取资产信息失败:{}" -#: assets/tasks.py:117 +#: assets/tasks.py:123 msgid "Update some assets hardware info" msgstr "更新资产硬件信息" -#: assets/tasks.py:134 +#: assets/tasks.py:140 msgid "Update asset hardware info: {}" msgstr "更新资产硬件信息: {}" -#: assets/tasks.py:159 +#: assets/tasks.py:165 msgid "Test assets connectivity" msgstr "测试资产可连接性" -#: assets/tasks.py:183 +#: assets/tasks.py:189 msgid "Test assets connectivity: {}" msgstr "测试资产可连接性: {}" -#: assets/tasks.py:225 +#: assets/tasks.py:231 msgid "Test admin user connectivity period: {}" msgstr "定期测试管理账号可连接性: {}" -#: assets/tasks.py:232 +#: assets/tasks.py:238 msgid "Test admin user connectivity: {}" msgstr "测试管理行号可连接性: {}" -#: assets/tasks.py:271 +#: assets/tasks.py:277 msgid "Test system user connectivity: {}" msgstr "测试系统用户可连接性: {}" -#: assets/tasks.py:278 +#: assets/tasks.py:284 msgid "Test system user connectivity: {} => {}" msgstr "测试系统用户可连接性: {} => {}" -#: assets/tasks.py:291 +#: assets/tasks.py:297 msgid "Test system user connectivity period: {}" msgstr "定期测试系统用户可连接性: {}" -#: assets/tasks.py:368 +#: assets/tasks.py:374 msgid "" "Push system user task skip, auto push not enable or protocol is not ssh: {}" msgstr "推送系统用户任务跳过,自动推送没有打开,或协议不是ssh: {}" -#: assets/tasks.py:388 assets/tasks.py:402 +#: assets/tasks.py:396 assets/tasks.py:410 msgid "Push system users to assets: {}" msgstr "推送系统用户到入资产: {}" -#: assets/tasks.py:394 +#: assets/tasks.py:402 msgid "Push system users to asset: {} => {}" msgstr "推送系统用户到入资产: {} => {}" +#: assets/tasks.py:459 +msgid "Test asset user connectivity: {}" +msgstr "测试资产用户可连接性: {}" + #: assets/templates/assets/_asset_group_bulk_update_modal.html:5 msgid "Update asset group" msgstr "更新用户组" @@ -835,7 +882,7 @@ msgstr "选择资产" #: assets/templates/assets/_asset_group_bulk_update_modal.html:21 #: assets/templates/assets/cmd_filter_detail.html:89 #: assets/templates/assets/cmd_filter_list.html:26 -#: assets/templates/assets/user_asset_list.html:48 +#: assets/templates/assets/user_asset_list.html:47 #: users/templates/users/user_granted_asset.html:47 msgid "System users" msgstr "系统用户" @@ -875,6 +922,14 @@ msgstr "如果设置了id,则会使用该行信息更新该id的资产" msgid "Asset list" msgstr "资产列表" +#: assets/templates/assets/_asset_user_auth_modal.html:4 +msgid "Update asset user auth" +msgstr "更新资产用户认证信息" + +#: assets/templates/assets/_asset_user_auth_modal.html:23 +msgid "Please input password" +msgstr "请输入密码" + #: assets/templates/assets/_system_user.html:37 #: assets/templates/assets/asset_create.html:16 #: assets/templates/assets/asset_update.html:21 @@ -971,7 +1026,8 @@ msgid "Submit" msgstr "提交" #: assets/templates/assets/_user_asset_detail_modal.html:11 -#: assets/templates/assets/asset_detail.html:20 assets/views/asset.py:175 +#: assets/templates/assets/asset_asset_user_list.html:17 +#: assets/templates/assets/asset_detail.html:20 assets/views/asset.py:189 msgid "Asset detail" msgstr "资产详情" @@ -1015,22 +1071,47 @@ msgid "Quick update" msgstr "快速更新" #: assets/templates/assets/admin_user_assets.html:70 -#: assets/templates/assets/asset_detail.html:176 +#: assets/templates/assets/asset_asset_user_list.html:71 +#: 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:113 -#: assets/templates/assets/asset_detail.html:179 +#: assets/templates/assets/admin_user_assets.html:114 +#: assets/templates/assets/asset_asset_user_list.html:74 +#: assets/templates/assets/asset_asset_user_list.html:122 +#: assets/templates/assets/asset_detail.html:182 #: assets/templates/assets/system_user_asset.html:75 -#: assets/templates/assets/system_user_asset.html:163 +#: assets/templates/assets/system_user_asset.html:164 #: assets/templates/assets/system_user_detail.html:151 msgid "Test" msgstr "测试" +#: assets/templates/assets/admin_user_assets.html:115 +#: assets/templates/assets/asset_asset_user_list.html:120 +#: assets/templates/assets/system_user_asset.html:166 +msgid "Update auth" +msgstr "更新认证" + +#: assets/templates/assets/admin_user_assets.html:191 +#: assets/templates/assets/asset_asset_user_list.html:176 +#: assets/templates/assets/asset_detail.html:311 +#: assets/templates/assets/system_user_asset.html:350 +#: users/templates/users/user_detail.html:307 +#: users/templates/users/user_detail.html:334 +#: xpack/plugins/interface/views.py:31 +msgid "Update successfully!" +msgstr "更新成功" + +#: assets/templates/assets/admin_user_assets.html:194 +#: assets/templates/assets/asset_asset_user_list.html:179 +#: assets/templates/assets/system_user_asset.html:353 +msgid "Update failed!" +msgstr "更新失败" + #: assets/templates/assets/admin_user_detail.html:24 #: assets/templates/assets/admin_user_list.html:88 -#: assets/templates/assets/asset_detail.html:24 +#: assets/templates/assets/asset_detail.html:27 #: assets/templates/assets/asset_list.html:177 #: assets/templates/assets/cmd_filter_detail.html:29 #: assets/templates/assets/cmd_filter_list.html:57 @@ -1062,7 +1143,7 @@ msgstr "更新" #: assets/templates/assets/admin_user_detail.html:28 #: assets/templates/assets/admin_user_list.html:89 -#: assets/templates/assets/asset_detail.html:28 +#: assets/templates/assets/asset_detail.html:31 #: assets/templates/assets/asset_list.html:178 #: assets/templates/assets/cmd_filter_detail.html:33 #: assets/templates/assets/cmd_filter_list.html:58 @@ -1074,7 +1155,7 @@ msgstr "更新" #: assets/templates/assets/label_list.html:39 #: assets/templates/assets/system_user_detail.html:30 #: assets/templates/assets/system_user_list.html:93 audits/models.py:33 -#: ops/templates/ops/task_list.html:72 +#: ops/templates/ops/task_list.html:64 #: perms/templates/perms/asset_permission_detail.html:34 #: perms/templates/perms/asset_permission_list.html:178 #: settings/templates/settings/terminal_setting.html:90 @@ -1104,7 +1185,7 @@ msgid "Select nodes" msgstr "选择节点" #: assets/templates/assets/admin_user_detail.html:100 -#: assets/templates/assets/asset_detail.html:208 +#: assets/templates/assets/asset_detail.html:211 #: assets/templates/assets/asset_list.html:636 #: assets/templates/assets/cmd_filter_detail.html:106 #: assets/templates/assets/system_user_asset.html:112 @@ -1155,6 +1236,29 @@ msgstr "创建管理用户" msgid "Ratio" msgstr "比例" +#: assets/templates/assets/asset_asset_user_list.html:20 +#: assets/templates/assets/asset_detail.html:23 assets/views/asset.py:67 +msgid "Asset user list" +msgstr "资产用户列表" + +#: assets/templates/assets/asset_asset_user_list.html:28 +msgid "Asset users of" +msgstr "资产用户" + +#: assets/templates/assets/asset_asset_user_list.html:51 +#: assets/templates/assets/cmd_filter_detail.html:73 +msgid "Date updated" +msgstr "更新日期" + +#: assets/templates/assets/asset_asset_user_list.html:64 +#: assets/templates/assets/asset_detail.html:148 +#: terminal/templates/terminal/session_detail.html:81 +#: users/templates/users/user_detail.html:138 +#: users/templates/users/user_profile.html:146 +#: xpack/plugins/license/templates/license/license_detail.html:93 +msgid "Quick modify" +msgstr "快速修改" + #: assets/templates/assets/asset_bulk_update.html:8 #: users/templates/users/user_bulk_update.html:8 msgid "Select properties that need to be modified" @@ -1165,30 +1269,22 @@ msgstr "选择需要修改属性" msgid "Select all" msgstr "全选" -#: assets/templates/assets/asset_detail.html:93 +#: assets/templates/assets/asset_detail.html:96 msgid "CPU" msgstr "CPU" -#: assets/templates/assets/asset_detail.html:101 +#: assets/templates/assets/asset_detail.html:104 msgid "Disk" msgstr "硬盘" -#: assets/templates/assets/asset_detail.html:129 +#: assets/templates/assets/asset_detail.html:132 #: users/templates/users/user_detail.html:115 #: users/templates/users/user_profile.html:104 msgid "Date joined" msgstr "创建日期" -#: assets/templates/assets/asset_detail.html:145 -#: terminal/templates/terminal/session_detail.html:81 -#: users/templates/users/user_detail.html:138 -#: users/templates/users/user_profile.html:146 -#: xpack/plugins/license/templates/license/license_detail.html:93 -msgid "Quick modify" -msgstr "快速修改" - -#: assets/templates/assets/asset_detail.html:151 -#: assets/templates/assets/user_asset_list.html:47 perms/models.py:34 +#: assets/templates/assets/asset_detail.html:154 +#: assets/templates/assets/user_asset_list.html:46 perms/models.py:34 #: perms/models.py:88 #: perms/templates/perms/asset_permission_create_update.html:52 #: perms/templates/perms/asset_permission_detail.html:120 @@ -1201,21 +1297,14 @@ msgstr "快速修改" msgid "Active" msgstr "激活中" -#: assets/templates/assets/asset_detail.html:168 +#: assets/templates/assets/asset_detail.html:171 msgid "Refresh hardware" msgstr "更新硬件信息" -#: assets/templates/assets/asset_detail.html:171 +#: assets/templates/assets/asset_detail.html:174 msgid "Refresh" msgstr "刷新" -#: assets/templates/assets/asset_detail.html:308 -#: users/templates/users/user_detail.html:307 -#: users/templates/users/user_detail.html:334 -#: xpack/plugins/interface/views.py:31 -msgid "Update successfully!" -msgstr "更新成功" - #: assets/templates/assets/asset_list.html:8 msgid "" "The left side is the asset tree, right click to create, delete, and change " @@ -1225,7 +1314,7 @@ msgstr "" "左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的," "右侧是属于该节点下的资产" -#: assets/templates/assets/asset_list.html:69 assets/views/asset.py:90 +#: assets/templates/assets/asset_list.html:69 assets/views/asset.py:104 msgid "Create asset" msgstr "创建资产" @@ -1378,10 +1467,6 @@ msgstr "配置" msgid "Rules" msgstr "规则" -#: assets/templates/assets/cmd_filter_detail.html:73 -msgid "Date updated" -msgstr "更新日期" - #: assets/templates/assets/cmd_filter_detail.html:97 msgid "Binding to system user" msgstr "绑定到系统用户" @@ -1500,7 +1585,7 @@ msgid "Push system user now" msgstr "立刻推送系统" #: assets/templates/assets/system_user_asset.html:84 -#: assets/templates/assets/system_user_asset.html:161 +#: assets/templates/assets/system_user_asset.html:162 #: assets/templates/assets/system_user_detail.html:142 msgid "Push" msgstr "推送" @@ -1585,23 +1670,23 @@ msgstr "更新管理用户" msgid "Admin user detail" msgstr "管理用户详情" -#: assets/views/asset.py:64 templates/_nav_user.html:4 +#: assets/views/asset.py:78 templates/_nav_user.html:4 msgid "My assets" msgstr "我的资产" -#: assets/views/asset.py:104 +#: assets/views/asset.py:118 msgid "Bulk update asset success" msgstr "批量更新资产成功" -#: assets/views/asset.py:134 +#: assets/views/asset.py:148 msgid "Bulk update asset" msgstr "批量更新资产" -#: assets/views/asset.py:151 +#: assets/views/asset.py:165 msgid "Update asset" msgstr "更新资产" -#: assets/views/asset.py:292 +#: assets/views/asset.py:306 msgid "already exists" msgstr "已经存在" @@ -1695,9 +1780,10 @@ msgstr "操作" msgid "Filename" msgstr "文件名" -#: audits/models.py:22 audits/templates/audits/ftp_log_list.html:76 +#: audits/models.py:22 audits/models.py:89 +#: audits/templates/audits/ftp_log_list.html:76 #: ops/templates/ops/command_execution_list.html:64 -#: ops/templates/ops/task_list.html:39 users/models/authentication.py:73 +#: ops/templates/ops/task_list.html:31 #: users/templates/users/user_detail.html:458 xpack/plugins/cloud/api.py:62 msgid "Success" msgstr "成功" @@ -1719,6 +1805,79 @@ msgstr "资源" msgid "Change by" msgstr "修改者" +#: audits/models.py:69 users/templates/users/user_detail.html:98 +msgid "Disabled" +msgstr "禁用" + +#: audits/models.py:70 settings/models.py:33 +#: users/templates/users/user_detail.html:96 +msgid "Enabled" +msgstr "启用" + +#: audits/models.py:71 audits/models.py:81 +msgid "-" +msgstr "" + +#: audits/models.py:82 +msgid "Username/password check failed" +msgstr "用户名/密码 校验失败" + +#: audits/models.py:83 +msgid "MFA authentication failed" +msgstr "MFA 认证失败" + +#: audits/models.py:84 +msgid "Username does not exist" +msgstr "用户名不存在" + +#: audits/models.py:85 +msgid "Password expired" +msgstr "密码过期" + +#: audits/models.py:90 xpack/plugins/cloud/models.py:164 +#: xpack/plugins/cloud/models.py:178 +msgid "Failed" +msgstr "失败" + +#: audits/models.py:94 +msgid "Login type" +msgstr "登录方式" + +#: audits/models.py:95 +msgid "Login ip" +msgstr "登录IP" + +#: audits/models.py:96 +msgid "Login city" +msgstr "登录城市" + +#: audits/models.py:97 +msgid "User agent" +msgstr "Agent" + +#: audits/models.py:98 audits/templates/audits/login_log_list.html:54 +#: users/forms.py:142 users/models/user.py:76 +#: users/templates/users/first_login.html:45 +msgid "MFA" +msgstr "MFA" + +#: audits/models.py:99 audits/templates/audits/login_log_list.html:55 +#: xpack/plugins/cloud/models.py:172 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:69 +msgid "Reason" +msgstr "原因" + +#: audits/models.py:100 audits/templates/audits/login_log_list.html:56 +#: xpack/plugins/cloud/models.py:171 xpack/plugins/cloud/models.py:188 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:70 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:67 +msgid "Status" +msgstr "状态" + +#: audits/models.py:101 +msgid "Date login" +msgstr "登录日期" + #: audits/templates/audits/ftp_log_list.html:77 #: ops/templates/ops/adhoc_history.html:52 #: ops/templates/ops/adhoc_history_detail.html:61 @@ -1740,7 +1899,7 @@ msgstr "选择用户" #: audits/templates/audits/password_change_log_list.html:42 #: ops/templates/ops/command_execution_list.html:42 #: ops/templates/ops/command_execution_list.html:47 -#: ops/templates/ops/task_list.html:21 ops/templates/ops/task_list.html:26 +#: ops/templates/ops/task_list.html:13 ops/templates/ops/task_list.html:18 #: templates/_base_list.html:43 templates/_header_bar.html:8 #: terminal/templates/terminal/command_list.html:60 #: terminal/templates/terminal/session_list.html:61 @@ -1767,28 +1926,8 @@ msgstr "Agent" msgid "City" msgstr "城市" -#: audits/templates/audits/login_log_list.html:54 users/forms.py:162 -#: users/models/authentication.py:82 users/models/user.py:77 -#: users/templates/users/first_login.html:45 -msgid "MFA" -msgstr "MFA" - -#: audits/templates/audits/login_log_list.html:55 -#: users/models/authentication.py:83 xpack/plugins/cloud/models.py:172 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:69 -msgid "Reason" -msgstr "原因" - -#: audits/templates/audits/login_log_list.html:56 -#: users/models/authentication.py:84 xpack/plugins/cloud/models.py:171 -#: xpack/plugins/cloud/models.py:188 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:70 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:67 -msgid "Status" -msgstr "状态" - #: audits/templates/audits/login_log_list.html:57 -#: ops/templates/ops/task_list.html:40 +#: ops/templates/ops/task_list.html:32 msgid "Date" msgstr "日期" @@ -1800,31 +1939,128 @@ msgstr "日期" msgid "Datetime" msgstr "日期" -#: audits/views.py:71 audits/views.py:115 audits/views.py:151 -#: audits/views.py:195 audits/views.py:226 templates/_nav.html:72 +#: audits/views.py:70 audits/views.py:114 audits/views.py:150 +#: audits/views.py:194 audits/views.py:225 templates/_nav.html:72 msgid "Audits" msgstr "日志审计" -#: audits/views.py:72 templates/_nav.html:76 +#: audits/views.py:71 templates/_nav.html:76 msgid "FTP log" msgstr "FTP日志" -#: audits/views.py:116 templates/_nav.html:77 +#: audits/views.py:115 templates/_nav.html:77 msgid "Operate log" msgstr "操作日志" -#: audits/views.py:152 templates/_nav.html:78 +#: audits/views.py:151 templates/_nav.html:78 msgid "Password change log" msgstr "改密日志" -#: audits/views.py:196 templates/_nav.html:75 +#: audits/views.py:195 templates/_nav.html:75 msgid "Login log" msgstr "登录日志" -#: audits/views.py:227 ops/views/command.py:44 +#: audits/views.py:226 ops/views/command.py:44 msgid "Command execution list" msgstr "命令执行列表" +#: authentication/api/auth.py:46 users/templates/users/login.html:52 +#: users/templates/users/new_login.html:71 +msgid "Log in frequently and try again later" +msgstr "登录频繁, 稍后重试" + +#: authentication/api/auth.py:64 +msgid "The user {} password has expired, please update." +msgstr "用户 {} 密码已经过期,请更新。" + +#: authentication/api/auth.py:83 +msgid "Please carry seed value and conduct MFA secondary certification" +msgstr "请携带seed值, 进行MFA二次认证" + +#: authentication/api/auth.py:163 +msgid "Please verify the user name and password first" +msgstr "请先进行用户名和密码验证" + +#: authentication/api/auth.py:168 +msgid "MFA certification failed" +msgstr "MFA认证失败" + +#: authentication/backends/api.py:52 +msgid "Invalid signature header. No credentials provided." +msgstr "" + +#: authentication/backends/api.py:55 +msgid "Invalid signature header. Signature string should not contain spaces." +msgstr "" + +#: authentication/backends/api.py:62 +msgid "Invalid signature header. Format like AccessKeyId:Signature" +msgstr "" + +#: authentication/backends/api.py:66 +msgid "" +"Invalid signature header. Signature string should not contain invalid " +"characters." +msgstr "" + +#: authentication/backends/api.py:86 authentication/backends/api.py:102 +msgid "Invalid signature." +msgstr "" + +#: authentication/backends/api.py:93 +msgid "HTTP header: Date not provide or not %a, %d %b %Y %H:%M:%S GMT" +msgstr "" + +#: authentication/backends/api.py:98 +msgid "Expired, more than 15 minutes" +msgstr "" + +#: authentication/backends/api.py:105 +msgid "User disabled." +msgstr "用户已禁用" + +#: authentication/backends/api.py:120 +msgid "Invalid token header. No credentials provided." +msgstr "" + +#: authentication/backends/api.py:123 +msgid "Invalid token header. Sign string should not contain spaces." +msgstr "" + +#: authentication/backends/api.py:130 +msgid "" +"Invalid token header. Sign string should not contain invalid characters." +msgstr "" + +#: authentication/backends/api.py:140 +msgid "Invalid token or cache refreshed." +msgstr "" + +#: authentication/forms.py:29 users/forms.py:21 +msgid "MFA code" +msgstr "MFA 验证码" + +#: authentication/models.py:33 +msgid "Private Token" +msgstr "ssh密钥" + +#: authentication/views/login.py:75 +msgid "Please enable cookies and try again." +msgstr "设置你的浏览器支持cookie" + +#: authentication/views/login.py:167 users/views/user.py:532 +#: users/views/user.py:557 +msgid "MFA code invalid, or ntp sync server time" +msgstr "MFA验证码不正确,或者服务器端时间不对" + +#: authentication/views/login.py:198 +msgid "Logout success" +msgstr "退出登录成功" + +#: authentication/views/login.py:199 +msgid "Logout success, return login page" +msgstr "退出登录成功,返回到登录页面" + #: common/const.py:6 #, python-format msgid "%(name)s was created successfully" @@ -1873,19 +2109,20 @@ msgstr "" msgid "Waiting task start" msgstr "等待任务开始" -#: ops/models/adhoc.py:38 +#: ops/models/adhoc.py:38 xpack/plugins/change_asset_password_plan/models.py:22 msgid "Interval" msgstr "间隔" -#: ops/models/adhoc.py:38 settings/forms.py:150 +#: ops/models/adhoc.py:38 settings/forms.py:151 +#: xpack/plugins/change_asset_password_plan/models.py:22 msgid "Units: seconds" msgstr "单位: 秒" -#: ops/models/adhoc.py:39 +#: ops/models/adhoc.py:39 xpack/plugins/change_asset_password_plan/models.py:23 msgid "Crontab" msgstr "Crontab" -#: ops/models/adhoc.py:39 +#: ops/models/adhoc.py:39 xpack/plugins/change_asset_password_plan/models.py:23 msgid "5 * * * *" msgstr "" @@ -1908,7 +2145,7 @@ msgstr "选项" #: ops/models/adhoc.py:161 ops/templates/ops/adhoc_detail.html:53 #: ops/templates/ops/command_execution_list.html:58 -#: ops/templates/ops/task_adhoc.html:59 ops/templates/ops/task_list.html:38 +#: ops/templates/ops/task_adhoc.html:59 ops/templates/ops/task_list.html:30 #: settings/templates/settings/command_storage_create.html:49 msgid "Hosts" msgstr "主机" @@ -1951,7 +2188,7 @@ msgid "End time" msgstr "完成时间" #: ops/models/adhoc.py:325 ops/templates/ops/adhoc_history.html:57 -#: ops/templates/ops/task_history.html:63 ops/templates/ops/task_list.html:41 +#: ops/templates/ops/task_history.html:63 ops/templates/ops/task_list.html:33 msgid "Time" msgstr "时间" @@ -1997,7 +2234,7 @@ msgid "Version detail" msgstr "版本详情" #: ops/templates/ops/adhoc_detail.html:22 -#: ops/templates/ops/adhoc_history.html:22 ops/views/adhoc.py:127 +#: ops/templates/ops/adhoc_history.html:22 ops/views/adhoc.py:122 msgid "Version run history" msgstr "执行历史" @@ -2008,7 +2245,7 @@ msgstr "执行历史" msgid "Run as" msgstr "运行用户" -#: ops/templates/ops/adhoc_detail.html:94 ops/templates/ops/task_list.html:36 +#: ops/templates/ops/adhoc_detail.html:94 ops/templates/ops/task_list.html:28 msgid "Run times" msgstr "执行次数" @@ -2055,13 +2292,7 @@ msgstr "执行历史" msgid "F/S/T" msgstr "失败/成功/总" -#: 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 "版本" - -#: ops/templates/ops/adhoc_history_detail.html:19 ops/views/adhoc.py:140 +#: ops/templates/ops/adhoc_history_detail.html:19 ops/views/adhoc.py:135 msgid "Run history detail" msgstr "执行历史详情" @@ -2137,12 +2368,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:75 +#: ops/templates/ops/task_history.html:19 ops/views/adhoc.py:70 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:88 +#: ops/templates/ops/task_history.html:22 ops/views/adhoc.py:83 msgid "Task versions" msgstr "任务各版本" @@ -2164,24 +2395,20 @@ msgstr "版本" msgid "Total versions" msgstr "版本数量" -#: ops/templates/ops/task_detail.html:72 -msgid "Latest version" -msgstr "最新版本" - #: ops/templates/ops/task_detail.html:92 msgid "Contents" msgstr "内容" -#: ops/templates/ops/task_list.html:37 +#: ops/templates/ops/task_list.html:29 msgid "Versions" msgstr "版本" -#: ops/templates/ops/task_list.html:71 +#: ops/templates/ops/task_list.html:63 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:52 msgid "Run" msgstr "执行" -#: ops/templates/ops/task_list.html:125 +#: ops/templates/ops/task_list.html:117 msgid "Task start: " msgstr "任务开始: " @@ -2189,17 +2416,17 @@ msgstr "任务开始: " msgid "Update task content: {}" msgstr "更新任务内容: {}" -#: ops/views/adhoc.py:49 ops/views/adhoc.py:74 ops/views/adhoc.py:87 -#: ops/views/adhoc.py:100 ops/views/adhoc.py:113 ops/views/adhoc.py:126 -#: ops/views/adhoc.py:139 ops/views/command.py:43 ops/views/command.py:67 +#: 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:43 ops/views/command.py:67 msgid "Ops" msgstr "作业中心" -#: ops/views/adhoc.py:50 templates/_nav.html:66 +#: ops/views/adhoc.py:45 templates/_nav.html:66 msgid "Task list" msgstr "任务列表" -#: ops/views/adhoc.py:101 +#: ops/views/adhoc.py:96 msgid "Task run history" msgstr "执行历史" @@ -2216,7 +2443,7 @@ msgstr "组织管理" #: perms/templates/perms/asset_permission_list.html:55 #: perms/templates/perms/asset_permission_list.html:75 #: perms/templates/perms/asset_permission_list.html:122 templates/_nav.html:14 -#: users/forms.py:273 users/models/group.py:26 users/models/user.py:61 +#: users/forms.py:253 users/models/group.py:26 users/models/user.py:60 #: users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_detail.html:213 #: users/templates/users/user_list.html:26 @@ -2234,7 +2461,7 @@ msgstr "资产和节点至少选一个" #: perms/models.py:36 perms/models.py:89 #: perms/templates/perms/asset_permission_detail.html:90 -#: users/models/user.py:93 users/templates/users/user_detail.html:107 +#: users/models/user.py:92 users/templates/users/user_detail.html:107 #: users/templates/users/user_profile.html:116 msgid "Date expired" msgstr "失效日期" @@ -2435,7 +2662,7 @@ msgstr "SMTP密码" msgid "Some provider use token except password" msgstr "一些邮件提供商需要输入的是Token" -#: settings/forms.py:86 settings/forms.py:124 +#: settings/forms.py:86 settings/forms.py:125 msgid "Use SSL" msgstr "使用SSL" @@ -2467,20 +2694,20 @@ msgstr "用户OU" msgid "Use | split User OUs" msgstr "使用|分隔各OU" -#: settings/forms.py:111 +#: settings/forms.py:112 msgid "User search filter" msgstr "用户过滤器" -#: settings/forms.py:112 +#: settings/forms.py:113 #, python-format msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)" -#: settings/forms.py:115 +#: settings/forms.py:116 msgid "User attr map" msgstr "LDAP属性映射" -#: settings/forms.py:117 +#: settings/forms.py:118 msgid "" "User attr map present how to map LDAP user attr to jumpserver, username,name," "email is jumpserver attr" @@ -2488,43 +2715,43 @@ msgstr "" "用户属性映射代表怎样将LDAP中用户属性映射到jumpserver用户上,username, name," "email 是jumpserver的属性" -#: settings/forms.py:126 +#: settings/forms.py:127 msgid "Enable LDAP auth" msgstr "启用LDAP认证" -#: settings/forms.py:135 +#: settings/forms.py:136 msgid "All" msgstr "全部" -#: settings/forms.py:136 +#: settings/forms.py:137 msgid "Auto" msgstr "自动" -#: settings/forms.py:143 +#: settings/forms.py:144 msgid "Password auth" msgstr "密码认证" -#: settings/forms.py:146 +#: settings/forms.py:147 msgid "Public key auth" msgstr "密钥认证" -#: settings/forms.py:149 +#: settings/forms.py:150 msgid "Heartbeat interval" msgstr "心跳间隔" -#: settings/forms.py:153 +#: settings/forms.py:154 msgid "List sort by" msgstr "资产列表排序" -#: settings/forms.py:156 +#: settings/forms.py:157 msgid "List page size" msgstr "资产分页每页数量" -#: settings/forms.py:159 +#: settings/forms.py:160 msgid "Session keep duration" msgstr "会话保留时长" -#: settings/forms.py:160 +#: settings/forms.py:161 msgid "" "Units: days, Session, record, command will be delete if more than duration, " "only in database" @@ -2532,54 +2759,54 @@ msgstr "" "单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不" "受影响)" -#: settings/forms.py:164 +#: settings/forms.py:165 msgid "Telnet login regex" msgstr "Telnet 成功正则表达式" -#: settings/forms.py:165 +#: settings/forms.py:166 msgid "ex: Last\\s*login|success|成功" msgstr "" "登录telnet服务器成功后的提示正则表达式,如: Last\\s*login|success|成功 " -#: settings/forms.py:176 +#: settings/forms.py:177 msgid "MFA Secondary certification" msgstr "MFA 二次认证" -#: settings/forms.py:178 +#: settings/forms.py:179 msgid "" "After opening, the user login must use MFA secondary authentication (valid " "for all users, including administrators)" msgstr "开启后,用户登录必须使用MFA二次认证(对所有用户有效,包括管理员)" -#: settings/forms.py:184 +#: settings/forms.py:185 msgid "Limit the number of login failures" msgstr "限制登录失败次数" -#: settings/forms.py:188 +#: settings/forms.py:189 msgid "No logon interval" msgstr "禁止登录时间间隔" -#: settings/forms.py:190 +#: settings/forms.py:191 msgid "" "Tip: (unit/minute) if the user has failed to log in for a limited number of " "times, no login is allowed during this time interval." msgstr "" "提示:(单位:分)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录" -#: settings/forms.py:196 +#: settings/forms.py:197 msgid "Connection max idle time" msgstr "SSH最大空闲时间" -#: settings/forms.py:198 +#: settings/forms.py:199 msgid "" "If idle time more than it, disconnect connection(only ssh now) Unit: minute" msgstr "提示:(单位:分)如果超过该配置没有操作,连接会被断开(仅ssh)" -#: settings/forms.py:204 +#: settings/forms.py:205 msgid "Password expiration time" msgstr "密码过期时间" -#: settings/forms.py:207 +#: settings/forms.py:208 msgid "" "Tip: (unit: day) If the user does not update the password during the time, " "the user password will expire failure;The password expiration reminder mail " @@ -2589,55 +2816,50 @@ msgstr "" "提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期" "提醒邮件将在密码过期前5天内由系统(每天)自动发送给用户" -#: settings/forms.py:216 +#: settings/forms.py:217 msgid "Password minimum length" msgstr "密码最小长度 " -#: settings/forms.py:220 +#: settings/forms.py:221 msgid "Must contain capital letters" msgstr "必须包含大写字母" -#: settings/forms.py:222 +#: settings/forms.py:223 msgid "" "After opening, the user password changes and resets must contain uppercase " "letters" msgstr "开启后,用户密码修改、重置必须包含大写字母" -#: settings/forms.py:227 +#: settings/forms.py:228 msgid "Must contain lowercase letters" msgstr "必须包含小写字母" -#: settings/forms.py:228 +#: settings/forms.py:229 msgid "" "After opening, the user password changes and resets must contain lowercase " "letters" msgstr "开启后,用户密码修改、重置必须包含小写字母" -#: settings/forms.py:233 +#: settings/forms.py:234 msgid "Must contain numeric characters" msgstr "必须包含数字字符" -#: settings/forms.py:234 +#: settings/forms.py:235 msgid "" "After opening, the user password changes and resets must contain numeric " "characters" msgstr "开启后,用户密码修改、重置必须包含数字字符" -#: settings/forms.py:239 +#: settings/forms.py:240 msgid "Must contain special characters" msgstr "必须包含特殊字符" -#: settings/forms.py:240 +#: settings/forms.py:241 msgid "" "After opening, the user password changes and resets must contain special " "characters" msgstr "开启后,用户密码修改、重置必须包含特殊字符" -#: settings/models.py:33 users/models/authentication.py:54 -#: users/templates/users/user_detail.html:96 -msgid "Enabled" -msgstr "启用" - #: settings/models.py:126 users/templates/users/reset_password.html:68 #: users/templates/users/user_profile.html:20 msgid "Setting" @@ -2832,7 +3054,7 @@ msgstr "文档" msgid "Commercial support" msgstr "商业支持" -#: templates/_header_bar.html:89 templates/_nav_user.html:14 users/forms.py:141 +#: templates/_header_bar.html:89 templates/_nav_user.html:14 users/forms.py:121 #: users/templates/users/_user.html:43 #: users/templates/users/first_login.html:39 #: users/templates/users/user_password_update.html:40 @@ -2926,7 +3148,7 @@ msgstr "" #: 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:360 users/views/user.py:68 users/views/user.py:83 +#: users/views/login.py:151 users/views/user.py:68 users/views/user.py:83 #: users/views/user.py:113 users/views/user.py:194 users/views/user.py:355 #: users/views/user.py:405 users/views/user.py:445 msgid "Users" @@ -3374,87 +3596,11 @@ msgid "" "You should use your ssh client tools connect terminal: {}

    {}" msgstr "你可以使用ssh客户端工具连接终端" -#: users/api/auth.py:40 users/templates/users/login.html:52 -#: users/templates/users/new_login.html:71 -msgid "Log in frequently and try again later" -msgstr "登录频繁, 稍后重试" - -#: users/api/auth.py:67 -msgid "The user {} password has expired, please update." -msgstr "用户 {} 密码已经过期,请更新。" - -#: users/api/auth.py:92 -msgid "Please carry seed value and conduct MFA secondary certification" -msgstr "请携带seed值, 进行MFA二次认证" - -#: users/api/auth.py:204 -msgid "Please verify the user name and password first" -msgstr "请先进行用户名和密码验证" - -#: users/api/auth.py:216 -msgid "MFA certification failed" -msgstr "MFA认证失败" - #: users/api/user.py:145 msgid "Could not reset self otp, use profile reset instead" msgstr "不能再该页面重置MFA, 请去个人信息页面重置" -#: users/authentication.py:53 -msgid "Invalid signature header. No credentials provided." -msgstr "" - -#: users/authentication.py:56 -msgid "Invalid signature header. Signature string should not contain spaces." -msgstr "" - -#: users/authentication.py:63 -msgid "Invalid signature header. Format like AccessKeyId:Signature" -msgstr "" - -#: users/authentication.py:67 -msgid "" -"Invalid signature header. Signature string should not contain invalid " -"characters." -msgstr "" - -#: users/authentication.py:87 users/authentication.py:103 -msgid "Invalid signature." -msgstr "" - -#: users/authentication.py:94 -msgid "HTTP header: Date not provide or not %a, %d %b %Y %H:%M:%S GMT" -msgstr "" - -#: users/authentication.py:99 -msgid "Expired, more than 15 minutes" -msgstr "" - -#: users/authentication.py:106 -msgid "User disabled." -msgstr "用户已禁用" - -#: users/authentication.py:121 -msgid "Invalid token header. No credentials provided." -msgstr "" - -#: users/authentication.py:124 -msgid "Invalid token header. Sign string should not contain spaces." -msgstr "" - -#: users/authentication.py:131 -msgid "" -"Invalid token header. Sign string should not contain invalid characters." -msgstr "" - -#: users/authentication.py:142 -msgid "Invalid token or cache refreshed." -msgstr "" - -#: users/forms.py:41 -msgid "MFA code" -msgstr "MFA 验证码" - -#: users/forms.py:52 users/models/user.py:65 +#: users/forms.py:32 users/models/user.py:64 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:87 #: users/templates/users/user_list.html:25 @@ -3462,31 +3608,31 @@ msgstr "MFA 验证码" msgid "Role" msgstr "角色" -#: users/forms.py:55 users/forms.py:220 +#: users/forms.py:35 users/forms.py:200 msgid "ssh public key" msgstr "ssh公钥" -#: users/forms.py:56 users/forms.py:221 +#: users/forms.py:36 users/forms.py:201 msgid "ssh-rsa AAAA..." msgstr "" -#: users/forms.py:57 +#: users/forms.py:37 msgid "Paste user id_rsa.pub here." msgstr "复制用户公钥到这里" -#: users/forms.py:71 users/templates/users/user_detail.html:221 +#: users/forms.py:51 users/templates/users/user_detail.html:221 msgid "Join user groups" msgstr "添加到用户组" -#: users/forms.py:105 users/forms.py:235 +#: users/forms.py:85 users/forms.py:215 msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" -#: users/forms.py:109 users/forms.py:239 users/serializers/v1.py:38 +#: users/forms.py:89 users/forms.py:219 users/serializers/v1.py:38 msgid "Not a valid ssh public key" msgstr "ssh密钥不合法" -#: users/forms.py:147 +#: users/forms.py:127 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 " @@ -3495,11 +3641,11 @@ msgstr "" "提示:启用之后您将会在下次登录时进入MFA绑定流程;您也可以在(个人信息->快速修" "改->更改MFA设置)中直接绑定!" -#: users/forms.py:157 +#: users/forms.py:137 msgid "* Enable MFA authentication to make the account more secure." msgstr "* 启用MFA认证,使账号更加安全." -#: users/forms.py:167 +#: users/forms.py:147 msgid "" "In order to protect you and your company, please keep your account, password " "and key sensitive information properly. (for example: setting complex " @@ -3508,163 +3654,101 @@ msgstr "" "为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:" "设置复杂密码,启用MFA认证)" -#: users/forms.py:174 users/templates/users/first_login.html:48 +#: users/forms.py:154 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:180 +#: users/forms.py:160 msgid "Old password" msgstr "原来密码" -#: users/forms.py:185 +#: users/forms.py:165 msgid "New password" msgstr "新密码" -#: users/forms.py:190 +#: users/forms.py:170 msgid "Confirm password" msgstr "确认密码" -#: users/forms.py:200 +#: users/forms.py:180 msgid "Old password error" msgstr "原来密码错误" -#: users/forms.py:208 +#: users/forms.py:188 msgid "Password does not match" msgstr "密码不一致" -#: users/forms.py:218 +#: users/forms.py:198 msgid "Automatically configure and download the SSH key" msgstr "自动配置并下载SSH密钥" -#: users/forms.py:222 +#: users/forms.py:202 msgid "Paste your id_rsa.pub here." msgstr "复制你的公钥到这里" -#: users/forms.py:250 users/models/user.py:85 -#: 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 -#: users/templates/users/user_pubkey_update.html:43 -msgid "Public key" -msgstr "ssh公钥" - -#: users/forms.py:256 users/forms.py:261 users/forms.py:307 +#: users/forms.py:236 users/forms.py:241 users/forms.py:287 #: xpack/plugins/orgs/forms.py:30 msgid "Select users" msgstr "选择用户" -#: users/models/authentication.py:39 -msgid "Private Token" -msgstr "ssh密钥" - -#: users/models/authentication.py:53 users/templates/users/user_detail.html:98 -msgid "Disabled" -msgstr "禁用" - -#: users/models/authentication.py:55 users/models/authentication.py:65 -msgid "-" -msgstr "" - -#: users/models/authentication.py:66 -msgid "Username/password check failed" -msgstr "用户名/密码 校验失败" - -#: users/models/authentication.py:67 -msgid "MFA authentication failed" -msgstr "MFA 认证失败" - -#: users/models/authentication.py:68 -msgid "Username does not exist" -msgstr "用户名不存在" - -#: users/models/authentication.py:69 -msgid "Password expired" -msgstr "密码过期" - -#: users/models/authentication.py:74 xpack/plugins/cloud/models.py:164 -#: xpack/plugins/cloud/models.py:178 -msgid "Failed" -msgstr "失败" - -#: users/models/authentication.py:78 -msgid "Login type" -msgstr "登录方式" - -#: users/models/authentication.py:79 -msgid "Login ip" -msgstr "登录IP" - -#: users/models/authentication.py:80 -msgid "Login city" -msgstr "登录城市" - -#: users/models/authentication.py:81 -msgid "User agent" -msgstr "Agent" - -#: users/models/authentication.py:85 -msgid "Date login" -msgstr "登录日期" - -#: users/models/user.py:32 users/models/user.py:437 +#: users/models/user.py:31 users/models/user.py:453 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:34 +#: users/models/user.py:33 msgid "Application" msgstr "应用程序" -#: users/models/user.py:37 users/templates/users/user_profile.html:92 +#: users/models/user.py:36 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:38 users/templates/users/user_profile.html:90 +#: users/models/user.py:37 users/templates/users/user_profile.html:90 #: users/templates/users/user_profile.html:166 msgid "Enable" msgstr "启用" -#: users/models/user.py:39 users/templates/users/user_profile.html:88 +#: users/models/user.py:38 users/templates/users/user_profile.html:88 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:57 users/templates/users/user_detail.html:71 +#: users/models/user.py:56 users/templates/users/user_detail.html:71 #: users/templates/users/user_profile.html:59 msgid "Email" msgstr "邮件" -#: users/models/user.py:68 +#: users/models/user.py:67 msgid "Avatar" msgstr "头像" -#: users/models/user.py:71 users/templates/users/user_detail.html:82 +#: users/models/user.py:70 users/templates/users/user_detail.html:82 msgid "Wechat" msgstr "微信" -#: users/models/user.py:100 users/templates/users/user_detail.html:103 +#: users/models/user.py:99 users/templates/users/user_detail.html:103 #: users/templates/users/user_list.html:27 #: users/templates/users/user_profile.html:100 msgid "Source" msgstr "用户来源" -#: users/models/user.py:104 +#: users/models/user.py:103 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:128 users/templates/users/user_update.html:22 -#: users/views/login.py:254 users/views/login.py:313 users/views/user.py:418 +#: users/models/user.py:129 users/templates/users/user_update.html:22 +#: users/views/login.py:45 users/views/login.py:104 users/views/user.py:418 msgid "User auth from {}, go there change password" msgstr "用户认证源来自 {}, 请去相应系统修改密码" -#: users/models/user.py:440 +#: users/models/user.py:456 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/serializers/v2.py:40 +#: users/serializers/v2.py:34 msgid "name not unique" msgstr "名称重复" @@ -3880,7 +3964,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:78 +#: users/templates/users/user_detail.html:373 users/utils.py:77 msgid "Reset password" msgstr "重置密码" @@ -4204,11 +4288,11 @@ msgstr "新的公钥已设置成功,请下载对应的私钥" msgid "Update user" msgstr "更新用户" -#: users/utils.py:39 +#: users/utils.py:38 msgid "Create account successfully" msgstr "创建账户成功" -#: users/utils.py:41 +#: users/utils.py:40 #, python-format msgid "" "\n" @@ -4253,7 +4337,7 @@ msgstr "" "
    \n" " " -#: users/utils.py:80 +#: users/utils.py:79 #, python-format msgid "" "\n" @@ -4297,11 +4381,11 @@ msgstr "" "
    \n" " " -#: users/utils.py:111 +#: users/utils.py:110 msgid "Security notice" msgstr "安全通知" -#: users/utils.py:113 +#: users/utils.py:112 #, python-format msgid "" "\n" @@ -4350,11 +4434,11 @@ msgstr "" "
    \n" " " -#: users/utils.py:149 +#: users/utils.py:148 msgid "SSH Key Reset" msgstr "重置ssh密钥" -#: users/utils.py:151 +#: users/utils.py:150 #, python-format msgid "" "\n" @@ -4379,15 +4463,15 @@ msgstr "" "
    \n" " " -#: users/utils.py:184 +#: users/utils.py:183 msgid "User not exist" msgstr "用户不存在" -#: users/utils.py:186 +#: users/utils.py:185 msgid "Disabled or expired" msgstr "禁用或失效" -#: users/utils.py:199 +#: users/utils.py:198 msgid "Password or SSH public key invalid" msgstr "密码或密钥不合法" @@ -4403,56 +4487,40 @@ msgstr "更新用户组" msgid "User group granted asset" msgstr "用户组授权资产" -#: users/views/login.py:80 -msgid "Please enable cookies and try again." -msgstr "设置你的浏览器支持cookie" - -#: users/views/login.py:202 users/views/user.py:532 users/views/user.py:557 -msgid "MFA code invalid, or ntp sync server time" -msgstr "MFA验证码不正确,或者服务器端时间不对" - -#: users/views/login.py:234 -msgid "Logout success" -msgstr "退出登录成功" - -#: users/views/login.py:235 -msgid "Logout success, return login page" -msgstr "退出登录成功,返回到登录页面" - -#: users/views/login.py:251 +#: users/views/login.py:42 msgid "Email address invalid, please input again" msgstr "邮箱地址错误,重新输入" -#: users/views/login.py:267 +#: users/views/login.py:58 msgid "Send reset password message" msgstr "发送重置密码邮件" -#: users/views/login.py:268 +#: users/views/login.py:59 msgid "Send reset password mail success, login your mail box and follow it " msgstr "" "发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)" -#: users/views/login.py:281 +#: users/views/login.py:72 msgid "Reset password success" msgstr "重置密码成功" -#: users/views/login.py:282 +#: users/views/login.py:73 msgid "Reset password success, return to login page" msgstr "重置密码成功,返回到登录页面" -#: users/views/login.py:297 users/views/login.py:316 +#: users/views/login.py:88 users/views/login.py:107 msgid "Token invalid or expired" msgstr "Token错误或失效" -#: users/views/login.py:309 +#: users/views/login.py:100 msgid "Password not same" msgstr "密码不一致" -#: users/views/login.py:322 users/views/user.py:128 users/views/user.py:428 +#: users/views/login.py:113 users/views/user.py:128 users/views/user.py:428 msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" -#: users/views/login.py:360 +#: users/views/login.py:151 msgid "First login" msgstr "首次登录" diff --git a/apps/ops/inventory.py b/apps/ops/inventory.py index 7e21d156d..98ee94acc 100644 --- a/apps/ops/inventory.py +++ b/apps/ops/inventory.py @@ -4,11 +4,16 @@ from .ansible.inventory import BaseInventory from assets.utils import get_assets_by_id_list, get_system_user_by_id +from common.utils import get_logger + __all__ = [ 'JMSInventory' ] +logger = get_logger(__file__) + + class JMSInventory(BaseInventory): """ JMS Inventory is the manager with jumpserver assets, so you can @@ -18,7 +23,7 @@ class JMSInventory(BaseInventory): """ :param host_id_list: ["test1", ] :param run_as_admin: True 是否使用管理用户去执行, 每台服务器的管理用户可能不同 - :param run_as: 是否统一使用某个系统用户去执行 + :param run_as: 用户名(添加了统一的资产用户管理器之后AssetUserManager加上之后修改为username) :param become_info: 是否become成某个用户去执行 """ self.assets = assets @@ -33,8 +38,8 @@ class JMSInventory(BaseInventory): host_list.append(info) if run_as: - run_user_info = self.get_run_user_info() for host in host_list: + run_user_info = self.get_run_user_info(host) host.update(run_user_info) if become_info: @@ -69,12 +74,20 @@ class JMSInventory(BaseInventory): info["groups"].append("domain_"+asset.domain.name) return info - def get_run_user_info(self): - system_user = self.run_as - if not system_user: + def get_run_user_info(self, host): + from assets.backends.multi 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) + except Exception as e: + logger.error(e, exc_info=True) return {} else: - return system_user._to_secret_json() + return run_user._to_secret_json() @staticmethod def make_proxy_command(asset): diff --git a/apps/ops/models/adhoc.py b/apps/ops/models/adhoc.py index e9d264ed1..f4d67b945 100644 --- a/apps/ops/models/adhoc.py +++ b/apps/ops/models/adhoc.py @@ -149,7 +149,7 @@ class AdHoc(models.Model): _options: ansible options, more see ops.ansible.runner.Options _hosts: ["hostname1", "hostname2"], hostname must be unique key of cmdb run_as_admin: if true, then need get every host admin user run it, because every host may be have different admin user, so we choise host level - run_as: if not run as admin, it run it as a system/common user from cmdb + run_as: username(Add the uniform AssetUserManager and change it to username) _become: May be using become [sudo, su] options. {method: "sudo", user: "user", pass: "pass"] pattern: Even if we set _hosts, We only use that to make inventory, We also can set `patter` to run task on match hosts """ @@ -161,7 +161,7 @@ class AdHoc(models.Model): _hosts = models.TextField(blank=True, verbose_name=_('Hosts')) # ['hostname1', 'hostname2'] hosts = models.ManyToManyField('assets.Asset', verbose_name=_("Host")) run_as_admin = models.BooleanField(default=False, verbose_name=_('Run as admin')) - run_as = models.ForeignKey('assets.SystemUser', null=True, on_delete=models.CASCADE) + run_as = models.CharField(max_length=64, default='', null=True, verbose_name=_('Username')) _become = models.CharField(max_length=1024, default='', verbose_name=_("Become")) created_by = models.CharField(max_length=64, default='', null=True, verbose_name=_('Create by')) date_created = models.DateTimeField(auto_now_add=True, db_index=True)