From 90cbb4be2313f50f695e466e8193a123013bee30 Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 22 Oct 2024 17:30:20 +0800 Subject: [PATCH] perf: add check account --- .../accounts/api/automations/check_account.py | 58 +++++++++ apps/accounts/const/automation.py | 3 + .../migrations/0005_account_secret_reset.py | 7 ++ ...0006_accountcheckautomation_accountrisk.py | 112 ++++++++++++++++++ apps/accounts/models/automations/__init__.py | 1 + .../models/automations/scan_account.py | 47 ++++++++ .../serializers/automations/__init__.py | 1 + .../serializers/automations/check_accounts.py | 26 ++++ apps/accounts/tasks/__init__.py | 1 + apps/accounts/tasks/scan_account.py | 19 +++ apps/assets/models/asset/common.py | 4 + apps/assets/serializers/asset/common.py | 8 +- apps/i18n/lina/zh.json | 4 +- 13 files changed, 286 insertions(+), 5 deletions(-) create mode 100644 apps/accounts/api/automations/check_account.py create mode 100644 apps/accounts/migrations/0006_accountcheckautomation_accountrisk.py create mode 100644 apps/accounts/models/automations/scan_account.py create mode 100644 apps/accounts/serializers/automations/check_accounts.py create mode 100644 apps/accounts/tasks/scan_account.py diff --git a/apps/accounts/api/automations/check_account.py b/apps/accounts/api/automations/check_account.py new file mode 100644 index 000000000..6aeb4a196 --- /dev/null +++ b/apps/accounts/api/automations/check_account.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.response import Response + +from accounts import serializers +from accounts.const import AutomationTypes +from accounts.models import AccountCheckAutomation +from accounts.models import AccountRisk +from orgs.mixins.api import OrgBulkModelViewSet +from .base import AutomationExecutionViewSet + +__all__ = [ + 'CheckAccountsAutomationViewSet', 'CheckAccountExecutionViewSet', + 'AccountRiskViewSet' +] + + +class CheckAccountsAutomationViewSet(OrgBulkModelViewSet): + model = AccountCheckAutomation + filterset_fields = ('name',) + search_fields = filterset_fields + serializer_class = serializers.CheckAccountsAutomationSerializer + + +class CheckAccountExecutionViewSet(AutomationExecutionViewSet): + rbac_perms = ( + ("list", "accounts.view_gatheraccountsexecution"), + ("retrieve", "accounts.view_gatheraccountsexecution"), + ("create", "accounts.add_gatheraccountsexecution"), + ) + + tp = AutomationTypes.gather_accounts + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.filter(automation__type=self.tp) + return queryset + + +class AccountRiskViewSet(OrgBulkModelViewSet): + model = AccountRisk + search_fields = ('username',) + filterset_class = AccountRiskFilterSet + serializer_classes = { + 'default': serializers.AccountRiskSerializer, + } + rbac_perms = { + 'sync_accounts': 'assets.add_AccountRisk', + } + + @action(methods=['post'], detail=False, url_path='sync-accounts') + def sync_accounts(self, request, *args, **kwargs): + gathered_account_ids = request.data.get('gathered_account_ids') + gathered_accounts = self.model.objects.filter(id__in=gathered_account_ids) + self.model.sync_accounts(gathered_accounts) + return Response(status=status.HTTP_201_CREATED) diff --git a/apps/accounts/const/automation.py b/apps/accounts/const/automation.py index 4dcdf9041..510283099 100644 --- a/apps/accounts/const/automation.py +++ b/apps/accounts/const/automation.py @@ -27,18 +27,21 @@ class AutomationTypes(models.TextChoices): remove_account = 'remove_account', _('Remove account') gather_accounts = 'gather_accounts', _('Gather accounts') verify_gateway_account = 'verify_gateway_account', _('Verify gateway account') + check_account = 'check_account', _('Check account') @classmethod def get_type_model(cls, tp): from accounts.models import ( PushAccountAutomation, ChangeSecretAutomation, VerifyAccountAutomation, GatherAccountsAutomation, + AccountCheckAutomation, ) type_model_dict = { cls.push_account: PushAccountAutomation, cls.change_secret: ChangeSecretAutomation, cls.verify_account: VerifyAccountAutomation, cls.gather_accounts: GatherAccountsAutomation, + cls.check_account: AccountCheckAutomation, } return type_model_dict.get(tp) diff --git a/apps/accounts/migrations/0005_account_secret_reset.py b/apps/accounts/migrations/0005_account_secret_reset.py index 9e6166acb..c9f9b7b34 100644 --- a/apps/accounts/migrations/0005_account_secret_reset.py +++ b/apps/accounts/migrations/0005_account_secret_reset.py @@ -25,4 +25,11 @@ class Migration(migrations.Migration): verbose_name="Start Datetime", ), ), + migrations.AddField( + model_name="account", + name="date_last_access", + field=models.DateTimeField( + blank=True, null=True, verbose_name="Date last access" + ), + ), ] diff --git a/apps/accounts/migrations/0006_accountcheckautomation_accountrisk.py b/apps/accounts/migrations/0006_accountcheckautomation_accountrisk.py new file mode 100644 index 000000000..155eccf9a --- /dev/null +++ b/apps/accounts/migrations/0006_accountcheckautomation_accountrisk.py @@ -0,0 +1,112 @@ +# Generated by Django 4.1.13 on 2024-10-22 06:36 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ("assets", "0006_baseautomation_start_time"), + ("accounts", "0005_account_secret_reset"), + ] + + operations = [ + migrations.CreateModel( + name="AccountCheckAutomation", + fields=[ + ( + "baseautomation_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="assets.baseautomation", + ), + ), + ], + options={ + "verbose_name": "Gather account automation", + }, + bases=("accounts.accountbaseautomation",), + ), + migrations.CreateModel( + name="AccountRisk", + fields=[ + ( + "created_by", + models.CharField( + blank=True, max_length=128, null=True, verbose_name="Created by" + ), + ), + ( + "updated_by", + models.CharField( + blank=True, max_length=128, null=True, verbose_name="Updated by" + ), + ), + ( + "date_created", + models.DateTimeField( + auto_now_add=True, null=True, verbose_name="Date created" + ), + ), + ( + "date_updated", + models.DateTimeField(auto_now=True, verbose_name="Date updated"), + ), + ( + "comment", + models.TextField(blank=True, default="", verbose_name="Comment"), + ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, primary_key=True, serialize=False + ), + ), + ( + "org_id", + models.CharField( + blank=True, + db_index=True, + default="", + max_length=36, + verbose_name="Organization", + ), + ), + ( + "risk", + models.CharField( + choices=[ + ("zombie", "Zombie"), + ("ghost", "Ghost"), + ("weak_password", "Weak password"), + ("long_time_no_change", "Long time no change"), + ], + max_length=128, + verbose_name="Risk", + ), + ), + ( + "confirmed", + models.BooleanField(default=False, verbose_name="Confirmed"), + ), + ( + "account", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="risks", + to="accounts.account", + verbose_name="Account", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/apps/accounts/models/automations/__init__.py b/apps/accounts/models/automations/__init__.py index 682b182b6..4c329d57b 100644 --- a/apps/accounts/models/automations/__init__.py +++ b/apps/accounts/models/automations/__init__.py @@ -3,4 +3,5 @@ from .backup_account import * from .change_secret import * from .gather_account import * from .push_account import * +from .scan_account import * from .verify_account import * diff --git a/apps/accounts/models/automations/scan_account.py b/apps/accounts/models/automations/scan_account.py new file mode 100644 index 000000000..a34f9f5fe --- /dev/null +++ b/apps/accounts/models/automations/scan_account.py @@ -0,0 +1,47 @@ +from django.db import models +from django.db.models import TextChoices +from django.utils.translation import gettext_lazy as _ + +from common.const import Trigger +from orgs.mixins.models import JMSOrgBaseModel +from .base import AccountBaseAutomation +from ...const import AutomationTypes + +__all__ = ['AccountCheckAutomation', 'AccountRisk'] + + +class AccountCheckAutomation(AccountBaseAutomation): + + def get_register_task(self): + from ...tasks import check_accounts_task + name = "check_accounts_task_period_{}".format(str(self.id)[:8]) + task = check_accounts_task.name + args = (str(self.id), Trigger.timing) + kwargs = {} + return name, task, args, kwargs + + def to_attr_json(self): + attr_json = super().to_attr_json() + attr_json.update({ + }) + return attr_json + + def save(self, *args, **kwargs): + self.type = AutomationTypes.check_account + super().save(*args, **kwargs) + + class Meta: + verbose_name = _('Gather account automation') + + +class RiskChoice(TextChoices): + zombie = 'zombie', _('Zombie') # 好久没登录的账号 + ghost = 'ghost', _('Ghost') # 未被纳管的账号 + weak_password = 'weak_password', _('Weak password') + longtime_no_change = 'long_time_no_change', _('Long time no change') + + +class AccountRisk(JMSOrgBaseModel): + account = models.ForeignKey('Account', on_delete=models.CASCADE, related_name='risks', verbose_name=_('Account')) + risk = models.CharField(max_length=128, verbose_name=_('Risk'), choices=RiskChoice.choices) + confirmed = models.BooleanField(default=False, verbose_name=_('Confirmed')) diff --git a/apps/accounts/serializers/automations/__init__.py b/apps/accounts/serializers/automations/__init__.py index 2b0aa0029..1be63f67e 100644 --- a/apps/accounts/serializers/automations/__init__.py +++ b/apps/accounts/serializers/automations/__init__.py @@ -1,4 +1,5 @@ from .base import * from .change_secret import * +from .check_accounts import * from .gather_accounts import * from .push_account import * diff --git a/apps/accounts/serializers/automations/check_accounts.py b/apps/accounts/serializers/automations/check_accounts.py new file mode 100644 index 000000000..7ab230f41 --- /dev/null +++ b/apps/accounts/serializers/automations/check_accounts.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# +from accounts.const import AutomationTypes +from accounts.models import AccountCheckAutomation +from common.utils import get_logger + +from .base import BaseAutomationSerializer + +logger = get_logger(__file__) + +__all__ = [ + 'CheckAccountsAutomationSerializer', +] + + +class CheckAccountsAutomationSerializer(BaseAutomationSerializer): + class Meta: + model = AccountCheckAutomation + read_only_fields = BaseAutomationSerializer.Meta.read_only_fields + fields = BaseAutomationSerializer.Meta.fields \ + + [] + read_only_fields + extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs + + @property + def model_type(self): + return AutomationTypes.check_account diff --git a/apps/accounts/tasks/__init__.py b/apps/accounts/tasks/__init__.py index a20eba291..2e5b60eef 100644 --- a/apps/accounts/tasks/__init__.py +++ b/apps/accounts/tasks/__init__.py @@ -3,5 +3,6 @@ from .backup_account import * from .gather_accounts import * from .push_account import * from .remove_account import * +from .scan_account import * from .template import * from .verify_account import * diff --git a/apps/accounts/tasks/scan_account.py b/apps/accounts/tasks/scan_account.py new file mode 100644 index 000000000..ad6f005a2 --- /dev/null +++ b/apps/accounts/tasks/scan_account.py @@ -0,0 +1,19 @@ +from celery import shared_task +from django.utils.translation import gettext_lazy as _ + +from common.utils import get_logger + +logger = get_logger(__file__) +__all__ = [ + 'check_accounts_task', +] + + +@shared_task( + queue="ansible", + verbose_name=_('Scan accounts'), + activity_callback=lambda self, node_ids, task_name=None, *args, **kwargs: (node_ids, None), + description=_("Unused") +) +def check_accounts_task(node_ids, task_name=None): + pass diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py index 7388f27a8..92b3c30b8 100644 --- a/apps/assets/models/asset/common.py +++ b/apps/assets/models/asset/common.py @@ -238,6 +238,10 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin, auto_config.update(model_to_dict(automation)) return auto_config + @lazyproperty + def accounts_amount(self): + return self.accounts.count() + def get_target_ip(self): return self.address diff --git a/apps/assets/serializers/asset/common.py b/apps/assets/serializers/asset/common.py index c5708ef3b..6ab538551 100644 --- a/apps/assets/serializers/asset/common.py +++ b/apps/assets/serializers/asset/common.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # -from django.db.models import F +from django.db.models import F, Count from django.db.transaction import atomic from django.utils.translation import gettext_lazy as _ from rest_framework import serializers @@ -148,6 +148,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Accounts')) nodes_display = NodeDisplaySerializer(read_only=False, required=False, label=_("Node path")) platform = ObjectRelatedField(queryset=Platform.objects, required=True, label=_('Platform'), attrs=('id', 'name', 'type')) + accounts_amount = serializers.IntegerField(read_only=True, label=_('Accounts amount')) _accounts = None class Meta: @@ -160,7 +161,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa 'nodes_display', 'accounts', ] read_only_fields = [ - 'category', 'type', 'connectivity', 'auto_config', + 'accounts_amount', 'category', 'type', 'connectivity', 'auto_config', 'date_verified', 'created_by', 'date_created', 'date_updated', ] fields = fields_small + fields_fk + fields_m2m + read_only_fields @@ -228,7 +229,8 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa queryset = queryset.prefetch_related('domain', 'nodes', 'protocols', ) \ .prefetch_related('platform', 'platform__automation') \ .annotate(category=F("platform__category")) \ - .annotate(type=F("platform__type")) + .annotate(type=F("platform__type")) \ + .annotate(assets_amount=Count('accounts')) if queryset.model is Asset: queryset = queryset.prefetch_related('labels__label', 'labels') else: diff --git a/apps/i18n/lina/zh.json b/apps/i18n/lina/zh.json index e1f99b313..3da1b5400 100644 --- a/apps/i18n/lina/zh.json +++ b/apps/i18n/lina/zh.json @@ -7,7 +7,6 @@ "Accept": "同意", "AccessIP": "IP 白名单", "AccessKey": "访问密钥", - "Account": "账号信息", "AccountBackup": "账号备份", "AccountBackupCreate": "创建账号备份", "AccountBackupDetail": "账号备份详情", @@ -22,7 +21,8 @@ "AccountDiscoverTaskCreate": "创建账号发现任务", "AccountDiscoverTaskList": "账号发现任务", "AccountDiscoverTaskUpdate": "更新账号发现任务", - "AccountList": "账号", + "Account": "账号", + "AccountList": "账号列表", "AccountPolicy": "账号策略", "AccountPolicyHelpText": "创建时对于不符合要求的账号,如:密钥类型不合规,唯一键约束,可选择以上策略。", "AccountPushCreate": "创建账号推送",