From b961d1f9ee261b2158bfb3835d771725284c2320 Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 18 Jul 2022 11:12:21 +0800 Subject: [PATCH 1/8] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=20accounts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/models/account.py | 6 ++++-- apps/assets/models/user.py | 10 ++++++++++ apps/assets/signal_handlers/system_user.py | 6 +++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/assets/models/account.py b/apps/assets/models/account.py index 7660ff682..3c0588cdf 100644 --- a/apps/assets/models/account.py +++ b/apps/assets/models/account.py @@ -14,8 +14,10 @@ class Account(BaseUser, AbsConnectivity, ProtocolMixin): common = 'common', _('Common user') admin = 'admin', _('Admin user') - protocol = models.CharField(max_length=16, choices=ProtocolMixin.Protocol.choices, - default='ssh', verbose_name=_('Protocol')) + protocol = models.CharField( + max_length=16, choices=ProtocolMixin.Protocol.choices, + default='ssh', verbose_name=_('Protocol') + ) type = models.CharField(max_length=16, choices=Type.choices, default=Type.common, verbose_name=_("Type")) asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset')) version = models.IntegerField(default=0, verbose_name=_('Version')) diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 661f89480..5affa7367 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -199,6 +199,10 @@ class SystemUser(ProtocolMixin, BaseUser): return self.su_from.assets.add(*tuple(assets_or_ids)) + @classmethod + def create_accounts_with_assets(cls, asset_ids, system_user_ids): + pass + class Meta: ordering = ['name'] unique_together = [('name', 'org_id')] @@ -208,6 +212,12 @@ class SystemUser(ProtocolMixin, BaseUser): ] +class SystemUserAccount(models.Model): + system_user = models.ForeignKey('SystemUser', on_delete=models.CASCADE, related_name='accounts') + account = models.ForeignKey('assets.Account', on_delete=models.CASCADE, related_name='system_users') + date_created = models.DateTimeField(auto_now_add=True) + + # Deprecated: 准备废弃 class AdminUser(BaseUser): """ diff --git a/apps/assets/signal_handlers/system_user.py b/apps/assets/signal_handlers/system_user.py index 64a656c29..e0dfe3ebf 100644 --- a/apps/assets/signal_handlers/system_user.py +++ b/apps/assets/signal_handlers/system_user.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # from django.db.models.signals import ( - post_save, m2m_changed, pre_save, pre_delete, post_delete + post_save, m2m_changed ) from django.dispatch import receiver @@ -32,6 +32,9 @@ def on_system_user_assets_change(instance, action, model, pk_set, **kwargs): logger.debug('No system user found') return + if action != POST_ADD: + return + if model == Asset: system_user_ids = [instance.id] asset_ids = pk_set @@ -39,6 +42,7 @@ def on_system_user_assets_change(instance, action, model, pk_set, **kwargs): system_user_ids = pk_set asset_ids = [instance.id] # todo: Auto create account if need + SystemUser.create_accounts_with_assets(asset_ids, system_user_ids) @receiver(m2m_changed, sender=SystemUser.users.through) From c9becca63394950ff976ea30e140da76c3040f1a Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 20 Jul 2022 12:56:41 +0800 Subject: [PATCH 2/8] stash --- apps/assets/api/system_user.py | 24 +++++++++++++++++------- apps/assets/models/user.py | 16 ++++++++++++++++ apps/assets/urls/api_urls.py | 1 + 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index f95303a5e..5113cc30a 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -9,9 +9,8 @@ from common.mixins.api import SuggestionMixin from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics from orgs.utils import tmp_to_root_org -from ..models import SystemUser, CommandFilterRule +from ..models import SystemUser, CommandFilterRule, Account from .. import serializers -from ..serializers import SystemUserWithAuthInfoSerializer, SystemUserTempAuthSerializer from ..tasks import ( push_system_user_to_assets_manual, test_system_user_connectivity_manual, push_system_user_to_assets @@ -21,7 +20,7 @@ logger = get_logger(__file__) __all__ = [ 'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi', 'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi', 'SystemUserAssetsListView', - 'SystemUserTempAuthInfoApi', 'SystemUserAppAuthInfoApi', + 'SystemUserTempAuthInfoApi', 'SystemUserAppAuthInfoApi', 'SystemUserAssetAccountApi' ] @@ -77,12 +76,23 @@ class SystemUserViewSet(SuggestionMixin, OrgBulkModelViewSet): return Response(serializer.data) +class SystemUserAssetAccountApi(generics.RetrieveUpdateDestroyAPIView): + model = Account + serializer_class = serializers.AccountSerializer + + def get_object(self): + asset_id = self.kwargs.get('asset_id') + user_id = self.request.query_params.get("user_id") + system_user = super().get_object() + return system_user.get_account(user_id, asset_id) + + class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): """ Get system user auth info """ model = SystemUser - serializer_class = SystemUserWithAuthInfoSerializer + serializer_class = serializers.SystemUserWithAuthInfoSerializer rbac_perms = { 'retrieve': 'assets.view_systemusersecret', 'list': 'assets.view_systemusersecret', @@ -99,7 +109,7 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): class SystemUserTempAuthInfoApi(generics.CreateAPIView): model = SystemUser permission_classes = (IsValidUser,) - serializer_class = SystemUserTempAuthSerializer + serializer_class = serializers.SystemUserTempAuthSerializer def create(self, request, *args, **kwargs): serializer = super().get_serializer(data=request.data) @@ -120,7 +130,7 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView): Get system user with asset auth info """ model = SystemUser - serializer_class = SystemUserWithAuthInfoSerializer + serializer_class = serializers.SystemUserWithAuthInfoSerializer def get_object(self): instance = super().get_object() @@ -136,7 +146,7 @@ class SystemUserAppAuthInfoApi(generics.RetrieveAPIView): Get system user with asset auth info """ model = SystemUser - serializer_class = SystemUserWithAuthInfoSerializer + serializer_class = serializers.SystemUserWithAuthInfoSerializer rbac_perms = { 'retrieve': 'assets.view_systemusersecret', } diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 5affa7367..0f6b51e0f 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator, MaxValueValidator from common.utils import signer +from users.models import User from .base import BaseUser from .asset import Asset @@ -203,6 +204,21 @@ class SystemUser(ProtocolMixin, BaseUser): def create_accounts_with_assets(cls, asset_ids, system_user_ids): pass + def get_manual_account(self, user_id, asset_id): + pass + + def get_auto_account(self, user_id, asset_id): + username = self.username + if self.username_same_with_user: + user = get_object_or_404(User, id=user_id) + username = user.username + + def get_account(self, user_id, asset_id): + if self.login_mode == self.LOGIN_AUTO: + return self.get_manual_account(user_id, asset_id) + else: + return self.get_auto_account(user_id, asset_id) + class Meta: ordering = ['name'] unique_together = [('name', 'org_id')] diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index e8658ec0f..5876c5030 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -49,6 +49,7 @@ urlpatterns = [ 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'), path('system-users//applications//auth-info/', api.SystemUserAppAuthInfoApi.as_view(), name='system-user-app-auth-info'), + path('system-users//assets//account/', api.SystemUserAssetAccountApi.as_view(), name='system-user-asset-account'), path('system-users//temp-auth/', api.SystemUserTempAuthInfoApi.as_view(), name='system-user-asset-temp-info'), path('system-users//tasks/', api.SystemUserTaskApi.as_view(), name='system-user-task-create'), path('system-users//cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'), From d176ccde4bc596f07624737a5aefd4869d264d7b Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 20 Jul 2022 16:52:01 +0800 Subject: [PATCH 3/8] perf: stash --- apps/assets/api/system_user.py | 5 +++-- apps/assets/models/user.py | 17 ++++++++++------- apps/assets/urls/api_urls.py | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index 5113cc30a..89d5d909b 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -82,9 +82,10 @@ class SystemUserAssetAccountApi(generics.RetrieveUpdateDestroyAPIView): def get_object(self): asset_id = self.kwargs.get('asset_id') - user_id = self.request.query_params.get("user_id") + user_id = self.kwargs.get("user_id") system_user = super().get_object() - return system_user.get_account(user_id, asset_id) + account = system_user.get_account(user_id, asset_id) + return account class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 0f6b51e0f..9e37b2fee 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -7,6 +7,8 @@ import logging from django.db import models from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator, MaxValueValidator +from django.shortcuts import get_object_or_404 +from django.core.cache import cache from common.utils import signer from users.models import User @@ -205,13 +207,20 @@ class SystemUser(ProtocolMixin, BaseUser): pass def get_manual_account(self, user_id, asset_id): - pass + cache_key = 'manual_account_{}_{}_{}'.format(self.id, user_id, asset_id) + return cache.get(cache_key) + + def create_manual_account(self, user_id, asset_id, account, ttl=300): + cache_key = 'manual_account_{}_{}_{}'.format(self.id, user_id, asset_id) + cache.set(cache_key, account, ttl) def get_auto_account(self, user_id, asset_id): + from .account import Account username = self.username if self.username_same_with_user: user = get_object_or_404(User, id=user_id) username = user.username + return get_object_or_404(Account, asset_id=asset_id, username=username) def get_account(self, user_id, asset_id): if self.login_mode == self.LOGIN_AUTO: @@ -228,12 +237,6 @@ class SystemUser(ProtocolMixin, BaseUser): ] -class SystemUserAccount(models.Model): - system_user = models.ForeignKey('SystemUser', on_delete=models.CASCADE, related_name='accounts') - account = models.ForeignKey('assets.Account', on_delete=models.CASCADE, related_name='system_users') - date_created = models.DateTimeField(auto_now_add=True) - - # Deprecated: 准备废弃 class AdminUser(BaseUser): """ diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 5876c5030..8f2d4c8cf 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -49,7 +49,7 @@ urlpatterns = [ 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'), path('system-users//applications//auth-info/', api.SystemUserAppAuthInfoApi.as_view(), name='system-user-app-auth-info'), - path('system-users//assets//account/', api.SystemUserAssetAccountApi.as_view(), name='system-user-asset-account'), + path('system-users//assets//users//account/', api.SystemUserAssetAccountApi.as_view(), name='system-user-asset-account'), path('system-users//temp-auth/', api.SystemUserTempAuthInfoApi.as_view(), name='system-user-asset-temp-info'), path('system-users//tasks/', api.SystemUserTaskApi.as_view(), name='system-user-task-create'), path('system-users//cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'), From 43d3791ddca1f0d447cbea045b1c0d76d0ec2ccf Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 27 Jul 2022 16:51:39 +0800 Subject: [PATCH 4/8] stash --- apps/assets/api/system_user.py | 55 +++++++++++++++++-- .../migrations/0092_auto_20220711_1409.py | 2 +- apps/assets/models/account.py | 8 +-- apps/assets/models/user.py | 2 +- apps/assets/serializers/system_user.py | 6 +- apps/assets/urls/api_urls.py | 4 +- 6 files changed, 61 insertions(+), 16 deletions(-) diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index 89d5d909b..14c02aa8d 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -2,6 +2,7 @@ from django.shortcuts import get_object_or_404 from rest_framework.response import Response from rest_framework.decorators import action +from rest_framework.viewsets import GenericViewSet from common.utils import get_logger, get_object_or_none from common.permissions import IsValidUser @@ -20,7 +21,8 @@ logger = get_logger(__file__) __all__ = [ 'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi', 'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi', 'SystemUserAssetsListView', - 'SystemUserTempAuthInfoApi', 'SystemUserAppAuthInfoApi', 'SystemUserAssetAccountApi' + 'SystemUserTempAuthInfoApi', 'SystemUserAppAuthInfoApi', 'SystemUserAssetAccountApi', + 'SystemUserAssetAccountSecretApi', ] @@ -76,24 +78,61 @@ class SystemUserViewSet(SuggestionMixin, OrgBulkModelViewSet): return Response(serializer.data) -class SystemUserAssetAccountApi(generics.RetrieveUpdateDestroyAPIView): +class SystemUserAccountViewSet(GenericViewSet): + model = Account + serializer_classes = { + 'default': serializers.AccountSerializer, + 'account_secret': serializers.AccountSecretSerializer, + } + + def get_object(self): + system_user_id = self.kwargs.get('pk') + asset_id = self.kwargs.get('asset_id') + user_id = self.kwargs.get("user_id") + system_user = SystemUser.objects.get(id=system_user_id) + account = system_user.get_account(user_id, asset_id) + return account + + @action(methods=['get'], detail=False, url_path='account') + def account(self, request, *args, **kwargs): + pass + + @action(methods=['get'], detail=False, url_path='account-secret') + def account_secret(self): + pass + + @action(methods=['put'], detail=False, url_path='manual-account') + def manual_account(self, request, *args, **kwargs): + pass + + +class SystemUserAssetAccountApi(generics.RetrieveAPIView): model = Account serializer_class = serializers.AccountSerializer def get_object(self): + system_user_id = self.kwargs.get('pk') asset_id = self.kwargs.get('asset_id') user_id = self.kwargs.get("user_id") - system_user = super().get_object() + system_user = SystemUser.objects.get(id=system_user_id) account = system_user.get_account(user_id, asset_id) return account +class SystemUserAssetAccountSecretApi(SystemUserAssetAccountApi): + model = Account + serializer_class = serializers.AccountSecretSerializer + rbac_perms = { + 'retrieve': 'assets.view_accountsecret' + } + + class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): """ Get system user auth info """ model = SystemUser - serializer_class = serializers.SystemUserWithAuthInfoSerializer + serializer_class = serializers.AccountSerializer rbac_perms = { 'retrieve': 'assets.view_systemusersecret', 'list': 'assets.view_systemusersecret', @@ -101,6 +140,14 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): 'destroy': 'assets.change_systemuser', } + def get_object(self): + system_user_id = self.kwargs.get('pk') + asset_id = self.kwargs.get('asset_id') + user_id = self.kwargs.get("user_id") + system_user = SystemUser.objects.get(id=system_user_id) + account = system_user.get_account(user_id, asset_id) + return account + def destroy(self, request, *args, **kwargs): instance = self.get_object() instance.clear_auth() diff --git a/apps/assets/migrations/0092_auto_20220711_1409.py b/apps/assets/migrations/0092_auto_20220711_1409.py index 8036cc09f..efcb59e9d 100644 --- a/apps/assets/migrations/0092_auto_20220711_1409.py +++ b/apps/assets/migrations/0092_auto_20220711_1409.py @@ -75,7 +75,7 @@ class Migration(migrations.Migration): ], options={ 'verbose_name': 'Account', - 'permissions': [('view_assetaccountsecret', 'Can view asset account secret'), ('change_assetaccountsecret', 'Can change asset account secret'), ('view_assethistoryaccount', 'Can view asset history account'), ('view_assethistoryaccountsecret', 'Can view asset history account secret')], + 'permissions': [('view_accountsecret', 'Can view asset account secret'), ('change_accountsecret', 'Can change asset account secret'), ('view_historyaccount', 'Can view asset history account'), ('view_historyaccountsecret', 'Can view asset history account secret')], 'unique_together': {('username', 'asset')}, }, bases=(models.Model, assets.models.base.AuthMixin, assets.models.user.ProtocolMixin), diff --git a/apps/assets/models/account.py b/apps/assets/models/account.py index 3c0588cdf..eb651965b 100644 --- a/apps/assets/models/account.py +++ b/apps/assets/models/account.py @@ -27,10 +27,10 @@ class Account(BaseUser, AbsConnectivity, ProtocolMixin): verbose_name = _('Account') unique_together = [('username', 'asset')] permissions = [ - ('view_assetaccountsecret', _('Can view asset account secret')), - ('change_assetaccountsecret', _('Can change asset account secret')), - ('view_assethistoryaccount', _('Can view asset history account')), - ('view_assethistoryaccountsecret', _('Can view asset history account secret')), + ('view_accountsecret', _('Can view asset account secret')), + ('change_accountsecret', _('Can change asset account secret')), + ('view_historyaccount', _('Can view asset history account')), + ('view_historyaccountsecret', _('Can view asset history account secret')), ] def __str__(self): diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 9e37b2fee..9770b2266 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -223,7 +223,7 @@ class SystemUser(ProtocolMixin, BaseUser): return get_object_or_404(Account, asset_id=asset_id, username=username) def get_account(self, user_id, asset_id): - if self.login_mode == self.LOGIN_AUTO: + if self.login_mode == self.LOGIN_MANUAL: return self.get_manual_account(user_id, asset_id) else: return self.get_auto_account(user_id, asset_id) diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index 7bdd13fe1..7d15041ec 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -298,10 +298,10 @@ class SystemUserAssetRelationSerializer(RelationMixin, serializers.ModelSerializ asset_display = serializers.ReadOnlyField(label=_('Asset hostname')) class Meta: - model = SystemUser + model = SystemUser.assets.through fields = [ - "id", "asset", "asset_display", 'systemuser', 'systemuser_display', - "connectivity", 'date_verified', 'org_id' + "id", "asset", "asset_display", + "systemuser", "systemuser_display", ] use_model_bulk_create = True model_bulk_create_kwargs = { diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 8f2d4c8cf..1c405a82a 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -45,12 +45,10 @@ urlpatterns = [ path('assets//perm-user-groups/', api.AssetPermUserGroupListApi.as_view(), name='asset-perm-user-group-list'), path('assets//perm-user-groups//permissions/', api.AssetPermUserGroupPermissionsListApi.as_view(), name='asset-perm-user-group-permission-list'), - 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'), path('system-users//applications//auth-info/', api.SystemUserAppAuthInfoApi.as_view(), name='system-user-app-auth-info'), path('system-users//assets//users//account/', api.SystemUserAssetAccountApi.as_view(), name='system-user-asset-account'), - path('system-users//temp-auth/', api.SystemUserTempAuthInfoApi.as_view(), name='system-user-asset-temp-info'), + path('system-users//assets//users//account-secret/', api.SystemUserAssetAccountSecretApi.as_view(), name='system-user-asset-account-secret'), path('system-users//tasks/', api.SystemUserTaskApi.as_view(), name='system-user-task-create'), path('system-users//cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'), path('cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='cmd-filter-rules'), From fb0fb71ea3f5bad16f4471ba38d79c245d69a0c2 Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 28 Jul 2022 18:50:58 +0800 Subject: [PATCH 5/8] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=94=A8=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/__init__.py | 2 - apps/assets/api/admin_user.py | 30 --- apps/assets/api/asset.py | 1 - apps/assets/api/system_user.py | 139 +--------- apps/assets/api/system_user_relation.py | 138 ---------- ...py => 0094_alter_systemuser_assets.py.bak} | 0 .../migrations/0094_auto_20220728_1125.py | 89 +++++++ ...1746.py => 0095_auto_20220713_1746.py.bak} | 0 ...1627.py => 0096_auto_20220714_1627.py.bak} | 0 apps/assets/models/__init__.py | 3 +- apps/assets/models/cluster.py | 40 --- apps/assets/models/protocol.py | 65 +++++ apps/assets/models/user.py | 199 +------------- apps/assets/serializers/__init__.py | 1 - apps/assets/serializers/admin_user.py | 26 -- apps/assets/serializers/asset.py | 14 +- apps/assets/serializers/system_user.py | 244 +----------------- apps/assets/signal_handlers/__init__.py | 3 +- .../{authbook.py => account.py} | 0 apps/assets/signal_handlers/common.py | 0 .../{system_user.py => system_user.py.bak} | 0 apps/assets/urls/api_urls.py | 7 - apps/audits/signal_handlers.py | 10 - .../api/application/user_permission/common.py | 7 +- .../api/asset/asset_permission_relation.py | 24 +- .../migrations/0029_auto_20220728_1728.py | 28 ++ apps/perms/models/asset_permission.py | 13 +- apps/perms/serializers/asset/permission.py | 21 +- .../serializers/asset/permission_relation.py | 11 - .../perms/signal_handlers/asset_permission.py | 138 +--------- apps/perms/urls/asset_permission.py | 1 - apps/perms/utils/asset/permission.py | 23 +- .../migrations/0018_auto_20220728_1125.py | 23 ++ utils/generate_fake_data/resources/perms.py | 7 - 34 files changed, 244 insertions(+), 1063 deletions(-) delete mode 100644 apps/assets/api/admin_user.py delete mode 100644 apps/assets/api/system_user_relation.py rename apps/assets/migrations/{0094_alter_systemuser_assets.py => 0094_alter_systemuser_assets.py.bak} (100%) create mode 100644 apps/assets/migrations/0094_auto_20220728_1125.py rename apps/assets/migrations/{0095_auto_20220713_1746.py => 0095_auto_20220713_1746.py.bak} (100%) rename apps/assets/migrations/{0096_auto_20220714_1627.py => 0096_auto_20220714_1627.py.bak} (100%) delete mode 100644 apps/assets/models/cluster.py create mode 100644 apps/assets/models/protocol.py delete mode 100644 apps/assets/serializers/admin_user.py rename apps/assets/signal_handlers/{authbook.py => account.py} (100%) delete mode 100644 apps/assets/signal_handlers/common.py rename apps/assets/signal_handlers/{system_user.py => system_user.py.bak} (100%) create mode 100644 apps/perms/migrations/0029_auto_20220728_1728.py create mode 100644 apps/tickets/migrations/0018_auto_20220728_1125.py diff --git a/apps/assets/api/__init__.py b/apps/assets/api/__init__.py index 3e95f59ab..e8b06b537 100644 --- a/apps/assets/api/__init__.py +++ b/apps/assets/api/__init__.py @@ -1,9 +1,7 @@ from .mixin import * -from .admin_user import * from .asset import * from .label import * from .system_user import * -from .system_user_relation import * from .accounts import * from .node import * from .domain import * diff --git a/apps/assets/api/admin_user.py b/apps/assets/api/admin_user.py deleted file mode 100644 index 1192599b9..000000000 --- a/apps/assets/api/admin_user.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.db.models import Count - -from orgs.mixins.api import OrgBulkModelViewSet -from common.utils import get_logger -from ..models import SystemUser -from .. import serializers -from rbac.permissions import RBACPermission - - -logger = get_logger(__file__) -__all__ = ['AdminUserViewSet'] - - -# 兼容一下老的 api -class AdminUserViewSet(OrgBulkModelViewSet): - """ - Admin user api set, for add,delete,update,list,retrieve resource - """ - model = SystemUser - filterset_fields = ("name", "username") - search_fields = filterset_fields - serializer_class = serializers.AdminUserSerializer - permission_classes = (RBACPermission,) - ordering_fields = ('name',) - ordering = ('name', ) - - def get_queryset(self): - queryset = super().get_queryset().filter(type=SystemUser.Type.admin) - queryset = queryset.annotate(assets_amount=Count('assets')) - return queryset diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index 3f4b7a209..a930bfd41 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -42,7 +42,6 @@ class AssetViewSet(SuggestionMixin, FilterAssetByNodeMixin, OrgBulkModelViewSet) filterset_fields = { 'hostname': ['exact'], 'ip': ['exact'], - 'system_users__id': ['exact'], 'platform__base': ['exact'], 'is_active': ['exact'], 'protocols': ['exact', 'icontains'] diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index 14c02aa8d..7ae682839 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -5,23 +5,17 @@ from rest_framework.decorators import action from rest_framework.viewsets import GenericViewSet from common.utils import get_logger, get_object_or_none -from common.permissions import IsValidUser from common.mixins.api import SuggestionMixin from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics -from orgs.utils import tmp_to_root_org from ..models import SystemUser, CommandFilterRule, Account from .. import serializers -from ..tasks import ( - push_system_user_to_assets_manual, test_system_user_connectivity_manual, - push_system_user_to_assets -) logger = get_logger(__file__) __all__ = [ - 'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi', - 'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi', 'SystemUserAssetsListView', - 'SystemUserTempAuthInfoApi', 'SystemUserAppAuthInfoApi', 'SystemUserAssetAccountApi', + 'SystemUserViewSet', 'SystemUserAuthInfoApi', + 'SystemUserCommandFilterRuleListApi', + 'SystemUserAssetAccountApi', 'SystemUserAssetAccountSecretApi', ] @@ -35,7 +29,6 @@ class SystemUserViewSet(SuggestionMixin, OrgBulkModelViewSet): 'name': ['exact'], 'username': ['exact'], 'protocol': ['exact', 'in'], - 'type': ['exact', 'in'], } search_fields = filterset_fields serializer_class = serializers.SystemUserSerializer @@ -154,116 +147,6 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): return Response(status=204) -class SystemUserTempAuthInfoApi(generics.CreateAPIView): - model = SystemUser - permission_classes = (IsValidUser,) - serializer_class = serializers.SystemUserTempAuthSerializer - - def create(self, request, *args, **kwargs): - serializer = super().get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - - pk = kwargs.get('pk') - data = serializer.validated_data - asset_or_app_id = data.get('instance_id') - - with tmp_to_root_org(): - instance = get_object_or_404(SystemUser, pk=pk) - instance.set_temp_auth(asset_or_app_id, self.request.user.id, data) - return Response(serializer.data, status=201) - - -class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView): - """ - Get system user with asset auth info - """ - model = SystemUser - serializer_class = serializers.SystemUserWithAuthInfoSerializer - - def get_object(self): - instance = super().get_object() - asset_id = self.kwargs.get('asset_id') - user_id = self.request.query_params.get("user_id") - username = self.request.query_params.get("username") - instance.load_asset_more_auth(asset_id, username, user_id) - return instance - - -class SystemUserAppAuthInfoApi(generics.RetrieveAPIView): - """ - Get system user with asset auth info - """ - model = SystemUser - serializer_class = serializers.SystemUserWithAuthInfoSerializer - rbac_perms = { - 'retrieve': 'assets.view_systemusersecret', - } - - def get_object(self): - instance = super().get_object() - app_id = self.kwargs.get('app_id') - user_id = self.request.query_params.get("user_id") - username = self.request.query_params.get("username") - instance.load_app_more_auth(app_id, username, user_id) - return instance - - -class SystemUserTaskApi(generics.CreateAPIView): - serializer_class = serializers.SystemUserTaskSerializer - - def do_push(self, system_user, asset_ids=None): - if asset_ids is None: - task = push_system_user_to_assets_manual.delay(system_user) - else: - username = self.request.query_params.get('username') - task = push_system_user_to_assets.delay( - system_user.id, asset_ids, username=username - ) - return task - - @staticmethod - def do_test(system_user, asset_ids): - task = test_system_user_connectivity_manual.delay(system_user, asset_ids) - return task - - def get_object(self): - pk = self.kwargs.get('pk') - return get_object_or_404(SystemUser, pk=pk) - - def check_permissions(self, request): - action = request.data.get('action') - action_perm_require = { - 'push': 'assets.push_assetsystemuser', - 'test': 'assets.test_assetconnectivity' - } - perm_required = action_perm_require.get(action) - has = self.request.user.has_perm(perm_required) - - if not has: - self.permission_denied(request) - - def perform_create(self, serializer): - action = serializer.validated_data["action"] - asset = serializer.validated_data.get('asset') - - if asset: - assets = [asset] - else: - assets = serializer.validated_data.get('assets') or [] - - asset_ids = [asset.id for asset in assets] - asset_ids = asset_ids if asset_ids else None - - system_user = self.get_object() - if action == 'push': - task = self.do_push(system_user, asset_ids) - else: - task = self.do_test(system_user, asset_ids) - data = getattr(serializer, '_data', {}) - data["task"] = task.id - setattr(serializer, '_data', data) - - class SystemUserCommandFilterRuleListApi(generics.ListAPIView): rbac_perms = { 'list': 'assets.view_commandfilterule' @@ -291,19 +174,3 @@ class SystemUserCommandFilterRuleListApi(generics.ListAPIView): ) return rules - -class SystemUserAssetsListView(generics.ListAPIView): - serializer_class = serializers.AssetSimpleSerializer - filterset_fields = ("hostname", "ip") - search_fields = filterset_fields - rbac_perms = { - 'list': 'assets.view_asset' - } - - def get_object(self): - pk = self.kwargs.get('pk') - return get_object_or_404(SystemUser, pk=pk) - - def get_queryset(self): - system_user = self.get_object() - return system_user.get_all_assets() diff --git a/apps/assets/api/system_user_relation.py b/apps/assets/api/system_user_relation.py deleted file mode 100644 index 36c16a09b..000000000 --- a/apps/assets/api/system_user_relation.py +++ /dev/null @@ -1,138 +0,0 @@ -# -*- coding: utf-8 -*- -# -from collections import defaultdict -from django.db.models import F, Value, Model -from django.db.models.signals import m2m_changed -from django.db.models.functions import Concat - -from common.utils import get_logger -from orgs.mixins.api import OrgBulkModelViewSet -from orgs.utils import current_org -from .. import models, serializers - -__all__ = [ - 'SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet', - 'SystemUserUserRelationViewSet', 'BaseRelationViewSet', -] - -logger = get_logger(__name__) - - -class RelationMixin: - model: Model - - def get_queryset(self): - queryset = self.model.objects.all() - if not current_org.is_root(): - org_id = current_org.org_id() - queryset = queryset.filter(systemuser__org_id=org_id) - - queryset = queryset.annotate(systemuser_display=Concat( - F('systemuser__name'), Value('('), - F('systemuser__username'), Value(')') - )) - return queryset - - def send_post_add_signal(self, instance): - if not isinstance(instance, list): - instance = [instance] - - system_users_objects_map = defaultdict(list) - model, object_field = self.get_objects_attr() - - for i in instance: - _id = getattr(i, object_field).id - system_users_objects_map[i.systemuser].append(_id) - - sender = self.get_sender() - 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=set(object_ids) - ) - - def get_sender(self): - return self.model - - def get_objects_attr(self): - return models.Asset, 'asset' - - def perform_create(self, serializer): - instance = serializer.save() - self.send_post_add_signal(instance) - - -class BaseRelationViewSet(RelationMixin, OrgBulkModelViewSet): - perm_model = models.SystemUser - - -class SystemUserAssetRelationViewSet(BaseRelationViewSet): - serializer_class = serializers.SystemUserAssetRelationSerializer - model = models.SystemUser.assets.through - filterset_fields = [ - 'id', 'asset', 'systemuser', - ] - search_fields = [ - "id", "asset__hostname", "asset__ip", - "systemuser__name", "systemuser__username", - ] - - def get_objects_attr(self): - return models.Asset, 'asset' - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate( - asset_display=Concat( - F('asset__hostname'), Value('('), - F('asset__ip'), Value(')') - ) - ) - return queryset - - -class SystemUserNodeRelationViewSet(BaseRelationViewSet): - serializer_class = serializers.SystemUserNodeRelationSerializer - model = models.SystemUser.nodes.through - filterset_fields = [ - 'id', 'node', 'systemuser', - ] - search_fields = [ - "node__value", "systemuser__name", "systemuser__username" - ] - - def get_objects_attr(self): - return models.Node, 'node' - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset \ - .annotate(node_key=F('node__key')) - return queryset - - -class SystemUserUserRelationViewSet(BaseRelationViewSet): - serializer_class = serializers.SystemUserUserRelationSerializer - model = models.SystemUser.users.through - filterset_fields = [ - 'id', 'user', 'systemuser', - ] - search_fields = [ - "user__username", "user__name", - "systemuser__name", "systemuser__username", - ] - - def get_objects_attr(self): - from users.models import User - return User, 'user' - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate( - user_display=Concat( - F('user__name'), Value('('), - F('user__username'), Value(')') - ) - ) - return queryset diff --git a/apps/assets/migrations/0094_alter_systemuser_assets.py b/apps/assets/migrations/0094_alter_systemuser_assets.py.bak similarity index 100% rename from apps/assets/migrations/0094_alter_systemuser_assets.py rename to apps/assets/migrations/0094_alter_systemuser_assets.py.bak diff --git a/apps/assets/migrations/0094_auto_20220728_1125.py b/apps/assets/migrations/0094_auto_20220728_1125.py new file mode 100644 index 000000000..adc477c1c --- /dev/null +++ b/apps/assets/migrations/0094_auto_20220728_1125.py @@ -0,0 +1,89 @@ +# Generated by Django 3.2.14 on 2022-07-28 03:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0093_auto_20220711_1413'), + ] + + operations = [ + migrations.RemoveField( + model_name='cluster', + name='admin_user', + ), + migrations.RemoveField( + model_name='systemuser', + name='ad_domain', + ), + migrations.RemoveField( + model_name='systemuser', + name='assets', + ), + migrations.RemoveField( + model_name='systemuser', + name='auto_push', + ), + migrations.RemoveField( + model_name='systemuser', + name='groups', + ), + migrations.RemoveField( + model_name='systemuser', + name='home', + ), + migrations.RemoveField( + model_name='systemuser', + name='nodes', + ), + migrations.RemoveField( + model_name='systemuser', + name='priority', + ), + migrations.RemoveField( + model_name='systemuser', + name='sftp_root', + ), + migrations.RemoveField( + model_name='systemuser', + name='shell', + ), + migrations.RemoveField( + model_name='systemuser', + name='sudo', + ), + migrations.RemoveField( + model_name='systemuser', + name='system_groups', + ), + migrations.RemoveField( + model_name='systemuser', + name='token', + ), + migrations.RemoveField( + model_name='systemuser', + name='type', + ), + migrations.RemoveField( + model_name='systemuser', + name='users', + ), + migrations.AlterField( + model_name='historicalaccount', + name='version', + field=models.IntegerField(default=0, verbose_name='Version'), + ), + migrations.AlterField( + model_name='systemuser', + name='login_mode', + field=models.CharField(choices=[('auto', '使用账号'), ('manual', 'Manually input')], default='auto', max_length=10, verbose_name='Login mode'), + ), + migrations.DeleteModel( + name='AdminUser', + ), + migrations.DeleteModel( + name='Cluster', + ), + ] diff --git a/apps/assets/migrations/0095_auto_20220713_1746.py b/apps/assets/migrations/0095_auto_20220713_1746.py.bak similarity index 100% rename from apps/assets/migrations/0095_auto_20220713_1746.py rename to apps/assets/migrations/0095_auto_20220713_1746.py.bak diff --git a/apps/assets/migrations/0096_auto_20220714_1627.py b/apps/assets/migrations/0096_auto_20220714_1627.py.bak similarity index 100% rename from apps/assets/migrations/0096_auto_20220714_1627.py rename to apps/assets/migrations/0096_auto_20220714_1627.py.bak diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index 9d1df04a1..da35178d1 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 .cluster import * from .group import * from .domain import * from .node import * @@ -12,5 +11,5 @@ from .utils import * from .authbook import * from .gathered_user import * from .favorite_asset import * -from .backup import * from .account import * +from .backup import * diff --git a/apps/assets/models/cluster.py b/apps/assets/models/cluster.py deleted file mode 100644 index 6c0692ab9..000000000 --- a/apps/assets/models/cluster.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# - -import logging -import uuid - -from django.db import models -from django.utils.translation import ugettext_lazy as _ - - -__all__ = ['Cluster'] -logger = logging.getLogger(__name__) - - -class Cluster(models.Model): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - name = models.CharField(max_length=32, verbose_name=_('Name')) - admin_user = models.ForeignKey('assets.AdminUser', null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("Admin user")) - bandwidth = models.CharField(max_length=32, blank=True, verbose_name=_('Bandwidth')) - contact = models.CharField(max_length=128, blank=True, verbose_name=_('Contact')) - phone = models.CharField(max_length=32, blank=True, verbose_name=_('Phone')) - address = models.CharField(max_length=128, blank=True, verbose_name=_("Address")) - intranet = models.TextField(blank=True, verbose_name=_('Intranet')) - extranet = models.TextField(blank=True, verbose_name=_('Extranet')) - date_created = models.DateTimeField(auto_now_add=True, null=True, verbose_name=_('Date created')) - operator = models.CharField(max_length=32, blank=True, verbose_name=_('Operator')) - created_by = models.CharField(max_length=32, blank=True, verbose_name=_('Created by')) - comment = models.TextField(blank=True, verbose_name=_('Comment')) - - def __str__(self): - return self.name - - @classmethod - def initial(cls): - return cls.objects.get_or_create(name=_('Default'), created_by=_('System'), comment=_('Default Cluster'))[0] - - class Meta: - ordering = ['name'] - verbose_name = _("Cluster") diff --git a/apps/assets/models/protocol.py b/apps/assets/models/protocol.py new file mode 100644 index 000000000..cd27e7b19 --- /dev/null +++ b/apps/assets/models/protocol.py @@ -0,0 +1,65 @@ +from django.db import models + + +class ProtocolMixin: + protocol: str + + class Protocol(models.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' + sqlserver = 'sqlserver', 'SQLServer' + redis = 'redis', 'Redis' + mongodb = 'mongodb', 'MongoDB' + 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.mariadb, Protocol.oracle, + Protocol.postgresql, Protocol.sqlserver, + Protocol.redis, Protocol.mongodb + ] + 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 AppType + if app_type in cls.APPLICATION_CATEGORY_PROTOCOLS: + protocol = app_type + elif app_type in AppType.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 + + @property + def is_asset_protocol(self): + return self.protocol in self.ASSET_CATEGORY_PROTOCOLS + diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 9770b2266..a5be417a2 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -6,83 +6,17 @@ import logging from django.db import models from django.utils.translation import ugettext_lazy as _ -from django.core.validators import MinValueValidator, MaxValueValidator from django.shortcuts import get_object_or_404 from django.core.cache import cache -from common.utils import signer -from users.models import User from .base import BaseUser -from .asset import Asset +from .protocol import ProtocolMixin -__all__ = ['AdminUser', 'SystemUser', 'ProtocolMixin'] +__all__ = ['SystemUser'] logger = logging.getLogger(__name__) -class ProtocolMixin: - protocol: str - - class Protocol(models.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' - sqlserver = 'sqlserver', 'SQLServer' - redis = 'redis', 'Redis' - mongodb = 'mongodb', 'MongoDB' - 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.mariadb, Protocol.oracle, - Protocol.postgresql, Protocol.sqlserver, - Protocol.redis, Protocol.mongodb - ] - 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 AppType - if app_type in cls.APPLICATION_CATEGORY_PROTOCOLS: - protocol = app_type - elif app_type in AppType.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 - - @property - def is_asset_protocol(self): - return self.protocol in self.ASSET_CATEGORY_PROTOCOLS - - class SystemUser(ProtocolMixin, BaseUser): LOGIN_AUTO = 'auto' LOGIN_MANUAL = 'manual' @@ -91,39 +25,10 @@ class SystemUser(ProtocolMixin, BaseUser): (LOGIN_MANUAL, _('Manually input')) ) - class Type(models.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"), - 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")) - 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')) login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode')) - # Todo: 重构平台后或许这里也得变化 - # 账号模版 - account_template_enabled = models.BooleanField(default=False, verbose_name=_("启用账号模版")) - auto_push_account = models.BooleanField(default=True, verbose_name=_('自动推送账号')) - type = models.CharField(max_length=16, choices=Type.choices, default=Type.common, verbose_name=_('Type')) - sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo')) - shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell')) - 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) - # linux su 命令 (switch user) # Todo: 修改为 username, 不必系统用户了 su_enabled = models.BooleanField(default=False, verbose_name=_('User switch')) @@ -135,32 +40,6 @@ class SystemUser(ProtocolMixin, BaseUser): username = '*' 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_account 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 @@ -178,30 +57,6 @@ class SystemUser(ProtocolMixin, BaseUser): 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 add_related_assets(self, assets_or_ids): - self.assets.add(*tuple(assets_or_ids)) - self.add_related_assets_to_su_from_if_need(assets_or_ids) - - def add_related_assets_to_su_from_if_need(self, assets_or_ids): - if self.protocol not in [self.Protocol.ssh.value]: - return - if not self.su_enabled: - return - if not self.su_from: - return - if self.su_from.protocol != self.protocol: - return - self.su_from.assets.add(*tuple(assets_or_ids)) - @classmethod def create_accounts_with_assets(cls, asset_ids, system_user_ids): pass @@ -216,6 +71,7 @@ class SystemUser(ProtocolMixin, BaseUser): def get_auto_account(self, user_id, asset_id): from .account import Account + from users.models import User username = self.username if self.username_same_with_user: user = get_object_or_404(User, id=user_id) @@ -235,52 +91,3 @@ class SystemUser(ProtocolMixin, BaseUser): permissions = [ ('match_systemuser', _('Can match system user')), ] - - -# Deprecated: 准备废弃 -class AdminUser(BaseUser): - """ - A privileged user that ansible can use it to push system user and so on - """ - BECOME_METHOD_CHOICES = ( - ('sudo', 'sudo'), - ('su', 'su'), - ) - become = models.BooleanField(default=True) - become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4) - become_user = models.CharField(default='root', max_length=64) - _become_pass = models.CharField(default='', blank=True, max_length=128) - CONNECTIVITY_CACHE_KEY = '_ADMIN_USER_CONNECTIVE_{}' - _prefer = "admin_user" - - def __str__(self): - return self.name - - @property - def become_pass(self): - password = signer.unsign(self._become_pass) - if password: - return password - else: - return "" - - @become_pass.setter - def become_pass(self, password): - self._become_pass = signer.sign(password) - - @property - def become_info(self): - if self.become: - info = { - "method": self.become_method, - "user": self.become_user, - "pass": self.become_pass, - } - else: - info = None - return info - - class Meta: - ordering = ['name'] - unique_together = [('name', 'org_id')] - verbose_name = _("Admin user") diff --git a/apps/assets/serializers/__init__.py b/apps/assets/serializers/__init__.py index 3f1222fc5..5c684652c 100644 --- a/apps/assets/serializers/__init__.py +++ b/apps/assets/serializers/__init__.py @@ -2,7 +2,6 @@ # from .asset import * -from .admin_user import * from .label import * from .system_user import * from .node import * diff --git a/apps/assets/serializers/admin_user.py b/apps/assets/serializers/admin_user.py deleted file mode 100644 index b6ab18af3..000000000 --- a/apps/assets/serializers/admin_user.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -# -from ..models import SystemUser -from .system_user import SystemUserSerializer as SuS - - -class AdminUserSerializer(SuS): - """ - 管理用户 - """ - - class Meta(SuS.Meta): - fields = SuS.Meta.fields_mini + \ - SuS.Meta.fields_write_only + \ - SuS.Meta.fields_m2m + \ - [ - 'type', 'protocol', "priority", 'sftp_root', 'ssh_key_fingerprint', - 'su_enabled', 'su_from', - 'date_created', 'date_updated', 'comment', 'created_by', - ] - - def validate_type(self, val): - return SystemUser.Type.admin - - def validate_protocol(self, val): - return 'ssh' diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index da1a0a0fc..40e845e15 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -91,7 +91,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer): 'cpu_info', 'hardware_info', ] fields_fk = [ - 'domain', 'domain_display', 'platform', 'admin_user', 'admin_user_display' + 'domain', 'domain_display', 'platform', ] fields_m2m = [ 'nodes', 'nodes_display', 'labels', 'labels_display', 'accounts' @@ -114,19 +114,10 @@ class AssetSerializer(BulkOrgResourceModelSerializer): self.accounts_data = data.pop('accounts', []) super().__init__(*args, **kwargs) - 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('domain', 'platform', 'admin_user') + queryset = queryset.prefetch_related('domain', 'platform') queryset = queryset.prefetch_related('nodes', 'labels') return queryset @@ -162,7 +153,6 @@ class AssetSerializer(BulkOrgResourceModelSerializer): def add_accounts(instance, accounts_data): for data in accounts_data: data['asset'] = instance.id - print("Data: ", accounts_data) serializer = AccountSerializer(data=accounts_data, many=True) try: serializer.is_valid(raise_exception=True) diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index 7d15041ec..dc1133e73 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -1,97 +1,38 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from django.db.models import Count -from common.mixins.serializers import BulkSerializerMixin -from common.utils import ssh_pubkey_gen -from common.drf.fields import EncryptedField -from common.drf.serializers import SecretReadableMixin from common.validators import alphanumeric_re, alphanumeric_cn_re, alphanumeric_win_re from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from ..models import SystemUser, Asset -from .utils import validate_password_for_ansible -from .base import AuthSerializerMixin +from ..models import SystemUser __all__ = [ 'SystemUserSerializer', 'MiniSystemUserSerializer', - 'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer', - 'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer', - 'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer', - 'SystemUserTempAuthSerializer', 'RelationMixin', + 'SystemUserSimpleSerializer', ] -class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): +class SystemUserSerializer(BulkOrgResourceModelSerializer): """ 系统用户 """ - password = EncryptedField( - label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024, - trim_whitespace=False, validators=[validate_password_for_ansible], - write_only=True - ) - auto_generate_key = serializers.BooleanField(initial=True, required=False, write_only=True) - type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display')) - ssh_key_fingerprint = serializers.ReadOnlyField(label=_('SSH key fingerprint')) - token = EncryptedField( - label=_('Token'), required=False, write_only=True, style={'base_template': 'textarea.html'} - ) - applications_amount = serializers.IntegerField( - source='apps_amount', read_only=True, label=_('Apps amount') - ) class Meta: model = SystemUser - fields_mini = ['id', 'name', 'username'] - fields_write_only = ['password', 'public_key', 'private_key', 'passphrase'] - fields_small = fields_mini + fields_write_only + [ - 'token', 'ssh_key_fingerprint', - 'type', 'type_display', 'protocol', 'is_asset_protocol', - 'account_template_enabled', 'login_mode', 'login_mode_display', 'priority', - 'sudo', 'shell', 'sftp_root', 'home', 'system_groups', 'ad_domain', - 'username_same_with_user', 'auto_push_account', 'auto_generate_key', - 'su_enabled', 'su_from', - 'date_created', 'date_updated', 'comment', 'created_by', + fields_mini = ['id', 'name', 'username', 'protocol'] + fields_small = fields_mini + [ + 'login_mode', 'su_enabled', 'su_from', + 'date_created', 'date_updated', 'comment', + 'created_by', ] - fields_m2m = ['cmd_filters', 'assets_amount', 'applications_amount', 'nodes'] - fields = fields_small + fields_m2m + fields = fields_small extra_kwargs = { 'cmd_filters': {"required": False, 'label': _('Command filter')}, - 'public_key': {"write_only": True}, - 'private_key': {"write_only": True}, - 'nodes_amount': {'label': _('Nodes amount')}, - 'assets_amount': {'label': _('Assets amount')}, 'login_mode_display': {'label': _('Login mode display')}, 'created_by': {'read_only': True}, 'ad_domain': {'required': False, 'allow_blank': True, 'label': _('Ad domain')}, - 'is_asset_protocol': {'label': _('Is asset protocol')}, 'su_from': {'help_text': _('Only ssh and automatic login system users are supported')} } - def validate_auto_push(self, value): - login_mode = self.get_initial_value("login_mode") - protocol = self.get_initial_value("protocol") - - if login_mode == SystemUser.LOGIN_MANUAL: - value = False - elif protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS: - value = False - return value - - def validate_auto_generate_key(self, value): - login_mode = self.get_initial_value("login_mode") - protocol = self.get_initial_value("protocol") - - if self.context["request"].method.lower() != "post": - value = False - elif self.instance: - value = False - elif login_mode == SystemUser.LOGIN_MANUAL: - value = False - elif protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS: - value = False - return value - def validate_username_same_with_user(self, username_same_with_user): if not username_same_with_user: return username_same_with_user @@ -132,12 +73,6 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): raise serializers.ValidationError(msg) return username - def validate_home(self, home): - username_same_with_user = self.get_initial_value("username_same_with_user") - if username_same_with_user: - return '' - return home - @staticmethod def validate_sftp_root(value): if value in ['home', 'tmp']: @@ -147,17 +82,6 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): raise serializers.ValidationError(error) return value - def validate_password(self, password): - super().validate_password(password) - auto_gen_key = self.get_initial_value('auto_generate_key', False) - private_key = self.get_initial_value('private_key') - login_mode = self.get_initial_value('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_su_from(self, su_from: SystemUser): # self: su enabled su_enabled = self.get_initial_value('su_enabled', default=False) @@ -181,70 +105,6 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): raise serializers.ValidationError(error) return su_from - def _validate_admin_user(self, attrs): - if self.instance: - tp = self.instance.type - else: - 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_account'] = False - return 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.SUPPORT_PUSH_PROTOCOLS: - return attrs - - # 自动生成 - if auto_gen_key and not self.instance: - password = SystemUser.gen_password() - attrs['password'] = password - 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则生成 - elif attrs.get('private_key'): - private_key = attrs['private_key'] - password = attrs.get('password') - public_key = ssh_pubkey_gen(private_key, password=password, username=username) - attrs['public_key'] = public_key - return attrs - - def _validate_login_mode(self, attrs): - if 'login_mode' in attrs: - login_mode = attrs['login_mode'] - else: - login_mode = self.instance.login_mode if self.instance else SystemUser.LOGIN_AUTO - - if login_mode == SystemUser.LOGIN_MANUAL: - attrs['password'] = '' - attrs['private_key'] = '' - attrs['public_key'] = '' - - return attrs - - def validate(self, attrs): - attrs = self._validate_admin_user(attrs) - attrs = self._validate_gen_key(attrs) - attrs = self._validate_login_mode(attrs) - return attrs - - @classmethod - def setup_eager_loading(cls, queryset): - """ Perform necessary eager loading of data. """ - queryset = queryset \ - .annotate(assets_amount=Count("assets")) \ - .prefetch_related('nodes', 'cmd_filters') - return queryset - class MiniSystemUserSerializer(serializers.ModelSerializer): class Meta: @@ -252,28 +112,6 @@ class MiniSystemUserSerializer(serializers.ModelSerializer): fields = SystemUserSerializer.Meta.fields_mini -class SystemUserWithAuthInfoSerializer(SecretReadableMixin, SystemUserSerializer): - class Meta(SystemUserSerializer.Meta): - 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', 'ad_domain', 'sftp_root', 'token', - "username_same_with_user", 'auto_push_account', 'auto_generate_key', - 'comment', - ] - fields = fields_small - extra_kwargs = { - 'nodes_amount': {'label': _('Node')}, - 'assets_amount': {'label': _('Asset')}, - 'login_mode_display': {'label': _('Login mode display')}, - 'created_by': {'read_only': True}, - 'password': {'write_only': False}, - 'private_key': {'write_only': False}, - 'token': {'write_only': False} - } - - class SystemUserSimpleSerializer(serializers.ModelSerializer): """ 系统用户最基本信息的数据结构 @@ -284,70 +122,6 @@ class SystemUserSimpleSerializer(serializers.ModelSerializer): fields = ('id', 'name', 'username') -class RelationMixin(BulkSerializerMixin, serializers.Serializer): - systemuser_display = serializers.ReadOnlyField(label=_("System user name")) - org_name = serializers.ReadOnlyField(label=_("Org name")) - - def get_field_names(self, declared_fields, info): - fields = super().get_field_names(declared_fields, info) - fields.extend(['systemuser', "systemuser_display", "org_name"]) - return fields - - -class SystemUserAssetRelationSerializer(RelationMixin, serializers.ModelSerializer): - asset_display = serializers.ReadOnlyField(label=_('Asset hostname')) - - class Meta: - model = SystemUser.assets.through - fields = [ - "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: - model = SystemUser.nodes.through - fields = [ - 'id', 'node', "node_display", - ] - - def get_node_display(self, obj): - return obj.node.full_value - - -class SystemUserUserRelationSerializer(RelationMixin, serializers.ModelSerializer): - user_display = serializers.ReadOnlyField() - - class Meta: - model = SystemUser.users.through - fields = [ - 'id', "user", "user_display", - ] - - -class SystemUserTaskSerializer(serializers.Serializer): - ACTION_CHOICES = ( - ("test", "test"), - ("push", "push"), - ) - action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True) - asset = serializers.PrimaryKeyRelatedField( - queryset=Asset.objects, allow_null=True, required=False, write_only=True - ) - assets = serializers.PrimaryKeyRelatedField( - queryset=Asset.objects, allow_null=True, required=False, write_only=True, - many=True - ) - task = serializers.CharField(read_only=True) - - class SystemUserTempAuthSerializer(SystemUserSerializer): instance_id = serializers.CharField() diff --git a/apps/assets/signal_handlers/__init__.py b/apps/assets/signal_handlers/__init__.py index 8a895544f..b337df001 100644 --- a/apps/assets/signal_handlers/__init__.py +++ b/apps/assets/signal_handlers/__init__.py @@ -1,5 +1,4 @@ from .asset import * -from .system_user import * -from .authbook import * +from .account import * from .node_assets_amount import * from .node_assets_mapping import * diff --git a/apps/assets/signal_handlers/authbook.py b/apps/assets/signal_handlers/account.py similarity index 100% rename from apps/assets/signal_handlers/authbook.py rename to apps/assets/signal_handlers/account.py diff --git a/apps/assets/signal_handlers/common.py b/apps/assets/signal_handlers/common.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/assets/signal_handlers/system_user.py b/apps/assets/signal_handlers/system_user.py.bak similarity index 100% rename from apps/assets/signal_handlers/system_user.py rename to apps/assets/signal_handlers/system_user.py.bak diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 1c405a82a..f3f192a3d 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -17,7 +17,6 @@ router.register(r'accounts-history', api.AccountHistoryViewSet, 'account-history router.register(r'account-history-secrets', api.AccountHistorySecretsViewSet, 'account-history-secret') router.register(r'platforms', api.AssetPlatformViewSet, 'platform') 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') @@ -25,9 +24,6 @@ router.register(r'gateways', api.GatewayViewSet, 'gateway') router.register(r'cmd-filters', api.CommandFilterViewSet, 'cmd-filter') router.register(r'gathered-users', api.GatheredUserViewSet, 'gathered-user') router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset') -router.register(r'system-users-assets-relations', api.SystemUserAssetRelationViewSet, 'system-users-assets-relation') -router.register(r'system-users-nodes-relations', api.SystemUserNodeRelationViewSet, 'system-users-nodes-relation') -router.register(r'system-users-users-relations', api.SystemUserUserRelationViewSet, 'system-users-users-relation') router.register(r'account-backup-plans', api.AccountBackupPlanViewSet, 'account-backup') router.register(r'account-backup-plan-executions', api.AccountBackupPlanExecutionViewSet, 'account-backup-execution') @@ -45,11 +41,8 @@ urlpatterns = [ path('assets//perm-user-groups/', api.AssetPermUserGroupListApi.as_view(), name='asset-perm-user-group-list'), path('assets//perm-user-groups//permissions/', api.AssetPermUserGroupPermissionsListApi.as_view(), name='asset-perm-user-group-permission-list'), - path('system-users//assets/', api.SystemUserAssetsListView.as_view(), name='system-user-assets'), - path('system-users//applications//auth-info/', api.SystemUserAppAuthInfoApi.as_view(), name='system-user-app-auth-info'), path('system-users//assets//users//account/', api.SystemUserAssetAccountApi.as_view(), name='system-user-asset-account'), path('system-users//assets//users//account-secret/', api.SystemUserAssetAccountSecretApi.as_view(), name='system-user-asset-account-secret'), - path('system-users//tasks/', api.SystemUserTaskApi.as_view(), name='system-user-task-create'), path('system-users//cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'), path('cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='cmd-filter-rules'), diff --git a/apps/audits/signal_handlers.py b/apps/audits/signal_handlers.py index 9ac565bc6..af6959f48 100644 --- a/apps/audits/signal_handlers.py +++ b/apps/audits/signal_handlers.py @@ -69,11 +69,6 @@ M2M_NEED_RECORD = { _('{User} JOINED {UserGroup}'), _('{User} LEFT {UserGroup}') ), - SystemUser.assets.through._meta.object_name: ( - _('Asset and SystemUser'), - _('{Asset} ADD {SystemUser}'), - _('{Asset} REMOVE {SystemUser}') - ), Asset.nodes.through._meta.object_name: ( _('Node and Asset'), _('{Node} ADD {Asset}'), @@ -99,11 +94,6 @@ M2M_NEED_RECORD = { _('{AssetPermission} ADD {Node}'), _('{AssetPermission} REMOVE {Node}'), ), - AssetPermission.system_users.through._meta.object_name: ( - _('Asset permission and SystemUser'), - _('{AssetPermission} ADD {SystemUser}'), - _('{AssetPermission} REMOVE {SystemUser}'), - ), ApplicationPermission.users.through._meta.object_name: ( _('User application permissions'), _('{ApplicationPermission} ADD {User}'), diff --git a/apps/perms/api/application/user_permission/common.py b/apps/perms/api/application/user_permission/common.py index 34fcc4dd1..d4c68662a 100644 --- a/apps/perms/api/application/user_permission/common.py +++ b/apps/perms/api/application/user_permission/common.py @@ -62,20 +62,19 @@ class ValidateUserApplicationPermissionApi(APIView): def get(self, request, *args, **kwargs): user_id = request.query_params.get('user_id', '') application_id = request.query_params.get('application_id', '') - system_user_id = request.query_params.get('system_user_id', '') + account = system_user_id = request.query_params.get('account', '') data = { 'has_permission': False, 'expire_at': int(time.time()), 'actions': [] } - if not all((user_id, application_id, system_user_id)): + if not all((user_id, application_id, account)): return Response(data) user = User.objects.get(id=user_id) application = Application.objects.get(id=application_id) - system_user = SystemUser.objects.get(id=system_user_id) - has_perm, actions, expire_at = validate_permission(user, application, system_user) + has_perm, actions, expire_at = validate_permission(user, application, account) status_code = status.HTTP_200_OK if has_perm else status.HTTP_403_FORBIDDEN data = { 'has_permission': has_perm, diff --git a/apps/perms/api/asset/asset_permission_relation.py b/apps/perms/api/asset/asset_permission_relation.py index b0a67f858..16469695b 100644 --- a/apps/perms/api/asset/asset_permission_relation.py +++ b/apps/perms/api/asset/asset_permission_relation.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- # from rest_framework import generics -from django.db.models import F, Value -from django.db.models.functions import Concat +from django.db.models import F from django.shortcuts import get_object_or_404 from orgs.mixins.api import OrgRelationMixin @@ -15,8 +14,7 @@ from perms.utils.asset.user_permission import UserGrantedAssetsQueryUtils __all__ = [ 'AssetPermissionUserRelationViewSet', 'AssetPermissionUserGroupRelationViewSet', 'AssetPermissionAssetRelationViewSet', 'AssetPermissionNodeRelationViewSet', - 'AssetPermissionSystemUserRelationViewSet', 'AssetPermissionAllAssetListApi', - 'AssetPermissionAllUserListApi', + 'AssetPermissionAllAssetListApi', 'AssetPermissionAllUserListApi', ] @@ -117,21 +115,3 @@ class AssetPermissionNodeRelationViewSet(RelationMixin): .annotate(node_key=F('node__key')) return queryset - -class AssetPermissionSystemUserRelationViewSet(RelationMixin): - serializer_class = serializers.AssetPermissionSystemUserRelationSerializer - m2m_field = models.AssetPermission.system_users.field - filterset_fields = [ - 'id', 'systemuser', 'assetpermission', - ] - search_fields = [ - "assetpermission__name", "systemuser__name", "systemuser__username" - ] - - def get_queryset(self): - queryset = super().get_queryset() - queryset = queryset.annotate( - systemuser_display=Concat( - F('systemuser__name'), Value('('), F('systemuser__username'), Value(')') - )) - return queryset diff --git a/apps/perms/migrations/0029_auto_20220728_1728.py b/apps/perms/migrations/0029_auto_20220728_1728.py new file mode 100644 index 000000000..6f64dfbbd --- /dev/null +++ b/apps/perms/migrations/0029_auto_20220728_1728.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.14 on 2022-07-28 09:10 + +from django.db import migrations, models + + +def migrate_system_user_to_accounts(apps, schema_editor): + # Todo: 迁移 系统用户为账号 + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('perms', '0028_auto_20220316_2028'), + ] + + operations = [ + migrations.AddField( + model_name='assetpermission', + name='accounts', + field=models.JSONField(default=list, verbose_name='Accounts'), + ), + migrations.RunPython(migrate_system_user_to_accounts), + migrations.RemoveField( + model_name='assetpermission', + name='system_users', + ), + ] diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index ea795d889..b944e84e3 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -22,14 +22,13 @@ logger = logging.getLogger(__name__) class AssetPermission(BasePermission): assets = models.ManyToManyField('assets.Asset', related_name='granted_by_permissions', blank=True, verbose_name=_("Asset")) nodes = models.ManyToManyField('assets.Node', related_name='granted_by_permissions', blank=True, verbose_name=_("Nodes")) - system_users = models.ManyToManyField('assets.SystemUser', related_name='granted_by_permissions', blank=True, verbose_name=_("System user")) + accounts = models.JSONField(default=list, verbose_name=_("Accounts")) class Meta: unique_together = [('org_id', 'name')] verbose_name = _("Asset permission") ordering = ('name',) - permissions = [ - ] + permissions = [] @lazyproperty def users_amount(self): @@ -47,10 +46,6 @@ class AssetPermission(BasePermission): def nodes_amount(self): return self.nodes.count() - @lazyproperty - def system_users_amount(self): - return self.system_users.count() - @classmethod def get_queryset_with_prefetch(cls): return cls.objects.all().valid().prefetch_related( @@ -80,10 +75,6 @@ class AssetPermission(BasePermission): names = [asset.hostname for asset in self.assets.all()] return names - def system_users_display(self): - names = [system_user.name for system_user in self.system_users.all()] - return names - def nodes_display(self): names = [node.full_value for node in self.nodes.all()] return names diff --git a/apps/perms/serializers/asset/permission.py b/apps/perms/serializers/asset/permission.py index cd6c24723..9476dfaa3 100644 --- a/apps/perms/serializers/asset/permission.py +++ b/apps/perms/serializers/asset/permission.py @@ -22,7 +22,6 @@ class AssetPermissionSerializer(BasePermissionSerializer): user_groups_display = serializers.ListField(child=serializers.CharField(), label=_('User groups display'), required=False) assets_display = serializers.ListField(child=serializers.CharField(), label=_('Assets display'), required=False) nodes_display = serializers.ListField(child=serializers.CharField(), label=_('Nodes display'), required=False) - system_users_display = serializers.ListField(child=serializers.CharField(), label=_('System users display'), required=False) class Meta: model = AssetPermission @@ -34,9 +33,9 @@ class AssetPermissionSerializer(BasePermissionSerializer): ] fields_m2m = [ 'users', 'users_display', 'user_groups', 'user_groups_display', 'assets', - 'assets_display', 'nodes', 'nodes_display', 'system_users', 'system_users_display', + 'assets_display', 'nodes', 'nodes_display', 'accounts', 'users_amount', 'user_groups_amount', 'assets_amount', - 'nodes_amount', 'system_users_amount', + 'nodes_amount', ] fields = fields_small + fields_m2m read_only_fields = ['created_by', 'date_created', 'from_ticket'] @@ -48,30 +47,16 @@ class AssetPermissionSerializer(BasePermissionSerializer): 'user_groups_amount': {'label': _('User groups amount')}, 'assets_amount': {'label': _('Assets amount')}, 'nodes_amount': {'label': _('Nodes amount')}, - 'system_users_amount': {'label': _('System users amount')}, } @classmethod def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ queryset = queryset.prefetch_related( - 'users', 'user_groups', 'assets', 'nodes', 'system_users' + 'users', 'user_groups', 'assets', 'nodes', ) return queryset - def to_internal_value(self, data): - if 'system_users_display' in data: - # system_users_display 转化为 system_users - system_users = data.get('system_users', []) - system_users_display = data.pop('system_users_display') - - for name in system_users_display: - system_user = SystemUser.objects.filter(name=name).first() - if system_user and system_user.id not in system_users: - system_users.append(system_user.id) - data['system_users'] = system_users - return super().to_internal_value(data) - @staticmethod def perform_display_create(instance, **kwargs): # 用户 diff --git a/apps/perms/serializers/asset/permission_relation.py b/apps/perms/serializers/asset/permission_relation.py index ee1e05112..3bd5e8b1d 100644 --- a/apps/perms/serializers/asset/permission_relation.py +++ b/apps/perms/serializers/asset/permission_relation.py @@ -12,7 +12,6 @@ __all__ = [ 'AssetPermissionUserGroupRelationSerializer', "AssetPermissionAssetRelationSerializer", 'AssetPermissionNodeRelationSerializer', - 'AssetPermissionSystemUserRelationSerializer', 'AssetPermissionAllAssetSerializer', 'AssetPermissionAllUserSerializer', ] @@ -99,13 +98,3 @@ class AssetPermissionNodeRelationSerializer(RelationMixin, serializers.ModelSeri fields = [ 'id', 'node', "node_display", ] - - -class AssetPermissionSystemUserRelationSerializer(RelationMixin, serializers.ModelSerializer): - systemuser_display = serializers.ReadOnlyField() - - class Meta: - model = AssetPermission.system_users.through - fields = [ - 'id', 'systemuser', 'systemuser_display' - ] diff --git a/apps/perms/signal_handlers/asset_permission.py b/apps/perms/signal_handlers/asset_permission.py index e889c318a..68503b6df 100644 --- a/apps/perms/signal_handlers/asset_permission.py +++ b/apps/perms/signal_handlers/asset_permission.py @@ -1,138 +1,10 @@ # -*- coding: utf-8 -*- # -from django.db.models.signals import m2m_changed -from django.dispatch import receiver - -from users.models import User -from assets.models import SystemUser -from common.utils import get_logger -from common.decorator import on_transaction_commit -from common.exceptions import M2MReverseNotAllowed -from common.const.signals import POST_ADD -from perms.models import AssetPermission - - -logger = get_logger(__file__) - - -@receiver(m2m_changed, sender=User.groups.through) -@on_transaction_commit -def on_user_groups_change(sender, instance, action, reverse, pk_set, **kwargs): - """ - UserGroup 增加 User 时,增加的 User 需要与 UserGroup 关联的动态系统用户相关联 - """ - user: User - - if action != POST_ADD: - return - - if not reverse: - # 一个用户添加了多个用户组 - user_ids = [instance.id] - system_users = SystemUser.objects.filter(groups__id__in=pk_set).distinct() - else: - # 一个用户组添加了多个用户 - user_ids = pk_set - system_users = SystemUser.objects.filter(groups__id=instance.pk).distinct() - - for system_user in system_users: - system_user.users.add(*user_ids) - - -@receiver(m2m_changed, sender=AssetPermission.nodes.through) -@on_transaction_commit -def on_permission_nodes_changed(instance, action, reverse, pk_set, model, **kwargs): - if reverse: - raise M2MReverseNotAllowed - if action != POST_ADD: - return - - logger.debug("Asset permission nodes change signal received") - nodes = model.objects.filter(pk__in=pk_set) - system_users = instance.system_users.all() - - # TODO 待优化 - for system_user in system_users: - system_user.nodes.add(*nodes) - - -@receiver(m2m_changed, sender=AssetPermission.assets.through) -@on_transaction_commit -def on_permission_assets_changed(instance, action, reverse, pk_set, model, **kwargs): - if reverse: - raise M2MReverseNotAllowed - if action != POST_ADD: - return - - logger.debug("Asset permission assets change signal received") - assets = model.objects.filter(pk__in=pk_set) - - # TODO 待优化 - system_users = instance.system_users.all() - for system_user in system_users: - system_user: SystemUser - system_user.add_related_assets(assets) - - -@receiver(m2m_changed, sender=AssetPermission.system_users.through) -@on_transaction_commit -def on_asset_permission_system_users_changed(instance, action, reverse, **kwargs): - if reverse: - raise M2MReverseNotAllowed - if action != POST_ADD: - return - - logger.debug("Asset permission system_users change signal received") - system_users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) - assets = instance.assets.all().values_list('id', flat=True) - nodes = instance.nodes.all().values_list('id', flat=True) - - for system_user in system_users: - system_user.nodes.add(*tuple(nodes)) - system_user.add_related_assets(assets) - - # 动态系统用户,需要关联用户和用户组了 - if system_user.username_same_with_user: - users = instance.users.all().values_list('id', flat=True) - groups = instance.user_groups.all().values_list('id', flat=True) - system_user.groups.add(*tuple(groups)) - system_user.users.add(*tuple(users)) - - -@receiver(m2m_changed, sender=AssetPermission.users.through) -@on_transaction_commit -def on_asset_permission_users_changed(instance, action, reverse, pk_set, model, **kwargs): - if reverse: - raise M2MReverseNotAllowed - if action != POST_ADD: - return - - logger.debug("Asset permission users change signal received") - users = model.objects.filter(pk__in=pk_set) - system_users = instance.system_users.all() - - # TODO 待优化 - for system_user in system_users: - if system_user.username_same_with_user: - system_user.users.add(*tuple(users)) - - -@receiver(m2m_changed, sender=AssetPermission.user_groups.through) -@on_transaction_commit -def on_asset_permission_user_groups_changed(instance, action, pk_set, model, reverse, **kwargs): - if reverse: - raise M2MReverseNotAllowed - if action != POST_ADD: - return - - logger.debug("Asset permission user groups change signal received") - groups = model.objects.filter(pk__in=pk_set) - system_users = instance.system_users.all() - - # TODO 待优化 - for system_user in system_users: - if system_user.username_same_with_user: - system_user.groups.add(*tuple(groups)) + + + + + diff --git a/apps/perms/urls/asset_permission.py b/apps/perms/urls/asset_permission.py index f24b1b8ba..17fb22990 100644 --- a/apps/perms/urls/asset_permission.py +++ b/apps/perms/urls/asset_permission.py @@ -11,7 +11,6 @@ router.register('asset-permissions-users-relations', api.AssetPermissionUserRela router.register('asset-permissions-user-groups-relations', api.AssetPermissionUserGroupRelationViewSet, 'asset-permissions-user-groups-relation') router.register('asset-permissions-assets-relations', api.AssetPermissionAssetRelationViewSet, 'asset-permissions-assets-relation') router.register('asset-permissions-nodes-relations', api.AssetPermissionNodeRelationViewSet, 'asset-permissions-nodes-relation') -router.register('asset-permissions-system-users-relations', api.AssetPermissionSystemUserRelationViewSet, 'asset-permissions-system-users-relation') user_permission_urlpatterns = [ # 统一说明: diff --git a/apps/perms/utils/asset/permission.py b/apps/perms/utils/asset/permission.py index e749e630b..8b9991715 100644 --- a/apps/perms/utils/asset/permission.py +++ b/apps/perms/utils/asset/permission.py @@ -11,11 +11,7 @@ from perms.utils.asset.user_permission import get_user_all_asset_perm_ids logger = get_logger(__file__) -def validate_permission(user, asset, system_user, action='connect'): - - if not system_user.protocol in asset.protocols_as_dict.keys(): - return False, time.time() - +def validate_permission(user, asset, account, action='connect'): asset_perm_ids = get_user_all_asset_perm_ids(user) asset_perm_ids_from_asset = AssetPermission.assets.through.objects.filter( @@ -28,9 +24,7 @@ def validate_permission(user, asset, system_user, action='connect'): for node in nodes: ancestor_keys = node.get_ancestor_keys(with_self=True) node_keys.update(ancestor_keys) - node_ids = Node.objects.filter(key__in=node_keys).values_list('id', flat=True) - - node_ids = set(node_ids) + node_ids = set(Node.objects.filter(key__in=node_keys).values_list('id', flat=True)) asset_perm_ids_from_node = AssetPermission.nodes.through.objects.filter( assetpermission_id__in=asset_perm_ids, @@ -39,16 +33,9 @@ def validate_permission(user, asset, system_user, action='connect'): asset_perm_ids = {*asset_perm_ids_from_asset, *asset_perm_ids_from_node} - asset_perm_ids = AssetPermission.system_users.through.objects.filter( - assetpermission_id__in=asset_perm_ids, - systemuser_id=system_user.id - ).values_list('assetpermission_id', flat=True) - - asset_perm_ids = set(asset_perm_ids) - - asset_perms = AssetPermission.objects.filter( - id__in=asset_perm_ids - ).order_by('-date_expired') + asset_perms = AssetPermission.objects\ + .filter(id__in=asset_perm_ids, accounts__contains=account)\ + .order_by('-date_expired') if asset_perms: actions = set() diff --git a/apps/tickets/migrations/0018_auto_20220728_1125.py b/apps/tickets/migrations/0018_auto_20220728_1125.py new file mode 100644 index 000000000..70bb7c6bd --- /dev/null +++ b/apps/tickets/migrations/0018_auto_20220728_1125.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.14 on 2022-07-28 03:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0017_auto_20220623_1027'), + ] + + operations = [ + migrations.AlterField( + model_name='applyapplicationticket', + name='apply_permission_name', + field=models.CharField(max_length=128, verbose_name='Permission name'), + ), + migrations.AlterField( + model_name='applyassetticket', + name='apply_permission_name', + field=models.CharField(max_length=128, verbose_name='Permission name'), + ), + ] diff --git a/utils/generate_fake_data/resources/perms.py b/utils/generate_fake_data/resources/perms.py index 953712a5d..e3e866feb 100644 --- a/utils/generate_fake_data/resources/perms.py +++ b/utils/generate_fake_data/resources/perms.py @@ -47,12 +47,6 @@ class AssetPermissionGenerator(FakeDataGenerator): relation_name = 'node_id' self.set_relations(perms, through, relation_name, choices) - def set_system_users(self, perms): - through = AssetPermission.system_users.through - choices = self.system_user_ids - relation_name = 'systemuser_id' - self.set_relations(perms, through, relation_name, choices) - def set_relations(self, perms, through, relation_name, choices, choice_count=None): relations = [] @@ -79,4 +73,3 @@ class AssetPermissionGenerator(FakeDataGenerator): self.set_user_groups(created) self.set_assets(created) self.set_nodes(created) - self.set_system_users(created) From 0dc3d43ee53aa0de7ed49015996fb6402e018529 Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 28 Jul 2022 19:12:27 +0800 Subject: [PATCH 6/8] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=94=A8=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/applications/models/application.py | 2 +- apps/assets/api/system_user.py | 176 ---------- apps/assets/models/__init__.py | 5 +- apps/assets/models/account.py | 2 +- apps/assets/models/authbook.py | 140 -------- apps/assets/models/cmd_filter.py | 223 ------------- apps/assets/models/user.py | 17 - .../assets/signal_handlers/system_user.py.bak | 119 ------- apps/assets/tasks/push_system_user.py | 307 ------------------ apps/assets/tasks/system_user_connectivity.py | 151 --------- apps/assets/urls/api_urls.py | 12 +- apps/perms/models/asset_permission.py | 2 +- 12 files changed, 5 insertions(+), 1151 deletions(-) delete mode 100644 apps/assets/api/system_user.py delete mode 100644 apps/assets/models/authbook.py delete mode 100644 apps/assets/models/cmd_filter.py delete mode 100644 apps/assets/signal_handlers/system_user.py.bak delete mode 100644 apps/assets/tasks/push_system_user.py delete mode 100644 apps/assets/tasks/system_user_connectivity.py diff --git a/apps/applications/models/application.py b/apps/applications/models/application.py index af1e27c2d..000094b4f 100644 --- a/apps/applications/models/application.py +++ b/apps/applications/models/application.py @@ -9,7 +9,7 @@ from orgs.mixins.models import OrgModelMixin from common.mixins import CommonModelMixin from common.tree import TreeNode from common.utils import is_uuid -from assets.models import Asset, SystemUser +from assets.models import Asset from ..utils import KubernetesTree from .. import const diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py deleted file mode 100644 index 7ae682839..000000000 --- a/apps/assets/api/system_user.py +++ /dev/null @@ -1,176 +0,0 @@ -# ~*~ coding: utf-8 ~*~ -from django.shortcuts import get_object_or_404 -from rest_framework.response import Response -from rest_framework.decorators import action -from rest_framework.viewsets import GenericViewSet - -from common.utils import get_logger, get_object_or_none -from common.mixins.api import SuggestionMixin -from orgs.mixins.api import OrgBulkModelViewSet -from orgs.mixins import generics -from ..models import SystemUser, CommandFilterRule, Account -from .. import serializers - -logger = get_logger(__file__) -__all__ = [ - 'SystemUserViewSet', 'SystemUserAuthInfoApi', - 'SystemUserCommandFilterRuleListApi', - 'SystemUserAssetAccountApi', - 'SystemUserAssetAccountSecretApi', -] - - -class SystemUserViewSet(SuggestionMixin, OrgBulkModelViewSet): - """ - System user api set, for add,delete,update,list,retrieve resource - """ - model = SystemUser - filterset_fields = { - 'name': ['exact'], - 'username': ['exact'], - 'protocol': ['exact', 'in'], - } - search_fields = filterset_fields - serializer_class = serializers.SystemUserSerializer - serializer_classes = { - 'default': serializers.SystemUserSerializer, - 'suggestion': serializers.MiniSystemUserSerializer - } - ordering_fields = ('name', 'protocol', 'login_mode') - ordering = ('name', ) - rbac_perms = { - 'su_from': 'assets.view_systemuser', - 'su_to': 'assets.view_systemuser', - 'match': 'assets.match_systemuser' - } - - @action(methods=['get'], detail=False, url_path='su-from') - def su_from(self, request, *args, **kwargs): - """ API 获取可选的 su_from 系统用户""" - queryset = self.filter_queryset(self.get_queryset()) - queryset = queryset.filter( - protocol=SystemUser.Protocol.ssh, login_mode=SystemUser.LOGIN_AUTO - ) - return self.get_paginate_response_if_need(queryset) - - @action(methods=['get'], detail=True, url_path='su-to') - def su_to(self, request, *args, **kwargs): - """ 获取系统用户的所有 su_to 系统用户 """ - pk = kwargs.get('pk') - system_user = get_object_or_404(SystemUser, pk=pk) - queryset = system_user.su_to.all() - queryset = self.filter_queryset(queryset) - return self.get_paginate_response_if_need(queryset) - - def get_paginate_response_if_need(self, queryset): - page = self.paginate_queryset(queryset) - if page is not None: - serializer = self.get_serializer(page, many=True) - return self.get_paginated_response(serializer.data) - serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data) - - -class SystemUserAccountViewSet(GenericViewSet): - model = Account - serializer_classes = { - 'default': serializers.AccountSerializer, - 'account_secret': serializers.AccountSecretSerializer, - } - - def get_object(self): - system_user_id = self.kwargs.get('pk') - asset_id = self.kwargs.get('asset_id') - user_id = self.kwargs.get("user_id") - system_user = SystemUser.objects.get(id=system_user_id) - account = system_user.get_account(user_id, asset_id) - return account - - @action(methods=['get'], detail=False, url_path='account') - def account(self, request, *args, **kwargs): - pass - - @action(methods=['get'], detail=False, url_path='account-secret') - def account_secret(self): - pass - - @action(methods=['put'], detail=False, url_path='manual-account') - def manual_account(self, request, *args, **kwargs): - pass - - -class SystemUserAssetAccountApi(generics.RetrieveAPIView): - model = Account - serializer_class = serializers.AccountSerializer - - def get_object(self): - system_user_id = self.kwargs.get('pk') - asset_id = self.kwargs.get('asset_id') - user_id = self.kwargs.get("user_id") - system_user = SystemUser.objects.get(id=system_user_id) - account = system_user.get_account(user_id, asset_id) - return account - - -class SystemUserAssetAccountSecretApi(SystemUserAssetAccountApi): - model = Account - serializer_class = serializers.AccountSecretSerializer - rbac_perms = { - 'retrieve': 'assets.view_accountsecret' - } - - -class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): - """ - Get system user auth info - """ - model = SystemUser - serializer_class = serializers.AccountSerializer - rbac_perms = { - 'retrieve': 'assets.view_systemusersecret', - 'list': 'assets.view_systemusersecret', - 'change': 'assets.change_systemuser', - 'destroy': 'assets.change_systemuser', - } - - def get_object(self): - system_user_id = self.kwargs.get('pk') - asset_id = self.kwargs.get('asset_id') - user_id = self.kwargs.get("user_id") - system_user = SystemUser.objects.get(id=system_user_id) - account = system_user.get_account(user_id, asset_id) - return account - - def destroy(self, request, *args, **kwargs): - instance = self.get_object() - instance.clear_auth() - return Response(status=204) - - -class SystemUserCommandFilterRuleListApi(generics.ListAPIView): - rbac_perms = { - 'list': 'assets.view_commandfilterule' - } - - def get_serializer_class(self): - from ..serializers import CommandFilterRuleSerializer - return CommandFilterRuleSerializer - - def get_queryset(self): - user_id = self.request.query_params.get('user_id') - user_group_id = self.request.query_params.get('user_group_id') - system_user_id = self.kwargs.get('pk', None) - system_user = get_object_or_none(SystemUser, pk=system_user_id) - if not system_user: - system_user_id = self.request.query_params.get('system_user_id') - asset_id = self.request.query_params.get('asset_id') - application_id = self.request.query_params.get('application_id') - rules = CommandFilterRule.get_queryset( - user_id=user_id, - user_group_id=user_group_id, - system_user_id=system_user_id, - asset_id=asset_id, - application_id=application_id - ) - return rules - diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index da35178d1..4eeab7b0b 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -1,15 +1,12 @@ from .base import * from .asset import * from .label import Label -from .user import * from .group import * from .domain import * from .node import * -from .cmd_filter import * -from .authbook import * from .utils import * -from .authbook import * from .gathered_user import * from .favorite_asset import * from .account import * from .backup import * +from .user import * diff --git a/apps/assets/models/account.py b/apps/assets/models/account.py index eb651965b..641ba1a76 100644 --- a/apps/assets/models/account.py +++ b/apps/assets/models/account.py @@ -2,7 +2,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from simple_history.models import HistoricalRecords -from .user import ProtocolMixin +from .protocol import ProtocolMixin from .base import BaseUser, AbsConnectivity diff --git a/apps/assets/models/authbook.py b/apps/assets/models/authbook.py deleted file mode 100644 index f5d9e457d..000000000 --- a/apps/assets/models/authbook.py +++ /dev/null @@ -1,140 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from django.db import models -from django.db.models import F -from django.utils.translation import ugettext_lazy as _ -from simple_history.models import HistoricalRecords - -from common.utils import lazyproperty, get_logger -from .base import BaseUser, AbsConnectivity - -logger = get_logger(__name__) - - -__all__ = ['AuthBook'] - - -class AuthBook(BaseUser, AbsConnectivity): - asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset')) - 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() - - auth_attrs = ['username', 'password', 'private_key', 'public_key'] - - class Meta: - verbose_name = _('AuthBook') - unique_together = [('username', 'asset', 'systemuser')] - permissions = [ - ('test_authbook', _('Can test asset account connectivity')), - ('view_assetaccountsecret', _('Can view asset account secret')), - ('change_assetaccountsecret', _('Can change asset account secret')), - ('view_assethistoryaccount', _('Can view asset history account')), - ('view_assethistoryaccountsecret', _('Can view asset history account secret')), - ] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.auth_snapshot = {} - - 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 '' - - 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) - - 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 username_display(self): - return self.get_or_systemuser_attr('username') or '*' - - @lazyproperty - def systemuser_display(self): - if not self.systemuser: - return '' - return str(self.systemuser) - - @property - def smart_name(self): - username = self.username_display - - if self.asset: - asset = str(self.asset) - else: - asset = '*' - return '{}@{}'.format(username, asset) - - 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) - - # 不触发post_save信号 - self.__class__.objects.bulk_update(matched, fields=['password', 'private_key', 'public_key']) - - def remove_asset_admin_user_if_need(self): - if not self.asset or not self.systemuser: - return - if not self.systemuser.is_admin_user or self.asset.admin_user != self.systemuser: - return - self.asset.admin_user = None - self.asset.save() - logger.debug('Remove asset admin user: {} {}'.format(self.asset, self.systemuser)) - - def update_asset_admin_user_if_need(self): - if not self.asset or not self.systemuser: - return - if not self.systemuser.is_admin_user or self.asset.admin_user == self.systemuser: - return - self.asset.admin_user = self.systemuser - self.asset.save() - logger.debug('Update asset admin user: {} {}'.format(self.asset, self.systemuser)) - - @classmethod - def get_queryset(cls, is_history_model=False): - model = cls.history.model if is_history_model else cls - queryset = model.objects.all() \ - .annotate(ip=F('asset__ip')) \ - .annotate(hostname=F('asset__hostname')) \ - .annotate(platform=F('asset__platform__name')) \ - .annotate(protocols=F('asset__protocols')) - return queryset - - def __str__(self): - return self.smart_name - - diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py deleted file mode 100644 index b17a4263d..000000000 --- a/apps/assets/models/cmd_filter.py +++ /dev/null @@ -1,223 +0,0 @@ -# -*- coding: utf-8 -*- -# -import uuid -import re - -from django.db import models -from django.db.models import Q -from django.core.validators import MinValueValidator, MaxValueValidator -from django.utils.translation import ugettext_lazy as _ - -from users.models import User, UserGroup -from applications.models import Application -from ..models import SystemUser, Asset - -from common.utils import lazyproperty, get_logger, get_object_or_none -from orgs.mixins.models import OrgModelMixin - -logger = get_logger(__file__) - -__all__ = [ - 'CommandFilter', 'CommandFilterRule' -] - - -class CommandFilter(OrgModelMixin): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - name = models.CharField(max_length=64, verbose_name=_("Name")) - users = models.ManyToManyField( - 'users.User', related_name='cmd_filters', blank=True, - verbose_name=_("User") - ) - user_groups = models.ManyToManyField( - 'users.UserGroup', related_name='cmd_filters', blank=True, - verbose_name=_("User group"), - ) - assets = models.ManyToManyField( - 'assets.Asset', related_name='cmd_filters', blank=True, - verbose_name=_("Asset") - ) - system_users = models.ManyToManyField( - 'assets.SystemUser', related_name='cmd_filters', blank=True, - verbose_name=_("System user")) - applications = models.ManyToManyField( - 'applications.Application', related_name='cmd_filters', blank=True, - verbose_name=_("Application") - ) - is_active = models.BooleanField(default=True, verbose_name=_('Is active')) - comment = models.TextField(blank=True, default='', verbose_name=_("Comment")) - date_created = models.DateTimeField(auto_now_add=True) - date_updated = models.DateTimeField(auto_now=True) - created_by = models.CharField( - max_length=128, blank=True, default='', verbose_name=_('Created by') - ) - - def __str__(self): - return self.name - - class Meta: - unique_together = [('org_id', 'name')] - verbose_name = _("Command filter") - - -class CommandFilterRule(OrgModelMixin): - TYPE_REGEX = 'regex' - TYPE_COMMAND = 'command' - TYPE_CHOICES = ( - (TYPE_REGEX, _('Regex')), - (TYPE_COMMAND, _('Command')), - ) - - ACTION_UNKNOWN = 10 - - class ActionChoices(models.IntegerChoices): - deny = 0, _('Deny') - allow = 9, _('Allow') - confirm = 2, _('Reconfirm') - - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - filter = models.ForeignKey( - 'CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules' - ) - type = models.CharField(max_length=16, default=TYPE_COMMAND, choices=TYPE_CHOICES, verbose_name=_("Type")) - priority = models.IntegerField( - default=50, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), - validators=[MinValueValidator(1), MaxValueValidator(100)] - ) - content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command")) - ignore_case = models.BooleanField(default=True, verbose_name=_('Ignore case')) - action = models.IntegerField(default=ActionChoices.deny, choices=ActionChoices.choices, verbose_name=_("Action")) - # 动作: 附加字段 - # - confirm: 命令复核人 - reviewers = models.ManyToManyField( - 'users.User', related_name='review_cmd_filter_rules', blank=True, - verbose_name=_("Reviewers") - ) - comment = models.CharField(max_length=64, blank=True, default='', verbose_name=_("Comment")) - date_created = models.DateTimeField(auto_now_add=True) - date_updated = models.DateTimeField(auto_now=True) - created_by = models.CharField(max_length=128, blank=True, default='', verbose_name=_('Created by')) - - class Meta: - ordering = ('priority', 'action') - verbose_name = _("Command filter rule") - - @lazyproperty - def pattern(self): - if self.type == 'command': - s = self.construct_command_regex(content=self.content) - else: - s = r'{0}'.format(self.content) - - return s - - @classmethod - def construct_command_regex(cls, content): - regex = [] - content = content.replace('\r\n', '\n') - for _cmd in content.split('\n'): - cmd = re.sub(r'\s+', ' ', _cmd) - cmd = re.escape(cmd) - cmd = cmd.replace('\\ ', '\s+') - - # 有空格就不能 铆钉单词了 - if ' ' in _cmd: - regex.append(cmd) - continue - - # 如果是单个字符 - if cmd[-1].isalpha(): - regex.append(r'\b{0}\b'.format(cmd)) - else: - regex.append(r'\b{0}'.format(cmd)) - s = r'{}'.format('|'.join(regex)) - return s - - @staticmethod - def compile_regex(regex, ignore_case): - try: - if ignore_case: - pattern = re.compile(regex, re.IGNORECASE) - else: - pattern = re.compile(regex) - except Exception as e: - error = _('The generated regular expression is incorrect: {}').format(str(e)) - logger.error(error) - return False, error, None - return True, '', pattern - - def match(self, data): - succeed, error, pattern = self.compile_regex(self.pattern, self.ignore_case) - if not succeed: - return self.ACTION_UNKNOWN, '' - - found = pattern.search(data) - if not found: - return self.ACTION_UNKNOWN, '' - - if self.action == self.ActionChoices.allow: - return self.ActionChoices.allow, found.group() - else: - return self.ActionChoices.deny, found.group() - - def __str__(self): - return '{} % {}'.format(self.type, self.content) - - def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id): - from tickets.const import TicketType - from tickets.models import ApplyCommandTicket - data = { - 'title': _('Command confirm') + ' ({})'.format(session.user), - 'type': TicketType.command_confirm, - 'applicant': session.user_obj, - 'apply_run_user_id': session.user_id, - 'apply_run_asset': str(session.asset), - 'apply_run_system_user_id': session.system_user_id, - 'apply_run_command': run_command[:4090], - 'apply_from_session_id': str(session.id), - 'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id), - 'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id), - 'org_id': org_id, - } - ticket = ApplyCommandTicket.objects.create(**data) - assignees = self.reviewers.all() - ticket.open_by_system(assignees) - return ticket - - @classmethod - def get_queryset(cls, user_id=None, user_group_id=None, system_user_id=None, - asset_id=None, application_id=None, org_id=None): - user_groups = [] - user = get_object_or_none(User, pk=user_id) - if user: - user_groups.extend(list(user.groups.all())) - user_group = get_object_or_none(UserGroup, pk=user_group_id) - if user_group: - org_id = user_group.org_id - user_groups.append(user_group) - system_user = get_object_or_none(SystemUser, pk=system_user_id) - asset = get_object_or_none(Asset, pk=asset_id) - application = get_object_or_none(Application, pk=application_id) - q = Q() - if user: - q |= Q(users=user) - if user_groups: - q |= Q(user_groups__in=set(user_groups)) - if system_user: - org_id = system_user.org_id - q |= Q(system_users=system_user) - if asset: - org_id = asset.org_id - q |= Q(assets=asset) - if application: - org_id = application.org_id - q |= Q(applications=application) - if q: - cmd_filters = CommandFilter.objects.filter(q).filter(is_active=True) - if org_id: - cmd_filters = cmd_filters.filter(org_id=org_id) - rule_ids = cmd_filters.values_list('rules', flat=True) - rules = cls.objects.filter(id__in=rule_ids) - else: - rules = cls.objects.none() - return rules diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index a5be417a2..79d6e96e2 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -40,23 +40,6 @@ class SystemUser(ProtocolMixin, BaseUser): username = '*' return '{0.name}({1})'.format(self, username) - @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 - @classmethod def create_accounts_with_assets(cls, asset_ids, system_user_ids): pass diff --git a/apps/assets/signal_handlers/system_user.py.bak b/apps/assets/signal_handlers/system_user.py.bak deleted file mode 100644 index e0dfe3ebf..000000000 --- a/apps/assets/signal_handlers/system_user.py.bak +++ /dev/null @@ -1,119 +0,0 @@ -# -*- coding: utf-8 -*- -# -from django.db.models.signals import ( - post_save, m2m_changed -) -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 -from users.models import User -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 action != POST_ADD: - return - - if model == Asset: - system_user_ids = [instance.id] - asset_ids = pk_set - else: - system_user_ids = pk_set - asset_ids = [instance.id] - # todo: Auto create account if need - SystemUser.create_accounts_with_assets(asset_ids, system_user_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]) - # add assets to su_from - instance.add_related_assets_to_su_from_if_need(assets) diff --git a/apps/assets/tasks/push_system_user.py b/apps/assets/tasks/push_system_user.py deleted file mode 100644 index 8834a29e9..000000000 --- a/apps/assets/tasks/push_system_user.py +++ /dev/null @@ -1,307 +0,0 @@ -# ~*~ coding: utf-8 ~*~ - -from itertools import groupby -from celery import shared_task -from common.db.utils import get_object_if_need, get_objects -from django.utils.translation import ugettext as _, gettext_noop -from django.db.models import Empty - -from common.utils import encrypt_password, get_logger -from assets.models import SystemUser, Asset -from orgs.utils import org_aware_func, tmp_to_root_org -from . import const -from .utils import clean_ansible_task_hosts, group_asset_by_platform - - -logger = get_logger(__file__) -__all__ = [ - 'push_system_user_util', 'push_system_user_to_assets', - 'push_system_user_to_assets_manual', 'push_system_user_a_asset_manual', - 'push_system_users_a_asset' -] - - -def _split_by_comma(raw: str): - try: - return [i.strip() for i in raw.split(',')] - except AttributeError: - return [] - - -def _dump_args(args: dict): - return ' '.join([f'{k}={v}' for k, v in args.items() if v is not Empty]) - - -def get_push_unixlike_system_user_tasks(system_user, username=None, **kwargs): - algorithm = kwargs.get('algorithm') - if username is None: - username = system_user.username - - comment = system_user.name - if system_user.username_same_with_user: - from users.models import User - user = User.objects.filter(username=username).only('name', 'username').first() - if user: - comment = f'{system_user.name}[{str(user)}]' - comment = comment.replace(' ', '') - - password = system_user.password - public_key = system_user.public_key - - groups = _split_by_comma(system_user.system_groups) - - if groups: - groups = '"%s"' % ','.join(groups) - - add_user_args = { - 'name': username, - 'shell': system_user.shell or Empty, - 'state': 'present', - 'home': system_user.home or Empty, - 'expires': -1, - 'groups': groups or Empty, - 'comment': comment - } - - tasks = [ - { - 'name': 'Add user {}'.format(username), - 'action': { - 'module': 'user', - 'args': _dump_args(add_user_args), - } - }, - { - 'name': 'Add group {}'.format(username), - 'action': { - 'module': 'group', - 'args': 'name={} state=present'.format(username), - } - } - ] - if not system_user.home: - tasks.extend([ - { - 'name': 'Check home dir exists', - 'action': { - 'module': 'stat', - 'args': 'path=/home/{}'.format(username) - }, - 'register': 'home_existed' - }, - { - 'name': "Set home dir permission", - 'action': { - 'module': 'file', - 'args': "path=/home/{0} owner={0} group={0} mode=700".format(username) - }, - 'when': 'home_existed.stat.exists == true' - } - ]) - if password: - tasks.append({ - 'name': 'Set {} password'.format(username), - 'action': { - 'module': 'user', - 'args': 'name={} shell={} state=present password={}'.format( - username, system_user.shell, - encrypt_password(password, salt="K3mIlKK", algorithm=algorithm), - ), - } - }) - if public_key: - tasks.append({ - 'name': 'Set {} authorized key'.format(username), - 'action': { - 'module': 'authorized_key', - 'args': "user={} state=present key='{}'".format( - username, public_key - ) - } - }) - if system_user.sudo: - sudo = system_user.sudo.replace('\r\n', '\n').replace('\r', '\n') - sudo_list = sudo.split('\n') - sudo_tmp = [] - for s in sudo_list: - sudo_tmp.append(s.strip(',')) - sudo = ','.join(sudo_tmp) - tasks.append({ - 'name': 'Set {} sudo setting'.format(username), - 'action': { - 'module': 'lineinfile', - 'args': "dest=/etc/sudoers state=present regexp='^{0} ALL=' " - "line='{0} ALL=(ALL) NOPASSWD: {1}' " - "validate='visudo -cf %s'".format(username, sudo) - } - }) - - return tasks - - -def get_push_windows_system_user_tasks(system_user: SystemUser, username=None, **kwargs): - if username is None: - username = system_user.username - password = system_user.password - groups = {'Users', 'Remote Desktop Users'} - if system_user.system_groups: - groups.update(_split_by_comma(system_user.system_groups)) - groups = ','.join(groups) - - tasks = [] - if not password: - logger.error("Error: no password found") - return tasks - - if system_user.ad_domain: - logger.error('System user with AD domain do not support push.') - return tasks - - task = { - 'name': 'Add user {}'.format(username), - 'action': { - 'module': 'win_user', - 'args': 'fullname={} ' - 'name={} ' - 'password={} ' - 'state=present ' - 'update_password=always ' - 'password_expired=no ' - 'password_never_expires=yes ' - 'groups="{}" ' - 'groups_action=add ' - ''.format(username, username, password, groups), - } - } - tasks.append(task) - return tasks - - -def get_push_system_user_tasks(system_user, platform="unixlike", username=None, algorithm=None): - """ - 获取推送系统用户的 ansible 命令,跟资产无关 - :param system_user: - :param platform: - :param username: 当动态时,近推送某个 - :return: - """ - get_task_map = { - "unixlike": get_push_unixlike_system_user_tasks, - "windows": get_push_windows_system_user_tasks, - } - get_tasks = get_task_map.get(platform, get_push_unixlike_system_user_tasks) - if not system_user.username_same_with_user: - return get_tasks(system_user, algorithm=algorithm) - tasks = [] - # 仅推送这个username - if username is not None: - tasks.extend(get_tasks(system_user, username, algorithm=algorithm)) - return tasks - users = system_user.users.all().values_list('username', flat=True) - print(_("System user is dynamic: {}").format(list(users))) - for _username in users: - tasks.extend(get_tasks(system_user, _username, algorithm=algorithm)) - return tasks - - -@org_aware_func("system_user") -def push_system_user_util(system_user, assets, task_name, username=None): - from ops.utils import update_or_create_ansible_task - assets = clean_ansible_task_hosts(assets, system_user=system_user) - if not assets: - return {} - - # 资产按平台分类 - assets_sorted = sorted(assets, key=group_asset_by_platform) - platform_hosts = groupby(assets_sorted, key=group_asset_by_platform) - - if system_user.username_same_with_user: - if username is None: - # 动态系统用户,但是没有指定 username - usernames = list(system_user.users.all().values_list('username', flat=True).distinct()) - else: - usernames = [username] - else: - # 非动态系统用户指定 username 无效 - assert username is None, 'Only Dynamic user can assign `username`' - usernames = [system_user.username] - - def run_task(_tasks, _hosts): - if not _tasks: - return - task, created = update_or_create_ansible_task( - task_name=task_name, hosts=_hosts, tasks=_tasks, pattern='all', - options=const.TASK_OPTIONS, run_as_admin=True, - ) - task.run() - - for platform, _assets in platform_hosts: - _assets = list(_assets) - if not _assets: - continue - print(_("Start push system user for platform: [{}]").format(platform)) - print(_("Hosts count: {}").format(len(_assets))) - - for u in usernames: - for a in _assets: - system_user.load_asset_special_auth(a, u) - algorithm = 'des' if a.platform.name == 'AIX' else 'sha512' - tasks = get_push_system_user_tasks( - system_user, platform, username=u, - algorithm=algorithm - ) - run_task(tasks, [a]) - - -@shared_task(queue="ansible") -@tmp_to_root_org() -def push_system_user_to_assets_manual(system_user, username=None): - """ - 将系统用户推送到与它关联的所有资产上 - """ - system_user = get_object_if_need(SystemUser, system_user) - assets = system_user.get_related_assets() - task_name = gettext_noop("Push system users to assets: ") + system_user.name - return push_system_user_util(system_user, assets, task_name=task_name, username=username) - - -@shared_task(queue="ansible") -@tmp_to_root_org() -def push_system_user_a_asset_manual(system_user, asset, username=None): - """ - 将系统用户推送到一个资产上 - """ - # if username is None: - # username = system_user.username - task_name = gettext_noop("Push system users to asset: ") + "{}({}) => {}".format( - system_user.name, username or system_user.username, asset - ) - return push_system_user_util(system_user, [asset], task_name=task_name, username=username) - - -@shared_task(queue="ansible") -@tmp_to_root_org() -def push_system_users_a_asset(system_users, asset): - for system_user in system_users: - push_system_user_a_asset_manual(system_user, asset) - - -@shared_task(queue="ansible") -@tmp_to_root_org() -def push_system_user_to_assets(system_user_id, asset_ids, username=None): - """ - 推送系统用户到指定的若干资产上 - """ - system_user = SystemUser.objects.get(id=system_user_id) - assets = get_objects(Asset, asset_ids) - task_name = gettext_noop("Push system users to assets: ") + system_user.name - - return push_system_user_util(system_user, assets, task_name, username=username) - -# @shared_task -# @register_as_period_task(interval=3600) -# @after_app_ready_start -# @after_app_shutdown_clean_periodic -# def push_system_user_period(): -# for system_user in SystemUser.objects.all(): -# push_system_user_related_nodes(system_user) diff --git a/apps/assets/tasks/system_user_connectivity.py b/apps/assets/tasks/system_user_connectivity.py deleted file mode 100644 index 2213bfa26..000000000 --- a/apps/assets/tasks/system_user_connectivity.py +++ /dev/null @@ -1,151 +0,0 @@ - -from itertools import groupby -from collections import defaultdict - -from celery import shared_task -from django.utils.translation import ugettext as _, gettext_noop - -from assets.models import Asset -from common.utils import get_logger -from orgs.utils import tmp_to_org, org_aware_func -from ..models import SystemUser, Connectivity, Account -from . import const -from .utils import ( - clean_ansible_task_hosts, group_asset_by_platform -) - -logger = get_logger(__name__) -__all__ = [ - 'test_system_user_connectivity_util', 'test_system_user_connectivity_manual', - 'test_system_user_connectivity_period', 'test_system_user_connectivity_a_asset', - 'test_system_users_connectivity_a_asset' -] - - -def set_assets_accounts_connectivity(system_user, 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) - - accounts_ok = Account.objects.filter(asset_id__in=asset_ids_ok, systemuser=system_user) - accounts_failed = Account.objects.filter(asset_id__in=asset_ids_failed, systemuser=system_user) - - Account.bulk_set_connectivity(accounts_ok, Connectivity.ok) - Account.bulk_set_connectivity(accounts_failed, Connectivity.failed) - - -@org_aware_func("system_user") -def test_system_user_connectivity_util(system_user, assets, task_name): - """ - Test system cant connect his assets or not. - :param system_user: - :param assets: - :param task_name: - :return: - """ - from ops.utils import update_or_create_ansible_task - - if system_user.username_same_with_user: - logger.error(_("Dynamic system user not support test")) - return - - # hosts = clean_ansible_task_hosts(assets, system_user=system_user) - # TODO: 这里不传递系统用户,因为clean_ansible_task_hosts会通过system_user来判断是否可以推送, - # 不符合测试可连接性逻辑, 后面需要优化此逻辑 - hosts = clean_ansible_task_hosts(assets) - if not hosts: - return {} - platform_hosts_map = {} - hosts_sorted = sorted(hosts, key=group_asset_by_platform) - platform_hosts = groupby(hosts_sorted, key=group_asset_by_platform) - for i in platform_hosts: - platform_hosts_map[i[0]] = list(i[1]) - - platform_tasks_map = { - "unixlike": const.PING_UNIXLIKE_TASKS, - "windows": const.PING_WINDOWS_TASKS - } - - results_summary = dict( - contacted=defaultdict(dict), dark=defaultdict(dict), success=True - ) - - def run_task(_tasks, _hosts, _username): - old_name = "{}".format(system_user) - new_name = "{}({})".format(system_user.name, _username) - _task_name = task_name.replace(old_name, new_name) - _task, created = update_or_create_ansible_task( - task_name=_task_name, hosts=_hosts, tasks=_tasks, - pattern='all', options=const.TASK_OPTIONS, - run_as=_username, system_user=system_user - ) - raw, summary = _task.run() - success = summary.get('success', False) - contacted = summary.get('contacted', {}) - dark = summary.get('dark', {}) - - results_summary['success'] &= success - results_summary['contacted'].update(contacted) - results_summary['dark'].update(dark) - - for platform, _hosts in platform_hosts_map.items(): - if not _hosts: - continue - if platform not in ["unixlike", "windows"]: - continue - - tasks = platform_tasks_map[platform] - print(_("Start test system user connectivity for platform: [{}]").format(platform)) - print(_("Hosts count: {}").format(len(_hosts))) - # 用户名不是动态的,用户名则是一个 - logger.debug("System user not has special auth") - run_task(tasks, _hosts, system_user.username) - - set_assets_accounts_connectivity(system_user, hosts, results_summary) - return results_summary - - -@shared_task(queue="ansible") -@org_aware_func("system_user") -def test_system_user_connectivity_manual(system_user, asset_ids=None): - task_name = gettext_noop("Test system user connectivity: ") + str(system_user) - if asset_ids: - assets = Asset.objects.filter(id__in=asset_ids) - else: - assets = system_user.get_related_assets() - test_system_user_connectivity_util(system_user, assets, task_name) - - -@shared_task(queue="ansible") -@org_aware_func("system_user") -def test_system_user_connectivity_a_asset(system_user, asset): - task_name = gettext_noop("Test system user connectivity: ") + "{} => {}".format( - system_user, asset - ) - test_system_user_connectivity_util(system_user, [asset], task_name) - - -@shared_task(queue="ansible") -def test_system_users_connectivity_a_asset(system_users, asset): - for system_user in system_users: - test_system_user_connectivity_a_asset(system_user, asset) - - -@shared_task(queue="ansible") -def test_system_user_connectivity_period(): - if not const.PERIOD_TASK_ENABLED: - logger.debug("Period task disabled, test system user connectivity pass") - return - queryset_map = SystemUser.objects.all_group_by_org() - for org, system_user in queryset_map.items(): - task_name = gettext_noop("Test system user connectivity period: ") + str(system_user) - with tmp_to_org(org): - assets = system_user.get_related_assets() - test_system_user_connectivity_util(system_user, assets, task_name) diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index f3f192a3d..b312a41b7 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -16,7 +16,6 @@ router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret') router.register(r'accounts-history', api.AccountHistoryViewSet, 'account-history') router.register(r'account-history-secrets', api.AccountHistorySecretsViewSet, 'account-history-secret') router.register(r'platforms', api.AssetPlatformViewSet, 'platform') -router.register(r'system-users', api.SystemUserViewSet, 'system-user') router.register(r'labels', api.LabelViewSet, 'label') router.register(r'nodes', api.NodeViewSet, 'node') router.register(r'domains', api.DomainViewSet, 'domain') @@ -41,11 +40,6 @@ urlpatterns = [ path('assets//perm-user-groups/', api.AssetPermUserGroupListApi.as_view(), name='asset-perm-user-group-list'), path('assets//perm-user-groups//permissions/', api.AssetPermUserGroupPermissionsListApi.as_view(), name='asset-perm-user-group-permission-list'), - path('system-users//assets//users//account/', api.SystemUserAssetAccountApi.as_view(), name='system-user-asset-account'), - path('system-users//assets//users//account-secret/', api.SystemUserAssetAccountSecretApi.as_view(), name='system-user-asset-account-secret'), - path('system-users//cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'), - path('cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='cmd-filter-rules'), - path('accounts/tasks/', api.AccountTaskCreateAPI.as_view(), name='account-task-create'), path('nodes/tree/', api.NodeListAsTreeApi.as_view(), name='node-tree'), @@ -65,9 +59,5 @@ urlpatterns = [ ] -old_version_urlpatterns = [ - re_path('(?Padmin-user|system-user|domain|gateway|cmd-filter|asset-user)/.*', capi.redirect_plural_name_api) -] - -urlpatterns += router.urls + cmd_filter_router.urls + old_version_urlpatterns +urlpatterns += router.urls + cmd_filter_router.urls diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index b944e84e3..d0b25d769 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -6,7 +6,7 @@ from django.db.models import F, TextChoices from orgs.mixins.models import OrgModelMixin from common.db import models from common.utils import lazyproperty -from assets.models import Asset, SystemUser, Node, FamilyMixin +from assets.models import Asset, Node, FamilyMixin from .base import BasePermission From 109db8886b20d40df9ab546b5dfad76376ffc8fb Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 28 Jul 2022 19:27:42 +0800 Subject: [PATCH 7/8] =?UTF-8?q?perf:=20=E8=BF=98=E5=8E=9F=E5=9B=9E=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/applications/models/application.py | 2 +- apps/assets/api/__init__.py | 2 - apps/assets/api/asset.py | 4 +- apps/assets/api/cmd_filter.py | 85 --------- apps/assets/models/__init__.py | 1 + apps/assets/models/cmd_filter.py | 226 ++++++++++++++++++++++++ apps/assets/serializers/__init__.py | 1 - apps/assets/serializers/cmd_filter.py | 110 ------------ apps/assets/signal_handlers/asset.py | 17 +- apps/assets/tasks/__init__.py | 2 - apps/assets/tasks/common.py | 36 ---- apps/assets/urls/api_urls.py | 9 +- apps/orgs/signal_handlers/common.py | 3 +- 13 files changed, 240 insertions(+), 258 deletions(-) delete mode 100644 apps/assets/api/cmd_filter.py create mode 100644 apps/assets/models/cmd_filter.py delete mode 100644 apps/assets/serializers/cmd_filter.py diff --git a/apps/applications/models/application.py b/apps/applications/models/application.py index 000094b4f..af1e27c2d 100644 --- a/apps/applications/models/application.py +++ b/apps/applications/models/application.py @@ -9,7 +9,7 @@ from orgs.mixins.models import OrgModelMixin from common.mixins import CommonModelMixin from common.tree import TreeNode from common.utils import is_uuid -from assets.models import Asset +from assets.models import Asset, SystemUser from ..utils import KubernetesTree from .. import const diff --git a/apps/assets/api/__init__.py b/apps/assets/api/__init__.py index e8b06b537..b8cc9e7d8 100644 --- a/apps/assets/api/__init__.py +++ b/apps/assets/api/__init__.py @@ -1,11 +1,9 @@ from .mixin import * from .asset import * from .label import * -from .system_user import * from .accounts import * from .node import * from .domain import * -from .cmd_filter import * from .gathered_user import * from .favorite_asset import * from .account_backup import * diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index a930bfd41..d8ff1547f 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -19,8 +19,8 @@ from assets.api import FilterAssetByNodeMixin from ..models import Asset, Node, Platform, Gateway from .. import serializers from ..tasks import ( - update_assets_hardware_info_manual, test_assets_connectivity_manual, - test_system_users_connectivity_a_asset, push_system_users_a_asset + update_assets_hardware_info_manual, + test_assets_connectivity_manual, ) from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend diff --git a/apps/assets/api/cmd_filter.py b/apps/assets/api/cmd_filter.py deleted file mode 100644 index 0e09d5c73..000000000 --- a/apps/assets/api/cmd_filter.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from rest_framework.response import Response -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 ..models import CommandFilter, CommandFilterRule -from .. import serializers - -__all__ = [ - 'CommandFilterViewSet', 'CommandFilterRuleViewSet', 'CommandConfirmAPI', -] - - -class CommandFilterViewSet(OrgBulkModelViewSet): - model = CommandFilter - filterset_fields = ("name",) - search_fields = filterset_fields - serializer_class = serializers.CommandFilterSerializer - - -class CommandFilterRuleViewSet(OrgBulkModelViewSet): - model = CommandFilterRule - filterset_fields = ('content',) - search_fields = filterset_fields - serializer_class = serializers.CommandFilterRuleSerializer - - def get_queryset(self): - fpk = self.kwargs.get('filter_pk') - if not fpk: - return CommandFilterRule.objects.none() - cmd_filter = get_object_or_404(CommandFilter, pk=fpk) - return cmd_filter.rules.all() - - -class CommandConfirmAPI(CreateAPIView): - serializer_class = serializers.CommandConfirmSerializer - rbac_perms = { - 'POST': 'tickets.add_superticket' - } - - def create(self, request, *args, **kwargs): - ticket = self.create_command_confirm_ticket() - response_data = self.get_response_data(ticket) - return Response(data=response_data, status=200) - - def create_command_confirm_ticket(self): - ticket = self.serializer.cmd_filter_rule.create_command_confirm_ticket( - run_command=self.serializer.data.get('run_command'), - session=self.serializer.session, - cmd_filter_rule=self.serializer.cmd_filter_rule, - org_id=self.serializer.org.id, - ) - return ticket - - @staticmethod - def get_response_data(ticket): - confirm_status_url = reverse( - view_name='api-tickets:super-ticket-status', - kwargs={'pk': str(ticket.id)} - ) - ticket_detail_url = reverse( - view_name='api-tickets:ticket-detail', - kwargs={'pk': str(ticket.id)}, - external=True, api_to_ui=True - ) - ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type) - ticket_assignees = ticket.current_step.ticket_assignees.all() - return { - 'check_confirm_status': {'method': 'GET', 'url': confirm_status_url}, - 'close_confirm': {'method': 'DELETE', 'url': confirm_status_url}, - 'ticket_detail_url': ticket_detail_url, - 'reviewers': [str(ticket_assignee.assignee) for ticket_assignee in ticket_assignees] - } - - @lazyproperty - def serializer(self): - serializer = self.get_serializer(data=self.request.data) - serializer.is_valid(raise_exception=True) - return serializer - diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index 4eeab7b0b..7719ca63a 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -10,3 +10,4 @@ from .favorite_asset import * from .account import * from .backup import * from .user import * +from .cmd_filter import * diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py new file mode 100644 index 000000000..c7fa33aae --- /dev/null +++ b/apps/assets/models/cmd_filter.py @@ -0,0 +1,226 @@ +# -*- coding: utf-8 -*- +# +import uuid +import re + +from django.db import models +from django.db.models import Q +from django.core.validators import MinValueValidator, MaxValueValidator +from django.utils.translation import ugettext_lazy as _ + +from users.models import User, UserGroup +from applications.models import Application +from ..models import SystemUser, Asset + +from common.utils import lazyproperty, get_logger, get_object_or_none +from orgs.mixins.models import OrgModelMixin + +logger = get_logger(__file__) + +__all__ = [ + 'CommandFilter', 'CommandFilterRule' +] + + +class CommandFilter(OrgModelMixin): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + name = models.CharField(max_length=64, verbose_name=_("Name")) + users = models.ManyToManyField( + 'users.User', related_name='cmd_filters', blank=True, + verbose_name=_("User") + ) + user_groups = models.ManyToManyField( + 'users.UserGroup', related_name='cmd_filters', blank=True, + verbose_name=_("User group"), + ) + assets = models.ManyToManyField( + 'assets.Asset', related_name='cmd_filters', blank=True, + verbose_name=_("Asset") + ) + system_users = models.ManyToManyField( + 'assets.SystemUser', related_name='cmd_filters', blank=True, + verbose_name=_("System user")) + applications = models.ManyToManyField( + 'applications.Application', related_name='cmd_filters', blank=True, + verbose_name=_("Application") + ) + is_active = models.BooleanField(default=True, verbose_name=_('Is active')) + comment = models.TextField(blank=True, default='', verbose_name=_("Comment")) + date_created = models.DateTimeField(auto_now_add=True) + date_updated = models.DateTimeField(auto_now=True) + created_by = models.CharField( + max_length=128, blank=True, default='', verbose_name=_('Created by') + ) + + def __str__(self): + return self.name + + class Meta: + unique_together = [('org_id', 'name')] + verbose_name = _("Command filter") + + +class CommandFilterRule(OrgModelMixin): + TYPE_REGEX = 'regex' + TYPE_COMMAND = 'command' + TYPE_CHOICES = ( + (TYPE_REGEX, _('Regex')), + (TYPE_COMMAND, _('Command')), + ) + + ACTION_UNKNOWN = 10 + + class ActionChoices(models.IntegerChoices): + deny = 0, _('Deny') + allow = 9, _('Allow') + confirm = 2, _('Reconfirm') + + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + filter = models.ForeignKey( + 'CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules' + ) + type = models.CharField(max_length=16, default=TYPE_COMMAND, choices=TYPE_CHOICES, verbose_name=_("Type")) + priority = models.IntegerField( + default=50, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), + validators=[MinValueValidator(1), MaxValueValidator(100)] + ) + content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command")) + ignore_case = models.BooleanField(default=True, verbose_name=_('Ignore case')) + action = models.IntegerField(default=ActionChoices.deny, choices=ActionChoices.choices, verbose_name=_("Action")) + # 动作: 附加字段 + # - confirm: 命令复核人 + reviewers = models.ManyToManyField( + 'users.User', related_name='review_cmd_filter_rules', blank=True, + verbose_name=_("Reviewers") + ) + comment = models.CharField(max_length=64, blank=True, default='', verbose_name=_("Comment")) + date_created = models.DateTimeField(auto_now_add=True) + date_updated = models.DateTimeField(auto_now=True) + created_by = models.CharField(max_length=128, blank=True, default='', verbose_name=_('Created by')) + + class Meta: + ordering = ('priority', 'action') + verbose_name = _("Command filter rule") + + @lazyproperty + def pattern(self): + if self.type == 'command': + s = self.construct_command_regex(content=self.content) + else: + s = r'{0}'.format(self.content) + + return s + + @classmethod + def construct_command_regex(cls, content): + regex = [] + content = content.replace('\r\n', '\n') + for _cmd in content.split('\n'): + cmd = re.sub(r'\s+', ' ', _cmd) + cmd = re.escape(cmd) + cmd = cmd.replace('\\ ', '\s+') + + # 有空格就不能 铆钉单词了 + if ' ' in _cmd: + regex.append(cmd) + continue + + if not cmd: + continue + + # 如果是单个字符 + if cmd[-1].isalpha(): + regex.append(r'\b{0}\b'.format(cmd)) + else: + regex.append(r'\b{0}'.format(cmd)) + s = r'{}'.format('|'.join(regex)) + return s + + @staticmethod + def compile_regex(regex, ignore_case): + try: + if ignore_case: + pattern = re.compile(regex, re.IGNORECASE) + else: + pattern = re.compile(regex) + except Exception as e: + error = _('The generated regular expression is incorrect: {}').format(str(e)) + logger.error(error) + return False, error, None + return True, '', pattern + + def match(self, data): + succeed, error, pattern = self.compile_regex(self.pattern, self.ignore_case) + if not succeed: + return self.ACTION_UNKNOWN, '' + + found = pattern.search(data) + if not found: + return self.ACTION_UNKNOWN, '' + + if self.action == self.ActionChoices.allow: + return self.ActionChoices.allow, found.group() + else: + return self.ActionChoices.deny, found.group() + + def __str__(self): + return '{} % {}'.format(self.type, self.content) + + def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id): + from tickets.const import TicketType + from tickets.models import ApplyCommandTicket + data = { + 'title': _('Command confirm') + ' ({})'.format(session.user), + 'type': TicketType.command_confirm, + 'applicant': session.user_obj, + 'apply_run_user_id': session.user_id, + 'apply_run_asset': str(session.asset), + 'apply_run_system_user_id': session.system_user_id, + 'apply_run_command': run_command[:4090], + 'apply_from_session_id': str(session.id), + 'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id), + 'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id), + 'org_id': org_id, + } + ticket = ApplyCommandTicket.objects.create(**data) + assignees = self.reviewers.all() + ticket.open_by_system(assignees) + return ticket + + @classmethod + def get_queryset(cls, user_id=None, user_group_id=None, system_user_id=None, + asset_id=None, application_id=None, org_id=None): + user_groups = [] + user = get_object_or_none(User, pk=user_id) + if user: + user_groups.extend(list(user.groups.all())) + user_group = get_object_or_none(UserGroup, pk=user_group_id) + if user_group: + org_id = user_group.org_id + user_groups.append(user_group) + system_user = get_object_or_none(SystemUser, pk=system_user_id) + asset = get_object_or_none(Asset, pk=asset_id) + application = get_object_or_none(Application, pk=application_id) + q = Q() + if user: + q |= Q(users=user) + if user_groups: + q |= Q(user_groups__in=set(user_groups)) + if system_user: + org_id = system_user.org_id + q |= Q(system_users=system_user) + if asset: + org_id = asset.org_id + q |= Q(assets=asset) + if application: + org_id = application.org_id + q |= Q(applications=application) + if q: + cmd_filters = CommandFilter.objects.filter(q).filter(is_active=True) + if org_id: + cmd_filters = cmd_filters.filter(org_id=org_id) + rule_ids = cmd_filters.values_list('rules', flat=True) + rules = cls.objects.filter(id__in=rule_ids) + else: + rules = cls.objects.none() + return rules diff --git a/apps/assets/serializers/__init__.py b/apps/assets/serializers/__init__.py index 5c684652c..9f41d1020 100644 --- a/apps/assets/serializers/__init__.py +++ b/apps/assets/serializers/__init__.py @@ -6,7 +6,6 @@ from .label import * from .system_user import * from .node import * from .domain import * -from .cmd_filter import * from .gathered_user import * from .favorite_asset import * from .account import * diff --git a/apps/assets/serializers/cmd_filter.py b/apps/assets/serializers/cmd_filter.py deleted file mode 100644 index 9a33dd6fa..000000000 --- a/apps/assets/serializers/cmd_filter.py +++ /dev/null @@ -1,110 +0,0 @@ -# -*- coding: utf-8 -*- -# -import re -from rest_framework import serializers - -from django.utils.translation import ugettext_lazy as _ -from ..models import CommandFilter, CommandFilterRule -from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from orgs.utils import tmp_to_root_org -from common.utils import get_object_or_none, lazyproperty -from terminal.models import Session - - -class CommandFilterSerializer(BulkOrgResourceModelSerializer): - class Meta: - model = CommandFilter - fields_mini = ['id', 'name'] - fields_small = fields_mini + [ - 'org_id', 'org_name', 'is_active', - 'date_created', 'date_updated', - 'comment', 'created_by', - ] - fields_fk = ['rules'] - fields_m2m = ['users', 'user_groups', 'system_users', 'assets', 'applications'] - fields = fields_small + fields_fk + fields_m2m - extra_kwargs = { - 'rules': {'read_only': True}, - 'date_created': {'label': _("Date created")}, - 'date_updated': {'label': _("Date updated")}, - } - - -class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer): - type_display = serializers.ReadOnlyField(source='get_type_display', label=_("Type display")) - action_display = serializers.ReadOnlyField(source='get_action_display', label=_("Action display")) - - class Meta: - model = CommandFilterRule - fields_mini = ['id'] - fields_small = fields_mini + [ - 'type', 'type_display', 'content', 'ignore_case', 'pattern', - 'priority', 'action', 'action_display', 'reviewers', - 'date_created', 'date_updated', 'comment', 'created_by', - ] - fields_fk = ['filter'] - fields = fields_small + fields_fk - extra_kwargs = { - 'date_created': {'label': _("Date created")}, - 'date_updated': {'label': _("Date updated")}, - 'action_display': {'label': _("Action display")}, - 'pattern': {'label': _("Pattern")} - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.set_action_choices() - - def set_action_choices(self): - from django.conf import settings - action = self.fields.get('action') - if not action: - return - choices = action._choices - if not settings.XPACK_ENABLED: - choices.pop(CommandFilterRule.ActionChoices.confirm, None) - action._choices = choices - - def validate_content(self, content): - tp = self.initial_data.get("type") - if tp == CommandFilterRule.TYPE_COMMAND: - regex = CommandFilterRule.construct_command_regex(content) - else: - regex = content - ignore_case = self.initial_data.get('ignore_case') - succeed, error, pattern = CommandFilterRule.compile_regex(regex, ignore_case) - if not succeed: - raise serializers.ValidationError(error) - return content - - -class CommandConfirmSerializer(serializers.Serializer): - session_id = serializers.UUIDField(required=True, allow_null=False) - cmd_filter_rule_id = serializers.UUIDField(required=True, allow_null=False) - run_command = serializers.CharField(required=True, allow_null=False) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.session = None - self.cmd_filter_rule = None - - def validate_session_id(self, session_id): - self.session = self.validate_object_exist(Session, session_id) - return session_id - - def validate_cmd_filter_rule_id(self, cmd_filter_rule_id): - self.cmd_filter_rule = self.validate_object_exist(CommandFilterRule, cmd_filter_rule_id) - return cmd_filter_rule_id - - @staticmethod - def validate_object_exist(model, field_id): - with tmp_to_root_org(): - obj = get_object_or_none(model, id=field_id) - if not obj: - error = '{} Model object does not exist'.format(model.__name__) - raise serializers.ValidationError(error) - return obj - - @lazyproperty - def org(self): - return self.session.org diff --git a/apps/assets/signal_handlers/asset.py b/apps/assets/signal_handlers/asset.py index a7e466c9b..5aac26319 100644 --- a/apps/assets/signal_handlers/asset.py +++ b/apps/assets/signal_handlers/asset.py @@ -8,11 +8,10 @@ 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.models import Asset, Node from assets.tasks import ( update_assets_hardware_info_util, test_asset_connectivity_util, - push_system_user_to_assets, ) logger = get_logger(__file__) @@ -77,15 +76,15 @@ def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs): 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) + # 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')) + # 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: diff --git a/apps/assets/tasks/__init__.py b/apps/assets/tasks/__init__.py index 22ccbf503..fcd5fbe46 100644 --- a/apps/assets/tasks/__init__.py +++ b/apps/assets/tasks/__init__.py @@ -6,7 +6,5 @@ from .asset_connectivity import * from .account_connectivity import * from .gather_asset_users import * from .gather_asset_hardware_info import * -from .push_system_user import * -from .system_user_connectivity import * from .nodes_amount import * from .backup import * diff --git a/apps/assets/tasks/common.py b/apps/assets/tasks/common.py index 6ad17b4c3..ec51c5a2b 100644 --- a/apps/assets/tasks/common.py +++ b/apps/assets/tasks/common.py @@ -1,38 +1,2 @@ # -*- coding: utf-8 -*- # - -from celery import shared_task - -from orgs.utils import tmp_to_root_org -from assets.models import AuthBook - -__all__ = ['add_nodes_assets_to_system_users'] - - -# Todo: 等待优化 -@shared_task -@tmp_to_root_org() -def add_nodes_assets_to_system_users(nodes_keys, system_users): - from ..models import Node - from assets.tasks import push_system_user_to_assets - - nodes = Node.objects.filter(key__in=nodes_keys) - assets = Node.get_nodes_all_assets(*nodes) - - for system_user in system_users: - """ 解决资产和节点进行关联时,已经关联过的节点不会触发 authbook post_save 信号, - 无法更新节点下所有资产的管理用户的问题 """ - need_push_asset_ids = [] - for asset in assets: - defaults = {'asset': asset, 'systemuser': system_user, 'org_id': asset.org_id} - instance, created = AuthBook.objects.update_or_create( - defaults=defaults, asset=asset, systemuser=system_user - ) - if created: - need_push_asset_ids.append(asset.id) - # 不再自动更新资产管理用户,只允许用户手动指定。 - # 只要关联都需要更新资产的管理用户 - # instance.update_asset_admin_user_if_need() - - if need_push_asset_ids: - push_system_user_to_assets.delay(system_user.id, need_push_asset_ids) diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index b312a41b7..d3262bd96 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -20,15 +20,11 @@ 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'gathered-users', api.GatheredUserViewSet, 'gathered-user') router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset') router.register(r'account-backup-plans', api.AccountBackupPlanViewSet, 'account-backup') router.register(r'account-backup-plan-executions', api.AccountBackupPlanExecutionViewSet, 'account-backup-execution') -cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filters', lookup='filter') -cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule') - urlpatterns = [ path('assets//gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'), @@ -54,10 +50,7 @@ urlpatterns = [ path('nodes//tasks/', api.NodeTaskCreateApi.as_view(), name='node-task-create'), path('gateways//test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'), - - path('cmd-filters/command-confirm/', api.CommandConfirmAPI.as_view(), name='command-confirm'), - ] -urlpatterns += router.urls + cmd_filter_router.urls +urlpatterns += router.urls diff --git a/apps/orgs/signal_handlers/common.py b/apps/orgs/signal_handlers/common.py index 48576a828..fc8b4af12 100644 --- a/apps/orgs/signal_handlers/common.py +++ b/apps/orgs/signal_handlers/common.py @@ -20,7 +20,6 @@ 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 -from assets.models import CommandFilterRule from users.signals import post_user_leave_org @@ -141,7 +140,7 @@ def _clear_users_from_org(org, users): for m in models: _remove_users(m, users, org) - _remove_users(CommandFilterRule, users, org, user_field_name='reviewers') + # _remove_users(CommandFilterRule, users, org, user_field_name='reviewers') @receiver(post_save, sender=User) From 65423ea893b0b8cd4364355a0ef4ddd0ce1984c1 Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 3 Aug 2022 15:58:06 +0800 Subject: [PATCH 8/8] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96migrations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/applications/models/application.py | 3 +- apps/assets/migrations/0001_initial.py | 9 - .../migrations/0003_auto_20180109_2331.py | 5 - .../migrations/0092_auto_20220711_1409.py | 5 +- .../migrations/0094_auto_20220728_1125.py | 89 --------- .../migrations/0094_auto_20220803_1448.py | 16 ++ apps/assets/models/__init__.py | 5 +- apps/assets/models/_authbook.py | 137 +++++++++++++ apps/assets/models/_user.py | 184 ++++++++++++++++++ apps/assets/models/asset.py | 11 -- apps/assets/models/cmd_filter.py | 2 +- apps/assets/models/user.py | 76 -------- 12 files changed, 346 insertions(+), 196 deletions(-) delete mode 100644 apps/assets/migrations/0094_auto_20220728_1125.py create mode 100644 apps/assets/migrations/0094_auto_20220803_1448.py create mode 100644 apps/assets/models/_authbook.py create mode 100644 apps/assets/models/_user.py delete mode 100644 apps/assets/models/user.py diff --git a/apps/applications/models/application.py b/apps/applications/models/application.py index af1e27c2d..258f2fe6e 100644 --- a/apps/applications/models/application.py +++ b/apps/applications/models/application.py @@ -9,7 +9,8 @@ from orgs.mixins.models import OrgModelMixin from common.mixins import CommonModelMixin from common.tree import TreeNode from common.utils import is_uuid -from assets.models import Asset, SystemUser +from assets.models import Asset +from assets.models._user import SystemUser from ..utils import KubernetesTree from .. import const diff --git a/apps/assets/migrations/0001_initial.py b/apps/assets/migrations/0001_initial.py index 7c0a9e95a..03c945c57 100644 --- a/apps/assets/migrations/0001_initial.py +++ b/apps/assets/migrations/0001_initial.py @@ -16,14 +16,6 @@ def add_default_group(apps, schema_editor): ) -def add_default_cluster(apps, schema_editor): - cluster_model = apps.get_model("assets", "Cluster") - db_alias = schema_editor.connection.alias - cluster_model.objects.using(db_alias).create( - name="Default" - ) - - class Migration(migrations.Migration): initial = True @@ -163,6 +155,5 @@ class Migration(migrations.Migration): unique_together=set([('ip', 'port')]), ), - migrations.RunPython(add_default_cluster), migrations.RunPython(add_default_group), ] diff --git a/apps/assets/migrations/0003_auto_20180109_2331.py b/apps/assets/migrations/0003_auto_20180109_2331.py index 254de6236..097bc607a 100644 --- a/apps/assets/migrations/0003_auto_20180109_2331.py +++ b/apps/assets/migrations/0003_auto_20180109_2331.py @@ -14,9 +14,4 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AlterField( - model_name='asset', - name='cluster', - field=models.ForeignKey(default=assets.models.asset.default_cluster, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='assets', to='assets.Cluster', verbose_name='Cluster'), - ), ] diff --git a/apps/assets/migrations/0092_auto_20220711_1409.py b/apps/assets/migrations/0092_auto_20220711_1409.py index efcb59e9d..6515392a0 100644 --- a/apps/assets/migrations/0092_auto_20220711_1409.py +++ b/apps/assets/migrations/0092_auto_20220711_1409.py @@ -1,7 +1,6 @@ # Generated by Django 3.2.12 on 2022-07-11 08:59 import assets.models.base -import assets.models.user import common.db.fields from django.conf import settings from django.db import migrations, models @@ -36,7 +35,7 @@ class Migration(migrations.Migration): ('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')), ('protocol', models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol')), ('type', models.CharField(choices=[('common', 'Common user'), ('admin', 'Admin user')], default='common', max_length=16, verbose_name='Type')), - ('version', models.IntegerField(default=1, verbose_name='Version')), + ('version', models.IntegerField(default=0, verbose_name='Version')), ('history_id', models.AutoField(primary_key=True, serialize=False)), ('history_date', models.DateTimeField(db_index=True)), ('history_change_reason', models.CharField(max_length=100, null=True)), @@ -78,6 +77,6 @@ class Migration(migrations.Migration): 'permissions': [('view_accountsecret', 'Can view asset account secret'), ('change_accountsecret', 'Can change asset account secret'), ('view_historyaccount', 'Can view asset history account'), ('view_historyaccountsecret', 'Can view asset history account secret')], 'unique_together': {('username', 'asset')}, }, - bases=(models.Model, assets.models.base.AuthMixin, assets.models.user.ProtocolMixin), + bases=(models.Model, assets.models.base.AuthMixin, assets.models.protocol.ProtocolMixin), ), ] diff --git a/apps/assets/migrations/0094_auto_20220728_1125.py b/apps/assets/migrations/0094_auto_20220728_1125.py deleted file mode 100644 index adc477c1c..000000000 --- a/apps/assets/migrations/0094_auto_20220728_1125.py +++ /dev/null @@ -1,89 +0,0 @@ -# Generated by Django 3.2.14 on 2022-07-28 03:25 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0093_auto_20220711_1413'), - ] - - operations = [ - migrations.RemoveField( - model_name='cluster', - name='admin_user', - ), - migrations.RemoveField( - model_name='systemuser', - name='ad_domain', - ), - migrations.RemoveField( - model_name='systemuser', - name='assets', - ), - migrations.RemoveField( - model_name='systemuser', - name='auto_push', - ), - migrations.RemoveField( - model_name='systemuser', - name='groups', - ), - migrations.RemoveField( - model_name='systemuser', - name='home', - ), - migrations.RemoveField( - model_name='systemuser', - name='nodes', - ), - migrations.RemoveField( - model_name='systemuser', - name='priority', - ), - migrations.RemoveField( - model_name='systemuser', - name='sftp_root', - ), - migrations.RemoveField( - model_name='systemuser', - name='shell', - ), - migrations.RemoveField( - model_name='systemuser', - name='sudo', - ), - migrations.RemoveField( - model_name='systemuser', - name='system_groups', - ), - migrations.RemoveField( - model_name='systemuser', - name='token', - ), - migrations.RemoveField( - model_name='systemuser', - name='type', - ), - migrations.RemoveField( - model_name='systemuser', - name='users', - ), - migrations.AlterField( - model_name='historicalaccount', - name='version', - field=models.IntegerField(default=0, verbose_name='Version'), - ), - migrations.AlterField( - model_name='systemuser', - name='login_mode', - field=models.CharField(choices=[('auto', '使用账号'), ('manual', 'Manually input')], default='auto', max_length=10, verbose_name='Login mode'), - ), - migrations.DeleteModel( - name='AdminUser', - ), - migrations.DeleteModel( - name='Cluster', - ), - ] diff --git a/apps/assets/migrations/0094_auto_20220803_1448.py b/apps/assets/migrations/0094_auto_20220803_1448.py new file mode 100644 index 000000000..eafb9caca --- /dev/null +++ b/apps/assets/migrations/0094_auto_20220803_1448.py @@ -0,0 +1,16 @@ +# Generated by Django 3.2.14 on 2022-08-03 06:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0093_auto_20220711_1413'), + ] + + operations = [ + migrations.DeleteModel( + name='Cluster', + ), + ] diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index 7719ca63a..e433a95ef 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -1,4 +1,5 @@ from .base import * +from ._user import * from .asset import * from .label import Label from .group import * @@ -9,5 +10,7 @@ from .gathered_user import * from .favorite_asset import * from .account import * from .backup import * -from .user import * +# 废弃以下 +from ._authbook import * +from .protocol import * from .cmd_filter import * diff --git a/apps/assets/models/_authbook.py b/apps/assets/models/_authbook.py new file mode 100644 index 000000000..e96196d22 --- /dev/null +++ b/apps/assets/models/_authbook.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +# + +from django.db import models +from django.db.models import F +from django.utils.translation import ugettext_lazy as _ +from simple_history.models import HistoricalRecords + +from common.utils import lazyproperty, get_logger +from .base import BaseUser, AbsConnectivity + +logger = get_logger(__name__) + + +__all__ = ['AuthBook'] + + +class AuthBook(BaseUser, AbsConnectivity): + asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset')) + 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() + + auth_attrs = ['username', 'password', 'private_key', 'public_key'] + + class Meta: + verbose_name = _('AuthBook') + unique_together = [('username', 'asset', 'systemuser')] + permissions = [ + ('test_authbook', _('Can test asset account connectivity')), + ('view_assetaccountsecret', _('Can view asset account secret')), + ('change_assetaccountsecret', _('Can change asset account secret')), + ('view_assethistoryaccount', _('Can view asset history account')), + ('view_assethistoryaccountsecret', _('Can view asset history account secret')), + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.auth_snapshot = {} + + 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 '' + + 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) + + 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 username_display(self): + return self.get_or_systemuser_attr('username') or '*' + + @lazyproperty + def systemuser_display(self): + if not self.systemuser: + return '' + return str(self.systemuser) + + @property + def smart_name(self): + username = self.username_display + + if self.asset: + asset = str(self.asset) + else: + asset = '*' + return '{}@{}'.format(username, asset) + + 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) + + # 不触发post_save信号 + self.__class__.objects.bulk_update(matched, fields=['password', 'private_key', 'public_key']) + + def remove_asset_admin_user_if_need(self): + if not self.asset or not self.systemuser: + return + if not self.systemuser.is_admin_user or self.asset.admin_user != self.systemuser: + return + self.asset.admin_user = None + self.asset.save() + logger.debug('Remove asset admin user: {} {}'.format(self.asset, self.systemuser)) + + def update_asset_admin_user_if_need(self): + if not self.asset or not self.systemuser: + return + if not self.systemuser.is_admin_user or self.asset.admin_user == self.systemuser: + return + self.asset.admin_user = self.systemuser + self.asset.save() + logger.debug('Update asset admin user: {} {}'.format(self.asset, self.systemuser)) + + @classmethod + def get_queryset(cls): + queryset = cls.objects.all() \ + .annotate(ip=F('asset__ip')) \ + .annotate(hostname=F('asset__hostname')) \ + .annotate(platform=F('asset__platform__name')) \ + .annotate(protocols=F('asset__protocols')) + return queryset + + def __str__(self): + return self.smart_name diff --git a/apps/assets/models/_user.py b/apps/assets/models/_user.py new file mode 100644 index 000000000..565513c11 --- /dev/null +++ b/apps/assets/models/_user.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# + +import logging + +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.core.validators import MinValueValidator, MaxValueValidator + +from .base import BaseUser +from .protocol import ProtocolMixin + + +__all__ = ['SystemUser'] +logger = logging.getLogger(__name__) + + +class SystemUser(ProtocolMixin, BaseUser): + LOGIN_AUTO = 'auto' + LOGIN_MANUAL = 'manual' + LOGIN_MODE_CHOICES = ( + (LOGIN_AUTO, _('Automatic managed')), + (LOGIN_MANUAL, _('Manually input')) + ) + + class Type(models.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')) + 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) + # linux su 命令 (switch user) + su_enabled = models.BooleanField(default=False, verbose_name=_('User switch')) + su_from = models.ForeignKey('self', on_delete=models.SET_NULL, related_name='su_to', null=True, verbose_name=_("Switch from")) + + def __str__(self): + username = self.username + if self.username_same_with_user: + username = '*' + 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, Asset + 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 add_related_assets(self, assets_or_ids): + self.assets.add(*tuple(assets_or_ids)) + self.add_related_assets_to_su_from_if_need(assets_or_ids) + + def add_related_assets_to_su_from_if_need(self, assets_or_ids): + if self.protocol not in [self.Protocol.ssh.value]: + return + if not self.su_enabled: + return + if not self.su_from: + return + if self.su_from.protocol != self.protocol: + return + self.su_from.assets.add(*tuple(assets_or_ids)) + + class Meta: + ordering = ['name'] + unique_together = [('name', 'org_id')] + verbose_name = _("System user") + permissions = [ + ('match_systemuser', _('Can match system user')), + ] + + +# Deprecated: 准备废弃 +class AdminUser(BaseUser): + """ + A privileged user that ansible can use it to push system user and so on + """ + BECOME_METHOD_CHOICES = ( + ('sudo', 'sudo'), + ('su', 'su'), + ) + become = models.BooleanField(default=True) + become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4) + become_user = models.CharField(default='root', max_length=64) + _become_pass = models.CharField(default='', blank=True, max_length=128) + CONNECTIVITY_CACHE_KEY = '_ADMIN_USER_CONNECTIVE_{}' + _prefer = "admin_user" + + def __str__(self): + return self.name + + @property + def become_pass(self): + password = signer.unsign(self._become_pass) + if password: + return password + else: + return "" + + @become_pass.setter + def become_pass(self, password): + self._become_pass = signer.sign(password) + + @property + def become_info(self): + if self.become: + info = { + "method": self.become_method, + "user": self.become_user, + "pass": self.become_pass, + } + else: + info = None + return info + + class Meta: + ordering = ['name'] + unique_together = [('name', 'org_id')] + verbose_name = _("Admin user") diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index 157895dae..1eedc21dd 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -9,7 +9,6 @@ from collections import OrderedDict from django.db import models from django.utils.translation import ugettext_lazy as _ -from rest_framework.exceptions import ValidationError from common.db.fields import JsonDictTextField from common.utils import lazyproperty @@ -21,16 +20,6 @@ __all__ = ['Asset', 'ProtocolsMixin', 'Platform', 'AssetQuerySet'] logger = logging.getLogger(__name__) -def default_cluster(): - from .cluster import Cluster - name = "Default" - defaults = {"name": name} - cluster, created = Cluster.objects.get_or_create( - defaults=defaults, name=name - ) - return cluster.id - - def default_node(): try: from .node import Node diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py index c7fa33aae..86e613d3d 100644 --- a/apps/assets/models/cmd_filter.py +++ b/apps/assets/models/cmd_filter.py @@ -9,7 +9,6 @@ from django.core.validators import MinValueValidator, MaxValueValidator from django.utils.translation import ugettext_lazy as _ from users.models import User, UserGroup -from applications.models import Application from ..models import SystemUser, Asset from common.utils import lazyproperty, get_logger, get_object_or_none @@ -190,6 +189,7 @@ class CommandFilterRule(OrgModelMixin): @classmethod def get_queryset(cls, user_id=None, user_group_id=None, system_user_id=None, asset_id=None, application_id=None, org_id=None): + from applications.models import Application user_groups = [] user = get_object_or_none(User, pk=user_id) if user: diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py deleted file mode 100644 index 79d6e96e2..000000000 --- a/apps/assets/models/user.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# - -import logging - -from django.db import models -from django.utils.translation import ugettext_lazy as _ -from django.shortcuts import get_object_or_404 -from django.core.cache import cache - -from .base import BaseUser -from .protocol import ProtocolMixin - - -__all__ = ['SystemUser'] -logger = logging.getLogger(__name__) - - -class SystemUser(ProtocolMixin, BaseUser): - LOGIN_AUTO = 'auto' - LOGIN_MANUAL = 'manual' - LOGIN_MODE_CHOICES = ( - (LOGIN_AUTO, _('使用账号')), - (LOGIN_MANUAL, _('Manually input')) - ) - - username_same_with_user = models.BooleanField(default=False, verbose_name=_("Username same with user")) - protocol = models.CharField(max_length=16, choices=ProtocolMixin.Protocol.choices, default='ssh', verbose_name=_('Protocol')) - login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode')) - - # linux su 命令 (switch user) - # Todo: 修改为 username, 不必系统用户了 - su_enabled = models.BooleanField(default=False, verbose_name=_('User switch')) - su_from = models.ForeignKey('self', on_delete=models.SET_NULL, related_name='su_to', null=True, verbose_name=_("Switch from")) - - def __str__(self): - username = self.username - if self.username_same_with_user: - username = '*' - return '{0.name}({1})'.format(self, username) - - @classmethod - def create_accounts_with_assets(cls, asset_ids, system_user_ids): - pass - - def get_manual_account(self, user_id, asset_id): - cache_key = 'manual_account_{}_{}_{}'.format(self.id, user_id, asset_id) - return cache.get(cache_key) - - def create_manual_account(self, user_id, asset_id, account, ttl=300): - cache_key = 'manual_account_{}_{}_{}'.format(self.id, user_id, asset_id) - cache.set(cache_key, account, ttl) - - def get_auto_account(self, user_id, asset_id): - from .account import Account - from users.models import User - username = self.username - if self.username_same_with_user: - user = get_object_or_404(User, id=user_id) - username = user.username - return get_object_or_404(Account, asset_id=asset_id, username=username) - - def get_account(self, user_id, asset_id): - if self.login_mode == self.LOGIN_MANUAL: - return self.get_manual_account(user_id, asset_id) - else: - return self.get_auto_account(user_id, asset_id) - - class Meta: - ordering = ['name'] - unique_together = [('name', 'org_id')] - verbose_name = _("System user") - permissions = [ - ('match_systemuser', _('Can match system user')), - ]