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/applications/migrations/0009_applicationuser.py b/apps/applications/migrations/0009_applicationuser.py new file mode 100644 index 000000000..7b3368ef9 --- /dev/null +++ b/apps/applications/migrations/0009_applicationuser.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1.6 on 2021-06-23 09:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0070_auto_20210426_1515'), + ('applications', '0008_auto_20210104_0435'), + ] + + operations = [ + migrations.CreateModel( + name='ApplicationUser', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('assets.systemuser',), + ), + ] 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 b64aeb758..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,240 +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) - - if self.username_same_with_user: - if user and not username: - username = user.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 1ac5435be..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, @@ -155,8 +155,14 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView data = '' for k, v in options.items(): data += f'{k}:{v}\n' + if asset: + name = asset.hostname + elif application: + name = application.name + else: + name = '*' response = HttpResponse(data, content_type='application/octet-stream') - filename = "{}-{}-jumpserver.rdp".format(user.username, asset.hostname) + filename = "{}-{}-jumpserver.rdp".format(user.username, name) filename = urllib.parse.quote(filename) response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename return response @@ -210,6 +216,8 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView from users.models import User from assets.models import SystemUser, Asset from applications.models import Application + from perms.utils.asset.permission import validate_permission as asset_validate_permission + from perms.utils.application.permission import validate_permission as app_validate_permission key = self.CACHE_KEY_PREFIX.format(token) value = cache.get(key, None) @@ -226,23 +234,24 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView app = None if value.get('type') == 'asset': asset = get_object_or_404(Asset, id=value.get('asset')) + if not asset.is_active: + raise serializers.ValidationError("Asset disabled") + + has_perm, expired_at = asset_validate_permission(user, asset, system_user, 'connect') else: app = get_object_or_404(Application, id=value.get('application')) + has_perm, expired_at = app_validate_permission(user, app, system_user) - if asset and not asset.is_active: - raise serializers.ValidationError("Asset disabled") - - try: - self.check_resource_permission(user, asset, app, system_user) - except PermissionDenied: + if not has_perm: raise serializers.ValidationError('Permission expired or invalid') - return value, user, system_user, asset, app + + return value, user, system_user, asset, app, expired_at @action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail') def get_secret_detail(self, request, *args, **kwargs): token = request.data.get('token', '') try: - value, user, system_user, asset, app = self.valid_token(token) + value, user, system_user, asset, app, expired_at = self.valid_token(token) except serializers.ValidationError as e: post_auth_failed.send( sender=self.__class__, username='', request=self.request, @@ -250,7 +259,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView ) raise e - data = dict(user=user, system_user=system_user) + data = dict(user=user, system_user=system_user, expired_at=expired_at) if asset: asset_detail = self._get_asset_secret_detail(asset, user=user, system_user=system_user) system_user.load_asset_more_auth(asset.id, user.username, user.id) 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/authentication/backends/api.py b/apps/authentication/backends/api.py index 308c441a2..892ebcc7c 100644 --- a/apps/authentication/backends/api.py +++ b/apps/authentication/backends/api.py @@ -8,7 +8,7 @@ from django.core.cache import cache from django.utils.translation import ugettext as _ from six import text_type from django.contrib.auth import get_user_model -from django.contrib.auth.backends import ModelBackend as DJModelBackend +from django.contrib.auth.backends import ModelBackend from rest_framework import HTTP_HEADER_ENCODING from rest_framework import authentication, exceptions from common.auth import signature @@ -17,6 +17,9 @@ from common.utils import get_object_or_none, make_signature, http_to_unixtime from ..models import AccessKey, PrivateToken +UserModel = get_user_model() + + def get_request_date_header(request): date = request.META.get('HTTP_DATE', b'') if isinstance(date, text_type): @@ -25,9 +28,16 @@ def get_request_date_header(request): return date -class ModelBackend(DJModelBackend): +class JMSModelBackend(ModelBackend): def user_can_authenticate(self, user): - return user.is_valid + return True + + def get_user(self, user_id): + try: + user = UserModel._default_manager.get(pk=user_id) + except UserModel.DoesNotExist: + return None + return user if user.is_valid else None class AccessKeyAuthentication(authentication.BaseAuthentication): @@ -203,7 +213,7 @@ class SignatureAuthentication(signature.SignatureAuthentication): return None, None -class SSOAuthentication(ModelBackend): +class SSOAuthentication(JMSModelBackend): """ 什么也不做呀😺 """ @@ -212,7 +222,7 @@ class SSOAuthentication(ModelBackend): pass -class WeComAuthentication(ModelBackend): +class WeComAuthentication(JMSModelBackend): """ 什么也不做呀😺 """ @@ -221,7 +231,7 @@ class WeComAuthentication(ModelBackend): pass -class DingTalkAuthentication(ModelBackend): +class DingTalkAuthentication(JMSModelBackend): """ 什么也不做呀😺 """ @@ -230,7 +240,7 @@ class DingTalkAuthentication(ModelBackend): pass -class AuthorizationTokenAuthentication(ModelBackend): +class AuthorizationTokenAuthentication(JMSModelBackend): """ 什么也不做呀😺 """ diff --git a/apps/authentication/forms.py b/apps/authentication/forms.py index 447d842bd..a4b07700c 100644 --- a/apps/authentication/forms.py +++ b/apps/authentication/forms.py @@ -3,7 +3,7 @@ from django import forms from django.conf import settings -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import ugettext_lazy as _ from captcha.fields import CaptchaField, CaptchaTextInput @@ -23,12 +23,17 @@ class UserLoginForm(forms.Form): max_length=1024, strip=False ) auto_login = forms.BooleanField( - label=_("{} days auto login").format(days_auto_login or 1), - required=False, initial=False, widget=forms.CheckboxInput( + required=False, initial=False, + widget=forms.CheckboxInput( attrs={'disabled': disable_days_auto_login} ) ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + auto_login_field = self.fields['auto_login'] + auto_login_field.label = _("{} days auto login").format(self.days_auto_login or 1) + def confirm_login_allowed(self, user): if not user.is_staff: raise forms.ValidationError( diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 5eeceb7c3..a05d3926f 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -236,6 +236,11 @@ class AuthMixin: ip = self.get_request_ip() request = self.request + if user.is_expired: + self.raise_credential_error(errors.reason_user_expired) + elif not user.is_active: + self.raise_credential_error(errors.reason_user_inactive) + self._set_partial_credential_error(user.username, ip, request) self._check_is_local_user(user) self._check_is_block(user.username) diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py index 11381c4cb..e6932388b 100644 --- a/apps/authentication/serializers.py +++ b/apps/authentication/serializers.py @@ -196,6 +196,7 @@ class ConnectionTokenSecretSerializer(serializers.Serializer): system_user = ConnectionTokenSystemUserSerializer(read_only=True) gateway = ConnectionTokenGatewaySerializer(read_only=True) actions = ActionsField() + expired_at = serializers.IntegerField() class RDPFileSerializer(ConnectionTokenSerializer): diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 083418940..0aa9f90b5 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -44,14 +44,15 @@ class UserLoginView(mixins.AuthMixin, FormView): # show jumpserver login page if request http://{JUMP-SERVER}/?admin=1 if self.request.GET.get("admin", 0): return None + next_url = request.GET.get('next') or '/' auth_type = '' auth_url = '' if settings.AUTH_OPENID: auth_type = 'OIDC' - auth_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME) + auth_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME) + f'?next={next_url}' elif settings.AUTH_CAS: auth_type = 'CAS' - auth_url = reverse(settings.CAS_LOGIN_URL_NAME) + auth_url = reverse(settings.CAS_LOGIN_URL_NAME) + f'?next={next_url}' if not auth_url: return None diff --git a/apps/common/cache.py b/apps/common/cache.py index b16d1e7dd..aad069c47 100644 --- a/apps/common/cache.py +++ b/apps/common/cache.py @@ -126,7 +126,7 @@ class Cache(metaclass=CacheType): return data def save_data_to_db(self, data): - logger.info(f'Set data to cache: key={self.key} data={data}') + logger.debug(f'Set data to cache: key={self.key} data={data}') self.redis.hset(self.key, mapping=data) self.load_data_from_db() @@ -143,10 +143,10 @@ class Cache(metaclass=CacheType): def init_all_values(self): t_start = time.time() - logger.info(f'Start init cache: key={self.key}') + logger.debug(f'Start init cache: key={self.key}') data = self.compute_values(*self.field_names) self.save_data_to_db(data) - logger.info(f'End init cache: cost={time.time()-t_start} key={self.key}') + logger.debug(f'End init cache: cost={time.time()-t_start} key={self.key}') return data def refresh(self, *fields): @@ -173,11 +173,11 @@ class Cache(metaclass=CacheType): def expire(self, *fields): self._data = None if not fields: - logger.info(f'Delete cached key: key={self.key}') + logger.debug(f'Delete cached key: key={self.key}') self.redis.delete(self.key) else: self.redis.hdel(self.key, *fields) - logger.info(f'Expire cached fields: key={self.key} fields={fields}') + logger.debug(f'Expire cached fields: key={self.key} fields={fields}') class CacheValueDesc: @@ -201,7 +201,7 @@ class CacheValueDesc: def compute_value(self, instance: Cache): t_start = time.time() - logger.info(f'Start compute cache field: field={self.field_name} key={instance.key}') + logger.debug(f'Start compute cache field: field={self.field_name} key={instance.key}') if self.field_type.queryset is not None: new_value = self.field_type.queryset.count() else: @@ -214,7 +214,7 @@ class CacheValueDesc: new_value = compute_func() new_value = self.field_type.field_type(new_value) - logger.info(f'End compute cache field: cost={time.time()-t_start} field={self.field_name} value={new_value} key={instance.key}') + logger.debug(f'End compute cache field: cost={time.time()-t_start} field={self.field_name} value={new_value} key={instance.key}') return new_value def to_internal_value(self, value): 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/common/utils/random.py b/apps/common/utils/random.py index a9ef0421f..1a7449ef0 100644 --- a/apps/common/utils/random.py +++ b/apps/common/utils/random.py @@ -6,7 +6,7 @@ import socket import string -string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_{}~' +string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~' def random_datetime(date_start, date_end): diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 81188eb8a..449ae7974 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -212,6 +212,10 @@ class Config(dict): 'CAS_ROOT_PROXIED_AS': '', 'CAS_LOGOUT_COMPLETELY': True, 'CAS_VERSION': 3, + 'CAS_USERNAME_ATTRIBUTE': 'uid', + 'CAS_APPLY_ATTRIBUTES_TO_USER': False, + 'CAS_RENAME_ATTRIBUTES': {}, + 'CAS_CREATE_USER': True, 'AUTH_SSO': False, 'AUTH_SSO_AUTHKEY_TTL': 60 * 15, diff --git a/apps/jumpserver/context_processor.py b/apps/jumpserver/context_processor.py index fbd7016d3..79b1e450f 100644 --- a/apps/jumpserver/context_processor.py +++ b/apps/jumpserver/context_processor.py @@ -11,11 +11,11 @@ def jumpserver_processor(request): 'DEFAULT_PK': '00000000-0000-0000-0000-000000000000', 'LOGO_URL': static('img/logo.png'), 'LOGO_TEXT_URL': static('img/logo_text.png'), - 'LOGIN_IMAGE_URL': static('img/login_image.png'), + 'LOGIN_IMAGE_URL': static('img/login_image.jpg'), 'FAVICON_URL': static('img/facio.ico'), 'LOGIN_CAS_LOGO_URL': static('img/login_cas_logo.png'), - 'LOGIN_WECOM_LOGO_URL': static('img/login_wecom_log.png'), - 'LOGIN_DINGTALK_LOGO_URL': static('img/login_dingtalk_log.png'), + 'LOGIN_WECOM_LOGO_URL': static('img/login_wecom_logo.png'), + 'LOGIN_DINGTALK_LOGO_URL': static('img/login_dingtalk_logo.png'), 'JMS_TITLE': _('JumpServer Open Source Bastion Host'), 'VERSION': settings.VERSION, 'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2021', diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index a4b2fb296..264566620 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -96,6 +96,10 @@ CAS_LOGOUT_COMPLETELY = CONFIG.CAS_LOGOUT_COMPLETELY CAS_VERSION = CONFIG.CAS_VERSION CAS_ROOT_PROXIED_AS = CONFIG.CAS_ROOT_PROXIED_AS CAS_CHECK_NEXT = lambda _next_page: True +CAS_USERNAME_ATTRIBUTE = CONFIG.CAS_USERNAME_ATTRIBUTE +CAS_APPLY_ATTRIBUTES_TO_USER = CONFIG.CAS_APPLY_ATTRIBUTES_TO_USER +CAS_RENAME_ATTRIBUTES = CONFIG.CAS_RENAME_ATTRIBUTES +CAS_CREATE_USER = CONFIG.CAS_CREATE_USER # SSO Auth AUTH_SSO = CONFIG.AUTH_SSO @@ -120,7 +124,7 @@ LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS -AUTH_BACKEND_MODEL = 'authentication.backends.api.ModelBackend' +AUTH_BACKEND_MODEL = 'authentication.backends.api.JMSModelBackend' AUTH_BACKEND_PUBKEY = 'authentication.backends.pubkey.PublicKeyAuthBackend' AUTH_BACKEND_LDAP = 'authentication.backends.ldap.LDAPAuthorizationBackend' AUTH_BACKEND_OIDC_PASSWORD = 'jms_oidc_rp.backends.OIDCAuthPasswordBackend' 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 c34d92992..319593bfd 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 0df73a8ea..8519fd042 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-06-16 17:47+0800\n" +"POT-Creation-Date: 2021-06-29 17:02+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -19,13 +19,13 @@ 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 -#: terminal/models/storage.py:23 terminal/models/storage.py:90 -#: terminal/models/task.py:16 terminal/models/terminal.py:100 -#: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py:550 +#: terminal/models/storage.py:23 terminal/models/task.py:16 +#: terminal/models/terminal.py:100 users/forms/profile.py:32 +#: users/models/group.py:15 users/models/user.py:550 #: users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 @@ -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,7 +54,7 @@ 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 @@ -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,7 +177,7 @@ 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 @@ -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,10 +252,10 @@ 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:26 terminal/models/storage.py:93 +#: terminal/models/storage.py:47 terminal/models/storage.py:108 #: tickets/models/ticket.py:38 #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:27 msgid "Type" @@ -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 @@ -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 @@ -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:304 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:146 -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 "命令过滤器" @@ -719,61 +698,61 @@ msgstr "ssh私钥" 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 "密钥不合法" @@ -1202,7 +1154,7 @@ msgstr "主机 (显示名称)" msgid "Result" msgstr "结果" -#: audits/serializers.py:92 terminal/serializers/storage.py:189 +#: audits/serializers.py:92 terminal/serializers/storage.py:195 msgid "Hosts" msgstr "主机" @@ -1242,7 +1194,7 @@ msgstr "企业微信" msgid "DingTalk" msgstr "钉钉" -#: authentication/api/connection_token.py:249 +#: authentication/api/connection_token.py:255 msgid "Invalid token" msgstr "无效的令牌" @@ -1422,12 +1374,12 @@ msgstr "您的密码已过期,先修改再登录" msgid "Your password is invalid" msgstr "您的密码无效" -#: authentication/forms.py:26 +#: authentication/forms.py:35 msgid "{} days auto login" msgstr "{} 天内自动登录" -#: authentication/forms.py:41 authentication/forms.py:54 -#: authentication/forms.py:56 users/forms/profile.py:27 +#: authentication/forms.py:46 authentication/forms.py:59 +#: authentication/forms.py:61 users/forms/profile.py:27 msgid "MFA code" 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 "秘钥" @@ -1632,19 +1589,19 @@ msgstr "请使用密码登录,然后绑定企业微信" msgid "Binding DingTalk failed" msgstr "绑定钉钉失败" -#: authentication/views/login.py:59 +#: authentication/views/login.py:60 msgid "Redirecting" msgstr "跳转中" -#: authentication/views/login.py:60 +#: authentication/views/login.py:61 msgid "Redirecting to {} authentication" msgstr "正在跳转到 {} 认证" -#: authentication/views/login.py:84 +#: authentication/views/login.py:85 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:202 +#: authentication/views/login.py:203 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -1652,15 +1609,15 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
\n" " 不要关闭本页面" -#: authentication/views/login.py:207 +#: authentication/views/login.py:208 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:239 +#: authentication/views/login.py:240 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:240 +#: authentication/views/login.py:241 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" @@ -2195,13 +2152,13 @@ msgstr "欢迎使用JumpServer开源堡垒机" msgid "Test success" msgstr "测试成功" -#: settings/api/ldap.py:189 +#: settings/api/ldap.py:197 msgid "Get ldap users is None" msgstr "获取 LDAP 用户为 None" -#: settings/api/ldap.py:196 -msgid "Imported {} users successfully" -msgstr "导入 {} 个用户成功" +#: settings/api/ldap.py:206 +msgid "Imported {} users successfully (Organization: {})" +msgstr "成功导入 {} 个用户 ( 组织: {} )" #: settings/models.py:123 users/templates/users/reset_password.html:29 msgid "Setting" @@ -2543,100 +2500,100 @@ msgstr "启用企业微信认证" msgid "Enable DingTalk Auth" msgstr "启用钉钉认证" -#: settings/utils/ldap.py:417 +#: settings/utils/ldap.py:416 msgid "Host or port is disconnected: {}" msgstr "主机或端口不可连接: {}" -#: settings/utils/ldap.py:419 +#: settings/utils/ldap.py:418 msgid "The port is not the port of the LDAP service: {}" msgstr "端口不是LDAP服务端口: {}" -#: settings/utils/ldap.py:421 +#: settings/utils/ldap.py:420 msgid "Please add certificate: {}" msgstr "请添加证书" -#: settings/utils/ldap.py:423 settings/utils/ldap.py:450 -#: settings/utils/ldap.py:480 settings/utils/ldap.py:508 +#: settings/utils/ldap.py:422 settings/utils/ldap.py:449 +#: settings/utils/ldap.py:479 settings/utils/ldap.py:507 msgid "Unknown error: {}" msgstr "未知错误: {}" -#: settings/utils/ldap.py:437 +#: settings/utils/ldap.py:436 msgid "Bind DN or Password incorrect" msgstr "绑定DN或密码错误" -#: settings/utils/ldap.py:444 +#: settings/utils/ldap.py:443 msgid "Please enter Bind DN: {}" msgstr "请输入绑定DN: {}" -#: settings/utils/ldap.py:446 +#: settings/utils/ldap.py:445 msgid "Please enter Password: {}" msgstr "请输入密码: {}" -#: settings/utils/ldap.py:448 +#: settings/utils/ldap.py:447 msgid "Please enter correct Bind DN and Password: {}" msgstr "请输入正确的绑定DN和密码: {}" -#: settings/utils/ldap.py:466 +#: settings/utils/ldap.py:465 msgid "Invalid User OU or User search filter: {}" msgstr "不合法的用户OU或用户过滤器: {}" -#: settings/utils/ldap.py:497 +#: settings/utils/ldap.py:496 msgid "LDAP User attr map not include: {}" msgstr "LDAP属性映射没有包含: {}" -#: settings/utils/ldap.py:504 +#: settings/utils/ldap.py:503 msgid "LDAP User attr map is not dict" msgstr "LDAP属性映射不合法" -#: settings/utils/ldap.py:523 +#: settings/utils/ldap.py:522 msgid "LDAP authentication is not enabled" msgstr "LDAP认证没有启用" -#: settings/utils/ldap.py:541 +#: settings/utils/ldap.py:540 msgid "Error (Invalid LDAP server): {}" msgstr "错误 (不合法的LDAP服务器地址): {}" -#: settings/utils/ldap.py:543 +#: settings/utils/ldap.py:542 msgid "Error (Invalid Bind DN): {}" msgstr "错误(不合法的绑定DN): {}" -#: settings/utils/ldap.py:545 +#: settings/utils/ldap.py:544 msgid "Error (Invalid LDAP User attr map): {}" msgstr "错误(不合法的LDAP属性映射): {}" -#: settings/utils/ldap.py:547 +#: settings/utils/ldap.py:546 msgid "Error (Invalid User OU or User search filter): {}" msgstr "错误(不合法的用户OU或用户过滤器): {}" -#: settings/utils/ldap.py:549 +#: settings/utils/ldap.py:548 msgid "Error (Not enabled LDAP authentication): {}" msgstr "错误(没有启用LDAP认证): {}" -#: settings/utils/ldap.py:551 +#: settings/utils/ldap.py:550 msgid "Error (Unknown): {}" msgstr "错误(未知): {}" -#: settings/utils/ldap.py:554 +#: settings/utils/ldap.py:553 msgid "Succeed: Match {} s user" msgstr "成功匹配 {} 个用户" -#: settings/utils/ldap.py:587 +#: settings/utils/ldap.py:586 msgid "Authentication failed (configuration incorrect): {}" msgstr "认证失败(配置错误): {}" -#: settings/utils/ldap.py:589 +#: settings/utils/ldap.py:588 msgid "Authentication failed (before login check failed): {}" msgstr "认证失败(登录前检查失败): {}" -#: settings/utils/ldap.py:591 +#: settings/utils/ldap.py:590 msgid "Authentication failed (username or password incorrect): {}" msgstr "认证失败 (用户名或密码不正确): {}" -#: settings/utils/ldap.py:593 +#: settings/utils/ldap.py:592 msgid "Authentication failed (Unknown): {}" msgstr "认证失败: (未知): {}" -#: settings/utils/ldap.py:596 +#: settings/utils/ldap.py:595 msgid "Authentication success: {}" msgstr "认证成功: {}" @@ -2825,7 +2782,7 @@ msgstr "数据库应用" msgid "Perms" msgstr "权限管理" -#: templates/_nav.html:97 terminal/notifications.py:15 +#: templates/_nav.html:97 terminal/notifications.py:16 msgid "Sessions" msgstr "会话管理" @@ -3252,6 +3209,10 @@ msgstr "线程数" msgid "Boot Time" msgstr "运行时间" +#: terminal/models/storage.py:25 +msgid "Default storage" +msgstr "默认存储" + #: terminal/models/task.py:17 msgid "Args" msgstr "参数" @@ -3280,11 +3241,11 @@ msgstr "命令存储" msgid "Replay storage" msgstr "录像存储" -#: terminal/notifications.py:35 +#: terminal/notifications.py:48 msgid "Danger command alert" msgstr "危险命令告警" -#: terminal/notifications.py:44 +#: terminal/notifications.py:57 #, python-format msgid "" "\n" @@ -3314,18 +3275,18 @@ msgstr "" "
\n" " " -#: terminal/notifications.py:79 +#: terminal/notifications.py:92 #, python-format msgid "" "Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $" "%(command)s" msgstr "危险命令告警: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s" -#: terminal/notifications.py:97 +#: terminal/notifications.py:110 msgid "Batch danger command alert" msgstr "批量危险命令告警" -#: terminal/notifications.py:108 +#: terminal/notifications.py:121 #, python-format msgid "" "\n" @@ -3356,7 +3317,7 @@ msgstr "" " ----------------- 命令 ----------------
\n" " " -#: terminal/notifications.py:133 +#: terminal/notifications.py:146 #, python-format msgid "Insecure Web Command Execution Alert: [%(name)s]" msgstr "批量危险命令告警: [%(name)s]" @@ -3426,27 +3387,27 @@ msgstr "账户密钥" msgid "Endpoint suffix" msgstr "端点后缀" -#: terminal/serializers/storage.py:166 +#: terminal/serializers/storage.py:172 msgid "The address format is incorrect" msgstr "地址格式不正确" -#: terminal/serializers/storage.py:173 +#: terminal/serializers/storage.py:179 msgid "Host invalid" msgstr "主机无效" -#: terminal/serializers/storage.py:176 +#: terminal/serializers/storage.py:182 msgid "Port invalid" msgstr "端口无效" -#: terminal/serializers/storage.py:192 +#: terminal/serializers/storage.py:198 msgid "Index" msgstr "索引" -#: terminal/serializers/storage.py:194 +#: terminal/serializers/storage.py:200 msgid "Doc type" msgstr "文档类型" -#: terminal/serializers/storage.py:196 +#: terminal/serializers/storage.py:202 msgid "Ignore Certificate Verification" msgstr "忽略证书认证" @@ -3923,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 "系统管理员" @@ -3947,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 "最后更新密码日期" @@ -5099,8 +5073,35 @@ msgstr "旗舰版" 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/notifications/notifications.py b/apps/notifications/notifications.py index 4c4db11d9..bbf9fe7ee 100644 --- a/apps/notifications/notifications.py +++ b/apps/notifications/notifications.py @@ -32,16 +32,6 @@ class MessageType(type): } if issubclass(clz, SystemMessage): system_msgs.append(msg) - try: - if not SystemMsgSubscription.objects.filter(message_type=message_type).exists(): - sub = SystemMsgSubscription.objects.create(message_type=message_type) - clz.post_insert_to_db(sub) - except ProgrammingError as e: - if e.args[0] == 1146: - # 表不存在 - pass - else: - raise elif issubclass(clz, UserMessage): user_msgs.append(msg) diff --git a/apps/notifications/signals_handler.py b/apps/notifications/signals_handler.py index 13ebdc4bc..451377557 100644 --- a/apps/notifications/signals_handler.py +++ b/apps/notifications/signals_handler.py @@ -1,13 +1,20 @@ import json +from importlib import import_module +import inspect from django.utils.functional import LazyObject from django.db.models.signals import post_save +from django.db.models.signals import post_migrate from django.dispatch import receiver +from django.db.utils import DEFAULT_DB_ALIAS +from django.apps import apps as global_apps +from django.apps import AppConfig from common.utils.connection import RedisPubSub from common.utils import get_logger from common.decorator import on_transaction_commit -from .models import SiteMessage +from .models import SiteMessage, SystemMsgSubscription +from .notifications import SystemMessage logger = get_logger(__name__) @@ -41,3 +48,37 @@ def on_site_message_create(sender, instance, created, **kwargs): } data = json.dumps(data) new_site_msg_chan.publish(data) + + +@receiver(post_migrate, dispatch_uid='notifications.signals_handler.create_system_messages') +def create_system_messages(app_config: AppConfig, **kwargs): + try: + notifications_module = import_module('.notifications', app_config.module.__package__) + + for name, obj in notifications_module.__dict__.items(): + if name.startswith('_'): + continue + + if not inspect.isclass(obj): + continue + + if not issubclass(obj, SystemMessage): + continue + + attrs = obj.__dict__ + if 'message_type_label' not in attrs: + continue + + if 'category' not in attrs: + continue + + if 'category_label' not in attrs: + continue + + message_type = obj.get_message_type() + sub, created = SystemMsgSubscription.objects.get_or_create(message_type=message_type) + if created: + obj.post_insert_to_db(sub) + logger.info(f'Create SystemMsgSubscription: package={app_config.module.__package__} type={message_type}') + except ModuleNotFoundError: + pass diff --git a/apps/ops/celery/utils.py b/apps/ops/celery/utils.py index 959e4c907..c35db0f0e 100644 --- a/apps/ops/celery/utils.py +++ b/apps/ops/celery/utils.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # import json -import os import redis_lock import redis @@ -12,6 +11,7 @@ from django_celery_beat.models import ( PeriodicTask, IntervalSchedule, CrontabSchedule, PeriodicTasks ) +from common.utils.timezone import now from common.utils import get_logger logger = get_logger(__name__) @@ -36,6 +36,8 @@ def create_or_update_celery_periodic_tasks(tasks): for name, detail in tasks.items(): interval = None crontab = None + last_run_at = None + try: IntervalSchedule.objects.all().count() except (ProgrammingError, OperationalError): @@ -50,6 +52,7 @@ def create_or_update_celery_periodic_tasks(tasks): interval = IntervalSchedule.objects.filter(**kwargs).first() if interval is None: interval = IntervalSchedule.objects.create(**kwargs) + last_run_at = now() elif isinstance(detail.get("crontab"), str): try: minute, hour, day, month, week = detail["crontab"].split() @@ -75,7 +78,8 @@ def create_or_update_celery_periodic_tasks(tasks): enabled=detail.get('enabled', True), args=json.dumps(detail.get('args', [])), kwargs=json.dumps(detail.get('kwargs', {})), - description=detail.get('description') or '' + description=detail.get('description') or '', + last_run_at=last_run_at, ) task = PeriodicTask.objects.update_or_create( defaults=defaults, name=name, 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/ops/models/command.py b/apps/ops/models/command.py index e89520390..819acc3d5 100644 --- a/apps/ops/models/command.py +++ b/apps/ops/models/command.py @@ -86,8 +86,10 @@ class CommandExecution(OrgModelMixin): host = self.hosts.first() if host and host.is_windows(): shell = 'win_shell' - else: + elif host and host.is_unixlike(): shell = 'shell' + else: + shell = 'raw' result = runner.execute(self.command, 'all', module=shell) self.result = result.results_command except SoftTimeLimitExceeded as e: diff --git a/apps/ops/notifications.py b/apps/ops/notifications.py index 61e9d5630..4a65d8a4e 100644 --- a/apps/ops/notifications.py +++ b/apps/ops/notifications.py @@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _ from notifications.notifications import SystemMessage from notifications.models import SystemMsgSubscription from users.models import User +from notifications.backends import BACKEND __all__ = ('ServerPerformanceMessage',) @@ -24,3 +25,5 @@ class ServerPerformanceMessage(SystemMessage): def post_insert_to_db(cls, subscription: SystemMsgSubscription): admins = User.objects.filter(role=User.ROLE.ADMIN) subscription.users.add(*admins) + subscription.receive_backends = [BACKEND.EMAIL] + subscription.save() diff --git a/apps/orgs/caches.py b/apps/orgs/caches.py index 5ca445479..cc4f71a2a 100644 --- a/apps/orgs/caches.py +++ b/apps/orgs/caches.py @@ -38,7 +38,7 @@ class OrgRelatedCache(Cache): 在事务提交之后再发送信号,防止因事务的隔离性导致未获得最新的数据 """ def func(): - logger.info(f'CACHE: Send refresh task {self}.{fields}') + logger.debug(f'CACHE: Send refresh task {self}.{fields}') refresh_org_cache_task.delay(self, *fields) on_commit(func) @@ -93,7 +93,7 @@ class OrgResourceStatisticsCache(OrgRelatedCache): return node.assets_amount def compute_total_count_online_users(self): - return len(set(Session.objects.filter(is_finished=False).values_list('user_id', flat=True))) + return Session.objects.filter(is_finished=False).values_list('user_id').distinct().count() def compute_total_count_online_sessions(self): return Session.objects.filter(is_finished=False).count() 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/cache.py b/apps/orgs/signals_handler/cache.py index 626975991..96d2a6186 100644 --- a/apps/orgs/signals_handler/cache.py +++ b/apps/orgs/signals_handler/cache.py @@ -1,5 +1,5 @@ from django.db.models.signals import m2m_changed -from django.db.models.signals import post_save, pre_delete, pre_save +from django.db.models.signals import post_save, pre_delete, pre_save, post_delete from django.dispatch import receiver from orgs.models import Organization, OrganizationMember @@ -60,7 +60,6 @@ class OrgResourceStatisticsRefreshUtil: Node: ['nodes_amount'], Asset: ['assets_amount'], UserGroup: ['groups_amount'], - Session: ['total_count_online_users', 'total_count_online_sessions'] } @classmethod @@ -72,11 +71,40 @@ class OrgResourceStatisticsRefreshUtil: OrgResourceStatisticsCache(Organization.root()).expire(*cache_field_name) -@receiver(pre_save) -def on_post_save_refresh_org_resource_statistics_cache(sender, instance, **kwargs): +@receiver(post_save) +def on_post_save_refresh_org_resource_statistics_cache(sender, instance, created, **kwargs): + if created: + OrgResourceStatisticsRefreshUtil.refresh_if_need(instance) + + +@receiver(post_delete) +def on_post_delete_refresh_org_resource_statistics_cache(sender, instance, **kwargs): OrgResourceStatisticsRefreshUtil.refresh_if_need(instance) -@receiver(pre_delete) -def on_pre_delete_refresh_org_resource_statistics_cache(sender, instance, **kwargs): - OrgResourceStatisticsRefreshUtil.refresh_if_need(instance) +def _refresh_session_org_resource_statistics_cache(instance: Session): + cache_field_name = ['total_count_online_users', 'total_count_online_sessions'] + + org_cache = OrgResourceStatisticsCache(instance.org) + org_cache.expire(*cache_field_name) + OrgResourceStatisticsCache(Organization.root()).expire(*cache_field_name) + + +@receiver(pre_save, sender=Session) +def on_session_pre_save(sender, instance: Session, **kwargs): + old = Session.objects.filter(id=instance.id).values_list('is_finished', flat=True) + if old: + instance._signal_old_is_finished = old[0] + else: + instance._signal_old_is_finished = None + + +@receiver(post_save, sender=Session) +def on_session_changed_refresh_org_resource_statistics_cache(sender, instance, created, **kwargs): + if created or instance.is_finished != instance._signal_old_is_finished: + _refresh_session_org_resource_statistics_cache(instance) + + +@receiver(post_delete, sender=Session) +def on_session_deleted_refresh_org_resource_statistics_cache(sender, instance, **kwargs): + _refresh_session_org_resource_statistics_cache(instance) diff --git a/apps/orgs/signals_handler/common.py b/apps/orgs/signals_handler/common.py index fb8b2a2d1..c7aee3c32 100644 --- a/apps/orgs/signals_handler/common.py +++ b/apps/orgs/signals_handler/common.py @@ -15,6 +15,7 @@ from orgs.hands import set_current_org, Node, get_current_org from perms.models import (AssetPermission, ApplicationPermission) from users.models import UserGroup, User from common.const.signals import PRE_REMOVE, POST_REMOVE +from common.decorator import on_transaction_commit from common.signals import django_ready from common.utils import get_logger from common.utils.connection import RedisPubSub @@ -35,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) @@ -53,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() @@ -64,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) @@ -167,3 +167,13 @@ def on_org_user_changed(action, instance, reverse, pk_set, **kwargs): leaved_users = set(pk_set) - set(org.members.filter(id__in=user_pk_set).values_list('id', flat=True)) _clear_users_from_org(org, leaved_users) + + +@receiver(post_save, sender=User) +@on_transaction_commit +def on_user_created_set_default_org(sender, instance, created, **kwargs): + if not created: + return + if instance.orgs.count() > 0: + return + Organization.default().members.add(instance) 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/settings/api/common.py b/apps/settings/api/common.py index 03c885d78..1cb39e62d 100644 --- a/apps/settings/api/common.py +++ b/apps/settings/api/common.py @@ -81,7 +81,7 @@ class PublicSettingApi(generics.RetrieveAPIView): logo_urls = { 'logo_logout': static('img/logo.png'), 'logo_index': static('img/logo_text.png'), - 'login_image': static('img/login_image.png'), + 'login_image': static('img/login_image.jpg'), 'favicon': static('img/facio.ico') } if not settings.XPACK_ENABLED: diff --git a/apps/settings/api/ldap.py b/apps/settings/api/ldap.py index 0c982d1b1..dd5c15563 100644 --- a/apps/settings/api/ldap.py +++ b/apps/settings/api/ldap.py @@ -7,8 +7,7 @@ from collections.abc import Iterable from smtplib import SMTPSenderRefused from rest_framework import generics from rest_framework.views import Response, APIView -from django.conf import settings -from django.core.mail import send_mail, get_connection +from orgs.models import Organization from django.utils.translation import ugettext_lazy as _ from ..utils import ( @@ -17,11 +16,12 @@ from ..utils import ( ) from ..tasks import sync_ldap_user from common.permissions import IsOrgAdmin, IsSuperUser -from common.utils import get_logger +from common.utils import get_logger, is_uuid from ..serializers import ( MailTestSerializer, LDAPTestConfigSerializer, LDAPUserSerializer, PublicSettingSerializer, LDAPTestLoginSerializer, SettingsSerializer ) +from orgs.utils import current_org from users.models import User logger = get_logger(__file__) @@ -170,6 +170,14 @@ class LDAPUserListApi(generics.ListAPIView): class LDAPUserImportAPI(APIView): permission_classes = (IsSuperUser,) + def get_org(self): + org_id = self.request.data.get('org_id') + if is_uuid(org_id): + org = Organization.objects.get(id=org_id) + else: + org = current_org + return org + def get_ldap_users(self): username_list = self.request.data.get('username_list', []) cache_police = self.request.query_params.get('cache_police', True) @@ -188,12 +196,15 @@ class LDAPUserImportAPI(APIView): if users is None: return Response({'msg': _('Get ldap users is None')}, status=400) - errors = LDAPImportUtil().perform_import(users) + org = self.get_org() + errors = LDAPImportUtil().perform_import(users, org) if errors: return Response({'errors': errors}, status=400) count = users if users is None else len(users) - return Response({'msg': _('Imported {} users successfully').format(count)}) + return Response({ + 'msg': _('Imported {} users successfully (Organization: {})').format(count, org) + }) class LDAPCacheRefreshAPI(generics.RetrieveAPIView): diff --git a/apps/settings/utils/ldap.py b/apps/settings/utils/ldap.py index 6eeab1daa..366d21614 100644 --- a/apps/settings/utils/ldap.py +++ b/apps/settings/utils/ldap.py @@ -362,20 +362,19 @@ class LDAPImportUtil(object): ) return obj, created - def perform_import(self, users): + def perform_import(self, users, org=None): logger.info('Start perform import ldap users, count: {}'.format(len(users))) errors = [] - instances = [] + objs = [] for user in users: try: obj, created = self.update_or_create(user) - if created: - instances.append(obj) + objs.append(obj) except Exception as e: errors.append({user['username']: str(e)}) logger.error(e) - # 默认添加用户到 Default 组织 - Organization.default().members.add(*instances) + if org and not org.is_root(): + org.members.add(*objs) logger.info('End perform import ldap users') return errors diff --git a/apps/static/img/authenticator_iphone.png b/apps/static/img/authenticator_iphone.png index fd5b4e8eb..ed31d38b3 100644 Binary files a/apps/static/img/authenticator_iphone.png and b/apps/static/img/authenticator_iphone.png differ diff --git a/apps/static/img/login_dingtalk_log.png b/apps/static/img/login_dingtalk_logo.png similarity index 100% rename from apps/static/img/login_dingtalk_log.png rename to apps/static/img/login_dingtalk_logo.png diff --git a/apps/static/img/login_image.jpg b/apps/static/img/login_image.jpg new file mode 100644 index 000000000..9c9958506 Binary files /dev/null and b/apps/static/img/login_image.jpg differ diff --git a/apps/static/img/login_image.png b/apps/static/img/login_image.png deleted file mode 100644 index 0273e3669..000000000 Binary files a/apps/static/img/login_image.png and /dev/null differ diff --git a/apps/static/img/login_wecom_log.png b/apps/static/img/login_wecom_log.png deleted file mode 100644 index d5a58d0ba..000000000 Binary files a/apps/static/img/login_wecom_log.png and /dev/null differ diff --git a/apps/static/img/login_wecom_logo.png b/apps/static/img/login_wecom_logo.png new file mode 100644 index 000000000..8a6b66c1c Binary files /dev/null and b/apps/static/img/login_wecom_logo.png differ diff --git a/apps/static/img/logo_text.png b/apps/static/img/logo_text.png index 8d741116c..1fca6d189 100644 Binary files a/apps/static/img/logo_text.png and b/apps/static/img/logo_text.png differ diff --git a/apps/terminal/api/storage.py b/apps/terminal/api/storage.py index 4aa3fecbb..db4470f75 100644 --- a/apps/terminal/api/storage.py +++ b/apps/terminal/api/storage.py @@ -36,7 +36,7 @@ class BaseStorageViewSetMixin: class CommandStorageViewSet(BaseStorageViewSetMixin, viewsets.ModelViewSet): - search_fields = ('name', 'type',) + search_fields = ('name', 'type') queryset = CommandStorage.objects.all() serializer_class = CommandStorageSerializer permission_classes = (IsSuperUser,) @@ -103,7 +103,7 @@ class CommandStorageViewSet(BaseStorageViewSetMixin, viewsets.ModelViewSet): class ReplayStorageViewSet(BaseStorageViewSetMixin, viewsets.ModelViewSet): - filterset_fields = ('name', 'type',) + filterset_fields = ('name', 'type', 'is_default') search_fields = filterset_fields queryset = ReplayStorage.objects.all() serializer_class = ReplayStorageSerializer diff --git a/apps/terminal/filters.py b/apps/terminal/filters.py index a102c149c..81f548162 100644 --- a/apps/terminal/filters.py +++ b/apps/terminal/filters.py @@ -71,7 +71,7 @@ class CommandStorageFilter(filters.FilterSet): class Meta: model = CommandStorage - fields = ['real', 'name', 'type'] + fields = ['real', 'name', 'type', 'is_default'] def filter_real(self, queryset, name, value): if value: diff --git a/apps/terminal/migrations/0037_auto_20210623_1748.py b/apps/terminal/migrations/0037_auto_20210623_1748.py new file mode 100644 index 000000000..fde10b3b7 --- /dev/null +++ b/apps/terminal/migrations/0037_auto_20210623_1748.py @@ -0,0 +1,37 @@ +# Generated by Django 3.1.6 on 2021-06-23 09:48 + +from django.db import migrations, models + + +def set_default_storage(apps, schema_editor): + command_storage_model = apps.get_model("terminal", "CommandStorage") + command_storage = command_storage_model.objects.filter(name='default', type='server').first() + if command_storage: + command_storage.is_default = True + command_storage.save() + replay_storage_model = apps.get_model("terminal", "ReplayStorage") + replay_storage = replay_storage_model.objects.filter(name='default', type='server').first() + if replay_storage: + replay_storage.is_default = True + replay_storage.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0036_auto_20210604_1124'), + ] + + operations = [ + migrations.AddField( + model_name='commandstorage', + name='is_default', + field=models.BooleanField(default=False, verbose_name='Default storage'), + ), + migrations.AddField( + model_name='replaystorage', + name='is_default', + field=models.BooleanField(default=False, verbose_name='Default storage'), + ), + migrations.RunPython(set_default_storage) + ] 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/models/storage.py b/apps/terminal/models/storage.py index 883e5f67a..7d4efcfbc 100644 --- a/apps/terminal/models/storage.py +++ b/apps/terminal/models/storage.py @@ -19,17 +19,41 @@ from .. import const logger = get_logger(__file__) -class CommandStorage(CommonModelMixin): +class CommonStorageModelMixin(models.Model): name = models.CharField(max_length=128, verbose_name=_("Name"), unique=True) + meta = EncryptJsonDictTextField(default={}) + is_default = models.BooleanField(default=False, verbose_name=_('Default storage')) + comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) + + class Meta: + abstract = True + + def __str__(self): + return self.name + + def set_to_default(self): + self.is_default = True + self.save() + self.__class__.objects.select_for_update()\ + .filter(is_default=True)\ + .exclude(id=self.id)\ + .update(is_default=False) + + @classmethod + def default(cls): + objs = cls.objects.filter(is_default=True) + if not objs: + objs = cls.objects.filter(name='default', type='server') + if not objs: + objs = cls.objects.all() + return objs.first() + + +class CommandStorage(CommonStorageModelMixin, CommonModelMixin): type = models.CharField( max_length=16, choices=const.CommandStorageTypeChoices.choices, default=const.CommandStorageTypeChoices.server.value, verbose_name=_('Type'), ) - meta = EncryptJsonDictTextField(default={}) - comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) - - def __str__(self): - return self.name @property def type_null(self): @@ -86,17 +110,11 @@ class CommandStorage(CommonModelMixin): backend.pre_use_check() -class ReplayStorage(CommonModelMixin): - name = models.CharField(max_length=128, verbose_name=_("Name"), unique=True) +class ReplayStorage(CommonStorageModelMixin, CommonModelMixin): type = models.CharField( max_length=16, choices=const.ReplayStorageTypeChoices.choices, default=const.ReplayStorageTypeChoices.server.value, verbose_name=_('Type') ) - meta = EncryptJsonDictTextField(default={}) - comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) - - def __str__(self): - return self.name @property def type_null(self): diff --git a/apps/terminal/notifications.py b/apps/terminal/notifications.py index 088e8ecc0..e9c83135e 100644 --- a/apps/terminal/notifications.py +++ b/apps/terminal/notifications.py @@ -32,14 +32,16 @@ class CommandAlertMixin: db_setting = Setting.objects.filter(name='SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER').first() if db_setting: emails = db_setting.value - emails = emails or settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER + else: + emails = settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER emails = emails.split(',') emails = [email.strip().strip('"') for email in emails] users = User.objects.filter(email__in=emails) - subscription.users.add(*users) - subscription.receive_backends = [BACKEND.EMAIL] - subscription.save() + if users: + subscription.users.add(*users) + subscription.receive_backends = [BACKEND.EMAIL] + subscription.save() class CommandAlertMessage(CommandAlertMixin, SystemMessage): 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/storage.py b/apps/terminal/serializers/storage.py index cdd6e75a3..d0f803928 100644 --- a/apps/terminal/serializers/storage.py +++ b/apps/terminal/serializers/storage.py @@ -119,44 +119,6 @@ replay_storage_type_serializer_classes_mapping = { const.ReplayStorageTypeChoices.obs.value: ReplayStorageTypeOBSSerializer } -# ReplayStorageSerializer - - -class ReplayStorageSerializer(serializers.ModelSerializer): - meta = MethodSerializer() - - class Meta: - model = ReplayStorage - fields = ['id', 'name', 'type', 'meta', 'comment'] - - def validate_meta(self, meta): - _meta = self.instance.meta if self.instance else {} - _meta.update(meta) - return _meta - - def get_meta_serializer(self): - default_serializer = serializers.Serializer(read_only=True) - - if isinstance(self.instance, ReplayStorage): - _type = self.instance.type - else: - _type = self.context['request'].query_params.get('type') - - if _type: - serializer_class = replay_storage_type_serializer_classes_mapping.get(_type) - else: - serializer_class = default_serializer - - if not serializer_class: - serializer_class = default_serializer - - if isinstance(serializer_class, type): - serializer = serializer_class() - else: - serializer = serializer_class - return serializer - - # Command storage serializers # --------------------------- @@ -204,15 +166,17 @@ command_storage_type_serializer_classes_mapping = { const.CommandStorageTypeChoices.es.value: CommandStorageTypeESSerializer } -# CommandStorageSerializer + +# BaseStorageSerializer -class CommandStorageSerializer(serializers.ModelSerializer): +class BaseStorageSerializer(serializers.ModelSerializer): + storage_type_serializer_classes_mapping = {} meta = MethodSerializer() class Meta: - model = CommandStorage - fields = ['id', 'name', 'type', 'meta', 'comment'] + model = None + fields = ['id', 'name', 'type', 'meta', 'is_default', 'comment'] def validate_meta(self, meta): _meta = self.instance.meta if self.instance else {} @@ -222,13 +186,13 @@ class CommandStorageSerializer(serializers.ModelSerializer): def get_meta_serializer(self): default_serializer = serializers.Serializer(read_only=True) - if isinstance(self.instance, CommandStorage): + if isinstance(self.instance, self.__class__.Meta.model): _type = self.instance.type else: _type = self.context['request'].query_params.get('type') if _type: - serializer_class = command_storage_type_serializer_classes_mapping.get(_type) + serializer_class = self.storage_type_serializer_classes_mapping.get(_type) else: serializer_class = default_serializer @@ -240,3 +204,30 @@ class CommandStorageSerializer(serializers.ModelSerializer): else: serializer = serializer_class return serializer + + def save(self, **kwargs): + instance = super().save(**kwargs) + if self.validated_data.get('is_default', False): + instance.set_to_default() + return instance + + +# CommandStorageSerializer + + +class CommandStorageSerializer(BaseStorageSerializer): + storage_type_serializer_classes_mapping = command_storage_type_serializer_classes_mapping + + class Meta(BaseStorageSerializer.Meta): + model = CommandStorage + + +# ReplayStorageSerializer + + +class ReplayStorageSerializer(BaseStorageSerializer): + storage_type_serializer_classes_mapping = replay_storage_type_serializer_classes_mapping + + class Meta(BaseStorageSerializer.Meta): + model = ReplayStorage + diff --git a/apps/terminal/serializers/terminal.py b/apps/terminal/serializers/terminal.py index e4640e454..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' @@ -119,5 +118,7 @@ class TerminalRegistrationSerializer(serializers.ModelSerializer): instance.remote_addr = get_request_ip(request) sa = self.service_account.save() instance.user = sa + instance.command_storage = CommandStorage.default().name + instance.replay_storage = ReplayStorage.default().name instance.save() return instance 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/deb_buster_requirements.txt b/requirements/deb_buster_requirements.txt index 8177173b2..72cd44b7a 100644 --- a/requirements/deb_buster_requirements.txt +++ b/requirements/deb_buster_requirements.txt @@ -12,7 +12,7 @@ default-mysql-client default-libmysqlclient-dev # Pillow -# libffi-dev +libffi-dev # libfreetype6-dev # libfribidi-dev # libharfbuzz-dev @@ -36,4 +36,3 @@ sqlite # ansible sshpass - diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 97dac7a49..d8e34784e 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -73,8 +73,8 @@ vine==1.3.0 drf-yasg==1.20.0 Werkzeug==0.15.3 drf-nested-routers==0.91 -aliyun-python-sdk-core-v3==2.9.1 -aliyun-python-sdk-ecs==4.10.1 +aliyun-python-sdk-core-v3==2.9.1 +aliyun-python-sdk-ecs==4.10.1 rest_condition==1.0.3 python-ldap==3.3.1 tencentcloud-sdk-python==3.0.40 @@ -112,3 +112,5 @@ pyvmomi==7.0.1 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) diff --git a/utils/migrate_unorg_users_to_default_org.sh b/utils/migrate_unorg_users_to_default_org.sh new file mode 100644 index 000000000..e26ca7525 --- /dev/null +++ b/utils/migrate_unorg_users_to_default_org.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# + +python ../apps/manage.py shell << EOF +from users.models import User +from orgs.models import Organization +unorgs_users = [user for user in User.objects.all() if user.orgs.count() == 0] +Organization.default().members.add(*unorgs_users) +EOF