From 0dc3d43ee53aa0de7ed49015996fb6402e018529 Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 28 Jul 2022 19:12:27 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=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