From ec8dca90d6a98e8eebde61390b6b03e7c3fa820b Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Thu, 8 Jul 2021 14:23:18 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=95=B4=E5=90=88=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=94=A8=E6=88=B7=E5=92=8C=E7=AE=A1=E7=90=86=E7=94=A8?= =?UTF-8?q?=E6=88=B7=20(#6236)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: 整合系统用户和管理用户 * stash stash perf: 优化系统用户和资产的表结构 * perf: 添加信号 * perf: 添加算法 * perf: 去掉 asset user backends * perf: 整理系统用户api * perfF: 暂存一下 * stash * perf: 暂存一下 * perf: 暂存 * xxx * perf: ... * stash it * xxx * xxx * xxx * xxx * xxx * stash it * 修改Protocols * perf: 修改创建authbook信号 * perf: 添加auth info * .stash * perf: 基本完成 * perf: 修复完成 * perf: 修复更改的id * perf: 修复迁移过去数量不对的问题 * perf: 修改systemuser * fix: 修复批量编辑近期的问题 * fix: 修复authbook加载的问题 * xxx Co-authored-by: ibuler --- apps/acls/serializers/login_asset_acl.py | 2 +- apps/assets/api/__init__.py | 2 +- apps/assets/api/accounts.py | 51 ++ apps/assets/api/admin_user.py | 93 +--- apps/assets/api/asset.py | 5 +- apps/assets/api/asset_user.py | 151 ------ apps/assets/api/cmd_filter.py | 4 +- apps/assets/api/system_user.py | 3 +- apps/assets/api/system_user_relation.py | 12 +- apps/assets/backends/__init__.py | 1 - apps/assets/backends/base.py | 48 -- apps/assets/backends/db.py | 332 ------------ apps/assets/backends/manager.py | 162 ------ apps/assets/backends/utils.py | 7 - apps/assets/backends/vault.py | 4 - .../assets/migrations/0071_systemuser_type.py | 66 +++ .../migrations/0072_historicalauthbook.py | 85 +++ .../migrations/0073_auto_20210606_1142.py | 105 ++++ .../0074_remove_systemuser_assets.py | 27 + .../migrations/0075_auto_20210705_1759.py | 53 ++ apps/assets/models/__init__.py | 1 - apps/assets/models/asset.py | 66 ++- apps/assets/models/asset_user.py | 15 - apps/assets/models/authbook.py | 130 ++--- apps/assets/models/base.py | 129 +---- apps/assets/models/domain.py | 12 +- apps/assets/models/user.py | 505 +++++++++--------- apps/assets/models/utils.py | 73 +-- apps/assets/serializers/__init__.py | 2 +- apps/assets/serializers/account.py | 42 ++ apps/assets/serializers/admin_user.py | 50 +- apps/assets/serializers/asset.py | 63 ++- apps/assets/serializers/asset_user.py | 98 ---- apps/assets/serializers/base.py | 18 - apps/assets/serializers/cmd_filter.py | 3 - apps/assets/serializers/domain.py | 13 - apps/assets/serializers/favorite_asset.py | 2 - apps/assets/serializers/label.py | 2 - apps/assets/serializers/system_user.py | 75 ++- apps/assets/signals_handler/__init__.py | 4 +- apps/assets/signals_handler/asset.py | 128 +++++ apps/assets/signals_handler/authbook.py | 46 ++ apps/assets/signals_handler/common.py | 223 -------- apps/assets/signals_handler/system_user.py | 140 +++++ apps/assets/tasks/__init__.py | 3 +- ...onnectivity.py => account_connectivity.py} | 71 +-- apps/assets/tasks/admin_user_connectivity.py | 69 --- apps/assets/tasks/asset_connectivity.py | 33 +- apps/assets/urls/api_urls.py | 13 +- apps/audits/serializers.py | 2 - apps/authentication/api/connection_token.py | 4 +- apps/authentication/api/mfa.py | 11 +- apps/common/drf/api.py | 10 +- apps/common/mixins/api.py | 42 +- apps/jumpserver/settings/base.py | 2 + apps/locale/zh/LC_MESSAGES/django.mo | Bin 76655 -> 76045 bytes apps/locale/zh/LC_MESSAGES/django.po | 313 ++++++----- apps/ops/inventory.py | 17 +- apps/ops/models/adhoc.py | 6 - apps/orgs/models.py | 4 +- apps/orgs/serializers.py | 2 - apps/orgs/signals_handler/common.py | 31 +- apps/perms/models/asset_permission.py | 4 +- .../application/permission_relation.py | 12 +- .../serializers/asset/permission_relation.py | 14 +- apps/terminal/models/session.py | 6 +- apps/terminal/serializers/session.py | 2 - apps/terminal/serializers/terminal.py | 1 - apps/users/models/user.py | 4 +- apps/users/serializers/group.py | 2 - requirements/requirements.txt | 1 + utils/generate_fake_data/resources/assets.py | 2 +- 72 files changed, 1524 insertions(+), 2210 deletions(-) create mode 100644 apps/assets/api/accounts.py delete mode 100644 apps/assets/api/asset_user.py delete mode 100644 apps/assets/backends/__init__.py delete mode 100644 apps/assets/backends/base.py delete mode 100644 apps/assets/backends/manager.py delete mode 100644 apps/assets/backends/utils.py delete mode 100644 apps/assets/backends/vault.py create mode 100644 apps/assets/migrations/0071_systemuser_type.py create mode 100644 apps/assets/migrations/0072_historicalauthbook.py create mode 100644 apps/assets/migrations/0073_auto_20210606_1142.py create mode 100644 apps/assets/migrations/0074_remove_systemuser_assets.py create mode 100644 apps/assets/migrations/0075_auto_20210705_1759.py delete mode 100644 apps/assets/models/asset_user.py create mode 100644 apps/assets/serializers/account.py delete mode 100644 apps/assets/serializers/asset_user.py create mode 100644 apps/assets/signals_handler/asset.py create mode 100644 apps/assets/signals_handler/authbook.py create mode 100644 apps/assets/signals_handler/system_user.py rename apps/assets/tasks/{asset_user_connectivity.py => account_connectivity.py} (51%) delete mode 100644 apps/assets/tasks/admin_user_connectivity.py diff --git a/apps/acls/serializers/login_asset_acl.py b/apps/acls/serializers/login_asset_acl.py index bbb31af94..df62a04f8 100644 --- a/apps/acls/serializers/login_asset_acl.py +++ b/apps/acls/serializers/login_asset_acl.py @@ -54,7 +54,7 @@ class LoginAssetACLSystemUsersSerializer(serializers.Serializer): protocol_group = serializers.ListField( default=['*'], child=serializers.CharField(max_length=16), label=_('Protocol'), help_text=protocol_group_help_text.format( - ', '.join([SystemUser.PROTOCOL_SSH, SystemUser.PROTOCOL_TELNET]) + ', '.join([SystemUser.Protocol.ssh, SystemUser.Protocol.telnet]) ) ) diff --git a/apps/assets/api/__init__.py b/apps/assets/api/__init__.py index 59cb8e602..3178c8e3f 100644 --- a/apps/assets/api/__init__.py +++ b/apps/assets/api/__init__.py @@ -4,9 +4,9 @@ from .asset import * from .label import * from .system_user import * from .system_user_relation import * +from .accounts import * from .node import * from .domain import * from .cmd_filter import * -from .asset_user import * from .gathered_user import * from .favorite_asset import * diff --git a/apps/assets/api/accounts.py b/apps/assets/api/accounts.py new file mode 100644 index 000000000..ff40d2788 --- /dev/null +++ b/apps/assets/api/accounts.py @@ -0,0 +1,51 @@ +from django.db.models import F +from django.conf import settings +from rest_framework.decorators import action +from rest_framework.response import Response + +from orgs.mixins.api import OrgBulkModelViewSet +from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, NeedMFAVerify +from ..tasks.account_connectivity import test_accounts_connectivity_manual +from ..models import AuthBook +from .. import serializers + +__all__ = ['AccountViewSet', 'AccountSecretsViewSet'] + + +class AccountViewSet(OrgBulkModelViewSet): + model = AuthBook + filterset_fields = ("username", "asset", "systemuser") + search_fields = filterset_fields + serializer_classes = { + 'default': serializers.AccountSerializer, + 'verify_account': serializers.AssetTaskSerializer + } + permission_classes = (IsOrgAdmin,) + + def get_queryset(self): + queryset = super().get_queryset()\ + .annotate(ip=F('asset__ip'))\ + .annotate(hostname=F('asset__hostname')) + return queryset + + @action(methods=['post'], detail=True, url_path='verify') + def verify_account(self, request, *args, **kwargs): + account = super().get_object() + task = test_accounts_connectivity_manual.delay([account]) + return Response(data={'task': task.id}) + + +class AccountSecretsViewSet(AccountViewSet): + """ + 因为可能要导出所有账号,所以单独建立了一个 viewset + """ + serializer_classes = { + 'default': serializers.AccountSecretSerializer + } + permission_classes = (IsOrgAdmin, NeedMFAVerify) + http_method_names = ['get'] + + def get_permissions(self): + if not settings.SECURITY_VIEW_AUTH_NEED_MFA: + self.permission_classes = [IsOrgAdminOrAppUser] + return super().get_permissions() diff --git a/apps/assets/api/admin_user.py b/apps/assets/api/admin_user.py index 5ad648635..043a30a1b 100644 --- a/apps/assets/api/admin_user.py +++ b/apps/assets/api/admin_user.py @@ -1,109 +1,28 @@ - - -from django.db import transaction from django.db.models import Count -from django.shortcuts import get_object_or_404 -from django.utils.translation import ugettext as _ -from rest_framework import status -from rest_framework.response import Response -from orgs.mixins.api import OrgBulkModelViewSet -from orgs.mixins import generics +from orgs.mixins.api import OrgBulkModelViewSet from common.utils import get_logger from ..hands import IsOrgAdmin -from ..models import AdminUser, Asset +from ..models import SystemUser from .. import serializers -from ..tasks import test_admin_user_connectivity_manual logger = get_logger(__file__) -__all__ = [ - 'AdminUserViewSet', 'ReplaceNodesAdminUserApi', - 'AdminUserTestConnectiveApi', 'AdminUserAuthApi', - 'AdminUserAssetsListView', -] +__all__ = ['AdminUserViewSet'] +# 兼容一下老的 api class AdminUserViewSet(OrgBulkModelViewSet): """ Admin user api set, for add,delete,update,list,retrieve resource """ - model = AdminUser + model = SystemUser filterset_fields = ("name", "username") search_fields = filterset_fields serializer_class = serializers.AdminUserSerializer permission_classes = (IsOrgAdmin,) - serializer_classes = { - 'default': serializers.AdminUserSerializer, - 'retrieve': serializers.AdminUserDetailSerializer, - } def get_queryset(self): - queryset = super().get_queryset() + queryset = super().get_queryset().filter(type=SystemUser.Type.admin) queryset = queryset.annotate(assets_amount=Count('assets')) return queryset - - def destroy(self, request, *args, **kwargs): - instance = self.get_object() - has_related_asset = instance.assets.exists() - if has_related_asset: - data = {'msg': _('Deleted failed, There are related assets')} - return Response(data=data, status=status.HTTP_400_BAD_REQUEST) - return super().destroy(request, *args, **kwargs) - - -class AdminUserAuthApi(generics.UpdateAPIView): - model = AdminUser - serializer_class = serializers.AdminUserAuthSerializer - permission_classes = (IsOrgAdmin,) - - -class ReplaceNodesAdminUserApi(generics.UpdateAPIView): - model = AdminUser - serializer_class = serializers.ReplaceNodeAdminUserSerializer - permission_classes = (IsOrgAdmin,) - - def update(self, request, *args, **kwargs): - admin_user = self.get_object() - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - nodes = serializer.validated_data['nodes'] - assets = [] - for node in nodes: - assets.extend([asset.id for asset in node.get_all_assets()]) - - with transaction.atomic(): - Asset.objects.filter(id__in=assets).update(admin_user=admin_user) - - return Response({"msg": "ok"}) - else: - return Response({'error': serializer.errors}, status=400) - - -class AdminUserTestConnectiveApi(generics.RetrieveAPIView): - """ - Test asset admin user assets_connectivity - """ - model = AdminUser - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.TaskIDSerializer - - def retrieve(self, request, *args, **kwargs): - admin_user = self.get_object() - task = test_admin_user_connectivity_manual.delay(admin_user) - return Response({"task": task.id}) - - -class AdminUserAssetsListView(generics.ListAPIView): - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.AssetSimpleSerializer - filterset_fields = ("hostname", "ip") - search_fields = filterset_fields - - def get_object(self): - pk = self.kwargs.get('pk') - return get_object_or_404(AdminUser, pk=pk) - - def get_queryset(self): - admin_user = self.get_object() - return admin_user.get_related_assets() diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index 2176f97aa..19e3dc9db 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -33,8 +33,7 @@ class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet): filterset_fields = { 'hostname': ['exact'], 'ip': ['exact'], - 'systemuser__id': ['exact'], - 'admin_user__id': ['exact'], + 'system_users__id': ['exact'], 'platform__base': ['exact'], 'is_active': ['exact'], 'protocols': ['exact', 'icontains'] @@ -43,7 +42,7 @@ class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet): ordering_fields = ("hostname", "ip", "port", "cpu_cores") serializer_classes = { 'default': serializers.AssetSerializer, - 'display': serializers.AssetDisplaySerializer, + 'single': serializers.AssetVerboseSerializer, } permission_classes = (IsOrgAdminOrAppUser,) extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend] diff --git a/apps/assets/api/asset_user.py b/apps/assets/api/asset_user.py deleted file mode 100644 index da0fe8c7e..000000000 --- a/apps/assets/api/asset_user.py +++ /dev/null @@ -1,151 +0,0 @@ -# -*- coding: utf-8 -*- -# -import coreapi -from django.conf import settings -from rest_framework.response import Response -from rest_framework import generics, filters -from rest_framework_bulk import BulkModelViewSet - -from common.permissions import IsOrgAdminOrAppUser, NeedMFAVerify -from common.utils import get_object_or_none, get_logger -from common.mixins import CommonApiMixin -from ..backends import AssetUserManager -from ..models import Node -from .. import serializers -from ..tasks import ( - test_asset_users_connectivity_manual -) - - -__all__ = [ - 'AssetUserViewSet', 'AssetUserAuthInfoViewSet', 'AssetUserTaskCreateAPI', -] - - -logger = get_logger(__name__) - - -class AssetUserFilterBackend(filters.BaseFilterBackend): - def filter_queryset(self, request, queryset, view): - kwargs = {} - for field in view.filterset_fields: - value = request.GET.get(field) - if not value: - continue - if field == "node_id": - value = get_object_or_none(Node, pk=value) - kwargs["node"] = value - continue - elif field == "asset_id": - field = "asset" - kwargs[field] = value - if kwargs: - queryset = queryset.filter(**kwargs) - logger.debug("Filter {}".format(kwargs)) - return queryset - - -class AssetUserSearchBackend(filters.BaseFilterBackend): - def filter_queryset(self, request, queryset, view): - value = request.GET.get('search') - if not value: - return queryset - queryset = queryset.search(value) - return queryset - - -class AssetUserLatestFilterBackend(filters.BaseFilterBackend): - def get_schema_fields(self, view): - return [ - coreapi.Field( - name='latest', location='query', required=False, - type='string', example='1', - description='Only the latest version' - ) - ] - - def filter_queryset(self, request, queryset, view): - latest = request.GET.get('latest') == '1' - if latest: - queryset = queryset.distinct() - return queryset - - -class AssetUserViewSet(CommonApiMixin, BulkModelViewSet): - serializer_classes = { - 'default': serializers.AssetUserWriteSerializer, - 'display': serializers.AssetUserReadSerializer, - 'retrieve': serializers.AssetUserReadSerializer, - } - permission_classes = [IsOrgAdminOrAppUser] - filterset_fields = [ - "id", "ip", "hostname", "username", - "asset_id", "node_id", - "prefer", "prefer_id", - ] - search_fields = ["ip", "hostname", "username"] - filter_backends = [ - AssetUserFilterBackend, AssetUserSearchBackend, - AssetUserLatestFilterBackend, - ] - - def allow_bulk_destroy(self, qs, filtered): - return False - - def get_object(self): - pk = self.kwargs.get("pk") - if pk is None: - return - queryset = self.get_queryset() - obj = queryset.get(id=pk) - return obj - - def perform_destroy(self, instance): - manager = AssetUserManager() - manager.delete(instance) - - def get_queryset(self): - manager = AssetUserManager() - queryset = manager.all() - return queryset - - -class AssetUserAuthInfoViewSet(AssetUserViewSet): - serializer_classes = {"default": serializers.AssetUserAuthInfoSerializer} - http_method_names = ['get', 'post'] - permission_classes = [IsOrgAdminOrAppUser] - - def get_permissions(self): - if settings.SECURITY_VIEW_AUTH_NEED_MFA: - self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify] - return super().get_permissions() - - -class AssetUserTaskCreateAPI(generics.CreateAPIView): - permission_classes = (IsOrgAdminOrAppUser,) - serializer_class = serializers.AssetUserTaskSerializer - filter_backends = AssetUserViewSet.filter_backends - filterset_fields = AssetUserViewSet.filterset_fields - - def get_asset_users(self): - manager = AssetUserManager() - queryset = manager.all() - for cls in self.filter_backends: - queryset = cls().filter_queryset(self.request, queryset, self) - return list(queryset) - - def perform_create(self, serializer): - asset_users = self.get_asset_users() - # action = serializer.validated_data["action"] - # only this - # if action == "test": - task = test_asset_users_connectivity_manual.delay(asset_users) - data = getattr(serializer, '_data', {}) - data["task"] = task.id - setattr(serializer, '_data', data) - return task - - def get_exception_handler(self): - def handler(e, context): - return Response({"error": str(e)}, status=400) - return handler diff --git a/apps/assets/api/cmd_filter.py b/apps/assets/api/cmd_filter.py index 56cbbd6c3..ca22c494f 100644 --- a/apps/assets/api/cmd_filter.py +++ b/apps/assets/api/cmd_filter.py @@ -2,14 +2,12 @@ # from rest_framework.response import Response -from rest_framework.generics import CreateAPIView, RetrieveDestroyAPIView +from rest_framework.generics import CreateAPIView from django.shortcuts import get_object_or_404 from common.utils import reverse from common.utils import lazyproperty from orgs.mixins.api import OrgBulkModelViewSet -from orgs.utils import tmp_to_root_org -from tickets.models import Ticket from tickets.api import GenericTicketStatusRetrieveCloseAPI from ..hands import IsOrgAdmin, IsAppUser from ..models import CommandFilter, CommandFilterRule diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index 25c3f58f4..6ffd1c3c5 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -32,7 +32,8 @@ class SystemUserViewSet(OrgBulkModelViewSet): filterset_fields = { 'name': ['exact'], 'username': ['exact'], - 'protocol': ['exact', 'in'] + 'protocol': ['exact', 'in'], + 'type': ['exact', 'in'], } search_fields = filterset_fields serializer_class = serializers.SystemUserSerializer diff --git a/apps/assets/api/system_user_relation.py b/apps/assets/api/system_user_relation.py index 66b9dc6ee..86037229e 100644 --- a/apps/assets/api/system_user_relation.py +++ b/apps/assets/api/system_user_relation.py @@ -6,6 +6,7 @@ from django.db.models.signals import m2m_changed from django.db.models.functions import Concat from common.permissions import IsOrgAdmin +from common.utils import get_logger from orgs.mixins.api import OrgBulkModelViewSet from orgs.utils import current_org from .. import models, serializers @@ -15,6 +16,8 @@ __all__ = [ 'SystemUserUserRelationViewSet', ] +logger = get_logger(__name__) + class RelationMixin: def get_queryset(self): @@ -24,8 +27,8 @@ class RelationMixin: queryset = queryset.filter(systemuser__org_id=org_id) queryset = queryset.annotate(systemuser_display=Concat( - F('systemuser__name'), Value('('), F('systemuser__username'), - Value(')') + F('systemuser__name'), Value('('), + F('systemuser__username'), Value(')') )) return queryset @@ -41,10 +44,11 @@ class RelationMixin: system_users_objects_map[i.systemuser].append(_id) sender = self.get_sender() - for system_user, objects in system_users_objects_map.items(): + for system_user, object_ids in system_users_objects_map.items(): + logger.debug('System user relation changed, send m2m_changed signals') m2m_changed.send( sender=sender, instance=system_user, action='post_add', - reverse=False, model=model, pk_set=objects + reverse=False, model=model, pk_set=set(object_ids) ) def get_sender(self): diff --git a/apps/assets/backends/__init__.py b/apps/assets/backends/__init__.py deleted file mode 100644 index 9a22a23dd..000000000 --- a/apps/assets/backends/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .manager import AssetUserManager diff --git a/apps/assets/backends/base.py b/apps/assets/backends/base.py deleted file mode 100644 index 17115afaa..000000000 --- a/apps/assets/backends/base.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -# -from abc import abstractmethod - -from ..models import Asset - - -class BaseBackend: - @abstractmethod - def all(self): - pass - - @abstractmethod - def filter(self, username=None, hostname=None, ip=None, assets=None, - node=None, prefer_id=None, **kwargs): - pass - - @abstractmethod - def search(self, item): - pass - - @abstractmethod - def get_queryset(self): - pass - - @abstractmethod - def delete(self, union_id): - pass - - @staticmethod - def qs_to_values(qs): - values = qs.values( - 'hostname', 'ip', "asset_id", - 'name', 'username', 'password', 'private_key', 'public_key', - 'score', 'version', - "asset_username", "union_id", - 'date_created', 'date_updated', - 'org_id', 'backend', 'backend_display' - ) - return values - - @staticmethod - def make_assets_as_ids(assets): - if not assets: - return [] - if isinstance(assets[0], Asset): - assets = [a.id for a in assets] - return assets diff --git a/apps/assets/backends/db.py b/apps/assets/backends/db.py index 0e5d288b9..e69de29bb 100644 --- a/apps/assets/backends/db.py +++ b/apps/assets/backends/db.py @@ -1,332 +0,0 @@ -# -*- coding: utf-8 -*- -# -from django.utils.translation import ugettext as _ -from functools import reduce -from django.db.models import F, CharField, Value, IntegerField, Q, Count -from django.db.models.functions import Concat -from rest_framework.exceptions import PermissionDenied - -from common.utils import get_object_or_none -from orgs.utils import current_org -from ..models import AuthBook, SystemUser, Asset, AdminUser -from .base import BaseBackend - - -class DBBackend(BaseBackend): - union_id_length = 2 - - def __init__(self, queryset=None): - if queryset is None: - queryset = self.all() - self.queryset = queryset - - def _clone(self): - return self.__class__(self.queryset) - - def all(self): - return AuthBook.objects.none() - - def count(self): - return self.queryset.count() - - def get_queryset(self): - return self.queryset - - def delete(self, union_id): - cleaned_union_id = union_id.split('_') - # 如果union_id通不过本检查,代表可能不是本backend, 应该返回空 - if not self._check_union_id(union_id, cleaned_union_id): - return - return self._perform_delete_by_union_id(cleaned_union_id) - - def _perform_delete_by_union_id(self, union_id_cleaned): - pass - - def filter(self, assets=None, node=None, prefer=None, prefer_id=None, - union_id=None, id__in=None, **kwargs): - clone = self._clone() - clone._filter_union_id(union_id) - clone._filter_prefer(prefer, prefer_id) - clone._filter_node(node) - clone._filter_assets(assets) - clone._filter_other(kwargs) - clone._filter_id_in(id__in) - return clone - - def _filter_union_id(self, union_id): - if not union_id: - return - cleaned_union_id = union_id.split('_') - # 如果union_id通不过本检查,代表可能不是本backend, 应该返回空 - if not self._check_union_id(union_id, cleaned_union_id): - self.queryset = self.queryset.none() - return - return self._perform_filter_union_id(union_id, cleaned_union_id) - - def _check_union_id(self, union_id, cleaned_union_id): - return union_id and len(cleaned_union_id) == self.union_id_length - - def _perform_filter_union_id(self, union_id, union_id_cleaned): - self.queryset = self.queryset.filter(union_id=union_id) - - def _filter_assets(self, assets): - asset_ids = self.make_assets_as_ids(assets) - if asset_ids: - self.queryset = self.queryset.filter(asset_id__in=asset_ids) - - def _filter_node(self, node): - pass - - def _filter_id_in(self, ids): - if ids and isinstance(ids, list): - self.queryset = self.queryset.filter(union_id__in=ids) - - @staticmethod - def clean_kwargs(kwargs): - return {k: v for k, v in kwargs.items() if v} - - def _filter_other(self, kwargs): - kwargs = self.clean_kwargs(kwargs) - if kwargs: - self.queryset = self.queryset.filter(**kwargs) - - def _filter_prefer(self, prefer, prefer_id): - pass - - def search(self, item): - qs = [] - for i in ['hostname', 'ip', 'username']: - kwargs = {i + '__startswith': item} - qs.append(Q(**kwargs)) - q = reduce(lambda x, y: x | y, qs) - clone = self._clone() - clone.queryset = clone.queryset.filter(q).distinct() - return clone - - -class SystemUserBackend(DBBackend): - model = SystemUser.assets.through - backend = 'system_user' - backend_display = _('System user') - prefer = backend - base_score = 0 - union_id_length = 2 - - def _filter_prefer(self, prefer, prefer_id): - if prefer and prefer != self.prefer: - self.queryset = self.queryset.none() - - if prefer_id: - self.queryset = self.queryset.filter(systemuser__id=prefer_id) - - def _perform_filter_union_id(self, union_id, union_id_cleaned): - system_user_id, asset_id = union_id_cleaned - self.queryset = self.queryset.filter( - asset_id=asset_id, systemuser__id=system_user_id, - ) - - def _perform_delete_by_union_id(self, union_id_cleaned): - system_user_id, asset_id = union_id_cleaned - system_user = get_object_or_none(SystemUser, pk=system_user_id) - asset = get_object_or_none(Asset, pk=asset_id) - if all((system_user, asset)): - system_user.assets.remove(asset) - - def _filter_node(self, node): - if node: - self.queryset = self.queryset.filter(asset__nodes__id=node.id) - - def get_annotate(self): - kwargs = dict( - hostname=F("asset__hostname"), - ip=F("asset__ip"), - name=F("systemuser__name"), - username=F("systemuser__username"), - password=F("systemuser__password"), - private_key=F("systemuser__private_key"), - public_key=F("systemuser__public_key"), - score=F("systemuser__priority") + self.base_score, - version=Value(0, IntegerField()), - date_created=F("systemuser__date_created"), - date_updated=F("systemuser__date_updated"), - asset_username=Concat(F("asset__id"), Value("_"), - F("systemuser__username"), - output_field=CharField()), - union_id=Concat(F("systemuser_id"), Value("_"), F("asset_id"), - output_field=CharField()), - org_id=F("asset__org_id"), - backend=Value(self.backend, CharField()), - backend_display=Value(self.backend_display, CharField()), - ) - return kwargs - - def get_filter(self): - return dict( - systemuser__username_same_with_user=False, - ) - - def all(self): - kwargs = self.get_annotate() - filters = self.get_filter() - qs = self.model.objects.all().annotate(**kwargs) - if not current_org.is_root(): - filters['org_id'] = current_org.org_id() - qs = qs.filter(**filters) - qs = self.qs_to_values(qs) - return qs - - -class DynamicSystemUserBackend(SystemUserBackend): - backend = 'system_user_dynamic' - backend_display = _('System user(Dynamic)') - prefer = 'system_user' - union_id_length = 3 - - def get_annotate(self): - kwargs = super().get_annotate() - kwargs.update(dict( - name=Concat( - F("systemuser__users__name"), Value('('), F("systemuser__name"), Value(')'), - output_field=CharField() - ), - username=F("systemuser__users__username"), - asset_username=Concat( - F("asset__id"), Value("_"), - F("systemuser__users__username"), - output_field=CharField() - ), - union_id=Concat( - F("systemuser_id"), Value("_"), F("asset_id"), - Value("_"), F("systemuser__users__id"), - output_field=CharField() - ), - users_count=Count('systemuser__users'), - )) - return kwargs - - def _perform_filter_union_id(self, union_id, union_id_cleaned): - system_user_id, asset_id, user_id = union_id_cleaned - self.queryset = self.queryset.filter( - asset_id=asset_id, systemuser_id=system_user_id, - union_id=union_id, - ) - - def _perform_delete_by_union_id(self, union_id_cleaned): - system_user_id, asset_id, user_id = union_id_cleaned - system_user = get_object_or_none(SystemUser, pk=system_user_id) - if not system_user: - return - system_user.users.remove(user_id) - if system_user.users.count() == 0: - system_user.assets.remove(asset_id) - - def get_filter(self): - return dict( - users_count__gt=0, - systemuser__username_same_with_user=True - ) - - -class AdminUserBackend(DBBackend): - model = Asset - backend = 'admin_user' - backend_display = _('Admin user') - prefer = backend - base_score = 200 - - def _filter_prefer(self, prefer, prefer_id): - if prefer and prefer != self.backend: - self.queryset = self.queryset.none() - if prefer_id: - self.queryset = self.queryset.filter(admin_user__id=prefer_id) - - def _filter_node(self, node): - if node: - self.queryset = self.queryset.filter(nodes__id=node.id) - - def _perform_filter_union_id(self, union_id, union_id_cleaned): - admin_user_id, asset_id = union_id_cleaned - self.queryset = self.queryset.filter( - id=asset_id, admin_user_id=admin_user_id, - ) - - def _perform_delete_by_union_id(self, union_id_cleaned): - raise PermissionDenied(_("Could not remove asset admin user")) - - def all(self): - qs = self.model.objects.all().annotate( - asset_id=F("id"), - name=F("admin_user__name"), - username=F("admin_user__username"), - password=F("admin_user__password"), - private_key=F("admin_user__private_key"), - public_key=F("admin_user__public_key"), - score=Value(self.base_score, IntegerField()), - version=Value(0, IntegerField()), - date_updated=F("admin_user__date_updated"), - asset_username=Concat(F("id"), Value("_"), F("admin_user__username"), output_field=CharField()), - union_id=Concat(F("admin_user_id"), Value("_"), F("id"), output_field=CharField()), - backend=Value(self.backend, CharField()), - backend_display=Value(self.backend_display, CharField()), - ) - qs = self.qs_to_values(qs) - return qs - - -class AuthbookBackend(DBBackend): - model = AuthBook - backend = 'db' - backend_display = _('Database') - prefer = backend - base_score = 400 - - def _filter_node(self, node): - if node: - self.queryset = self.queryset.filter(asset__nodes__id=node.id) - - def _filter_prefer(self, prefer, prefer_id): - if not prefer or not prefer_id: - return - if prefer.lower() == "admin_user": - model = AdminUser - elif prefer.lower() == "system_user": - model = SystemUser - else: - self.queryset = self.queryset.none() - return - obj = get_object_or_none(model, pk=prefer_id) - if obj is None: - self.queryset = self.queryset.none() - return - username = obj.get_username() - if isinstance(username, str): - self.queryset = self.queryset.filter(username=username) - # dynamic system user return more username - else: - self.queryset = self.queryset.filter(username__in=username) - - def _perform_filter_union_id(self, union_id, union_id_cleaned): - authbook_id, asset_id = union_id_cleaned - self.queryset = self.queryset.filter( - id=authbook_id, asset_id=asset_id, - ) - - def _perform_delete_by_union_id(self, union_id_cleaned): - authbook_id, asset_id = union_id_cleaned - authbook = get_object_or_none(AuthBook, pk=authbook_id) - if authbook.is_latest: - raise PermissionDenied(_("Latest version could not be delete")) - AuthBook.objects.filter(id=authbook_id).delete() - - def all(self): - qs = self.model.objects.all().annotate( - hostname=F("asset__hostname"), - ip=F("asset__ip"), - score=F('version') + self.base_score, - asset_username=Concat(F("asset__id"), Value("_"), F("username"), output_field=CharField()), - union_id=Concat(F("id"), Value("_"), F("asset_id"), output_field=CharField()), - backend=Value(self.backend, CharField()), - backend_display=Value(self.backend_display, CharField()), - ) - qs = self.qs_to_values(qs) - return qs diff --git a/apps/assets/backends/manager.py b/apps/assets/backends/manager.py deleted file mode 100644 index ee6650ed5..000000000 --- a/apps/assets/backends/manager.py +++ /dev/null @@ -1,162 +0,0 @@ -# -*- coding: utf-8 -*- -# -from itertools import chain, groupby -from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist - -from orgs.utils import current_org -from common.utils import get_logger, lazyproperty -from common.struct import QuerySetChain - -from ..models import AssetUser, AuthBook -from .db import ( - AuthbookBackend, SystemUserBackend, AdminUserBackend, - DynamicSystemUserBackend -) - -logger = get_logger(__name__) - - -class NotSupportError(Exception): - pass - - -class AssetUserQueryset: - ObjectDoesNotExist = ObjectDoesNotExist - MultipleObjectsReturned = MultipleObjectsReturned - - def __init__(self, backends=()): - self.backends = backends - self._distinct_queryset = None - - def backends_queryset(self): - return [b.get_queryset() for b in self.backends] - - @lazyproperty - def backends_counts(self): - return [b.count() for b in self.backends] - - def filter(self, hostname=None, ip=None, username=None, - assets=None, asset=None, node=None, - id=None, prefer_id=None, prefer=None, id__in=None): - if not assets and asset: - assets = [asset] - - kwargs = dict( - hostname=hostname, ip=ip, username=username, - assets=assets, node=node, prefer=prefer, prefer_id=prefer_id, - id__in=id__in, union_id=id, - ) - logger.debug("Filter: {}".format(kwargs)) - backends = [] - for backend in self.backends: - clone = backend.filter(**kwargs) - backends.append(clone) - return self._clone(backends) - - def _clone(self, backends=None): - if backends is None: - backends = self.backends - return self.__class__(backends) - - def search(self, item): - backends = [] - for backend in self.backends: - new = backend.search(item) - backends.append(new) - return self._clone(backends) - - def distinct(self): - logger.debug("Distinct asset user queryset") - queryset_chain = chain(*(backend.get_queryset() for backend in self.backends)) - queryset_sorted = sorted( - queryset_chain, - key=lambda item: (item["asset_username"], item["score"]), - reverse=True, - ) - results = groupby(queryset_sorted, key=lambda item: item["asset_username"]) - final = [next(result[1]) for result in results] - self._distinct_queryset = final - return self - - def get(self, latest=False, **kwargs): - queryset = self.filter(**kwargs) - if latest: - queryset = queryset.distinct() - queryset = list(queryset) - count = len(queryset) - if count == 1: - data = queryset[0] - return data - elif count > 1: - msg = 'Should return 1 record, but get {}'.format(count) - raise MultipleObjectsReturned(msg) - else: - msg = 'No record found(org is {})'.format(current_org.name) - raise ObjectDoesNotExist(msg) - - def get_latest(self, **kwargs): - return self.get(latest=True, **kwargs) - - @staticmethod - def to_asset_user(data): - obj = AssetUser() - for k, v in data.items(): - setattr(obj, k, v) - return obj - - @property - def queryset(self): - if self._distinct_queryset is not None: - return self._distinct_queryset - return QuerySetChain(self.backends_queryset()) - - def count(self): - if self._distinct_queryset is not None: - return len(self._distinct_queryset) - else: - return sum(self.backends_counts) - - def __getitem__(self, ndx): - return self.queryset.__getitem__(ndx) - - def __iter__(self): - self._data = iter(self.queryset) - return self - - def __next__(self): - return self.to_asset_user(next(self._data)) - - -class AssetUserManager: - support_backends = ( - ('db', AuthbookBackend), - ('system_user', SystemUserBackend), - ('admin_user', AdminUserBackend), - ('system_user_dynamic', DynamicSystemUserBackend), - ) - - def __init__(self): - self.backends = [backend() for name, backend in self.support_backends] - self._queryset = AssetUserQueryset(self.backends) - - def all(self): - return self._queryset - - def delete(self, obj): - name_backends_map = dict(self.support_backends) - backend_name = obj.backend - backend_cls = name_backends_map.get(backend_name) - union_id = obj.union_id - if backend_cls: - backend_cls().delete(union_id) - else: - raise ObjectDoesNotExist("Not backend found") - - @staticmethod - def create(**kwargs): - # 使用create方法创建AuthBook对象,解决并发创建问题(添加锁机制) - authbook = AuthBook.create(**kwargs) - return authbook - - def __getattr__(self, item): - return getattr(self._queryset, item) diff --git a/apps/assets/backends/utils.py b/apps/assets/backends/utils.py deleted file mode 100644 index fbe190ba3..000000000 --- a/apps/assets/backends/utils.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- -# - -# from django.conf import settings - -# from .vault import VaultBackend - diff --git a/apps/assets/backends/vault.py b/apps/assets/backends/vault.py deleted file mode 100644 index f19a64d9a..000000000 --- a/apps/assets/backends/vault.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -# - - diff --git a/apps/assets/migrations/0071_systemuser_type.py b/apps/assets/migrations/0071_systemuser_type.py new file mode 100644 index 000000000..cb18edc60 --- /dev/null +++ b/apps/assets/migrations/0071_systemuser_type.py @@ -0,0 +1,66 @@ +# Generated by Django 3.1.6 on 2021-06-04 16:46 + +from django.db import migrations, models, transaction + + +def migrate_admin_user_to_system_user(apps, schema_editor): + admin_user_model = apps.get_model("assets", "AdminUser") + system_user_model = apps.get_model("assets", "SystemUser") + db_alias = schema_editor.connection.alias + + admin_users = admin_user_model.objects.using(db_alias).all() + print() + for admin_user in admin_users: + kwargs = {} + for attr in [ + 'id', 'org_id', 'username', 'password', 'private_key', 'public_key', + 'comment', 'date_created', 'date_updated', 'created_by', + ]: + value = getattr(admin_user, attr) + kwargs[attr] = value + + name = admin_user.name + exist = system_user_model.objects.using(db_alias).filter( + name=admin_user.name, org_id=admin_user.org_id + ).exists() + if exist: + name = admin_user.name + '_' + str(admin_user.id)[:5] + kwargs.update({ + 'name': name, + 'type': 'admin', + 'protocol': 'ssh', + 'auto_push': False, + }) + + with transaction.atomic(): + s = system_user_model(**kwargs) + s.save() + print(" Migrate admin user to system user: {} => {}".format(admin_user.name, s.name)) + assets = admin_user.assets.all() + s.assets.set(assets) + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0070_auto_20210426_1515'), + ] + + operations = [ + migrations.AddField( + model_name='systemuser', + name='type', + field=models.CharField(choices=[('common', 'Common user'), ('admin', 'Admin user')], default='common', max_length=16, verbose_name='Type'), + ), + migrations.AlterField( + model_name='systemuser', + name='login_mode', + field=models.CharField(choices=[('auto', 'Automatic managed'), ('manual', 'Manually input')], default='auto', max_length=10, verbose_name='Login mode'), + ), + migrations.AlterField( + model_name='systemuser', + name='protocol', + field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'), + ), + migrations.RunPython(migrate_admin_user_to_system_user) + ] diff --git a/apps/assets/migrations/0072_historicalauthbook.py b/apps/assets/migrations/0072_historicalauthbook.py new file mode 100644 index 000000000..9a55e47f3 --- /dev/null +++ b/apps/assets/migrations/0072_historicalauthbook.py @@ -0,0 +1,85 @@ +# Generated by Django 3.1.6 on 2021-06-05 16:10 + +import common.fields.model +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import simple_history.models +import uuid +from django.utils import timezone +from django.db import migrations, transaction + + +def migrate_old_authbook_to_history(apps, schema_editor): + authbook_model = apps.get_model("assets", "AuthBook") + history_model = apps.get_model("assets", "HistoricalAuthBook") + db_alias = schema_editor.connection.alias + + print() + while True: + authbooks = authbook_model.objects.using(db_alias).filter(is_latest=False)[:20] + if not authbooks: + break + historys = [] + authbook_ids = [] + # Todo: 或许能优化成更新那样 + for authbook in authbooks: + authbook_ids.append(authbook.id) + history = history_model() + + for attr in [ + 'id', 'username', 'password', 'private_key', 'public_key', 'version', + 'comment', 'created_by', 'asset', 'date_created', 'date_updated' + ]: + setattr(history, attr, getattr(authbook, attr)) + history.history_type = '-' + history.history_date = timezone.now() + historys.append(history) + + with transaction.atomic(): + print(" Migrate old auth book to history table: {} items".format(len(authbook_ids))) + history_model.objects.bulk_create(historys, ignore_conflicts=True) + authbook_model.objects.filter(id__in=authbook_ids).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('assets', '0071_systemuser_type'), + ] + + operations = [ + migrations.CreateModel( + name='HistoricalAuthBook', + fields=[ + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4)), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')), + ('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')), + ('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')), + ('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')), + ('comment', models.TextField(blank=True, verbose_name='Comment')), + ('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')), + ('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')), + ('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')), + ('version', models.IntegerField(default=1, verbose_name='Version')), + ('is_latest', models.BooleanField(default=False, verbose_name='Latest version')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('asset', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='assets.asset', verbose_name='Asset')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical AuthBook', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.RunPython(migrate_old_authbook_to_history) + ] diff --git a/apps/assets/migrations/0073_auto_20210606_1142.py b/apps/assets/migrations/0073_auto_20210606_1142.py new file mode 100644 index 000000000..e3ca122ce --- /dev/null +++ b/apps/assets/migrations/0073_auto_20210606_1142.py @@ -0,0 +1,105 @@ +# Generated by Django 3.1.6 on 2021-06-06 03:42 + +from django.utils import timezone +from django.db import migrations, models, transaction +import django.db.models.deletion + + +def migrate_system_assets_to_authbook(apps, schema_editor): + system_user_model = apps.get_model("assets", "SystemUser") + system_user_asset_model = system_user_model.assets.through + authbook_model = apps.get_model('assets', 'AuthBook') + history_model = apps.get_model("assets", "HistoricalAuthBook") + + print() + system_users = system_user_model.objects.all() + for s in system_users: + while True: + systemuser_asset_relations = system_user_asset_model.objects.filter(systemuser=s)[:20] + if not systemuser_asset_relations: + break + authbooks = [] + relations_ids = [] + historys = [] + for i in systemuser_asset_relations: + authbook = authbook_model(asset=i.asset, systemuser=i.systemuser, org_id=s.org_id) + authbooks.append(authbook) + relations_ids.append(i.id) + + history = history_model( + asset=i.asset, systemuser=i.systemuser, + date_created=timezone.now(), date_updated=timezone.now(), + ) + history.history_type = '-' + history.history_date = timezone.now() + historys.append(history) + + with transaction.atomic(): + print(" Migrate system user assets relations: {} items".format(len(relations_ids))) + authbook_model.objects.bulk_create(authbooks, ignore_conflicts=True) + history_model.objects.bulk_create(historys) + system_user_asset_model.objects.filter(id__in=relations_ids).delete() + + +def migrate_authbook_secret_to_system_user(apps, schema_editor): + authbook_model = apps.get_model('assets', 'AuthBook') + history_model = apps.get_model('assets', 'HistoricalAuthBook') + + print() + authbooks_without_systemuser = authbook_model.objects.filter(systemuser__isnull=True) + for authbook in authbooks_without_systemuser: + matched = authbook_model.objects.filter( + asset=authbook.asset, systemuser__username=authbook.username + ) + if not matched: + continue + historys = [] + for i in matched: + history = history_model( + asset=i.asset, systemuser=i.systemuser, + date_created=timezone.now(), date_updated=timezone.now(), + version=authbook.version + ) + history.history_type = '-' + history.history_date = timezone.now() + historys.append(history) + + with transaction.atomic(): + print(" Migrate secret to system user assets account: {} items".format(len(historys))) + matched.update(password=authbook.password, private_key=authbook.private_key, + public_key=authbook.public_key, version=authbook.version) + history_model.objects.bulk_create(historys) + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0072_historicalauthbook'), + ] + + operations = [ + migrations.AddField( + model_name='authbook', + name='systemuser', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.systemuser', verbose_name='System user'), + ), + migrations.AddField( + model_name='historicalauthbook', + name='systemuser', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='assets.systemuser', verbose_name='System user'), + ), + migrations.AlterUniqueTogether( + name='authbook', + unique_together={('username', 'asset', 'systemuser')}, + ), + migrations.RunPython(migrate_system_assets_to_authbook), + migrations.RunPython(migrate_authbook_secret_to_system_user), + migrations.RemoveField( + model_name='authbook', + name='is_latest', + ), + migrations.RemoveField( + model_name='historicalauthbook', + name='is_latest', + ), + ] diff --git a/apps/assets/migrations/0074_remove_systemuser_assets.py b/apps/assets/migrations/0074_remove_systemuser_assets.py new file mode 100644 index 000000000..c9961f5ad --- /dev/null +++ b/apps/assets/migrations/0074_remove_systemuser_assets.py @@ -0,0 +1,27 @@ +# Generated by Django 3.1.6 on 2021-06-06 03:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0073_auto_20210606_1142'), + ] + + operations = [ + migrations.RemoveField( + model_name='systemuser', + name='assets', + ), + migrations.RenameField( + model_name='asset', + old_name='admin_user', + new_name='_admin_user', + ), + migrations.AddField( + model_name='systemuser', + name='assets', + field=models.ManyToManyField(blank=True, related_name='system_users', through='assets.AuthBook', to='assets.Asset', verbose_name='Assets'), + ), + ] diff --git a/apps/assets/migrations/0075_auto_20210705_1759.py b/apps/assets/migrations/0075_auto_20210705_1759.py new file mode 100644 index 000000000..6e5fdf480 --- /dev/null +++ b/apps/assets/migrations/0075_auto_20210705_1759.py @@ -0,0 +1,53 @@ +# Generated by Django 3.1 on 2021-07-05 09:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0074_remove_systemuser_assets'), + ] + + operations = [ + migrations.AddField( + model_name='asset', + name='connectivity', + field=models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity'), + ), + migrations.AddField( + model_name='asset', + name='date_verified', + field=models.DateTimeField(null=True), + ), + migrations.AddField( + model_name='authbook', + name='connectivity', + field=models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity'), + ), + migrations.AddField( + model_name='authbook', + name='date_verified', + field=models.DateTimeField(null=True), + ), + migrations.AddField( + model_name='historicalauthbook', + name='connectivity', + field=models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity'), + ), + migrations.AddField( + model_name='historicalauthbook', + name='date_verified', + field=models.DateTimeField(null=True), + ), + migrations.AlterField( + model_name='asset', + name='protocol', + field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC')], default='ssh', max_length=128, verbose_name='Protocol'), + ), + migrations.AlterField( + model_name='gateway', + name='protocol', + field=models.CharField(choices=[('ssh', 'SSH')], default='ssh', max_length=16, verbose_name='Protocol'), + ), + ] diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index 1bed74a16..0f6aec901 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -2,7 +2,6 @@ from .base import * from .asset import * from .label import Label from .user import * -from .asset_user import * from .cluster import * from .group import * from .domain import * diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index c009d00a1..1e5d82099 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -4,18 +4,20 @@ import uuid import logging -import random from functools import reduce from collections import OrderedDict +from django.utils import timezone from django.db import models +from common.db.models import TextChoices from django.utils.translation import ugettext_lazy as _ +from rest_framework.exceptions import ValidationError from common.fields.model import JsonDictTextField from common.utils import lazyproperty from orgs.mixins.models import OrgModelMixin, OrgManager -from .base import ConnectivityMixin -from .utils import Connectivity + +from .base import AbsConnectivity __all__ = ['Asset', 'ProtocolsMixin', 'Platform', 'AssetQuerySet'] logger = logging.getLogger(__name__) @@ -57,16 +59,12 @@ class AssetQuerySet(models.QuerySet): class ProtocolsMixin: protocols = '' - PROTOCOL_SSH = 'ssh' - PROTOCOL_RDP = 'rdp' - PROTOCOL_TELNET = 'telnet' - PROTOCOL_VNC = 'vnc' - PROTOCOL_CHOICES = ( - (PROTOCOL_SSH, 'ssh'), - (PROTOCOL_RDP, 'rdp'), - (PROTOCOL_TELNET, 'telnet'), - (PROTOCOL_VNC, 'vnc'), - ) + + class Protocol(TextChoices): + ssh = 'ssh', 'SSH' + rdp = 'rdp', 'RDP' + telnet = 'telnet', 'Telnet' + vnc = 'vnc', 'VNC' @property def protocols_as_list(self): @@ -167,7 +165,7 @@ class Platform(models.Model): # ordering = ('name',) -class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): +class Asset(AbsConnectivity, ProtocolsMixin, NodesRelationMixin, OrgModelMixin): # Important PLATFORM_CHOICES = ( ('Linux', 'Linux'), @@ -182,8 +180,8 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True) hostname = models.CharField(max_length=128, verbose_name=_('Hostname')) - protocol = models.CharField(max_length=128, default=ProtocolsMixin.PROTOCOL_SSH, - choices=ProtocolsMixin.PROTOCOL_CHOICES, + protocol = models.CharField(max_length=128, default=ProtocolsMixin.Protocol.ssh, + choices=ProtocolsMixin.Protocol.choices, verbose_name=_('Protocol')) port = models.IntegerField(default=22, verbose_name=_('Port')) protocols = models.CharField(max_length=128, default='ssh/22', blank=True, verbose_name=_("Protocols")) @@ -193,7 +191,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): is_active = models.BooleanField(default=True, verbose_name=_('Is active')) # Auth - admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user"), related_name='assets') + _admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user"), related_name='assets') # Some information public_ip = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Public IP')) @@ -223,11 +221,26 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) objects = AssetManager.from_queryset(AssetQuerySet)() - _connectivity = None def __str__(self): return '{0.hostname}({0.ip})'.format(self) + @property + def admin_user(self): + return self.system_users.filter(type='admin').first() + + @admin_user.setter + def admin_user(self, system_user): + if not system_user: + return + if system_user.type != 'admin': + raise ValidationError('System user should be type admin') + system_user.assets.add(self) + + def remove_admin_user(self): + from ..models import AuthBook + AuthBook.objects.filter(asset=self, systemuser__type='admin').delete() + @property def is_valid(self): warning = '' @@ -276,23 +289,6 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): else: return '' - @property - def connectivity(self): - if self._connectivity: - return self._connectivity - if not self.admin_user_username: - return Connectivity.unknown() - connectivity = ConnectivityMixin.get_asset_username_connectivity( - self, self.admin_user_username - ) - return connectivity - - @connectivity.setter - def connectivity(self, value): - if not self.admin_user: - return - self.admin_user.set_asset_connectivity(self, value) - def get_auth_info(self): if not self.admin_user: return {} diff --git a/apps/assets/models/asset_user.py b/apps/assets/models/asset_user.py deleted file mode 100644 index ac9112427..000000000 --- a/apps/assets/models/asset_user.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -# -from .authbook import AuthBook - - -class AssetUser(AuthBook): - hostname = "" - ip = "" - backend = "" - backend_display = "" - union_id = "" - asset_username = "" - - class Meta: - proxy = True diff --git a/apps/assets/models/authbook.py b/apps/assets/models/authbook.py index 3a17df7b7..cf72d37b0 100644 --- a/apps/assets/models/authbook.py +++ b/apps/assets/models/authbook.py @@ -1,92 +1,94 @@ # -*- coding: utf-8 -*- # -from django.db import models, transaction -from django.db.models import Max +from django.db import models from django.utils.translation import ugettext_lazy as _ -from rest_framework.exceptions import PermissionDenied -from orgs.mixins.models import OrgManager -from .base import BaseUser +from simple_history.models import HistoricalRecords + + +from .base import BaseUser, AbsConnectivity __all__ = ['AuthBook'] -class AuthBookQuerySet(models.QuerySet): - def delete(self): - if self.count() > 1: - raise PermissionDenied(_("Bulk delete deny")) - return super().delete() - - -class AuthBookManager(OrgManager): - pass - - -class AuthBook(BaseUser): +class AuthBook(BaseUser, AbsConnectivity): asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset')) - is_latest = models.BooleanField(default=False, verbose_name=_('Latest version')) + systemuser = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE, null=True, verbose_name=_("System user")) version = models.IntegerField(default=1, verbose_name=_('Version')) + history = HistoricalRecords() - objects = AuthBookManager.from_queryset(AuthBookQuerySet)() - backend = "db" - # 用于system user和admin_user的动态设置 - _connectivity = None - CONN_CACHE_KEY = "ASSET_USER_CONN_{}" + auth_attrs = ['username', 'password', 'private_key', 'public_key'] class Meta: verbose_name = _('AuthBook') + unique_together = [('username', 'asset', 'systemuser')] - def get_related_assets(self): - return [self.asset] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.auth_snapshot = {} + self.load_auth() - def generate_id_with_asset(self, asset): - return self.id + def get_or_systemuser_attr(self, attr): + val = getattr(self, attr, None) + if val: + return val + if self.systemuser: + return getattr(self.systemuser, attr, '') + return '' - @classmethod - def get_max_version(cls, username, asset): - version_max = cls.objects.filter(username=username, asset=asset) \ - .aggregate(Max('version')) - version_max = version_max['version__max'] or 0 - return version_max + def load_auth(self): + for attr in self.auth_attrs: + value = self.get_or_systemuser_attr(attr) + self.auth_snapshot[attr] = [getattr(self, attr), value] + setattr(self, attr, value) - @classmethod - def create(cls, **kwargs): - """ - 使用并发锁机制创建AuthBook对象, (主要针对并发创建 username, asset 相同的对象时) - 并更新其他对象的 is_latest=False (其他对象: 与当前对象的 username, asset 相同) - 同时设置自己的 is_latest=True, version=max_version + 1 - """ - username = kwargs['username'] - asset = kwargs.get('asset') or kwargs.get('asset_id') - with transaction.atomic(): - # 使用select_for_update限制并发创建相同的username、asset条目 - instances = cls.objects.select_for_update().filter(username=username, asset=asset) - instances.filter(is_latest=True).update(is_latest=False) - max_version = cls.get_max_version(username, asset) - kwargs.update({ - 'version': max_version + 1, - 'is_latest': True - }) - obj = cls.objects.create(**kwargs) - return obj + def unload_auth(self): + if not self.systemuser: + return + + for attr, values in self.auth_snapshot.items(): + origin_value, loaded_value = values + current_value = getattr(self, attr, '') + if current_value == loaded_value: + setattr(self, attr, origin_value) + + def save(self, *args, **kwargs): + self.unload_auth() + instance = super().save(*args, **kwargs) + self.load_auth() + return instance @property - def connectivity(self): - return self.get_asset_connectivity(self.asset) + def username_display(self): + return self.get_or_systemuser_attr('username') or '*' @property - def keyword(self): - return '{}_#_{}'.format(self.username, str(self.asset.id)) + def smart_name(self): + username = self.username_display - @property - def hostname(self): - return self.asset.hostname + if self.asset: + asset = str(self.asset) + else: + asset = '*' + return '{}@{}'.format(username, asset) - @property - def ip(self): - return self.asset.ip + def sync_to_system_user_account(self): + if self.systemuser: + return + matched = AuthBook.objects.filter( + asset=self.asset, systemuser__username=self.username + ) + if not matched: + return + + for i in matched: + i.password = self.password + i.private_key = self.private_key + i.public_key = self.public_key + i.comment = 'Update triggered by account {}'.format(self.id) + i.save(update_fields=['password', 'private_key', 'public_key']) def __str__(self): - return '{}@{}'.format(self.username, self.asset) + return self.smart_name diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 6623cc6a7..cb176202a 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -8,8 +8,10 @@ from hashlib import md5 import sshpubkeys from django.core.cache import cache from django.db import models +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django.conf import settings +from django.db.models import QuerySet from common.utils import random_string, signer from common.utils import ( @@ -19,85 +21,39 @@ from common.utils.encode import ssh_pubkey_gen from common.validators import alphanumeric from common import fields from orgs.mixins.models import OrgModelMixin -from .utils import Connectivity logger = get_logger(__file__) -class ConnectivityMixin: - CONNECTIVITY_ASSET_CACHE_KEY = "ASSET_USER_{}_{}_ASSET_CONNECTIVITY" - CONNECTIVITY_AMOUNT_CACHE_KEY = "ASSET_USER_{}_{}_CONNECTIVITY_AMOUNT" - ASSET_USER_CACHE_TIME = 3600 * 24 - id = '' - username = '' +class Connectivity(models.TextChoices): + unknown = 'unknown', _('Unknown') + ok = 'ok', _('Ok') + failed = 'failed', _('Failed') - @property - def part_id(self): - i = '-'.join(str(self.id).split('-')[:3]) - return i - def set_connectivity(self, summary): - unreachable = summary.get('dark', {}).keys() - reachable = summary.get('contacted', {}).keys() +class AbsConnectivity(models.Model): + connectivity = models.CharField( + choices=Connectivity.choices, default=Connectivity.unknown, + max_length=16, verbose_name=_('Connectivity') + ) + date_verified = models.DateTimeField(null=True) - assets = self.get_related_assets() - if not isinstance(assets, list): - assets = assets.only('id', 'hostname', 'admin_user__id') - for asset in assets: - if asset.hostname in unreachable: - self.set_asset_connectivity(asset, Connectivity.unreachable()) - elif asset.hostname in reachable: - self.set_asset_connectivity(asset, Connectivity.reachable()) - else: - self.set_asset_connectivity(asset, Connectivity.unknown()) - cache_key = self.CONNECTIVITY_AMOUNT_CACHE_KEY.format(self.username, self.part_id) - cache.delete(cache_key) - - @property - def connectivity(self): - assets = self.get_related_assets() - if not isinstance(assets, list): - assets = assets.only('id', 'hostname', 'admin_user__id') - data = { - 'unreachable': [], - 'reachable': [], - 'unknown': [], - } - for asset in assets: - connectivity = self.get_asset_connectivity(asset) - if connectivity.is_reachable(): - data["reachable"].append(asset.hostname) - elif connectivity.is_unreachable(): - data["unreachable"].append(asset.hostname) - else: - data["unknown"].append(asset.hostname) - return data - - @property - def connectivity_amount(self): - cache_key = self.CONNECTIVITY_AMOUNT_CACHE_KEY.format(self.username, self.part_id) - amount = cache.get(cache_key) - if not amount: - amount = {k: len(v) for k, v in self.connectivity.items()} - cache.set(cache_key, amount, self.ASSET_USER_CACHE_TIME) - return amount + def set_connectivity(self, val): + self.connectivity = val + self.date_verified = timezone.now() + self.save(update_fields=['connectivity', 'date_verified']) @classmethod - def get_asset_username_connectivity(cls, asset, username): - key = cls.CONNECTIVITY_ASSET_CACHE_KEY.format(username, asset.id) - return Connectivity.get(key) + def bulk_set_connectivity(cls, queryset_or_id, connectivity): + if not isinstance(queryset_or_id, QuerySet): + queryset = cls.objects.filter(id__in=queryset_or_id) + else: + queryset = queryset_or_id + queryset.update(connectivity=connectivity, date_verified=timezone.now()) - def get_asset_connectivity(self, asset): - key = self.get_asset_connectivity_key(asset) - return Connectivity.get(key) - - def get_asset_connectivity_key(self, asset): - return self.CONNECTIVITY_ASSET_CACHE_KEY.format(self.username, asset.id) - - def set_asset_connectivity(self, asset, c): - key = self.get_asset_connectivity_key(asset) - Connectivity.set(key, c) + class Meta: + abstract = True class AuthMixin: @@ -105,7 +61,6 @@ class AuthMixin: password = '' public_key = '' username = '' - _prefer = 'system_user' @property def ssh_key_fingerprint(self): @@ -173,38 +128,6 @@ class AuthMixin: if update_fields: self.save(update_fields=update_fields) - def has_special_auth(self, asset=None, username=None): - from .authbook import AuthBook - if username is None: - username = self.username - queryset = AuthBook.objects.filter(username=username) - if asset: - queryset = queryset.filter(asset=asset) - return queryset.exists() - - def get_asset_user(self, asset, username=None): - from ..backends import AssetUserManager - if username is None: - username = self.username - try: - manager = AssetUserManager() - other = manager.get_latest( - username=username, asset=asset, - prefer_id=self.id, prefer=self._prefer, - ) - return other - except Exception as e: - logger.error(e, exc_info=True) - return None - - def load_asset_special_auth(self, asset=None, username=None): - if not asset: - return self - - instance = self.get_asset_user(asset, username=username) - if instance: - self._merge_auth(instance) - def _merge_auth(self, other): if other.password: self.password = other.password @@ -244,7 +167,7 @@ class AuthMixin: ) -class BaseUser(OrgModelMixin, AuthMixin, ConnectivityMixin): +class BaseUser(OrgModelMixin, AuthMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_('Name')) username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True) @@ -259,8 +182,6 @@ class BaseUser(OrgModelMixin, AuthMixin, ConnectivityMixin): ASSETS_AMOUNT_CACHE_KEY = "ASSET_USER_{}_ASSETS_AMOUNT" ASSET_USER_CACHE_TIME = 600 - _prefer = "system_user" - def get_related_assets(self): assets = self.assets.filter(org_id=self.org_id) return assets diff --git a/apps/assets/models/domain.py b/apps/assets/models/domain.py index 1a2dbbc4e..4d500e503 100644 --- a/apps/assets/models/domain.py +++ b/apps/assets/models/domain.py @@ -7,6 +7,7 @@ import re import paramiko from django.db import models +from django.db.models import TextChoices from django.utils.translation import ugettext_lazy as _ from common.utils.strings import no_special_chars @@ -43,15 +44,12 @@ class Domain(OrgModelMixin): class Gateway(BaseUser): - PROTOCOL_SSH = 'ssh' - PROTOCOL_RDP = 'rdp' - PROTOCOL_CHOICES = ( - (PROTOCOL_SSH, 'ssh'), - (PROTOCOL_RDP, 'rdp'), - ) + class Protocol(TextChoices): + ssh = 'ssh', 'SSH' + ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True) port = models.IntegerField(default=22, verbose_name=_('Port')) - protocol = models.CharField(choices=PROTOCOL_CHOICES, max_length=16, default=PROTOCOL_SSH, verbose_name=_("Protocol")) + protocol = models.CharField(choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol")) domain = models.ForeignKey(Domain, on_delete=models.CASCADE, verbose_name=_("Domain")) comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Comment")) is_active = models.BooleanField(default=True, verbose_name=_("Is active")) diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 384272af7..fcdb9e244 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -10,15 +10,278 @@ from django.core.validators import MinValueValidator, MaxValueValidator from django.core.cache import cache from common.utils import signer, get_object_or_none -from common.exceptions import JMSException +from common.db.models import TextChoices from .base import BaseUser from .asset import Asset +from .authbook import AuthBook __all__ = ['AdminUser', 'SystemUser'] logger = logging.getLogger(__name__) +class ProtocolMixin: + protocol: str + + class Protocol(TextChoices): + ssh = 'ssh', 'SSH' + rdp = 'rdp', 'RDP' + telnet = 'telnet', 'Telnet' + vnc = 'vnc', 'VNC' + mysql = 'mysql', 'MySQL' + oracle = 'oracle', 'Oracle' + mariadb = 'mariadb', 'MariaDB' + postgresql = 'postgresql', 'PostgreSQL' + k8s = 'k8s', 'K8S' + + SUPPORT_PUSH_PROTOCOLS = [Protocol.ssh, Protocol.rdp] + + ASSET_CATEGORY_PROTOCOLS = [ + Protocol.ssh, Protocol.rdp, Protocol.telnet, Protocol.vnc + ] + APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS = [ + Protocol.rdp + ] + APPLICATION_CATEGORY_DB_PROTOCOLS = [ + Protocol.mysql, Protocol.oracle, Protocol.mariadb, Protocol.postgresql + ] + APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [ + Protocol.k8s + ] + APPLICATION_CATEGORY_PROTOCOLS = [ + *APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS, + *APPLICATION_CATEGORY_DB_PROTOCOLS, + *APPLICATION_CATEGORY_CLOUD_PROTOCOLS + ] + + @property + def is_protocol_support_push(self): + return self.protocol in self.SUPPORT_PUSH_PROTOCOLS + + @classmethod + def get_protocol_by_application_type(cls, app_type): + from applications.const import ApplicationTypeChoices + if app_type in cls.APPLICATION_CATEGORY_PROTOCOLS: + protocol = app_type + elif app_type in ApplicationTypeChoices.remote_app_types(): + protocol = cls.Protocol.rdp + else: + protocol = None + return protocol + + @property + def can_perm_to_asset(self): + return self.protocol in self.ASSET_CATEGORY_PROTOCOLS + + +class AuthMixin: + username_same_with_user: bool + protocol: str + ASSET_CATEGORY_PROTOCOLS: list + login_mode: str + LOGIN_MANUAL: str + id: str + username: str + password: str + private_key: str + public_key: str + + def set_temp_auth(self, asset_or_app_id, user_id, auth, ttl=300): + if not auth: + raise ValueError('Auth not set') + key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id) + logger.debug(f'Set system user temp auth: {key}') + cache.set(key, auth, ttl) + + def get_temp_auth(self, asset_or_app_id, user_id): + key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id) + logger.debug(f'Get system user temp auth: {key}') + password = cache.get(key) + return password + + def load_tmp_auth_if_has(self, asset_or_app_id, user): + if not asset_or_app_id or not user: + return + + if self.login_mode != self.LOGIN_MANUAL: + return + + auth = self.get_temp_auth(asset_or_app_id, user) + if not auth: + return + username = auth.get('username') + password = auth.get('password') + + if username: + self.username = username + if password: + self.password = password + + def load_app_more_auth(self, app_id=None, user_id=None): + from users.models import User + + if self.login_mode == self.LOGIN_MANUAL: + self.password = '' + self.private_key = '' + if not user_id: + return + user = get_object_or_none(User, pk=user_id) + if not user: + return + self.load_tmp_auth_if_has(app_id, user) + + def load_asset_special_auth(self, asset, username=''): + """ + """ + authbooks = list(AuthBook.objects.filter(asset=asset, systemuser=self)) + if len(authbooks) == 0: + return None + elif len(authbooks) == 1: + authbook = authbooks[0] + else: + authbooks.sort(key=lambda x: 1 if x.username == username else 0, reverse=True) + authbook = authbooks[0] + self.password = authbook.password + self.private_key = authbook.private_key + self.public_key = authbook.public_key + + def load_asset_more_auth(self, asset_id=None, username=None, user_id=None): + from users.models import User + + if self.login_mode == self.LOGIN_MANUAL: + self.password = '' + self.private_key = '' + + asset = None + if asset_id: + asset = get_object_or_none(Asset, pk=asset_id) + # 没有资产就没有必要继续了 + if not asset: + logger.debug('Asset not found, pass') + return + + user = None + if user_id: + user = get_object_or_none(User, pk=user_id) + + _username = self.username + if self.username_same_with_user: + if user and not username: + _username = user.username + else: + _username = username + self.username = _username + + # 加载某个资产的特殊配置认证信息 + self.load_asset_special_auth(asset, _username) + self.load_tmp_auth_if_has(asset_id, user) + + +class SystemUser(ProtocolMixin, AuthMixin, BaseUser): + LOGIN_AUTO = 'auto' + LOGIN_MANUAL = 'manual' + LOGIN_MODE_CHOICES = ( + (LOGIN_AUTO, _('Automatic managed')), + (LOGIN_MANUAL, _('Manually input')) + ) + + class Type(TextChoices): + common = 'common', _('Common user') + admin = 'admin', _('Admin user') + + username_same_with_user = models.BooleanField(default=False, verbose_name=_("Username same with user")) + nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes")) + assets = models.ManyToManyField( + 'assets.Asset', blank=True, verbose_name=_("Assets"), + through='assets.AuthBook', through_fields=['systemuser', 'asset'], + related_name='system_users' + ) + users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users")) + groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User groups")) + type = models.CharField(max_length=16, choices=Type.choices, default=Type.common, verbose_name=_('Type')) + priority = models.IntegerField(default=81, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)]) + protocol = models.CharField(max_length=16, choices=ProtocolMixin.Protocol.choices, default='ssh', verbose_name=_('Protocol')) + auto_push = models.BooleanField(default=True, verbose_name=_('Auto push')) + sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo')) + shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell')) + login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode')) + cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True) + sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root")) + token = models.TextField(default='', verbose_name=_('Token')) + home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True) + system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True) + ad_domain = models.CharField(default='', max_length=256) + + def __str__(self): + username = self.username + if self.username_same_with_user: + username = 'dynamic' + return '{0.name}({1})'.format(self, username) + + @property + def nodes_amount(self): + return self.nodes.all().count() + + @property + def login_mode_display(self): + return self.get_login_mode_display() + + def is_need_push(self): + if self.auto_push and self.is_protocol_support_push: + return True + else: + return False + + @property + def is_admin_user(self): + return self.type == self.Type.admin + + @property + def is_need_cmd_filter(self): + return self.protocol not in [self.Protocol.rdp, self.Protocol.vnc] + + @property + def is_need_test_asset_connective(self): + return self.protocol in self.ASSET_CATEGORY_PROTOCOLS + + @property + def cmd_filter_rules(self): + from .cmd_filter import CommandFilterRule + rules = CommandFilterRule.objects.filter( + filter__in=self.cmd_filters.all() + ).distinct() + return rules + + def is_command_can_run(self, command): + for rule in self.cmd_filter_rules: + action, matched_cmd = rule.match(command) + if action == rule.ActionChoices.allow: + return True, None + elif action == rule.ActionChoices.deny: + return False, matched_cmd + return True, None + + def get_all_assets(self): + from assets.models import Node + nodes_keys = self.nodes.all().values_list('key', flat=True) + asset_ids = set(self.assets.all().values_list('id', flat=True)) + nodes_asset_ids = Node.get_nodes_all_asset_ids_by_keys(nodes_keys) + asset_ids.update(nodes_asset_ids) + assets = Asset.objects.filter(id__in=asset_ids) + return assets + + def save(self, *args, **kwargs): + if self.username_same_with_user: + self.username = '*' + return super().save(*args, **kwargs) + + class Meta: + ordering = ['name'] + unique_together = [('name', 'org_id')] + verbose_name = _("System user") + + +# Todo: 准备废弃 class AdminUser(BaseUser): """ A privileged user that ansible can use it to push system user and so on @@ -65,243 +328,3 @@ class AdminUser(BaseUser): ordering = ['name'] unique_together = [('name', 'org_id')] verbose_name = _("Admin user") - - -class SystemUser(BaseUser): - PROTOCOL_SSH = 'ssh' - PROTOCOL_RDP = 'rdp' - PROTOCOL_TELNET = 'telnet' - PROTOCOL_VNC = 'vnc' - PROTOCOL_MYSQL = 'mysql' - PROTOCOL_ORACLE = 'oracle' - PROTOCOL_MARIADB = 'mariadb' - PROTOCOL_POSTGRESQL = 'postgresql' - PROTOCOL_K8S = 'k8s' - PROTOCOL_CHOICES = ( - (PROTOCOL_SSH, 'ssh'), - (PROTOCOL_RDP, 'rdp'), - (PROTOCOL_TELNET, 'telnet'), - (PROTOCOL_VNC, 'vnc'), - (PROTOCOL_MYSQL, 'mysql'), - (PROTOCOL_ORACLE, 'oracle'), - (PROTOCOL_MARIADB, 'mariadb'), - (PROTOCOL_POSTGRESQL, 'postgresql'), - (PROTOCOL_K8S, 'k8s'), - ) - - SUPPORT_PUSH_PROTOCOLS = [PROTOCOL_SSH, PROTOCOL_RDP] - - ASSET_CATEGORY_PROTOCOLS = [ - PROTOCOL_SSH, PROTOCOL_RDP, PROTOCOL_TELNET, PROTOCOL_VNC - ] - APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS = [ - PROTOCOL_RDP - ] - APPLICATION_CATEGORY_DB_PROTOCOLS = [ - PROTOCOL_MYSQL, PROTOCOL_ORACLE, PROTOCOL_MARIADB, PROTOCOL_POSTGRESQL - ] - APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [ - PROTOCOL_K8S - ] - APPLICATION_CATEGORY_PROTOCOLS = [ - *APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS, - *APPLICATION_CATEGORY_DB_PROTOCOLS, - *APPLICATION_CATEGORY_CLOUD_PROTOCOLS - ] - - LOGIN_AUTO = 'auto' - LOGIN_MANUAL = 'manual' - LOGIN_MODE_CHOICES = ( - (LOGIN_AUTO, _('Automatic login')), - (LOGIN_MANUAL, _('Manually login')) - ) - username_same_with_user = models.BooleanField(default=False, verbose_name=_("Username same with user")) - nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes")) - assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets")) - users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users")) - groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User groups")) - priority = models.IntegerField(default=81, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)]) - protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol')) - auto_push = models.BooleanField(default=True, verbose_name=_('Auto push')) - sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo')) - shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell')) - login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode')) - cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True) - sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root")) - token = models.TextField(default='', verbose_name=_('Token')) - home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True) - system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True) - ad_domain = models.CharField(default='', max_length=256) - _prefer = 'system_user' - - def __str__(self): - username = self.username - if self.username_same_with_user: - username = 'dynamic' - return '{0.name}({1})'.format(self, username) - - def get_username(self): - if self.username_same_with_user: - return list(self.users.values_list('username', flat=True)) - else: - return self.username - - @property - def nodes_amount(self): - return self.nodes.all().count() - - @property - def login_mode_display(self): - return self.get_login_mode_display() - - def is_need_push(self): - if self.auto_push and self.is_protocol_support_push: - return True - else: - return False - - @property - def is_protocol_support_push(self): - return self.protocol in self.SUPPORT_PUSH_PROTOCOLS - - @property - def is_need_cmd_filter(self): - return self.protocol not in [self.PROTOCOL_RDP, self.PROTOCOL_VNC] - - @property - def is_need_test_asset_connective(self): - return self.protocol in self.ASSET_CATEGORY_PROTOCOLS - - def has_special_auth(self, asset=None, username=None): - if username is None and self.username_same_with_user: - raise TypeError('System user is dynamic, username should be pass') - return super().has_special_auth(asset=asset, username=username) - - @property - def can_perm_to_asset(self): - return self.protocol in self.ASSET_CATEGORY_PROTOCOLS - - def _merge_auth(self, other): - super()._merge_auth(other) - if self.username_same_with_user: - self.username = other.username - - def set_temp_auth(self, asset_or_app_id, user_id, auth, ttl=300): - if not auth: - raise ValueError('Auth not set') - key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id) - logger.debug(f'Set system user temp auth: {key}') - cache.set(key, auth, ttl) - - def get_temp_auth(self, asset_or_app_id, user_id): - key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id) - logger.debug(f'Get system user temp auth: {key}') - password = cache.get(key) - return password - - def load_tmp_auth_if_has(self, asset_or_app_id, user): - if not asset_or_app_id or not user: - return - if self.login_mode != self.LOGIN_MANUAL: - pass - - auth = self.get_temp_auth(asset_or_app_id, user) - if not auth: - return - username = auth.get('username') - password = auth.get('password') - - if username: - self.username = username - if password: - self.password = password - - def load_app_more_auth(self, app_id=None, user_id=None): - from users.models import User - - if self.login_mode == self.LOGIN_MANUAL: - self.password = '' - self.private_key = '' - if not user_id: - return - user = get_object_or_none(User, pk=user_id) - if not user: - return - self.load_tmp_auth_if_has(app_id, user) - - def load_asset_more_auth(self, asset_id=None, username=None, user_id=None): - from users.models import User - - if self.login_mode == self.LOGIN_MANUAL: - self.password = '' - self.private_key = '' - - asset = None - if asset_id: - asset = get_object_or_none(Asset, pk=asset_id) - # 没有资产就没有必要继续了 - if not asset: - logger.debug('Asset not found, pass') - return - - user = None - if user_id: - user = get_object_or_none(User, pk=user_id) - - _username = self.username - if self.username_same_with_user: - if user and not username: - _username = user.username - else: - _username = username - - # 加载某个资产的特殊配置认证信息 - try: - self.load_asset_special_auth(asset, _username) - except Exception as e: - logger.error('Load special auth Error: ', e) - pass - - self.load_tmp_auth_if_has(asset_id, user) - - @property - def cmd_filter_rules(self): - from .cmd_filter import CommandFilterRule - rules = CommandFilterRule.objects.filter( - filter__in=self.cmd_filters.all() - ).distinct() - return rules - - def is_command_can_run(self, command): - for rule in self.cmd_filter_rules: - action, matched_cmd = rule.match(command) - if action == rule.ActionChoices.allow: - return True, None - elif action == rule.ActionChoices.deny: - return False, matched_cmd - return True, None - - def get_all_assets(self): - from assets.models import Node - nodes_keys = self.nodes.all().values_list('key', flat=True) - asset_ids = set(self.assets.all().values_list('id', flat=True)) - nodes_asset_ids = Node.get_nodes_all_asset_ids_by_keys(nodes_keys) - asset_ids.update(nodes_asset_ids) - assets = Asset.objects.filter(id__in=asset_ids) - return assets - - @classmethod - def get_protocol_by_application_type(cls, app_type): - from applications.const import ApplicationTypeChoices - if app_type in cls.APPLICATION_CATEGORY_PROTOCOLS: - protocol = app_type - elif app_type in ApplicationTypeChoices.remote_app_types(): - protocol = cls.PROTOCOL_RDP - else: - protocol = None - return protocol - - class Meta: - ordering = ['name'] - unique_together = [('name', 'org_id')] - verbose_name = _("System user") diff --git a/apps/assets/models/utils.py b/apps/assets/models/utils.py index 90c203e81..90b0ee178 100644 --- a/apps/assets/models/utils.py +++ b/apps/assets/models/utils.py @@ -11,7 +11,7 @@ from common.utils import validate_ssh_private_key __all__ = [ - 'init_model', 'generate_fake', 'private_key_validator', 'Connectivity', + 'init_model', 'generate_fake', 'private_key_validator', ] @@ -35,74 +35,3 @@ def private_key_validator(value): _('%(value)s is not an even number'), params={'value': value}, ) - - -class Connectivity: - UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3) - CONNECTIVITY_CHOICES = ( - (UNREACHABLE, _("Unreachable")), - (REACHABLE, _('Reachable')), - (UNKNOWN, _("Unknown")), - ) - - status = UNKNOWN - datetime = timezone.now() - - def __init__(self, status, datetime): - self.status = status - self.datetime = datetime - - def display(self): - return dict(self.__class__.CONNECTIVITY_CHOICES).get(self.status) - - def is_reachable(self): - return self.status == self.REACHABLE - - def is_unreachable(self): - return self.status == self.UNREACHABLE - - def is_unknown(self): - return self.status == self.UNKNOWN - - @classmethod - def unreachable(cls): - return cls(cls.UNREACHABLE, timezone.now()) - - @classmethod - def reachable(cls): - return cls(cls.REACHABLE, timezone.now()) - - @classmethod - def unknown(cls): - return cls(cls.UNKNOWN, timezone.now()) - - @classmethod - def set(cls, key, value, ttl=None): - cache.set(key, value, ttl) - - @classmethod - def get(cls, key): - value = cache.get(key, cls.unknown()) - if not isinstance(value, cls): - value = cls.unknown() - return value - - @classmethod - def set_unreachable(cls, key, ttl=0): - cls.set(key, cls.unreachable(), ttl) - - @classmethod - def set_reachable(cls, key, ttl=0): - cls.set(key, cls.reachable(), ttl) - - def __eq__(self, other): - return self.status == other.status - - def __gt__(self, other): - return self.status > other.status - - def __lt__(self, other): - return not self.__gt__(other) - - def __str__(self): - return self.display() diff --git a/apps/assets/serializers/__init__.py b/apps/assets/serializers/__init__.py index 2c3e9fbd4..55437f609 100644 --- a/apps/assets/serializers/__init__.py +++ b/apps/assets/serializers/__init__.py @@ -8,6 +8,6 @@ from .system_user import * from .node import * from .domain import * from .cmd_filter import * -from .asset_user import * from .gathered_user import * from .favorite_asset import * +from .account import * diff --git a/apps/assets/serializers/account.py b/apps/assets/serializers/account.py new file mode 100644 index 000000000..5f26b7d32 --- /dev/null +++ b/apps/assets/serializers/account.py @@ -0,0 +1,42 @@ +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ + +from assets.models import AuthBook +from orgs.mixins.serializers import BulkOrgResourceModelSerializer + +from .base import AuthSerializerMixin + + +class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): + ip = serializers.ReadOnlyField(label=_("IP")) + hostname = serializers.ReadOnlyField(label=_("Hostname")) + + class Meta: + model = AuthBook + fields_mini = ['id', 'username', 'ip', 'hostname', 'version'] + fields_write_only = ['password', 'private_key', "public_key"] + fields_other = ['date_created', 'date_updated', 'connectivity', 'date_verified', 'comment'] + fields_small = fields_mini + fields_write_only + fields_other + fields_fk = ['asset', 'systemuser'] + fields = fields_small + fields_fk + extra_kwargs = { + 'username': {'required': True}, + 'password': {'write_only': True}, + 'private_key': {'write_only': True}, + 'public_key': {'write_only': True}, + } + + @classmethod + def setup_eager_loading(cls, queryset): + """ Perform necessary eager loading of data. """ + queryset = queryset.prefetch_related('systemuser', 'asset') + return queryset + + +class AccountSecretSerializer(AccountSerializer): + class Meta(AccountSerializer.Meta): + extra_kwargs = { + 'password': {'write_only': False}, + 'private_key': {'write_only': False}, + 'public_key': {'write_only': False}, + } diff --git a/apps/assets/serializers/admin_user.py b/apps/assets/serializers/admin_user.py index 2e1913b47..bf16004f4 100644 --- a/apps/assets/serializers/admin_user.py +++ b/apps/assets/serializers/admin_user.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- # from django.utils.translation import ugettext_lazy as _ -from rest_framework import serializers -from ..models import Node, AdminUser +from ..models import SystemUser from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from .base import AuthSerializer, AuthSerializerMixin +from .base import AuthSerializerMixin class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): @@ -15,8 +14,8 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): """ class Meta: - model = AdminUser - fields_mini = ['id', 'name', 'username'] + model = SystemUser + fields_mini = ['id', 'name', 'username'] fields_write_only = ['password', 'private_key', 'public_key'] fields_small = fields_mini + fields_write_only + [ 'date_created', 'date_updated', @@ -34,39 +33,8 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): 'assets_amount': {'label': _('Asset')}, } - -class AdminUserDetailSerializer(AdminUserSerializer): - class Meta(AdminUserSerializer.Meta): - fields = AdminUserSerializer.Meta.fields + ['ssh_key_fingerprint'] - - -class AdminUserAuthSerializer(AuthSerializer): - - class Meta: - model = AdminUser - fields = ['password', 'private_key'] - - -class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer): - """ - 管理用户更新关联到的集群 - """ - nodes = serializers.PrimaryKeyRelatedField( - many=True, queryset=Node.objects - ) - - class Meta: - model = AdminUser - fields = ['id', 'nodes'] - - -class TaskIDSerializer(serializers.Serializer): - task = serializers.CharField(read_only=True) - - -class AssetUserTaskSerializer(serializers.Serializer): - ACTION_CHOICES = ( - ('test', 'test'), - ) - action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True) - task = serializers.CharField(read_only=True) + def create(self, validated_data): + data = {k: v for k, v in validated_data.items()} + data['protocol'] = 'ssh' + data['type'] = SystemUser.Type.admin + return super().create(data) diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index 80e923b00..7fc2d3105 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -1,24 +1,21 @@ # -*- coding: utf-8 -*- # from rest_framework import serializers -from django.db.models import F from django.core.validators import RegexValidator from django.utils.translation import ugettext_lazy as _ from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from ..models import Asset, Node, Platform -from .base import ConnectivitySerializer +from ..models import Asset, Node, Platform, SystemUser __all__ = [ - 'AssetSerializer', 'AssetSimpleSerializer', - 'AssetDisplaySerializer', + 'AssetSerializer', 'AssetSimpleSerializer', 'AssetVerboseSerializer', 'ProtocolsField', 'PlatformSerializer', - 'AssetDetailSerializer', 'AssetTaskSerializer', + 'AssetTaskSerializer', ] class ProtocolField(serializers.RegexField): - protocols = '|'.join(dict(Asset.PROTOCOL_CHOICES).keys()) + protocols = '|'.join(dict(Asset.Protocol.choices).keys()) default_error_messages = { 'invalid': _('Protocol format should {}/{}'.format(protocols, '1-65535')) } @@ -65,9 +62,11 @@ class AssetSerializer(BulkOrgResourceModelSerializer): platform = serializers.SlugRelatedField( slug_field='name', queryset=Platform.objects.all(), label=_("Platform") ) + admin_user = serializers.PrimaryKeyRelatedField( + queryset=SystemUser.objects, label=_('Admin user'), write_only=True + ) protocols = ProtocolsField(label=_('Protocols'), required=False, default=['ssh/22']) domain_display = serializers.ReadOnlyField(source='domain.name', label=_('Domain name')) - admin_user_display = serializers.ReadOnlyField(source='admin_user.name', label=_('Admin user name')) nodes_display = serializers.ListField(child=serializers.CharField(), label=_('Nodes name'), required=False) """ @@ -81,25 +80,18 @@ class AssetSerializer(BulkOrgResourceModelSerializer): 'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info', 'os', 'os_version', 'os_arch', 'hostname_raw', 'comment', - 'created_by', 'date_created', 'hardware_info', + 'hardware_info', 'connectivity', 'date_verified' ] fields_fk = [ - 'admin_user', 'admin_user_display', 'domain', 'domain_display', 'platform' + 'domain', 'domain_display', 'platform', 'admin_user' ] - fk_only_fields = { - 'platform': ['name'] - } fields_m2m = [ 'nodes', 'nodes_display', 'labels', ] - annotates_fields = { - # 'admin_user_display': 'admin_user__name' - } - fields_as = list(annotates_fields.keys()) - fields = fields_small + fields_fk + fields_m2m + fields_as read_only_fields = [ 'created_by', 'date_created', - ] + fields_as + ] + fields = fields_small + fields_fk + fields_m2m + read_only_fields extra_kwargs = { 'protocol': {'write_only': True}, @@ -108,10 +100,19 @@ class AssetSerializer(BulkOrgResourceModelSerializer): 'org_name': {'label': _('Org name')} } + def get_fields(self): + fields = super().get_fields() + + admin_user_field = fields.get('admin_user') + # 因为 mixin 中对 fields 有处理,可能不需要返回 admin_user + if admin_user_field: + admin_user_field.queryset = SystemUser.objects.filter(type=SystemUser.Type.admin) + return fields + @classmethod def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ - queryset = queryset.prefetch_related('admin_user', 'domain', 'platform') + queryset = queryset.prefetch_related('domain', 'platform') queryset = queryset.prefetch_related('nodes', 'labels') return queryset @@ -146,25 +147,26 @@ class AssetSerializer(BulkOrgResourceModelSerializer): def create(self, validated_data): self.compatible_with_old_protocol(validated_data) nodes_display = validated_data.pop('nodes_display', '') + admin_user = validated_data.pop('admin_user', '') instance = super().create(validated_data) self.perform_nodes_display_create(instance, nodes_display) + instance.admin_user = admin_user return instance def update(self, instance, validated_data): nodes_display = validated_data.pop('nodes_display', '') self.compatible_with_old_protocol(validated_data) + admin_user = validated_data.pop('admin_user', '') instance = super().update(instance, validated_data) self.perform_nodes_display_create(instance, nodes_display) + instance.admin_user = admin_user return instance -class AssetDisplaySerializer(AssetSerializer): - connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity")) - - class Meta(AssetSerializer.Meta): - fields = AssetSerializer.Meta.fields + [ - 'connectivity', - ] +class AssetVerboseSerializer(AssetSerializer): + admin_user = serializers.PrimaryKeyRelatedField( + queryset=SystemUser.objects, label=_('Admin user') + ) class PlatformSerializer(serializers.ModelSerializer): @@ -186,16 +188,11 @@ class PlatformSerializer(serializers.ModelSerializer): ] -class AssetDetailSerializer(AssetSerializer): - platform = PlatformSerializer(read_only=True) - - class AssetSimpleSerializer(serializers.ModelSerializer): - connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity")) class Meta: model = Asset - fields = ['id', 'hostname', 'ip', 'connectivity', 'port'] + fields = ['id', 'hostname', 'ip', 'port', 'connectivity', 'date_verified'] class AssetTaskSerializer(serializers.Serializer): diff --git a/apps/assets/serializers/asset_user.py b/apps/assets/serializers/asset_user.py deleted file mode 100644 index cd098537a..000000000 --- a/apps/assets/serializers/asset_user.py +++ /dev/null @@ -1,98 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from django.utils.translation import ugettext as _ -from rest_framework import serializers - -from common.drf.serializers import AdaptedBulkListSerializer -from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from ..models import AuthBook, Asset -from ..backends import AssetUserManager - -from .base import ConnectivitySerializer, AuthSerializerMixin - - -__all__ = [ - 'AssetUserWriteSerializer', 'AssetUserReadSerializer', - 'AssetUserAuthInfoSerializer', 'AssetUserPushSerializer', -] - - -class AssetUserWriteSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): - class Meta: - model = AuthBook - list_serializer_class = AdaptedBulkListSerializer - fields_mini = ['id', 'username'] - fields_write_only = ['password', 'private_key', "public_key"] - fields_small = fields_mini + fields_write_only + ['comment'] - fields_fk = ['asset'] - fields = fields_small + fields_fk - extra_kwargs = { - 'username': {'required': True}, - 'password': {'write_only': True}, - 'private_key': {'write_only': True}, - 'public_key': {'write_only': True}, - } - - def create(self, validated_data): - if not validated_data.get("name") and validated_data.get("username"): - validated_data["name"] = validated_data["username"] - instance = AssetUserManager.create(**validated_data) - return instance - - -class AssetUserReadSerializer(AssetUserWriteSerializer): - id = serializers.CharField(read_only=True, source='union_id', label=_("ID")) - hostname = serializers.CharField(read_only=True, label=_("Hostname")) - ip = serializers.CharField(read_only=True, label=_("IP")) - asset = serializers.CharField(source='asset_id', label=_('Asset')) - backend = serializers.CharField(read_only=True, label=_("Backend")) - backend_display = serializers.CharField(read_only=True, label=_("Source")) - - class Meta(AssetUserWriteSerializer.Meta): - read_only_fields = ( - 'date_created', 'date_updated', - 'created_by', 'version', - ) - fields_mini = ['id', 'name', 'username'] - fields_write_only = ['password', 'private_key', "public_key"] - fields_small = fields_mini + fields_write_only + [ - 'backend', 'backend_display', 'version', - 'date_created', "date_updated", - 'comment' - ] - fields_fk = ['asset', 'hostname', 'ip'] - fields = fields_small + fields_fk - extra_kwargs = { - 'name': {'required': False}, - 'username': {'required': True}, - 'password': {'write_only': True}, - 'private_key': {'write_only': True}, - 'public_key': {'write_only': True}, - } - - -class AssetUserAuthInfoSerializer(AssetUserReadSerializer): - password = serializers.CharField( - max_length=256, allow_blank=True, allow_null=True, - required=False, label=_('Password') - ) - public_key = serializers.CharField( - max_length=4096, allow_blank=True, allow_null=True, - required=False, label=_('Public key') - ) - private_key = serializers.CharField( - max_length=4096, allow_blank=True, allow_null=True, - required=False, label=_('Private key') - ) - - -class AssetUserPushSerializer(serializers.Serializer): - asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, label=_("Asset")) - username = serializers.CharField(max_length=1024) - - def create(self, validated_data): - pass - - def update(self, instance, validated_data): - pass diff --git a/apps/assets/serializers/base.py b/apps/assets/serializers/base.py index 2159999c7..fa63dc41d 100644 --- a/apps/assets/serializers/base.py +++ b/apps/assets/serializers/base.py @@ -5,7 +5,6 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers from common.utils import ssh_pubkey_gen, validate_ssh_private_key -from ..models import AssetUser class AuthSerializer(serializers.ModelSerializer): @@ -29,11 +28,6 @@ class AuthSerializer(serializers.ModelSerializer): return self.instance -class ConnectivitySerializer(serializers.Serializer): - status = serializers.IntegerField() - datetime = serializers.DateTimeField() - - class AuthSerializerMixin: def validate_password(self, password): return password @@ -64,15 +58,3 @@ class AuthSerializerMixin: def update(self, instance, validated_data): self.clean_auth_fields(validated_data) return super().update(instance, validated_data) - - -class AuthInfoSerializer(serializers.ModelSerializer): - private_key = serializers.ReadOnlyField(source='get_private_key') - - class Meta: - model = AssetUser - fields = [ - 'username', 'password', - 'private_key', 'public_key', - 'date_updated', - ] diff --git a/apps/assets/serializers/cmd_filter.py b/apps/assets/serializers/cmd_filter.py index 5cf419979..2f491aa21 100644 --- a/apps/assets/serializers/cmd_filter.py +++ b/apps/assets/serializers/cmd_filter.py @@ -3,7 +3,6 @@ import re from rest_framework import serializers -from common.drf.serializers import AdaptedBulkListSerializer from ..models import CommandFilter, CommandFilterRule from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.utils import tmp_to_root_org @@ -15,7 +14,6 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer): class Meta: model = CommandFilter - list_serializer_class = AdaptedBulkListSerializer fields_mini = ['id', 'name'] fields_small = fields_mini + [ 'org_id', 'org_name', @@ -48,7 +46,6 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer): ] fields_fk = ['filter'] fields = '__all__' - list_serializer_class = AdaptedBulkListSerializer def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index 5d8d0e8d8..1626bd711 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -3,7 +3,6 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from common.drf.serializers import AdaptedBulkListSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer from common.validators import NoSpecialChars from ..models import Domain, Gateway @@ -29,7 +28,6 @@ class DomainSerializer(BulkOrgResourceModelSerializer): extra_kwargs = { 'assets': {'required': False, 'label': _('Assets')}, } - list_serializer_class = AdaptedBulkListSerializer @staticmethod def get_asset_count(obj): @@ -47,7 +45,6 @@ class DomainSerializer(BulkOrgResourceModelSerializer): class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class Meta: model = Gateway - list_serializer_class = AdaptedBulkListSerializer fields_mini = ['id', 'name'] fields_write_only = [ 'password', 'private_key', 'public_key', @@ -66,16 +63,6 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): 'public_key': {"write_only": True}, } - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.protocol_limit_to_ssh() - - def protocol_limit_to_ssh(self): - protocol_field = self.fields['protocol'] - choices = protocol_field.choices - choices.pop('rdp') - protocol_field._choices = choices - class GatewayWithAuthSerializer(GatewaySerializer): class Meta(GatewaySerializer.Meta): diff --git a/apps/assets/serializers/favorite_asset.py b/apps/assets/serializers/favorite_asset.py index 067655b0a..7c024bf1a 100644 --- a/apps/assets/serializers/favorite_asset.py +++ b/apps/assets/serializers/favorite_asset.py @@ -4,7 +4,6 @@ from rest_framework import serializers from orgs.utils import tmp_to_root_org -from common.drf.serializers import AdaptedBulkListSerializer from common.mixins import BulkSerializerMixin from ..models import FavoriteAsset @@ -18,6 +17,5 @@ class FavoriteAssetSerializer(BulkSerializerMixin, serializers.ModelSerializer): ) class Meta: - list_serializer_class = AdaptedBulkListSerializer model = FavoriteAsset fields = ['user', 'asset'] diff --git a/apps/assets/serializers/label.py b/apps/assets/serializers/label.py index f922655ef..26ab0ceb9 100644 --- a/apps/assets/serializers/label.py +++ b/apps/assets/serializers/label.py @@ -3,7 +3,6 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from common.drf.serializers import AdaptedBulkListSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import Label @@ -30,7 +29,6 @@ class LabelSerializer(BulkOrgResourceModelSerializer): extra_kwargs = { 'assets': {'required': False} } - list_serializer_class = AdaptedBulkListSerializer @staticmethod def get_asset_count(obj): diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index 726f53e34..a9a9f763b 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -2,7 +2,6 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from django.db.models import Count -from common.drf.serializers import AdaptedBulkListSerializer from common.mixins.serializers import BulkSerializerMixin from common.utils import ssh_pubkey_gen from orgs.mixins.serializers import BulkOrgResourceModelSerializer @@ -23,21 +22,21 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): 系统用户 """ auto_generate_key = serializers.BooleanField(initial=True, required=False, write_only=True) + type_display = serializers.ReadOnlyField(source='get_type_display') class Meta: model = SystemUser - list_serializer_class = AdaptedBulkListSerializer fields_mini = ['id', 'name', 'username'] fields_write_only = ['password', 'public_key', 'private_key'] fields_small = fields_mini + fields_write_only + [ - 'protocol', 'login_mode', 'login_mode_display', 'priority', - 'sudo', 'shell', 'sftp_root', 'token', + 'type', 'type_display', 'protocol', 'login_mode', 'login_mode_display', + 'priority', 'sudo', 'shell', 'sftp_root', 'token', 'home', 'system_groups', 'ad_domain', 'username_same_with_user', 'auto_push', 'auto_generate_key', 'date_created', 'date_updated', 'comment', 'created_by', ] - fields_m2m = [ 'cmd_filters', 'assets_amount'] + fields_m2m = ['cmd_filters', 'assets_amount'] fields = fields_small + fields_m2m extra_kwargs = { 'password': {"write_only": True}, @@ -55,9 +54,9 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): login_mode = self.initial_data.get("login_mode") protocol = self.initial_data.get("protocol") - if login_mode == SystemUser.LOGIN_MANUAL or \ - protocol in [SystemUser.PROTOCOL_TELNET, - SystemUser.PROTOCOL_VNC]: + if login_mode == SystemUser.LOGIN_MANUAL: + value = False + elif protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS: value = False return value @@ -71,7 +70,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): value = False elif login_mode == SystemUser.LOGIN_MANUAL: value = False - elif protocol in [SystemUser.PROTOCOL_TELNET, SystemUser.PROTOCOL_VNC]: + elif protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS: value = False return value @@ -80,7 +79,8 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): return username_same_with_user protocol = self.initial_data.get("protocol", "ssh") queryset = SystemUser.objects.filter( - protocol=protocol, username_same_with_user=True + protocol=protocol, + username_same_with_user=True ) if self.instance: queryset = queryset.exclude(id=self.instance.id) @@ -96,12 +96,14 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): login_mode = self.initial_data.get("login_mode") protocol = self.initial_data.get("protocol") username_same_with_user = self.initial_data.get("username_same_with_user") - if username_same_with_user: - return '' + if login_mode == SystemUser.LOGIN_AUTO and \ - protocol != SystemUser.PROTOCOL_VNC: + protocol != SystemUser.Protocol.vnc: msg = _('* Automatic login mode must fill in the username.') raise serializers.ValidationError(msg) + + if username_same_with_user: + username = '*' return username def validate_home(self, home): @@ -118,40 +120,57 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): raise serializers.ValidationError(error) return value + @staticmethod + def validate_admin_user(attrs): + tp = attrs.get('type') + if tp != SystemUser.Type.admin: + return attrs + attrs['protocol'] = SystemUser.Protocol.ssh + attrs['login_mode'] = SystemUser.LOGIN_AUTO + attrs['username_same_with_user'] = False + attrs['auto_push'] = False + return attrs + def validate_password(self, password): super().validate_password(password) auto_gen_key = self.initial_data.get("auto_generate_key", False) private_key = self.initial_data.get("private_key") login_mode = self.initial_data.get("login_mode") + if not self.instance and not auto_gen_key and not password and \ not private_key and login_mode == SystemUser.LOGIN_AUTO: raise serializers.ValidationError(_("Password or private key required")) return password - def validate(self, attrs): + def validate_gen_key(self, attrs): username = attrs.get("username", "manual") auto_gen_key = attrs.pop("auto_generate_key", False) protocol = attrs.get("protocol") - if protocol not in [SystemUser.PROTOCOL_RDP, SystemUser.PROTOCOL_SSH]: + if protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS: return attrs - if auto_gen_key: + # 自动生成 + if auto_gen_key and not self.instance: password = SystemUser.gen_password() attrs["password"] = password - if protocol == SystemUser.PROTOCOL_SSH: + if protocol == SystemUser.Protocol.ssh: private_key, public_key = SystemUser.gen_key(username) attrs["private_key"] = private_key attrs["public_key"] = public_key - # 如果设置了private key,没有设置public key则生成 + # 如果设置了private key,没有设置public key则生成 elif attrs.get("private_key", None): private_key = attrs["private_key"] password = attrs.get("password") - public_key = ssh_pubkey_gen(private_key, password=password, - username=username) + public_key = ssh_pubkey_gen(private_key, password=password, username=username) attrs["public_key"] = public_key return attrs + def validate(self, attrs): + attrs = self.validate_admin_user(attrs) + attrs = self.validate_gen_key(attrs) + return attrs + class SystemUserListSerializer(SystemUserSerializer): @@ -222,24 +241,26 @@ class RelationMixin(BulkSerializerMixin, serializers.Serializer): fields.extend(['systemuser', "systemuser_display"]) return fields - class Meta: - list_serializer_class = AdaptedBulkListSerializer - class SystemUserAssetRelationSerializer(RelationMixin, serializers.ModelSerializer): asset_display = serializers.ReadOnlyField() - class Meta(RelationMixin.Meta): + class Meta: model = SystemUser.assets.through fields = [ - 'id', "asset", "asset_display", + "id", "asset", "asset_display", + 'systemuser', 'systemuser_display' ] + use_model_bulk_create = True + model_bulk_create_kwargs = { + 'ignore_conflicts': True + } class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerializer): node_display = serializers.SerializerMethodField() - class Meta(RelationMixin.Meta): + class Meta: model = SystemUser.nodes.through fields = [ 'id', 'node', "node_display", @@ -252,7 +273,7 @@ class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerialize class SystemUserUserRelationSerializer(RelationMixin, serializers.ModelSerializer): user_display = serializers.ReadOnlyField() - class Meta(RelationMixin.Meta): + class Meta: model = SystemUser.users.through fields = [ 'id', "user", "user_display", diff --git a/apps/assets/signals_handler/__init__.py b/apps/assets/signals_handler/__init__.py index c8f332f26..8a895544f 100644 --- a/apps/assets/signals_handler/__init__.py +++ b/apps/assets/signals_handler/__init__.py @@ -1,3 +1,5 @@ -from .common import * +from .asset import * +from .system_user import * +from .authbook import * from .node_assets_amount import * from .node_assets_mapping import * diff --git a/apps/assets/signals_handler/asset.py b/apps/assets/signals_handler/asset.py new file mode 100644 index 000000000..0a623f553 --- /dev/null +++ b/apps/assets/signals_handler/asset.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +# +from django.db.models.signals import ( + post_save, m2m_changed, pre_delete, post_delete, pre_save +) +from django.dispatch import receiver + +from common.const.signals import POST_ADD, POST_REMOVE, PRE_REMOVE +from common.utils import get_logger +from common.decorator import on_transaction_commit +from assets.models import Asset, SystemUser, Node +from assets.tasks import ( + update_assets_hardware_info_util, + test_asset_connectivity_util, + push_system_user_to_assets, +) + +logger = get_logger(__file__) + + +def update_asset_hardware_info_on_created(asset): + logger.debug("Update asset `{}` hardware info".format(asset)) + update_assets_hardware_info_util.delay([asset]) + + +def test_asset_conn_on_created(asset): + logger.debug("Test asset `{}` connectivity".format(asset)) + test_asset_connectivity_util.delay([asset]) + + +@receiver(pre_save, sender=Node) +def on_node_pre_save(sender, instance: Node, **kwargs): + instance.parent_key = instance.compute_parent_key() + + +@receiver(post_save, sender=Asset) +@on_transaction_commit +def on_asset_created_or_update(sender, instance=None, created=False, **kwargs): + """ + 当资产创建时,更新硬件信息,更新可连接性 + 确保资产必须属于一个节点 + """ + if created: + logger.info("Asset create signal recv: {}".format(instance)) + + # 获取资产硬件信息 + update_asset_hardware_info_on_created(instance) + test_asset_conn_on_created(instance) + + # 确保资产存在一个节点 + has_node = instance.nodes.all().exists() + if not has_node: + instance.nodes.add(Node.org_root()) + + +@receiver(m2m_changed, sender=Asset.nodes.through) +def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs): + """ + 本操作共访问 4 次数据库 + + 当资产的节点发生变化时,或者 当节点的资产关系发生变化时, + 节点下新增的资产,添加到节点关联的系统用户中 + """ + if action != POST_ADD: + return + logger.debug("Assets node add signal recv: {}".format(action)) + if reverse: + nodes = [instance.key] + asset_ids = pk_set + else: + nodes = Node.objects.filter(pk__in=pk_set).values_list('key', flat=True) + asset_ids = [instance.id] + + # 节点资产发生变化时,将资产关联到节点及祖先节点关联的系统用户, 只关注新增的 + nodes_ancestors_keys = set() + for node in nodes: + nodes_ancestors_keys.update(Node.get_node_ancestor_keys(node, with_self=True)) + + # 查询所有祖先节点关联的系统用户,都是要跟资产建立关系的 + system_user_ids = SystemUser.objects.filter( + nodes__key__in=nodes_ancestors_keys + ).distinct().values_list('id', flat=True) + + # 查询所有已存在的关系 + m2m_model = SystemUser.assets.through + exist = set(m2m_model.objects.filter( + system_user_id__in=system_user_ids, asset_id__in=asset_ids + ).values_list('system_user_id', 'asset_id')) + # TODO 优化 + to_create = [] + for system_user_id in system_user_ids: + asset_ids_to_push = [] + for asset_id in asset_ids: + if (system_user_id, asset_id) in exist: + continue + asset_ids_to_push.append(asset_id) + to_create.append(m2m_model( + system_user_id=system_user_id, + asset_id=asset_id + )) + if asset_ids_to_push: + push_system_user_to_assets.delay(system_user_id, asset_ids_to_push) + m2m_model.objects.bulk_create(to_create) + + +RELATED_NODE_IDS = '_related_node_ids' + + +@receiver(pre_delete, sender=Asset) +def on_asset_delete(instance: Asset, using, **kwargs): + node_ids = set(Node.objects.filter( + assets=instance + ).distinct().values_list('id', flat=True)) + setattr(instance, RELATED_NODE_IDS, node_ids) + m2m_changed.send( + sender=Asset.nodes.through, instance=instance, reverse=False, + model=Node, pk_set=node_ids, using=using, action=PRE_REMOVE + ) + + +@receiver(post_delete, sender=Asset) +def on_asset_post_delete(instance: Asset, using, **kwargs): + node_ids = getattr(instance, RELATED_NODE_IDS, None) + if node_ids: + m2m_changed.send( + sender=Asset.nodes.through, instance=instance, reverse=False, + model=Node, pk_set=node_ids, using=using, action=POST_REMOVE + ) diff --git a/apps/assets/signals_handler/authbook.py b/apps/assets/signals_handler/authbook.py new file mode 100644 index 000000000..0c6635c4c --- /dev/null +++ b/apps/assets/signals_handler/authbook.py @@ -0,0 +1,46 @@ +from django.dispatch import receiver +from django.apps import apps +from simple_history.signals import pre_create_historical_record +from django.db.models.signals import post_save, pre_save + +from common.utils import get_logger +from orgs.utils import tmp_to_root_org +from ..models import AuthBook, SystemUser + +AuthBookHistory = apps.get_model('assets', 'HistoricalAuthBook') +logger = get_logger(__name__) + + +@receiver(pre_create_historical_record, sender=AuthBookHistory) +def pre_create_historical_record_callback(sender, instance=None, history_instance=None, **kwargs): + attrs_to_copy = ['username', 'password', 'private_key'] + + for attr in attrs_to_copy: + if getattr(history_instance, attr): + continue + if not history_instance.systemuser: + continue + system_user_attr_value = getattr(history_instance.systemuser, attr) + if system_user_attr_value: + setattr(history_instance, attr, system_user_attr_value) + + +@receiver(post_save, sender=AuthBook) +def on_authbook_post_create(sender, instance, **kwargs): + # 去掉这个资产的管理用户 + if instance.systemuser and instance.systemuser.is_admin_user: + with tmp_to_root_org(): + deleted_count, other = AuthBook.objects.filter( + asset=instance.asset, + systemuser__type=SystemUser.Type.admin + ).exclude(id=instance.id).delete() + logger.debug('Remove asset old admin user: {}'.format(deleted_count)) + + if not instance.systemuser: + instance.sync_to_system_user_account() + + +@receiver(pre_save, sender=AuthBook) +def on_authbook_pre_create(sender, instance, **kwargs): + # 升级版本号 + instance.version = instance.history.all().count() + 1 diff --git a/apps/assets/signals_handler/common.py b/apps/assets/signals_handler/common.py index 50f7f41f1..e69de29bb 100644 --- a/apps/assets/signals_handler/common.py +++ b/apps/assets/signals_handler/common.py @@ -1,223 +0,0 @@ -# -*- coding: utf-8 -*- -# -from django.db.models.signals import ( - post_save, m2m_changed, pre_delete, post_delete, pre_save -) -from django.dispatch import receiver - -from common.exceptions import M2MReverseNotAllowed -from common.const.signals import POST_ADD, POST_REMOVE, PRE_REMOVE -from common.utils import get_logger -from common.decorator import on_transaction_commit -from assets.models import Asset, SystemUser, Node -from users.models import User -from assets.tasks import ( - update_assets_hardware_info_util, - test_asset_connectivity_util, - push_system_user_to_assets_manual, - push_system_user_to_assets, - add_nodes_assets_to_system_users -) - -logger = get_logger(__file__) - - -def update_asset_hardware_info_on_created(asset): - logger.debug("Update asset `{}` hardware info".format(asset)) - update_assets_hardware_info_util.delay([asset]) - - -def test_asset_conn_on_created(asset): - logger.debug("Test asset `{}` connectivity".format(asset)) - test_asset_connectivity_util.delay([asset]) - - -@receiver(pre_save, sender=Node) -def on_node_pre_save(sender, instance: Node, **kwargs): - instance.parent_key = instance.compute_parent_key() - - -@receiver(post_save, sender=Asset) -@on_transaction_commit -def on_asset_created_or_update(sender, instance=None, created=False, **kwargs): - """ - 当资产创建时,更新硬件信息,更新可连接性 - 确保资产必须属于一个节点 - """ - if created: - logger.info("Asset create signal recv: {}".format(instance)) - - # 获取资产硬件信息 - update_asset_hardware_info_on_created(instance) - test_asset_conn_on_created(instance) - - # 确保资产存在一个节点 - has_node = instance.nodes.all().exists() - if not has_node: - instance.nodes.add(Node.org_root()) - - -@receiver(post_save, sender=SystemUser, dispatch_uid="jms") -@on_transaction_commit -def on_system_user_update(instance: SystemUser, created, **kwargs): - """ - 当系统用户更新时,可能更新了秘钥,用户名等,这时要自动推送系统用户到资产上, - 其实应该当 用户名,密码,秘钥 sudo等更新时再推送,这里偷个懒, - 这里直接取了 instance.assets 因为nodes和系统用户发生变化时,会自动将nodes下的资产 - 关联到上面 - """ - if instance and not created: - logger.info("System user update signal recv: {}".format(instance)) - assets = instance.assets.all().valid() - push_system_user_to_assets.delay(instance.id, [_asset.id for _asset in assets]) - - -@receiver(m2m_changed, sender=SystemUser.assets.through) -@on_transaction_commit -def on_system_user_assets_change(instance, action, model, pk_set, **kwargs): - """ - 当系统用户和资产关系发生变化时,应该重新推送系统用户到新添加的资产中 - """ - if action != POST_ADD: - return - logger.debug("System user assets change signal recv: {}".format(instance)) - if model == Asset: - system_user_ids = [instance.id] - asset_ids = pk_set - else: - system_user_ids = pk_set - asset_ids = [instance.id] - for system_user_id in system_user_ids: - push_system_user_to_assets.delay(system_user_id, asset_ids) - - -@receiver(m2m_changed, sender=SystemUser.users.through) -@on_transaction_commit -def on_system_user_users_change(sender, instance: SystemUser, action, model, pk_set, reverse, **kwargs): - """ - 当系统用户和用户关系发生变化时,应该重新推送系统用户资产中 - """ - if action != POST_ADD: - return - - if reverse: - raise M2MReverseNotAllowed - - if not instance.username_same_with_user: - return - - logger.debug("System user users change signal recv: {}".format(instance)) - usernames = model.objects.filter(pk__in=pk_set).values_list('username', flat=True) - - for username in usernames: - push_system_user_to_assets_manual.delay(instance, username) - - -@receiver(m2m_changed, sender=SystemUser.nodes.through) -@on_transaction_commit -def on_system_user_nodes_change(sender, instance=None, action=None, model=None, pk_set=None, **kwargs): - """ - 当系统用户和节点关系发生变化时,应该将节点下资产关联到新的系统用户上 - """ - if action != POST_ADD: - return - logger.info("System user nodes update signal recv: {}".format(instance)) - - queryset = model.objects.filter(pk__in=pk_set) - if model == Node: - nodes_keys = queryset.values_list('key', flat=True) - system_users = [instance] - else: - nodes_keys = [instance.key] - system_users = queryset - add_nodes_assets_to_system_users.delay(nodes_keys, system_users) - - -@receiver(m2m_changed, sender=SystemUser.groups.through) -def on_system_user_groups_change(instance, action, pk_set, reverse, **kwargs): - """ - 当系统用户和用户组关系发生变化时,应该将组下用户关联到新的系统用户上 - """ - if action != POST_ADD: - return - if reverse: - raise M2MReverseNotAllowed - logger.info("System user groups update signal recv: {}".format(instance)) - - users = User.objects.filter(groups__id__in=pk_set).distinct() - instance.users.add(*users) - - -@receiver(m2m_changed, sender=Asset.nodes.through) -def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs): - """ - 本操作共访问 4 次数据库 - - 当资产的节点发生变化时,或者 当节点的资产关系发生变化时, - 节点下新增的资产,添加到节点关联的系统用户中 - """ - if action != POST_ADD: - return - logger.debug("Assets node add signal recv: {}".format(action)) - if reverse: - nodes = [instance.key] - asset_ids = pk_set - else: - nodes = Node.objects.filter(pk__in=pk_set).values_list('key', flat=True) - asset_ids = [instance.id] - - # 节点资产发生变化时,将资产关联到节点及祖先节点关联的系统用户, 只关注新增的 - nodes_ancestors_keys = set() - for node in nodes: - nodes_ancestors_keys.update(Node.get_node_ancestor_keys(node, with_self=True)) - - # 查询所有祖先节点关联的系统用户,都是要跟资产建立关系的 - system_user_ids = SystemUser.objects.filter( - nodes__key__in=nodes_ancestors_keys - ).distinct().values_list('id', flat=True) - - # 查询所有已存在的关系 - m2m_model = SystemUser.assets.through - exist = set(m2m_model.objects.filter( - systemuser_id__in=system_user_ids, asset_id__in=asset_ids - ).values_list('systemuser_id', 'asset_id')) - # TODO 优化 - to_create = [] - for system_user_id in system_user_ids: - asset_ids_to_push = [] - for asset_id in asset_ids: - if (system_user_id, asset_id) in exist: - continue - asset_ids_to_push.append(asset_id) - to_create.append(m2m_model( - systemuser_id=system_user_id, - asset_id=asset_id - )) - if asset_ids_to_push: - push_system_user_to_assets.delay(system_user_id, asset_ids_to_push) - m2m_model.objects.bulk_create(to_create) - - -RELATED_NODE_IDS = '_related_node_ids' - - -@receiver(pre_delete, sender=Asset) -def on_asset_delete(instance: Asset, using, **kwargs): - node_ids = set(Node.objects.filter( - assets=instance - ).distinct().values_list('id', flat=True)) - setattr(instance, RELATED_NODE_IDS, node_ids) - m2m_changed.send( - sender=Asset.nodes.through, instance=instance, reverse=False, - model=Node, pk_set=node_ids, using=using, action=PRE_REMOVE - ) - - -@receiver(post_delete, sender=Asset) -def on_asset_post_delete(instance: Asset, using, **kwargs): - node_ids = getattr(instance, RELATED_NODE_IDS, None) - if node_ids: - m2m_changed.send( - sender=Asset.nodes.through, instance=instance, reverse=False, - model=Node, pk_set=node_ids, using=using, action=POST_REMOVE - ) diff --git a/apps/assets/signals_handler/system_user.py b/apps/assets/signals_handler/system_user.py new file mode 100644 index 000000000..a5f021a64 --- /dev/null +++ b/apps/assets/signals_handler/system_user.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +# +from django.db.models.signals import ( + post_save, m2m_changed, pre_save, pre_delete, post_delete +) +from django.dispatch import receiver + +from common.exceptions import M2MReverseNotAllowed +from common.const.signals import POST_ADD +from common.utils import get_logger +from common.decorator import on_transaction_commit +from assets.models import Asset, SystemUser, Node, AuthBook +from users.models import User +from orgs.utils import get_current_org, tmp_to_root_org +from assets.tasks import ( + push_system_user_to_assets_manual, + push_system_user_to_assets, + add_nodes_assets_to_system_users +) + +logger = get_logger(__file__) + + +@receiver(m2m_changed, sender=SystemUser.assets.through) +@on_transaction_commit +def on_system_user_assets_change(instance, action, model, pk_set, **kwargs): + """ + 当系统用户和资产关系发生变化时,应该重新推送系统用户到新添加的资产中 + """ + logger.debug("System user assets change signal recv: {}".format(instance)) + + if not instance: + logger.debug('No system user found') + return + + if model == Asset: + system_user_ids = [instance.id] + asset_ids = pk_set + else: + system_user_ids = pk_set + asset_ids = [instance.id] + + # 通过 through 创建的没有 org_id + current_org_id = get_current_org().id + with tmp_to_root_org(): + authbooks = AuthBook.objects.filter( + asset_id__in=asset_ids, + systemuser_id__in=system_user_ids + ) + authbooks.update(org_id=current_org_id) + + save_action_mapper = { + 'pre_add': pre_save, + 'post_add': post_save, + 'pre_remove': pre_delete, + 'post_remove': post_delete + } + + for ab in authbooks: + ab.org_id = current_org_id + + post_action = save_action_mapper[action] + logger.debug('Send AuthBook post save signal: {} -> {}'.format(action, ab.id)) + post_action.send(sender=AuthBook, instance=ab, created=True) + + if action == 'post_add': + for system_user_id in system_user_ids: + push_system_user_to_assets.delay(system_user_id, asset_ids) + + +@receiver(m2m_changed, sender=SystemUser.users.through) +@on_transaction_commit +def on_system_user_users_change(sender, instance: SystemUser, action, model, pk_set, reverse, **kwargs): + """ + 当系统用户和用户关系发生变化时,应该重新推送系统用户资产中 + """ + if action != POST_ADD: + return + + if reverse: + raise M2MReverseNotAllowed + + if not instance.username_same_with_user: + return + + logger.debug("System user users change signal recv: {}".format(instance)) + usernames = model.objects.filter(pk__in=pk_set).values_list('username', flat=True) + + for username in usernames: + push_system_user_to_assets_manual.delay(instance, username) + + +@receiver(m2m_changed, sender=SystemUser.nodes.through) +@on_transaction_commit +def on_system_user_nodes_change(sender, instance=None, action=None, model=None, pk_set=None, **kwargs): + """ + 当系统用户和节点关系发生变化时,应该将节点下资产关联到新的系统用户上 + """ + if action != POST_ADD: + return + logger.info("System user nodes update signal recv: {}".format(instance)) + + queryset = model.objects.filter(pk__in=pk_set) + if model == Node: + nodes_keys = queryset.values_list('key', flat=True) + system_users = [instance] + else: + nodes_keys = [instance.key] + system_users = queryset + add_nodes_assets_to_system_users.delay(nodes_keys, system_users) + + +@receiver(m2m_changed, sender=SystemUser.groups.through) +def on_system_user_groups_change(instance, action, pk_set, reverse, **kwargs): + """ + 当系统用户和用户组关系发生变化时,应该将组下用户关联到新的系统用户上 + """ + if action != POST_ADD: + return + if reverse: + raise M2MReverseNotAllowed + logger.info("System user groups update signal recv: {}".format(instance)) + + users = User.objects.filter(groups__id__in=pk_set).distinct() + instance.users.add(*users) + + +@receiver(post_save, sender=SystemUser, dispatch_uid="jms") +@on_transaction_commit +def on_system_user_update(instance: SystemUser, created, **kwargs): + """ + 当系统用户更新时,可能更新了秘钥,用户名等,这时要自动推送系统用户到资产上, + 其实应该当 用户名,密码,秘钥 sudo等更新时再推送,这里偷个懒, + 这里直接取了 instance.assets 因为nodes和系统用户发生变化时,会自动将nodes下的资产 + 关联到上面 + """ + if instance and not created: + logger.info("System user update signal recv: {}".format(instance)) + assets = instance.assets.all().valid() + push_system_user_to_assets.delay(instance.id, [_asset.id for _asset in assets]) diff --git a/apps/assets/tasks/__init__.py b/apps/assets/tasks/__init__.py index b1866d5ec..7aaed0efd 100644 --- a/apps/assets/tasks/__init__.py +++ b/apps/assets/tasks/__init__.py @@ -2,9 +2,8 @@ # from .utils import * from .common import * -from .admin_user_connectivity import * from .asset_connectivity import * -from .asset_user_connectivity import * +from .account_connectivity import * from .gather_asset_users import * from .gather_asset_hardware_info import * from .push_system_user import * diff --git a/apps/assets/tasks/asset_user_connectivity.py b/apps/assets/tasks/account_connectivity.py similarity index 51% rename from apps/assets/tasks/asset_user_connectivity.py rename to apps/assets/tasks/account_connectivity.py index ab1c417cc..23e7fc7be 100644 --- a/apps/assets/tasks/asset_user_connectivity.py +++ b/apps/assets/tasks/account_connectivity.py @@ -3,9 +3,9 @@ from celery import shared_task from django.utils.translation import ugettext as _ -from common.utils import get_logger, get_object_or_none +from common.utils import get_logger from orgs.utils import org_aware_func -from ..models import Asset +from ..models import Connectivity from . import const from .utils import check_asset_can_run_ansible @@ -14,13 +14,13 @@ logger = get_logger(__file__) __all__ = [ - 'test_asset_user_connectivity_util', 'test_asset_users_connectivity_manual', - 'get_test_asset_user_connectivity_tasks', 'test_user_connectivity', + 'test_account_connectivity_util', 'test_accounts_connectivity_manual', + 'get_test_account_connectivity_tasks', 'test_user_connectivity', 'run_adhoc', ] -def get_test_asset_user_connectivity_tasks(asset): +def get_test_account_connectivity_tasks(asset): if asset.is_unixlike(): tasks = const.PING_UNIXLIKE_TASKS elif asset.is_windows(): @@ -57,7 +57,7 @@ def test_user_connectivity(task_name, asset, username, password=None, private_ke """ from ops.inventory import JMSCustomInventory - tasks = get_test_asset_user_connectivity_tasks(asset) + tasks = get_test_account_connectivity_tasks(asset) if not tasks: logger.debug("No tasks ") return {}, {} @@ -71,62 +71,37 @@ def test_user_connectivity(task_name, asset, username, password=None, private_ke return raw, summary -@org_aware_func("asset_user") -def test_asset_user_connectivity_util(asset_user, task_name): +@org_aware_func("account") +def test_account_connectivity_util(account, task_name): """ - :param asset_user: 对象 + :param account: 对象 :param task_name: :return: """ - if not check_asset_can_run_ansible(asset_user.asset): + if not check_asset_can_run_ansible(account.asset): return try: raw, summary = test_user_connectivity( - task_name=task_name, asset=asset_user.asset, - username=asset_user.username, password=asset_user.password, - private_key=asset_user.private_key_file + task_name=task_name, asset=account.asset, + username=account.username, password=account.password, + private_key=account.private_key_file ) except Exception as e: logger.warn("Failed run adhoc {}, {}".format(task_name, e)) return - asset_user.set_connectivity(summary) + + if summary.get('success'): + account.set_connectivity(Connectivity.ok) + else: + account.set_connectivity(Connectivity.failed) @shared_task(queue="ansible") -def test_asset_users_connectivity_manual(asset_users): +def test_accounts_connectivity_manual(accounts): """ - :param asset_users: 对象 + :param accounts: 对象 """ - 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(queue="ansible") -def push_asset_user_util(asset_user): - """ - :param asset_user: 对象 - """ - from .push_system_user import push_system_user_util - if not asset_user.backend.startswith('system_user'): - logger.error("Asset user is not from system user") - return - union_id = asset_user.union_id - union_id_list = union_id.split('_') - if len(union_id_list) < 2: - logger.error("Asset user union id length less than 2") - return - system_user_id = union_id_list[0] - asset_id = union_id_list[1] - asset = get_object_or_none(Asset, pk=asset_id) - system_user = None - if not asset: - return - hosts = check_asset_can_run_ansible([asset]) - if asset.is_unixlike: - pass - - - - + for account in accounts: + task_name = _("Test account connectivity: {}").format(account) + test_account_connectivity_util(account, task_name) diff --git a/apps/assets/tasks/admin_user_connectivity.py b/apps/assets/tasks/admin_user_connectivity.py deleted file mode 100644 index 1760c1f4d..000000000 --- a/apps/assets/tasks/admin_user_connectivity.py +++ /dev/null @@ -1,69 +0,0 @@ -# ~*~ coding: utf-8 ~*~ - -from celery import shared_task -from django.utils.translation import ugettext as _ -from django.core.cache import cache - -from orgs.utils import tmp_to_root_org, org_aware_func -from common.utils import get_logger -from ops.celery.decorator import register_as_period_task - -from ..models import AdminUser -from .utils import clean_ansible_task_hosts -from .asset_connectivity import test_asset_connectivity_util -from . import const - - -logger = get_logger(__file__) -__all__ = [ - 'test_admin_user_connectivity_util', 'test_admin_user_connectivity_manual', - 'test_admin_user_connectivity_period' -] - - -@org_aware_func("admin_user") -def test_admin_user_connectivity_util(admin_user, task_name): - """ - Test asset admin user can connect or not. Using ansible api do that - :param admin_user: - :param task_name: - :return: - """ - assets = admin_user.get_related_assets() - hosts = clean_ansible_task_hosts(assets) - if not hosts: - return {} - summary = test_asset_connectivity_util(hosts, task_name) - return summary - - -@shared_task(queue="ansible") -@register_as_period_task(interval=3600) -def test_admin_user_connectivity_period(): - """ - A period task that update the ansible task period - """ - if not const.PERIOD_TASK_ENABLED: - logger.debug('Period task off, skip') - return - key = '_JMS_TEST_ADMIN_USER_CONNECTIVITY_PERIOD' - prev_execute_time = cache.get(key) - if prev_execute_time: - logger.debug("Test admin user connectivity, less than 40 minutes, skip") - return - cache.set(key, 1, 60*40) - with tmp_to_root_org(): - admin_users = AdminUser.objects.all() - for admin_user in admin_users: - task_name = _("Test admin user connectivity period: {}").format( - admin_user.name - ) - test_admin_user_connectivity_util(admin_user, task_name) - cache.set(key, 1, 60*40) - - -@shared_task(queue="ansible") -def test_admin_user_connectivity_manual(admin_user): - task_name = _("Test admin user connectivity: {}").format(admin_user.name) - test_admin_user_connectivity_util(admin_user, task_name) - return True diff --git a/apps/assets/tasks/asset_connectivity.py b/apps/assets/tasks/asset_connectivity.py index ea4b90ea6..1ae98fd1d 100644 --- a/apps/assets/tasks/asset_connectivity.py +++ b/apps/assets/tasks/asset_connectivity.py @@ -6,7 +6,7 @@ from django.utils.translation import ugettext as _ from common.utils import get_logger from orgs.utils import org_aware_func -from ..models.utils import Connectivity +from ..models import Asset, Connectivity, AuthBook from . import const from .utils import clean_ansible_task_hosts, group_asset_by_platform @@ -18,6 +18,28 @@ __all__ = [ ] +def set_assets_accounts_connectivity(assets, results_summary): + asset_ids_ok = set() + asset_ids_failed = set() + + asset_hostnames_ok = results_summary.get('contacted', {}).keys() + + for asset in assets: + if asset.hostname in asset_hostnames_ok: + asset_ids_ok.add(asset.id) + else: + asset_ids_failed.add(asset.id) + + Asset.bulk_set_connectivity(asset_ids_ok, Connectivity.ok) + Asset.bulk_set_connectivity(asset_ids_failed, Connectivity.failed) + + accounts_ok = AuthBook.objects.filter(asset_id__in=asset_ids_ok, systemuser__type='admin') + accounts_failed = AuthBook.objects.filter(asset_id__in=asset_ids_failed, systemuser__type='admin') + + AuthBook.bulk_set_connectivity(accounts_ok, Connectivity.ok) + AuthBook.bulk_set_connectivity(accounts_failed, Connectivity.failed) + + @shared_task(queue="ansible") @org_aware_func("assets") def test_asset_connectivity_util(assets, task_name=None): @@ -60,14 +82,7 @@ def test_asset_connectivity_util(assets, task_name=None): results_summary['contacted'].update(contacted) results_summary['dark'].update(dark) continue - - for asset in assets: - if asset.hostname in results_summary.get('dark', {}).keys(): - asset.connectivity = Connectivity.unreachable() - elif asset.hostname in results_summary.get('contacted', {}).keys(): - asset.connectivity = Connectivity.reachable() - else: - asset.connectivity = Connectivity.unknown() + set_assets_accounts_connectivity(assets, results_summary) return results_summary diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 8bc20a162..af98fadbd 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -11,16 +11,16 @@ app_name = 'assets' router = BulkRouter() router.register(r'assets', api.AssetViewSet, 'asset') +router.register(r'accounts', api.AccountViewSet, 'account') +router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret') router.register(r'platforms', api.AssetPlatformViewSet, 'platform') -router.register(r'admin-users', api.AdminUserViewSet, 'admin-user') router.register(r'system-users', api.SystemUserViewSet, 'system-user') +router.register(r'admin-users', api.AdminUserViewSet, 'admin-user') router.register(r'labels', api.LabelViewSet, 'label') router.register(r'nodes', api.NodeViewSet, 'node') router.register(r'domains', api.DomainViewSet, 'domain') router.register(r'gateways', api.GatewayViewSet, 'gateway') router.register(r'cmd-filters', api.CommandFilterViewSet, 'cmd-filter') -router.register(r'asset-users', api.AssetUserViewSet, 'asset-user') -router.register(r'asset-user-auth-infos', api.AssetUserAuthInfoViewSet, 'asset-user-auth-info') router.register(r'gathered-users', api.GatheredUserViewSet, 'gathered-user') router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset') router.register(r'system-users-assets-relations', api.SystemUserAssetRelationViewSet, 'system-users-assets-relation') @@ -37,13 +37,6 @@ urlpatterns = [ path('assets//tasks/', api.AssetTaskCreateApi.as_view(), name='asset-task-create'), path('assets/tasks/', api.AssetsTaskCreateApi.as_view(), name='assets-task-create'), - path('asset-users/tasks/', api.AssetUserTaskCreateAPI.as_view(), name='asset-user-task-create'), - - path('admin-users//nodes/', api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'), - path('admin-users//auth/', api.AdminUserAuthApi.as_view(), name='admin-user-auth'), - path('admin-users//connective/', api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'), - path('admin-users//assets/', api.AdminUserAssetsListView.as_view(), name='admin-user-assets'), - path('system-users//auth-info/', api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'), path('system-users//assets/', api.SystemUserAssetsListView.as_view(), name='system-user-assets'), path('system-users//assets//auth-info/', api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'), diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index 7bab342ee..800176d37 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -5,7 +5,6 @@ from rest_framework import serializers from django.db.models import F from common.mixins import BulkSerializerMixin -from common.drf.serializers import AdaptedBulkListSerializer from terminal.models import Session from ops.models import CommandExecution from . import models @@ -108,7 +107,6 @@ class CommandExecutionHostsRelationSerializer(BulkSerializerMixin, serializers.M commandexecution_display = serializers.ReadOnlyField() class Meta: - list_serializer_class = AdaptedBulkListSerializer model = CommandExecution.hosts.through fields = [ 'id', 'asset', 'asset_display', 'commandexecution', 'commandexecution_display' diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index d4824c355..a160eea92 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -15,7 +15,7 @@ from rest_framework import serializers from authentication.signals import post_auth_failed, post_auth_success from common.utils import get_logger, random_string -from common.drf.api import SerializerMixin2 +from common.drf.api import SerializerMixin from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser from orgs.mixins.api import RootOrgViewMixin @@ -29,7 +29,7 @@ logger = get_logger(__name__) __all__ = ['UserConnectionTokenViewSet'] -class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericViewSet): +class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin, GenericViewSet): permission_classes = (IsSuperUserOrAppUser,) serializer_classes = { 'default': ConnectionTokenSerializer, diff --git a/apps/authentication/api/mfa.py b/apps/authentication/api/mfa.py index ac0ba66f4..b81eeee29 100644 --- a/apps/authentication/api/mfa.py +++ b/apps/authentication/api/mfa.py @@ -2,12 +2,13 @@ # import time from django.utils.translation import ugettext as _ +from django.conf import settings from rest_framework.permissions import AllowAny from rest_framework.generics import CreateAPIView from rest_framework.serializers import ValidationError from rest_framework.response import Response -from common.permissions import IsValidUser +from common.permissions import IsValidUser, NeedMFAVerify from ..serializers import OtpVerifySerializer from .. import serializers from .. import errors @@ -48,6 +49,9 @@ class UserOtpVerifyApi(CreateAPIView): permission_classes = (IsValidUser,) serializer_class = OtpVerifySerializer + def get(self, request, *args, **kwargs): + return Response({'code': 'valid', 'msg': 'verified'}) + def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -58,3 +62,8 @@ class UserOtpVerifyApi(CreateAPIView): return Response({"ok": "1"}) else: return Response({"error": _("Code is invalid")}, status=400) + + def get_permissions(self): + if self.request.method.lower() == 'get' and settings.SECURITY_VIEW_AUTH_NEED_MFA: + self.permission_classes = [NeedMFAVerify] + return super().get_permissions() diff --git a/apps/common/drf/api.py b/apps/common/drf/api.py index 22b1321d5..43765f822 100644 --- a/apps/common/drf/api.py +++ b/apps/common/drf/api.py @@ -2,12 +2,12 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework_bulk import BulkModelViewSet from ..mixins.api import ( - SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, PaginatedResponseMixin, - RelationMixin, AllowBulkDestoryMixin, RenderToJsonMixin, + SerializerMixin, QuerySetMixin, ExtraFilterFieldsMixin, PaginatedResponseMixin, + RelationMixin, AllowBulkDestroyMixin, RenderToJsonMixin, ) -class CommonMixin(SerializerMixin2, +class CommonMixin(SerializerMixin, QuerySetMixin, ExtraFilterFieldsMixin, PaginatedResponseMixin, @@ -26,13 +26,13 @@ class JMSModelViewSet(CommonMixin, class JMSBulkModelViewSet(CommonMixin, - AllowBulkDestoryMixin, + AllowBulkDestroyMixin, BulkModelViewSet): pass class JMSBulkRelationModelViewSet(CommonMixin, RelationMixin, - AllowBulkDestoryMixin, + AllowBulkDestroyMixin, BulkModelViewSet): pass diff --git a/apps/common/mixins/api.py b/apps/common/mixins/api.py index a0d5875c6..13323df0d 100644 --- a/apps/common/mixins/api.py +++ b/apps/common/mixins/api.py @@ -23,7 +23,7 @@ from ..utils import lazyproperty __all__ = [ 'JSONResponseMixin', 'CommonApiMixin', 'AsyncApiMixin', 'RelationMixin', - 'SerializerMixin2', 'QuerySetMixin', 'ExtraFilterFieldsMixin', 'RenderToJsonMixin', + 'QuerySetMixin', 'ExtraFilterFieldsMixin', 'RenderToJsonMixin', ] @@ -62,21 +62,27 @@ class RenderToJsonMixin: class SerializerMixin: """ 根据用户请求动作的不同,获取不同的 `serializer_class `""" + action: str + request: Request + serializer_classes = None + single_actions = ['put', 'retrieve', 'patch'] def get_serializer_class_by_view_action(self): if not hasattr(self, 'serializer_classes'): return None if not isinstance(self.serializer_classes, dict): return None - action = self.request.query_params.get('action') - serializer_class = None - if action: - # metadata方法 使用 action 参数获取 - serializer_class = self.serializer_classes.get(action) + view_action = self.request.query_params.get('action') or self.action or 'list' + serializer_class = self.serializer_classes.get(view_action) + if serializer_class is None: - serializer_class = self.serializer_classes.get(self.action) + view_method = self.request.method.lower() + serializer_class = self.serializer_classes.get(view_method) + + if serializer_class is None and view_action in self.single_actions: + serializer_class = self.serializer_classes.get('single') if serializer_class is None: serializer_class = self.serializer_classes.get('display') if serializer_class is None: @@ -301,36 +307,18 @@ class RelationMixin: self.send_m2m_changed_signal(instance, 'post_remove') -class SerializerMixin2: - serializer_classes = {} - - def get_serializer_class(self): - if self.serializer_classes: - serializer_class = self.serializer_classes.get( - self.action, self.serializer_classes.get('default') - ) - - if isinstance(serializer_class, dict): - serializer_class = serializer_class.get( - self.request.method.lower, serializer_class.get('default') - ) - - assert serializer_class, '`serializer_classes` config error' - return serializer_class - return super().get_serializer_class() - - class QuerySetMixin: def get_queryset(self): queryset = super().get_queryset() serializer_class = self.get_serializer_class() + if serializer_class and hasattr(serializer_class, 'setup_eager_loading'): queryset = serializer_class.setup_eager_loading(queryset) return queryset -class AllowBulkDestoryMixin: +class AllowBulkDestroyMixin: def allow_bulk_destroy(self, qs, filtered): """ 我们规定,批量删除的情况必须用 `id` 指定要删除的数据。 diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index ef69cec8e..a3b2c7cf6 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -67,6 +67,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'django.forms', + 'simple_history', ] @@ -86,6 +87,7 @@ MIDDLEWARE = [ 'orgs.middleware.OrgMiddleware', 'authentication.backends.oidc.middleware.OIDCRefreshIDTokenMiddleware', 'authentication.backends.cas.middleware.CASMiddleware', + 'simple_history.middleware.HistoryRequestMiddleware', ] diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 3d1ae71fe44dee0bbd06b8cfe0b813889664bfe3..319593bfdc018392b8c539dbf49b93712a45a3cb 100644 GIT binary patch delta 22311 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 T$d=Q|1LDQs@;r6GfYAR31{V25 delta 22844 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

6FzliP$98KQM83HV^fS4J)W4BqFj` zbZ_r?alykLCdw5R8PYW(x@S~m!H{O%qx#d0(m$$aL<}w7Q{sF^q)L(M^MMgxMMZ}+ z>K{FbrZ~Szqm$&MG9V^oP*nc`(S3S{bm=p&XJkn4J~1JkHMf=!H*tCyUw5H#M`!<- z&OfZi5UwJ+OL*LnH8=cIL=Rx9egmWXM@7cf-Et#u(u56L5=QTd%XJ~GPs)Tj>+et8 z6+d%Y!n&<-1uw2j7JKo=<&+O5d>21?eZs4qX#QRG&GXA~Y(;kdD@Zg&bwuY{+7y4v&SA2i{%J}^=oKJ)K zKl=Fu#@>39G_Ke`T@xpDo8p%K`#h~*7*~3K\n" "Language-Team: JumpServer team\n" @@ -19,7 +19,7 @@ msgstr "" #: acls/models/base.py:25 acls/serializers/login_asset_acl.py:47 #: applications/models/application.py:11 assets/models/asset.py:142 -#: assets/models/base.py:249 assets/models/cluster.py:18 +#: assets/models/base.py:216 assets/models/cluster.py:18 #: assets/models/cmd_filter.py:21 assets/models/domain.py:21 #: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24 #: orgs/models.py:23 perms/models/base.py:49 settings/models.py:29 @@ -40,7 +40,7 @@ msgid "Priority" msgstr "优先级" #: acls/models/base.py:28 assets/models/cmd_filter.py:54 -#: assets/models/user.py:123 +#: assets/models/user.py:246 msgid "1-100, the lower the value will be match first" msgstr "优先级可选范围为 1-100 (数值越小越优先)" @@ -54,16 +54,16 @@ msgstr "激活中" # msgstr "创建日期" #: acls/models/base.py:32 applications/models/application.py:24 #: assets/models/asset.py:147 assets/models/asset.py:223 -#: assets/models/base.py:254 assets/models/cluster.py:29 +#: assets/models/base.py:221 assets/models/cluster.py:29 #: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:64 #: assets/models/domain.py:22 assets/models/domain.py:56 #: assets/models/group.py:23 assets/models/label.py:23 ops/models/adhoc.py:37 #: orgs/models.py:26 perms/models/base.py:57 settings/models.py:34 -#: terminal/models/storage.py:26 terminal/models/terminal.py:114 -#: tickets/models/ticket.py:73 users/models/group.py:16 -#: users/models/user.py:583 xpack/plugins/change_auth_plan/models.py:77 -#: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:98 -#: xpack/plugins/gathered_user/models.py:26 +#: terminal/models/storage.py:29 terminal/models/storage.py:96 +#: terminal/models/terminal.py:114 tickets/models/ticket.py:73 +#: users/models/group.py:16 users/models/user.py:583 +#: xpack/plugins/change_auth_plan/models.py:77 xpack/plugins/cloud/models.py:35 +#: xpack/plugins/cloud/models.py:98 xpack/plugins/gathered_user/models.py:26 msgid "Comment" msgstr "备注" @@ -119,9 +119,8 @@ msgstr "系统用户" #: acls/models/login_asset_acl.py:22 #: applications/serializers/attrs/application_category/remote_app.py:33 -#: assets/models/asset.py:355 assets/models/authbook.py:27 -#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:34 -#: assets/serializers/asset_user.py:48 assets/serializers/asset_user.py:91 +#: assets/models/asset.py:372 assets/models/authbook.py:17 +#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:33 #: assets/serializers/system_user.py:202 audits/models.py:38 #: perms/models/asset_permission.py:99 templates/index.html:82 #: terminal/backends/command/models.py:19 @@ -158,7 +157,7 @@ msgstr "" #: acls/serializers/login_acl.py:30 acls/serializers/login_asset_acl.py:31 #: applications/serializers/attrs/application_type/mysql_workbench.py:18 #: assets/models/asset.py:183 assets/models/domain.py:52 -#: assets/serializers/asset_user.py:47 settings/serializers/settings.py:113 +#: settings/serializers/settings.py:113 #: users/templates/users/_granted_assets.html:26 #: users/templates/users/user_asset_permission.html:156 msgid "IP" @@ -178,13 +177,13 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " #: applications/serializers/attrs/application_type/custom.py:21 #: applications/serializers/attrs/application_type/mysql_workbench.py:30 #: applications/serializers/attrs/application_type/vmware_client.py:26 -#: assets/models/base.py:250 assets/models/gathered_user.py:15 +#: assets/models/base.py:217 assets/models/gathered_user.py:15 #: audits/models.py:100 authentication/forms.py:15 authentication/forms.py:17 #: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:548 #: users/templates/users/_select_user_modal.html:14 #: xpack/plugins/change_auth_plan/models.py:47 #: xpack/plugins/change_auth_plan/models.py:278 -#: xpack/plugins/cloud/serializers.py:65 +#: xpack/plugins/cloud/serializers.py:51 msgid "Username" msgstr "用户名" @@ -198,8 +197,7 @@ msgstr "" "10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)" #: acls/serializers/login_asset_acl.py:35 assets/models/asset.py:184 -#: assets/serializers/asset_user.py:46 assets/serializers/gathered_user.py:23 -#: settings/serializers/settings.py:112 +#: assets/serializers/gathered_user.py:23 settings/serializers/settings.py:112 #: users/templates/users/_granted_assets.html:25 #: users/templates/users/user_asset_permission.html:157 msgid "Hostname" @@ -212,7 +210,7 @@ msgid "" msgstr "格式为逗号分隔的字符串, * 表示匹配所有. 可选的协议有: {}" #: acls/serializers/login_asset_acl.py:55 assets/models/asset.py:187 -#: assets/models/domain.py:54 assets/models/user.py:124 +#: assets/models/domain.py:54 assets/models/user.py:247 #: terminal/serializers/session.py:32 terminal/serializers/storage.py:69 msgid "Protocol" msgstr "协议" @@ -254,7 +252,7 @@ msgid "Category" msgstr "类别" #: applications/models/application.py:16 assets/models/cmd_filter.py:53 -#: perms/models/application_permission.py:23 +#: assets/models/user.py:245 perms/models/application_permission.py:23 #: perms/serializers/application/permission.py:17 #: perms/serializers/application/user_permission.py:34 #: terminal/models/storage.py:47 terminal/models/storage.py:108 @@ -296,7 +294,7 @@ msgstr "应用类型" #: assets/serializers/system_user.py:49 assets/serializers/system_user.py:177 #: assets/serializers/system_user.py:203 msgid "Login mode display" -msgstr "登录模式(显示名称)" +msgstr "认证方式(显示名称)" #: applications/serializers/attrs/application_category/cloud.py:9 #: assets/models/cluster.py:40 @@ -304,7 +302,7 @@ msgid "Cluster" msgstr "集群" #: applications/serializers/attrs/application_category/db.py:11 -#: ops/models/adhoc.py:146 xpack/plugins/cloud/serializers.py:63 +#: ops/models/adhoc.py:146 xpack/plugins/cloud/serializers.py:49 msgid "Host" msgstr "主机" @@ -314,7 +312,7 @@ msgstr "主机" #: applications/serializers/attrs/application_type/oracle.py:11 #: applications/serializers/attrs/application_type/pgsql.py:11 #: assets/models/asset.py:188 assets/models/domain.py:53 -#: xpack/plugins/cloud/serializers.py:64 +#: xpack/plugins/cloud/serializers.py:50 msgid "Port" msgstr "端口" @@ -334,8 +332,8 @@ msgstr "目标URL" #: applications/serializers/attrs/application_type/custom.py:25 #: applications/serializers/attrs/application_type/mysql_workbench.py:34 #: applications/serializers/attrs/application_type/vmware_client.py:30 -#: assets/models/base.py:251 assets/serializers/asset_user.py:78 -#: audits/signals_handler.py:58 authentication/forms.py:22 +#: assets/models/base.py:218 audits/signals_handler.py:58 +#: authentication/forms.py:22 #: authentication/templates/authentication/login.html:164 #: settings/serializers/settings.py:94 users/forms/profile.py:21 #: users/templates/users/user_otp_check_password.html:13 @@ -344,7 +342,7 @@ msgstr "目标URL" #: xpack/plugins/change_auth_plan/models.py:68 #: xpack/plugins/change_auth_plan/models.py:190 #: xpack/plugins/change_auth_plan/models.py:285 -#: xpack/plugins/cloud/serializers.py:67 +#: xpack/plugins/cloud/serializers.py:53 msgid "Password" msgstr "密码" @@ -356,10 +354,6 @@ msgstr "运行参数" msgid "Target url" msgstr "目标URL" -#: assets/api/admin_user.py:50 -msgid "Deleted failed, There are related assets" -msgstr "删除失败,存在关联资产" - #: assets/api/domain.py:50 msgid "Number required" msgstr "需要为数字" @@ -376,38 +370,6 @@ msgstr "不能删除根节点 ({})" msgid "Deletion failed and the node contains assets" msgstr "删除失败,节点包含资产" -#: assets/backends/db.py:110 assets/models/user.py:307 audits/models.py:39 -#: perms/models/application_permission.py:31 -#: perms/models/asset_permission.py:101 templates/_nav.html:45 -#: terminal/backends/command/models.py:20 -#: terminal/backends/command/serializers.py:14 terminal/models/session.py:42 -#: users/templates/users/_granted_assets.html:27 -#: users/templates/users/user_asset_permission.html:42 -#: users/templates/users/user_asset_permission.html:76 -#: users/templates/users/user_asset_permission.html:159 -#: users/templates/users/user_database_app_permission.html:40 -#: users/templates/users/user_database_app_permission.html:67 -msgid "System user" -msgstr "系统用户" - -#: assets/backends/db.py:181 -msgid "System user(Dynamic)" -msgstr "系统用户(动态)" - -#: assets/backends/db.py:233 assets/models/asset.py:196 -#: assets/models/cluster.py:19 assets/models/user.py:67 templates/_nav.html:44 -#: xpack/plugins/cloud/models.py:92 xpack/plugins/cloud/serializers.py:181 -msgid "Admin user" -msgstr "管理用户" - -#: assets/backends/db.py:254 -msgid "Could not remove asset admin user" -msgstr "不能移除资产的管理用户账号" - -#: assets/backends/db.py:318 -msgid "Latest version could not be delete" -msgstr "最新版本的不能被删除" - #: assets/models/asset.py:143 msgid "Base" msgstr "基础" @@ -416,7 +378,7 @@ msgstr "基础" msgid "Charset" msgstr "编码" -#: assets/models/asset.py:145 assets/serializers/asset.py:171 +#: assets/models/asset.py:145 assets/serializers/asset.py:180 #: tickets/models/ticket.py:40 msgid "Meta" msgstr "元数据" @@ -426,16 +388,16 @@ msgid "Internal" msgstr "内部的" #: assets/models/asset.py:166 assets/models/asset.py:190 -#: assets/serializers/asset.py:66 perms/serializers/asset/user_permission.py:43 +#: assets/serializers/asset.py:65 perms/serializers/asset/user_permission.py:43 msgid "Platform" msgstr "系统平台" -#: assets/models/asset.py:189 assets/serializers/asset.py:68 +#: assets/models/asset.py:189 assets/serializers/asset.py:70 #: perms/serializers/asset/user_permission.py:41 msgid "Protocols" msgstr "协议组" -#: assets/models/asset.py:192 assets/models/user.py:119 +#: assets/models/asset.py:192 assets/models/user.py:237 #: perms/models/asset_permission.py:100 #: xpack/plugins/change_auth_plan/models.py:56 #: xpack/plugins/gathered_user/models.py:24 @@ -448,6 +410,13 @@ msgstr "节点" msgid "Is active" msgstr "激活" +#: assets/models/asset.py:196 assets/models/cluster.py:19 +#: assets/models/user.py:234 assets/models/user.py:370 +#: assets/serializers/asset.py:68 templates/_nav.html:44 +#: xpack/plugins/cloud/models.py:92 xpack/plugins/cloud/serializers.py:146 +msgid "Admin user" +msgstr "管理用户" + #: assets/models/asset.py:199 msgid "Public IP" msgstr "公网IP" @@ -516,7 +485,7 @@ msgstr "主机名原始" msgid "Labels" msgstr "标签管理" -#: assets/models/asset.py:221 assets/models/base.py:257 +#: assets/models/asset.py:221 assets/models/base.py:224 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:26 #: assets/models/cmd_filter.py:67 assets/models/group.py:21 #: common/db/models.py:70 common/mixins/models.py:49 orgs/models.py:24 @@ -528,7 +497,7 @@ msgstr "创建者" # msgid "Created by" # msgstr "创建者" -#: assets/models/asset.py:222 assets/models/base.py:255 +#: assets/models/asset.py:222 assets/models/base.py:222 #: assets/models/cluster.py:26 assets/models/domain.py:24 #: assets/models/gathered_user.py:19 assets/models/group.py:22 #: assets/models/label.py:25 common/db/models.py:72 common/mixins/models.py:50 @@ -538,35 +507,45 @@ msgstr "创建者" msgid "Date created" msgstr "创建日期" -#: assets/models/authbook.py:18 -msgid "Bulk delete deny" -msgstr "拒绝批量删除" +#: assets/models/authbook.py:18 assets/models/user.py:321 audits/models.py:39 +#: perms/models/application_permission.py:31 +#: perms/models/asset_permission.py:101 templates/_nav.html:45 +#: terminal/backends/command/models.py:20 +#: terminal/backends/command/serializers.py:14 terminal/models/session.py:42 +#: users/templates/users/_granted_assets.html:27 +#: users/templates/users/user_asset_permission.html:42 +#: users/templates/users/user_asset_permission.html:76 +#: users/templates/users/user_asset_permission.html:159 +#: users/templates/users/user_database_app_permission.html:40 +#: users/templates/users/user_database_app_permission.html:67 +msgid "System user" +msgstr "系统用户" -#: assets/models/authbook.py:28 -msgid "Latest version" -msgstr "最新版本" - -#: assets/models/authbook.py:29 +#: assets/models/authbook.py:20 msgid "Version" msgstr "版本" +#: assets/models/authbook.py:21 +msgid "Latest version" +msgstr "最新版本" + #: assets/models/authbook.py:38 msgid "AuthBook" -msgstr "" +msgstr "账号" -#: assets/models/base.py:252 xpack/plugins/change_auth_plan/models.py:72 +#: assets/models/base.py:219 xpack/plugins/change_auth_plan/models.py:72 #: xpack/plugins/change_auth_plan/models.py:197 #: xpack/plugins/change_auth_plan/models.py:292 msgid "SSH private key" msgstr "SSH密钥" -#: assets/models/base.py:253 xpack/plugins/change_auth_plan/models.py:75 +#: assets/models/base.py:220 xpack/plugins/change_auth_plan/models.py:75 #: xpack/plugins/change_auth_plan/models.py:193 #: xpack/plugins/change_auth_plan/models.py:288 msgid "SSH public key" msgstr "SSH公钥" -#: assets/models/base.py:256 assets/models/gathered_user.py:20 +#: assets/models/base.py:223 assets/models/gathered_user.py:20 #: common/db/models.py:73 common/mixins/models.py:51 ops/models/adhoc.py:39 #: orgs/models.py:421 msgid "Date updated" @@ -614,7 +593,7 @@ msgstr "系统" msgid "Default Cluster" msgstr "默认Cluster" -#: assets/models/cmd_filter.py:33 assets/models/user.py:129 +#: assets/models/cmd_filter.py:33 assets/models/user.py:252 msgid "Command filter" msgstr "命令过滤器" @@ -715,65 +694,65 @@ msgstr "ssh私钥" #: users/templates/users/user_asset_permission.html:41 #: users/templates/users/user_asset_permission.html:73 #: users/templates/users/user_asset_permission.html:158 -#: xpack/plugins/cloud/models.py:89 xpack/plugins/cloud/serializers.py:182 +#: xpack/plugins/cloud/models.py:89 xpack/plugins/cloud/serializers.py:147 msgid "Node" msgstr "节点" -#: assets/models/user.py:115 -msgid "Automatic login" -msgstr "自动登录" +#: assets/models/user.py:227 +msgid "Automatic managed" +msgstr "托管密码" -#: assets/models/user.py:116 -msgid "Manually login" -msgstr "手动登录" +#: assets/models/user.py:228 +msgid "Manually input" +msgstr "手动输入" -#: assets/models/user.py:118 +#: assets/models/user.py:236 msgid "Username same with user" msgstr "用户名与用户相同" -#: assets/models/user.py:120 assets/serializers/domain.py:30 +#: assets/models/user.py:239 assets/serializers/domain.py:30 #: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:52 msgid "Assets" msgstr "资产" -#: assets/models/user.py:121 templates/_nav.html:17 +#: assets/models/user.py:243 templates/_nav.html:17 #: users/views/profile/password.py:43 users/views/profile/pubkey.py:37 msgid "Users" msgstr "用户管理" -#: assets/models/user.py:122 +#: assets/models/user.py:244 msgid "User groups" msgstr "用户组" -#: assets/models/user.py:125 +#: assets/models/user.py:248 msgid "Auto push" msgstr "自动推送" -#: assets/models/user.py:126 +#: assets/models/user.py:249 msgid "Sudo" msgstr "Sudo" -#: assets/models/user.py:127 +#: assets/models/user.py:250 msgid "Shell" msgstr "Shell" -#: assets/models/user.py:128 +#: assets/models/user.py:251 msgid "Login mode" -msgstr "登录模式" +msgstr "认证方式" -#: assets/models/user.py:130 +#: assets/models/user.py:253 msgid "SFTP Root" msgstr "SFTP根路径" -#: assets/models/user.py:131 authentication/models.py:95 +#: assets/models/user.py:254 authentication/models.py:95 msgid "Token" msgstr "" -#: assets/models/user.py:132 +#: assets/models/user.py:255 msgid "Home" msgstr "家目录" -#: assets/models/user.py:133 +#: assets/models/user.py:256 msgid "System groups" msgstr "用户组" @@ -794,23 +773,19 @@ msgstr "可连接" msgid "Unknown" msgstr "未知" -#: assets/serializers/asset.py:23 +#: assets/serializers/asset.py:22 msgid "Protocol format should {}/{}" msgstr "协议格式 {}/{}" -#: assets/serializers/asset.py:40 +#: assets/serializers/asset.py:39 msgid "Protocol duplicate: {}" msgstr "协议重复: {}" -#: assets/serializers/asset.py:69 +#: assets/serializers/asset.py:71 msgid "Domain name" msgstr "网域名称" -#: assets/serializers/asset.py:70 -msgid "Admin user name" -msgstr "管理用户名称" - -#: assets/serializers/asset.py:71 perms/serializers/asset/permission.py:49 +#: assets/serializers/asset.py:72 perms/serializers/asset/permission.py:49 msgid "Nodes name" msgstr "节点名称" @@ -822,33 +797,10 @@ msgstr "硬件信息" msgid "Org name" msgstr "组织名称" -#: assets/serializers/asset.py:162 assets/serializers/asset.py:194 +#: assets/serializers/asset.py:171 assets/serializers/asset.py:203 msgid "Connectivity" msgstr "连接" -#: assets/serializers/asset_user.py:45 -#: authentication/templates/authentication/_access_key_modal.html:30 -#: users/serializers/group.py:37 -msgid "ID" -msgstr "ID" - -#: assets/serializers/asset_user.py:49 -msgid "Backend" -msgstr "后端" - -#: assets/serializers/asset_user.py:50 users/models/user.py:596 -msgid "Source" -msgstr "来源" - -#: assets/serializers/asset_user.py:82 users/forms/profile.py:160 -#: users/models/user.py:580 users/templates/users/user_password_update.html:48 -msgid "Public key" -msgstr "SSH公钥" - -#: assets/serializers/asset_user.py:86 users/models/user.py:577 -msgid "Private key" -msgstr "ssh私钥" - #: assets/serializers/base.py:47 msgid "private key invalid" msgstr "密钥不合法" @@ -1124,7 +1076,7 @@ msgstr "启用" msgid "-" msgstr "" -#: audits/models.py:97 xpack/plugins/cloud/const.py:27 +#: audits/models.py:97 xpack/plugins/cloud/const.py:25 msgid "Failed" msgstr "失败" @@ -1455,6 +1407,11 @@ msgstr "使用api key签名请求头,每个请求的头部是不一样的" msgid "docs" msgstr "文档" +#: authentication/templates/authentication/_access_key_modal.html:30 +#: users/serializers/group.py:37 +msgid "ID" +msgstr "ID" + #: authentication/templates/authentication/_access_key_modal.html:31 msgid "Secret" msgstr "秘钥" @@ -3927,6 +3884,11 @@ msgstr "不能和原来的密钥相同" msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" +#: users/forms/profile.py:160 users/models/user.py:580 +#: users/templates/users/user_password_update.html:48 +msgid "Public key" +msgstr "SSH公钥" + #: users/models/user.py:174 msgid "System administrator" msgstr "系统管理员" @@ -3951,6 +3913,14 @@ msgstr "头像" msgid "Wechat" msgstr "微信" +#: users/models/user.py:577 +msgid "Private key" +msgstr "ssh私钥" + +#: users/models/user.py:596 +msgid "Source" +msgstr "来源" + #: users/models/user.py:600 msgid "Date password last updated" msgstr "最后更新密码日期" @@ -4061,7 +4031,7 @@ msgid "Security token validation" msgstr "安全令牌验证" #: users/templates/users/_base_otp.html:14 xpack/plugins/cloud/models.py:78 -#: xpack/plugins/cloud/serializers.py:180 +#: xpack/plugins/cloud/serializers.py:145 msgid "Account" msgstr "账户" @@ -4762,39 +4732,31 @@ msgstr "" msgid "Nutanix" msgstr "" -#: xpack/plugins/cloud/const.py:17 -msgid "Huawei Private Cloud" -msgstr "华为私有云" - -#: xpack/plugins/cloud/const.py:18 -msgid "Qingyun Private Cloud" -msgstr "" - -#: xpack/plugins/cloud/const.py:22 +#: xpack/plugins/cloud/const.py:20 msgid "Instance name" msgstr "实例名称" -#: xpack/plugins/cloud/const.py:23 +#: xpack/plugins/cloud/const.py:21 msgid "Instance name and Partial IP" msgstr "实例名称和部分IP" -#: xpack/plugins/cloud/const.py:28 +#: xpack/plugins/cloud/const.py:26 msgid "Succeed" msgstr "成功" -#: xpack/plugins/cloud/const.py:32 +#: xpack/plugins/cloud/const.py:30 msgid "Unsync" msgstr "未同步" -#: xpack/plugins/cloud/const.py:33 +#: xpack/plugins/cloud/const.py:31 msgid "New Sync" msgstr "新同步" -#: xpack/plugins/cloud/const.py:34 +#: xpack/plugins/cloud/const.py:32 msgid "Synced" msgstr "已同步" -#: xpack/plugins/cloud/const.py:35 +#: xpack/plugins/cloud/const.py:33 msgid "Released" msgstr "已释放" @@ -4810,7 +4772,7 @@ msgstr "云服务商" msgid "Cloud account" msgstr "云账号" -#: xpack/plugins/cloud/models.py:81 xpack/plugins/cloud/serializers.py:161 +#: xpack/plugins/cloud/models.py:81 xpack/plugins/cloud/serializers.py:126 msgid "Regions" msgstr "地域" @@ -4818,7 +4780,7 @@ msgstr "地域" msgid "Hostname strategy" msgstr "主机名策略" -#: xpack/plugins/cloud/models.py:95 xpack/plugins/cloud/serializers.py:184 +#: xpack/plugins/cloud/models.py:95 xpack/plugins/cloud/serializers.py:149 msgid "Always update" msgstr "总是更新" @@ -5010,28 +4972,20 @@ msgstr "" msgid "Subscription ID" msgstr "" -#: xpack/plugins/cloud/serializers.py:49 -msgid "This field is required" -msgstr "这个字段是必填项" - -#: xpack/plugins/cloud/serializers.py:83 xpack/plugins/cloud/serializers.py:87 -msgid "API Endpoint" -msgstr "API 端点" - -#: xpack/plugins/cloud/serializers.py:159 +#: xpack/plugins/cloud/serializers.py:124 msgid "History count" msgstr "执行次数" -#: xpack/plugins/cloud/serializers.py:160 +#: xpack/plugins/cloud/serializers.py:125 msgid "Instance count" msgstr "实例个数" -#: xpack/plugins/cloud/serializers.py:183 +#: xpack/plugins/cloud/serializers.py:148 #: xpack/plugins/gathered_user/serializers.py:20 msgid "Periodic display" msgstr "定时执行" -#: xpack/plugins/cloud/utils.py:65 +#: xpack/plugins/cloud/utils.py:64 msgid "Account unavailable" msgstr "账户无效" @@ -5118,3 +5072,36 @@ msgstr "旗舰版" #: xpack/plugins/license/models.py:77 msgid "Community edition" msgstr "社区版" + +#~ msgid "Deleted failed, There are related assets" +#~ msgstr "删除失败,存在关联资产" + +#~ msgid "System user(Dynamic)" +#~ msgstr "系统用户(动态)" + +#~ msgid "Could not remove asset admin user" +#~ msgstr "不能移除资产的管理用户账号" + +#~ msgid "Latest version could not be delete" +#~ msgstr "最新版本的不能被删除" + +#~ msgid "Bulk delete deny" +#~ msgstr "拒绝批量删除" + +#~ msgid "Admin user name" +#~ msgstr "管理用户名称" + +#~ msgid "Backend" +#~ msgstr "后端" + +#~ msgid "Huawei Private Cloud" +#~ msgstr "华为私有云" + +#~ msgid "This field is required" +#~ msgstr "这个字段是必填项" + +#~ msgid "API Endpoint" +#~ msgstr "API 端点" + +#~ msgid "Terminal command alert" +#~ msgstr "终端命令告警" diff --git a/apps/ops/inventory.py b/apps/ops/inventory.py index f3de03210..fa077e903 100644 --- a/apps/ops/inventory.py +++ b/apps/ops/inventory.py @@ -31,7 +31,11 @@ class JMSBaseInventory(BaseInventory): if run_as_admin: info.update(asset.get_auth_info()) if asset.is_unixlike(): - info["become"] = asset.admin_user.become_info + info["become"] = { + "method": 'sudo', + "user": 'root', + "pass": '' + } if asset.is_windows(): info["vars"].update({ "ansible_connection": "ssh", @@ -103,8 +107,6 @@ class JMSInventory(JMSBaseInventory): super().__init__(host_list=host_list) def get_run_user_info(self, host): - from assets.backends import AssetUserManager - if not self.run_as and not self.system_user: return {} @@ -112,17 +114,12 @@ class JMSInventory(JMSBaseInventory): asset = self.assets.filter(id=asset_id).first() if not asset: logger.error('Host not found: ', asset_id) + return {} if self.system_user: self.system_user.load_asset_special_auth(asset=asset, username=self.run_as) return self.system_user._to_secret_json() - - try: - manager = AssetUserManager() - run_user = manager.get_latest(username=self.run_as, asset=asset, prefer='system_user') - return run_user._to_secret_json() - except Exception as e: - logger.error(e, exc_info=True) + else: return {} diff --git a/apps/ops/models/adhoc.py b/apps/ops/models/adhoc.py index 63050e436..083d40d4d 100644 --- a/apps/ops/models/adhoc.py +++ b/apps/ops/models/adhoc.py @@ -286,18 +286,12 @@ class AdHocExecution(OrgModelMixin): raw = '' try: - date_start_s = timezone.now().now().strftime('%Y-%m-%d %H:%M:%S') - print(_("{} Start task: {}").format(date_start_s, self.task.name)) raw, summary = self.start_runner() except Exception as e: logger.error(e, exc_info=True) raw = {"dark": {"all": str(e)}, "contacted": []} finally: self.clean_up(summary, time_start) - date_end = timezone.now().now() - date_end_s = date_end.strftime('%Y-%m-%d %H:%M:%S') - print(_("{} Task finish").format(date_end_s)) - print('.\n\n.') return raw, summary def clean_up(self, summary, time_start): diff --git a/apps/orgs/models.py b/apps/orgs/models.py index 72f47e1e3..9eb2eb9ad 100644 --- a/apps/orgs/models.py +++ b/apps/orgs/models.py @@ -9,10 +9,10 @@ from django.utils.translation import ugettext_lazy as _ from common.utils import lazyproperty, settings from common.const import choices -from common.db.models import ChoiceSet +from common.db.models import TextChoices -class ROLE(ChoiceSet): +class ROLE(TextChoices): ADMIN = choices.ADMIN, _('Organization administrator') AUDITOR = choices.AUDITOR, _("Organization auditor") USER = choices.USER, _('User') diff --git a/apps/orgs/serializers.py b/apps/orgs/serializers.py index 1a6aca0b0..28b59a705 100644 --- a/apps/orgs/serializers.py +++ b/apps/orgs/serializers.py @@ -4,7 +4,6 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from users.models.user import User -from common.drf.serializers import AdaptedBulkListSerializer from common.drf.serializers import BulkModelSerializer from common.db.models import concated_display as display from .models import Organization, OrganizationMember, ROLE @@ -35,7 +34,6 @@ class OrgSerializer(ModelSerializer): class Meta: model = Organization - list_serializer_class = AdaptedBulkListSerializer fields_mini = ['id', 'name'] fields_small = fields_mini + [ 'resource_statistics', diff --git a/apps/orgs/signals_handler/common.py b/apps/orgs/signals_handler/common.py index 942c82cbb..c7aee3c32 100644 --- a/apps/orgs/signals_handler/common.py +++ b/apps/orgs/signals_handler/common.py @@ -36,8 +36,8 @@ class OrgsMappingForMemoryPubSub(LazyObject): orgs_mapping_for_memory_pub_sub = OrgsMappingForMemoryPubSub() -def expire_orgs_mapping_for_memory(): - orgs_mapping_for_memory_pub_sub.publish('expire_orgs_mapping') +def expire_orgs_mapping_for_memory(org_id): + orgs_mapping_for_memory_pub_sub.publish(str(org_id)) @receiver(django_ready) @@ -54,7 +54,7 @@ def subscribe_orgs_mapping_expire(sender, **kwargs): if message['data'] == b'error': raise ValueError Organization.expire_orgs_mapping() - logger.debug('Expire orgs mapping') + logger.debug('Expire orgs mapping: ' + str(message['data'])) except Exception as e: logger.exception(f'subscribe_orgs_mapping_expire: {e}') Organization.expire_orgs_mapping() @@ -65,22 +65,21 @@ def subscribe_orgs_mapping_expire(sender, **kwargs): @receiver(post_save, sender=Organization) -def on_org_create_or_update(sender, instance=None, created=False, **kwargs): +def on_org_create_or_update(sender, instance, created=False, **kwargs): # 必须放到最开始, 因为下面调用Node.save方法时会获取当前组织的org_id(即instance.org_id), 如果不过期会找不到 - expire_orgs_mapping_for_memory() - if instance: - old_org = get_current_org() - set_current_org(instance) - node_root = Node.org_root() - if node_root.value != instance.name: - node_root.value = instance.name - node_root.save() - set_current_org(old_org) + expire_orgs_mapping_for_memory(instance.id) + old_org = get_current_org() + set_current_org(instance) + node_root = Node.org_root() + if node_root.value != instance.name: + node_root.value = instance.name + node_root.save() + set_current_org(old_org) -@receiver(post_delete, sender=Organization) -def on_org_delete(sender, **kwargs): - expire_orgs_mapping_for_memory() +@receiver(pre_delete, sender=Organization) +def on_org_delete(sender, instance, **kwargs): + expire_orgs_mapping_for_memory(instance.id) @receiver(pre_delete, sender=Organization) diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index 822cc363f..f763bee10 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -4,7 +4,7 @@ from functools import reduce from django.utils.translation import ugettext_lazy as _ from django.db.models import F -from common.db.models import ChoiceSet +from common.db.models import TextChoices from orgs.mixins.models import OrgModelMixin from common.db import models from common.utils import lazyproperty @@ -165,7 +165,7 @@ class AssetPermission(BasePermission): class UserAssetGrantedTreeNodeRelation(OrgModelMixin, FamilyMixin, models.JMSBaseModel): - class NodeFrom(ChoiceSet): + class NodeFrom(TextChoices): granted = 'granted', 'Direct node granted' child = 'child', 'Have children node' asset = 'asset', 'Direct asset granted' diff --git a/apps/perms/serializers/application/permission_relation.py b/apps/perms/serializers/application/permission_relation.py index 2ec416a66..941b03127 100644 --- a/apps/perms/serializers/application/permission_relation.py +++ b/apps/perms/serializers/application/permission_relation.py @@ -3,7 +3,6 @@ from rest_framework import serializers from common.mixins import BulkSerializerMixin -from common.drf.serializers import AdaptedBulkListSerializer from perms.models import ApplicationPermission __all__ = [ @@ -24,14 +23,11 @@ class RelationMixin(BulkSerializerMixin, serializers.Serializer): fields.extend(['applicationpermission', "applicationpermission_display"]) return fields - class Meta: - list_serializer_class = AdaptedBulkListSerializer - class ApplicationPermissionUserRelationSerializer(RelationMixin, serializers.ModelSerializer): user_display = serializers.ReadOnlyField() - class Meta(RelationMixin.Meta): + class Meta: model = ApplicationPermission.users.through fields = [ 'id', 'user', 'user_display', @@ -41,7 +37,7 @@ class ApplicationPermissionUserRelationSerializer(RelationMixin, serializers.Mod class ApplicationPermissionUserGroupRelationSerializer(RelationMixin, serializers.ModelSerializer): usergroup_display = serializers.ReadOnlyField() - class Meta(RelationMixin.Meta): + class Meta: model = ApplicationPermission.user_groups.through fields = [ 'id', 'usergroup', "usergroup_display", @@ -51,7 +47,7 @@ class ApplicationPermissionUserGroupRelationSerializer(RelationMixin, serializer class ApplicationPermissionApplicationRelationSerializer(RelationMixin, serializers.ModelSerializer): application_display = serializers.ReadOnlyField() - class Meta(RelationMixin.Meta): + class Meta: model = ApplicationPermission.applications.through fields = [ 'id', "application", "application_display", @@ -61,7 +57,7 @@ class ApplicationPermissionApplicationRelationSerializer(RelationMixin, serializ class ApplicationPermissionSystemUserRelationSerializer(RelationMixin, serializers.ModelSerializer): systemuser_display = serializers.ReadOnlyField() - class Meta(RelationMixin.Meta): + class Meta: model = ApplicationPermission.system_users.through fields = [ 'id', 'systemuser', 'systemuser_display' diff --git a/apps/perms/serializers/asset/permission_relation.py b/apps/perms/serializers/asset/permission_relation.py index f98c4bbf0..ee1e05112 100644 --- a/apps/perms/serializers/asset/permission_relation.py +++ b/apps/perms/serializers/asset/permission_relation.py @@ -3,7 +3,6 @@ from rest_framework import serializers from common.mixins import BulkSerializerMixin -from common.drf.serializers import AdaptedBulkListSerializer from assets.models import Asset, Node from perms.models import AssetPermission from users.models import User @@ -37,14 +36,11 @@ class RelationMixin(BulkSerializerMixin, serializers.Serializer): fields.extend(['assetpermission', "assetpermission_display"]) return fields - class Meta: - list_serializer_class = AdaptedBulkListSerializer - class AssetPermissionUserRelationSerializer(RelationMixin, serializers.ModelSerializer): user_display = serializers.ReadOnlyField() - class Meta(RelationMixin.Meta): + class Meta: model = AssetPermission.users.through fields = [ 'id', 'user', 'user_display', @@ -66,7 +62,7 @@ class AssetPermissionAllUserSerializer(serializers.Serializer): class AssetPermissionUserGroupRelationSerializer(RelationMixin, serializers.ModelSerializer): usergroup_display = serializers.ReadOnlyField() - class Meta(RelationMixin.Meta): + class Meta: model = AssetPermission.user_groups.through fields = [ 'id', 'usergroup', "usergroup_display", @@ -76,7 +72,7 @@ class AssetPermissionUserGroupRelationSerializer(RelationMixin, serializers.Mode class AssetPermissionAssetRelationSerializer(RelationMixin, serializers.ModelSerializer): asset_display = serializers.ReadOnlyField() - class Meta(RelationMixin.Meta): + class Meta: model = AssetPermission.assets.through fields = [ 'id', "asset", "asset_display", @@ -98,7 +94,7 @@ class AssetPermissionAllAssetSerializer(serializers.Serializer): class AssetPermissionNodeRelationSerializer(RelationMixin, serializers.ModelSerializer): node_display = serializers.CharField(source='node.full_value', read_only=True) - class Meta(RelationMixin.Meta): + class Meta: model = AssetPermission.nodes.through fields = [ 'id', 'node', "node_display", @@ -108,7 +104,7 @@ class AssetPermissionNodeRelationSerializer(RelationMixin, serializers.ModelSeri class AssetPermissionSystemUserRelationSerializer(RelationMixin, serializers.ModelSerializer): systemuser_display = serializers.ReadOnlyField() - class Meta(RelationMixin.Meta): + class Meta: model = AssetPermission.system_users.through fields = [ 'id', 'systemuser', 'systemuser_display' diff --git a/apps/terminal/models/session.py b/apps/terminal/models/session.py index b3202a9d9..546aabb16 100644 --- a/apps/terminal/models/session.py +++ b/apps/terminal/models/session.py @@ -13,17 +13,17 @@ from django.core.cache import cache from assets.models import Asset from users.models import User from orgs.mixins.models import OrgModelMixin -from common.db.models import ChoiceSet +from common.db.models import TextChoices from ..backends import get_multi_command_storage class Session(OrgModelMixin): - class LOGIN_FROM(ChoiceSet): + class LOGIN_FROM(TextChoices): ST = 'ST', 'SSH Terminal' RT = 'RT', 'RDP Terminal' WT = 'WT', 'Web Terminal' - class PROTOCOL(ChoiceSet): + class PROTOCOL(TextChoices): SSH = 'ssh', 'ssh' RDP = 'rdp', 'rdp' VNC = 'vnc', 'vnc' diff --git a/apps/terminal/serializers/session.py b/apps/terminal/serializers/session.py index 854bfd240..bccb108f1 100644 --- a/apps/terminal/serializers/session.py +++ b/apps/terminal/serializers/session.py @@ -2,7 +2,6 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from common.drf.serializers import AdaptedBulkListSerializer from ..models import Session __all__ = [ @@ -16,7 +15,6 @@ class SessionSerializer(BulkOrgResourceModelSerializer): class Meta: model = Session - list_serializer_class = AdaptedBulkListSerializer fields_mini = ["id"] fields_small = fields_mini + [ "user", "asset", "system_user", diff --git a/apps/terminal/serializers/terminal.py b/apps/terminal/serializers/terminal.py index 44cb2a3a5..5f813845f 100644 --- a/apps/terminal/serializers/terminal.py +++ b/apps/terminal/serializers/terminal.py @@ -85,7 +85,6 @@ class TaskSerializer(BulkModelSerializer): class Meta: fields = '__all__' model = Task - list_serializer_class = AdaptedBulkListSerializer ref_name = 'TerminalTaskSerializer' diff --git a/apps/users/models/user.py b/apps/users/models/user.py index f362e60ac..086bf2136 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -23,7 +23,7 @@ from orgs.models import OrganizationMember, Organization from common.utils import date_expired_default, get_logger, lazyproperty, random_string from common import fields from common.const import choices -from common.db.models import ChoiceSet +from common.db.models import TextChoices from users.exceptions import MFANotEnabled from ..signals import post_user_change_password @@ -170,7 +170,7 @@ class AuthMixin: class RoleMixin: - class ROLE(ChoiceSet): + class ROLE(TextChoices): ADMIN = choices.ADMIN, _('System administrator') AUDITOR = choices.AUDITOR, _('System auditor') USER = choices.USER, _('User') diff --git a/apps/users/serializers/group.py b/apps/users/serializers/group.py index 41d2282a8..1638a5b91 100644 --- a/apps/users/serializers/group.py +++ b/apps/users/serializers/group.py @@ -4,7 +4,6 @@ from django.utils.translation import ugettext_lazy as _ from django.db.models import Prefetch from rest_framework import serializers -from common.drf.serializers import AdaptedBulkListSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer from django.db.models import Count from ..models import User, UserGroup @@ -23,7 +22,6 @@ class UserGroupSerializer(BulkOrgResourceModelSerializer): class Meta: model = UserGroup - list_serializer_class = AdaptedBulkListSerializer fields_mini = ['id', 'name'] fields_small = fields_mini + [ 'comment', 'date_created', 'created_by' diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 94d6f9ea7..d8e34784e 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -113,3 +113,4 @@ termcolor==1.1.0 azure-identity==1.5.0 azure-mgmt-subscription==1.0.0 qingcloud-sdk==1.2.12 +django-simple-history==3.0.0 \ No newline at end of file diff --git a/utils/generate_fake_data/resources/assets.py b/utils/generate_fake_data/resources/assets.py index c6817e440..3aa11cc99 100644 --- a/utils/generate_fake_data/resources/assets.py +++ b/utils/generate_fake_data/resources/assets.py @@ -28,7 +28,7 @@ class AdminUsersGenerator(FakeDataGenerator): class SystemUsersGenerator(FakeDataGenerator): def do_generate(self, batch, batch_size): system_users = [] - protocols = list(dict(SystemUser.PROTOCOL_CHOICES).keys()) + protocols = list(dict(SystemUser.Protocol.choices).keys()) for i in batch: username = forgery_py.internet.user_name(True) protocol = random.choice(protocols)