diff --git a/apps/accounts/api/automations/gather_accounts.py b/apps/accounts/api/automations/gather_accounts.py index e125ed96b..6c669d800 100644 --- a/apps/accounts/api/automations/gather_accounts.py +++ b/apps/accounts/api/automations/gather_accounts.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # +from django.shortcuts import get_object_or_404 from rest_framework import status from rest_framework.decorators import action from rest_framework.response import Response @@ -7,8 +8,9 @@ from rest_framework.response import Response from accounts import serializers from accounts.const import AutomationTypes from accounts.filters import GatheredAccountFilterSet -from accounts.models import GatherAccountsAutomation +from accounts.models import GatherAccountsAutomation, AutomationExecution from accounts.models import GatheredAccount +from assets.models import Asset from orgs.mixins.api import OrgBulkModelViewSet from .base import AutomationExecutionViewSet @@ -49,8 +51,29 @@ class GatheredAccountViewSet(OrgBulkModelViewSet): } rbac_perms = { 'sync_accounts': 'assets.add_gatheredaccount', + 'discover': 'assets.add_gatheredaccount', } + @action(methods=['get'], detail=False, url_path='discover') + def discover(self, request, *args, **kwargs): + asset_id = request.query_params.get('asset_id') + if not asset_id: + return Response(status=status.HTTP_400_BAD_REQUEST, data={'asset_id': 'This field is required.'}) + asset = get_object_or_404(Asset, pk=asset_id) + execution = AutomationExecution() + execution.snapshot = { + 'assets': [asset_id], + 'nodes': [], + 'type': 'gather_accounts', + 'is_sync_account': True, + 'name': 'Adhoc gather accounts: {}'.format(asset_id), + } + execution.save() + execution.start() + accounts = self.model.objects.filter(asset=asset) + serializer = self.get_serializer(accounts, many=True) + return Response(status=status.HTTP_200_OK, data=serializer.data) + @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') diff --git a/apps/accounts/automations/gather_accounts/manager.py b/apps/accounts/automations/gather_accounts/manager.py index e97d78cd0..c78b3b891 100644 --- a/apps/accounts/automations/gather_accounts/manager.py +++ b/apps/accounts/automations/gather_accounts/manager.py @@ -3,6 +3,7 @@ from collections import defaultdict from accounts.const import AutomationTypes from accounts.models import GatheredAccount from assets.models import Asset +from common.const import ConfirmOrIgnore from common.utils import get_logger from orgs.utils import tmp_to_org from users.models import User @@ -70,8 +71,9 @@ class GatherAccountsManager(AccountBasePlaybookManager): def update_or_create_accounts(self): for asset, data in self.asset_account_info.items(): - with tmp_to_org(asset.org_id): + with (tmp_to_org(asset.org_id)): gathered_accounts = [] + # 把所有的设置为 present = False, 创建的时候如果有就会更新 GatheredAccount.objects.filter(asset=asset, present=True).update(present=False) for d in data: username = d['username'] @@ -79,10 +81,16 @@ class GatherAccountsManager(AccountBasePlaybookManager): defaults=d, asset=asset, username=username, ) gathered_accounts.append(gathered_account) + # 不存在的标识为待处理 + GatheredAccount.objects \ + .filter(asset=asset, present=False) \ + .exclude(status=ConfirmOrIgnore.ignored) \ + .update(status='') if not self.is_sync_account: continue GatheredAccount.sync_accounts(gathered_accounts) + def run(self, *args, **kwargs): super().run(*args, **kwargs) users, change_info = self.generate_send_users_and_change_info() diff --git a/apps/accounts/migrations/0010_alter_accountrisk_options_and_more.py b/apps/accounts/migrations/0010_alter_accountrisk_options_and_more.py new file mode 100644 index 000000000..afbde0723 --- /dev/null +++ b/apps/accounts/migrations/0010_alter_accountrisk_options_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 4.1.13 on 2024-10-28 08:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0009_remove_account_date_discovery_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="accountrisk", + options={"verbose_name": "Account risk"}, + ), + migrations.RenameField( + model_name="account", + old_name="date_last_access", + new_name="date_last_login", + ), + migrations.RenameField( + model_name="account", + old_name="access_by", + new_name="login_by", + ), + migrations.AddField( + model_name="gatheredaccount", + name="action", + field=models.CharField( + choices=[ + ("pending", "Pending"), + ("confirm", "Confirm"), + ("ignore", "Ignore"), + ], + default="pending", + max_length=32, + verbose_name="Action", + ), + ), + ] diff --git a/apps/accounts/migrations/0011_remove_gatheredaccount_action_gatheredaccount_status.py b/apps/accounts/migrations/0011_remove_gatheredaccount_action_gatheredaccount_status.py new file mode 100644 index 000000000..d057654ff --- /dev/null +++ b/apps/accounts/migrations/0011_remove_gatheredaccount_action_gatheredaccount_status.py @@ -0,0 +1,28 @@ +# Generated by Django 4.1.13 on 2024-10-28 08:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0010_alter_accountrisk_options_and_more"), + ] + + operations = [ + migrations.RemoveField( + model_name="gatheredaccount", + name="action", + ), + migrations.AddField( + model_name="gatheredaccount", + name="status", + field=models.CharField( + blank=True, + choices=[("confirmed", "Confirmed"), ("ignored", "Ignored")], + default="", + max_length=32, + verbose_name="Action", + ), + ), + ] diff --git a/apps/accounts/models/account.py b/apps/accounts/models/account.py index 70c4b537c..419ed4f8b 100644 --- a/apps/accounts/models/account.py +++ b/apps/accounts/models/account.py @@ -57,8 +57,8 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount): history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version']) source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source')) source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID')) - date_last_access = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last access')) - access_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Access by')) + date_last_login = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last access')) + login_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Access by')) date_change_secret = models.DateTimeField(null=True, blank=True, verbose_name=_('Date change secret')) change_secret_status = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('Change secret status')) diff --git a/apps/accounts/models/automations/gather_account.py b/apps/accounts/models/automations/gather_account.py index 6a40a0498..661ccfd46 100644 --- a/apps/accounts/models/automations/gather_account.py +++ b/apps/accounts/models/automations/gather_account.py @@ -1,9 +1,11 @@ from django.db import models from django.db.models import Q +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from accounts.const import AutomationTypes, Source from accounts.models import Account +from common.const import ConfirmOrIgnore from orgs.mixins.models import JMSOrgBaseModel from .base import AccountBaseAutomation @@ -11,19 +13,46 @@ __all__ = ['GatherAccountsAutomation', 'GatheredAccount'] class GatheredAccount(JMSOrgBaseModel): - present = models.BooleanField(default=True, verbose_name=_("Present")) + present = models.BooleanField(default=True, verbose_name=_("Present")) # 资产上是否还存在 date_last_login = models.DateTimeField(null=True, verbose_name=_("Date login")) asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_("Asset")) username = models.CharField(max_length=32, blank=True, db_index=True, verbose_name=_('Username')) address_last_login = models.CharField(max_length=39, default='', verbose_name=_("Address login")) + status = models.CharField(max_length=32, default='', blank=True, choices=ConfirmOrIgnore.choices, verbose_name=_("Action")) @property def address(self): return self.asset.address - @staticmethod - def sync_accounts(gathered_accounts): + @classmethod + def update_exists_accounts(cls, gathered_account, accounts): + if not gathered_account.date_last_login: + return + + for account in accounts: + if (not account.date_last_login or + account.date_last_login - gathered_account.date_last_login > timezone.timedelta(minutes=5)): + account.date_last_login = gathered_account.date_last_login + account.login_by = '{}({})'.format('unknown', gathered_account.address_last_login) + account.save(update_fields=['date_last_login', 'login_by']) + + @classmethod + def create_accounts(cls, gathered_account, accounts): account_objs = [] + asset_id = gathered_account.asset_id + username = gathered_account.username + access_by = '{}({})'.format('unknown', gathered_account.address_last_login) + account = Account( + asset_id=asset_id, username=username, + name=username, source=Source.COLLECTED, + date_last_access=gathered_account.date_last_login, + access_by=access_by + ) + account_objs.append(account) + Account.objects.bulk_create(account_objs) + + @classmethod + def sync_accounts(cls, gathered_accounts): for gathered_account in gathered_accounts: asset_id = gathered_account.asset_id username = gathered_account.username @@ -32,13 +61,12 @@ class GatheredAccount(JMSOrgBaseModel): Q(asset_id=asset_id, name=username) ) if accounts.exists(): - continue - account = Account( - asset_id=asset_id, username=username, - name=username, source=Source.COLLECTED - ) - account_objs.append(account) - Account.objects.bulk_create(account_objs) + cls.update_exists_accounts(gathered_account, accounts) + else: + cls.create_accounts(gathered_account, accounts) + + gathered_account.status = ConfirmOrIgnore.confirmed + gathered_account.save(update_fields=['action']) class Meta: verbose_name = _("Gather asset accounts") diff --git a/apps/accounts/serializers/account/account.py b/apps/accounts/serializers/account/account.py index 27e3f8303..70982f64e 100644 --- a/apps/accounts/serializers/account/account.py +++ b/apps/accounts/serializers/account/account.py @@ -236,7 +236,7 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize class Meta(BaseAccountSerializer.Meta): model = Account automation_fields = [ - 'date_last_access', 'access_by', 'date_verified', 'connectivity', + 'date_last_login', 'login_by', 'date_verified', 'connectivity', 'date_change_secret', 'change_secret_status' ] fields = BaseAccountSerializer.Meta.fields + [ diff --git a/apps/accounts/serializers/account/gathered_account.py b/apps/accounts/serializers/account/gathered_account.py index a36bddc5e..a3d35d9bc 100644 --- a/apps/accounts/serializers/account/gathered_account.py +++ b/apps/accounts/serializers/account/gathered_account.py @@ -2,10 +2,15 @@ from django.utils.translation import gettext_lazy as _ from accounts.models import GatheredAccount from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from .account import AccountAssetSerializer +from .account import AccountAssetSerializer as _AccountAssetSerializer from .base import BaseAccountSerializer +class AccountAssetSerializer(_AccountAssetSerializer): + class Meta(_AccountAssetSerializer.Meta): + fields = [f for f in _AccountAssetSerializer.Meta.fields if f != 'auto_config'] + + class GatheredAccountSerializer(BulkOrgResourceModelSerializer): asset = AccountAssetSerializer(label=_('Asset')) @@ -13,7 +18,8 @@ class GatheredAccountSerializer(BulkOrgResourceModelSerializer): model = GatheredAccount fields = [ 'id', 'present', 'asset', 'username', - 'date_updated', 'address_last_login', 'date_last_login' + 'date_updated', 'address_last_login', + 'date_last_login', 'status' ] @classmethod diff --git a/apps/accounts/tasks/gather_accounts.py b/apps/accounts/tasks/gather_accounts.py index 831a9fdf6..a02055a63 100644 --- a/apps/accounts/tasks/gather_accounts.py +++ b/apps/accounts/tasks/gather_accounts.py @@ -1,39 +1,33 @@ # ~*~ coding: utf-8 ~*~ -from celery import shared_task -from django.utils.translation import gettext_lazy as _ -from django.utils.translation import gettext_noop -from accounts.const import AutomationTypes -from accounts.tasks.common import quickstart_automation_by_snapshot -from assets.models import Node from common.utils import get_logger -from orgs.utils import org_aware_func -__all__ = ['gather_asset_accounts_task'] +# __all__ = ['gather_asset_accounts_task'] logger = get_logger(__name__) - -@org_aware_func("nodes") -def gather_asset_accounts_util(nodes, task_name): - from accounts.models import GatherAccountsAutomation - task_name = GatherAccountsAutomation.generate_unique_name(task_name) - - task_snapshot = { - 'nodes': [str(node.id) for node in nodes], - } - tp = AutomationTypes.verify_account - quickstart_automation_by_snapshot(task_name, tp, task_snapshot) - - -@shared_task( - queue="ansible", - verbose_name=_('Gather asset accounts'), - activity_callback=lambda self, node_ids, task_name=None, *args, **kwargs: (node_ids, None), - description=_("Unused") -) -def gather_asset_accounts_task(node_ids, task_name=None): - if task_name is None: - task_name = gettext_noop("Gather assets accounts") - - nodes = Node.objects.filter(id__in=node_ids) - gather_asset_accounts_util(nodes=nodes, task_name=task_name) +# +# @org_aware_func("nodes") +# def gather_asset_accounts_util(nodes, task_name): +# from accounts.models import GatherAccountsAutomation +# task_name = GatherAccountsAutomation.generate_unique_name(task_name) +# +# task_snapshot = { +# 'nodes': [str(node.id) for node in nodes], +# } +# tp = AutomationTypes.verify_account +# quickstart_automation_by_snapshot(task_name, tp, task_snapshot) +# +# +# @shared_task( +# queue="ansible", +# verbose_name=_('Gather asset accounts'), +# activity_callback=lambda self, node_ids, task_name=None, *args, **kwargs: (node_ids, None), +# description=_("Unused") +# ) +# def gather_asset_accounts_task(node_ids, task_name=None): +# if task_name is None: +# task_name = gettext_noop("Gather assets accounts") +# +# nodes = Node.objects.filter(id__in=node_ids) +# gather_asset_accounts_util(nodes=nodes, task_name=task_name) +# diff --git a/apps/assets/models/automations/base.py b/apps/assets/models/automations/base.py index da0f73a1e..660552b8f 100644 --- a/apps/assets/models/automations/base.py +++ b/apps/assets/models/automations/base.py @@ -131,8 +131,8 @@ class AutomationExecution(OrgModelMixin): return self.snapshot['type'] def get_all_asset_ids(self): - node_ids = self.snapshot['nodes'] - asset_ids = self.snapshot['assets'] + node_ids = self.snapshot.get('nodes', []) + asset_ids = self.snapshot.get('assets', []) nodes = Node.objects.filter(id__in=node_ids) node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True) asset_ids = set(list(asset_ids) + list(node_asset_ids)) diff --git a/apps/common/const/choices.py b/apps/common/const/choices.py index 3eb0f2a58..935c68992 100644 --- a/apps/common/const/choices.py +++ b/apps/common/const/choices.py @@ -75,4 +75,9 @@ class Language(models.TextChoices): jp = 'ja', '日本語', +class ConfirmOrIgnore(models.TextChoices): + confirmed = 'confirmed', _('Confirmed') + ignored = 'ignored', _('Ignored') + + COUNTRY_CALLING_CODES = get_country_phone_choices() diff --git a/apps/templates/_head_css_js.html b/apps/templates/_head_css_js.html index c9ff646e4..b6ed25e61 100644 --- a/apps/templates/_head_css_js.html +++ b/apps/templates/_head_css_js.html @@ -3,6 +3,7 @@ +