diff --git a/apps/accounts/api/automations/change_secret.py b/apps/accounts/api/automations/change_secret.py index 5ea945b57..ba5ab6101 100644 --- a/apps/accounts/api/automations/change_secret.py +++ b/apps/accounts/api/automations/change_secret.py @@ -6,10 +6,13 @@ from rest_framework.decorators import action from rest_framework.response import Response from accounts import serializers -from accounts.const import AutomationTypes, ChangeSecretRecordStatusChoice -from accounts.filters import ChangeSecretRecordFilterSet -from accounts.models import ChangeSecretAutomation, ChangeSecretRecord +from accounts.const import ( + AutomationTypes, ChangeSecretRecordStatusChoice +) +from accounts.filters import ChangeSecretRecordFilterSet, ChangeSecretStatusFilterSet +from accounts.models import ChangeSecretAutomation, ChangeSecretRecord, Account from accounts.tasks import execute_automation_record_task +from accounts.utils import account_secret_task_status from authentication.permissions import UserConfirmation, ConfirmType from common.permissions import IsValidLicense from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet @@ -23,7 +26,7 @@ __all__ = [ 'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet', 'ChangSecretExecutionViewSet', 'ChangSecretAssetsListApi', 'ChangSecretRemoveAssetApi', 'ChangSecretAddAssetApi', - 'ChangSecretNodeAddRemoveApi' + 'ChangSecretNodeAddRemoveApi', 'ChangeSecretStatusViewSet' ] @@ -154,3 +157,24 @@ class ChangSecretAddAssetApi(AutomationAddAssetApi): class ChangSecretNodeAddRemoveApi(AutomationNodeAddRemoveApi): model = ChangeSecretAutomation serializer_class = serializers.ChangeSecretUpdateNodeSerializer + + +class ChangeSecretStatusViewSet(OrgBulkModelViewSet): + perm_model = ChangeSecretAutomation + filterset_class = ChangeSecretStatusFilterSet + serializer_class = serializers.ChangeSecretAccountSerializer + + permission_classes = [RBACPermission, IsValidLicense] + http_method_names = ["get", "delete", "options"] + + def get_queryset(self): + account_ids = list(account_secret_task_status.account_ids) + return Account.objects.filter(id__in=account_ids).select_related('asset') + + def bulk_destroy(self, request, *args, **kwargs): + account_ids = request.data.get('account_ids') + if isinstance(account_ids, str): + account_ids = [account_ids] + for _id in account_ids: + account_secret_task_status.clear(_id) + return Response(status=status.HTTP_200_OK) diff --git a/apps/accounts/automations/base/manager.py b/apps/accounts/automations/base/manager.py index 4c8263e5d..b708490b8 100644 --- a/apps/accounts/automations/base/manager.py +++ b/apps/accounts/automations/base/manager.py @@ -5,9 +5,10 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from accounts.automations.methods import platform_automation_methods -from accounts.const import SSHKeyStrategy, SecretStrategy, SecretType, ChangeSecretRecordStatusChoice +from accounts.const import SSHKeyStrategy, SecretStrategy, SecretType, ChangeSecretRecordStatusChoice, \ + ChangeSecretAccountStatus from accounts.models import BaseAccountQuerySet -from accounts.utils import SecretGenerator +from accounts.utils import SecretGenerator, account_secret_task_status from assets.automations.base.manager import BasePlaybookManager from assets.const import HostTypes from common.db.utils import safe_atomic_db_connection @@ -132,10 +133,24 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager): for account in accounts: h = deepcopy(host) h['name'] += '(' + account.username + ')' # To distinguish different accounts + + account_status = account_secret_task_status.get_status(account.id) + if account_status == ChangeSecretAccountStatus.PROCESSING: + h['error'] = f'Account is already being processed, skipping: {account}' + inventory_hosts.append(h) + continue + try: h = self.gen_account_inventory(account, asset, h, path_dir) + account_secret_task_status.set_status( + account.id, + ChangeSecretAccountStatus.PROCESSING, + metadata={'execution_id': self.execution.id} + ) except Exception as e: h['error'] = str(e) + self.clear_account_queue_status(account) + inventory_hosts.append(h) return inventory_hosts @@ -144,6 +159,10 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager): def save_record(recorder): recorder.save(update_fields=['error', 'status', 'date_finished']) + @staticmethod + def clear_account_queue_status(account): + account_secret_task_status.clear(account.id) + def on_host_success(self, host, result): recorder = self.name_recorder_mapper.get(host) if not recorder: @@ -173,6 +192,7 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager): with safe_atomic_db_connection(): account.save(update_fields=['secret', 'date_updated', 'date_change_secret', 'change_secret_status']) self.save_record(recorder) + self.clear_account_queue_status(account) def on_host_error(self, host, error, result): recorder = self.name_recorder_mapper.get(host) @@ -201,3 +221,4 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager): with safe_atomic_db_connection(): account.save(update_fields=['change_secret_status', 'date_change_secret', 'date_updated']) self.save_record(recorder) + self.clear_account_queue_status(account) diff --git a/apps/accounts/const/automation.py b/apps/accounts/const/automation.py index 6db695a51..43d0cabcb 100644 --- a/apps/accounts/const/automation.py +++ b/apps/accounts/const/automation.py @@ -17,7 +17,7 @@ __all__ = [ 'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity', 'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice', 'PushAccountActionChoice', 'AccountBackupType', 'ChangeSecretRecordStatusChoice', - 'GatherAccountDetailField' + 'GatherAccountDetailField', 'ChangeSecretAccountStatus' ] @@ -117,6 +117,12 @@ class ChangeSecretRecordStatusChoice(models.TextChoices): pending = 'pending', _('Pending') +class ChangeSecretAccountStatus(models.TextChoices): + QUEUED = 'queued', _('Queued') + READY = 'ready', _('Ready') + PROCESSING = 'processing', _('Processing') + + class GatherAccountDetailField(models.TextChoices): can_login = 'can_login', _('Can login') superuser = 'superuser', _('Superuser') diff --git a/apps/accounts/filters.py b/apps/accounts/filters.py index 47d64b31d..5bb0d1085 100644 --- a/apps/accounts/filters.py +++ b/apps/accounts/filters.py @@ -17,6 +17,7 @@ from common.utils.timezone import local_zero_hour, local_now from .const.automation import ChangeSecretRecordStatusChoice from .models import Account, GatheredAccount, ChangeSecretRecord, PushSecretRecord, IntegrationApplication, \ AutomationExecution +from .utils import account_secret_task_status logger = get_logger(__file__) @@ -242,3 +243,26 @@ class PushAccountRecordFilterSet(SecretRecordMixin, UUIDFilterMixin, BaseFilterS class Meta: model = PushSecretRecord fields = ["id", "status", "asset_id", "execution_id"] + + +class ChangeSecretStatusFilterSet(BaseFilterSet): + asset_id = drf_filters.CharFilter(field_name="asset_id", lookup_expr="exact") + asset_name = drf_filters.CharFilter( + field_name="asset__name", lookup_expr="icontains" + ) + status = drf_filters.CharFilter(method='filter_dynamic') + execution_id = drf_filters.CharFilter(method='filter_dynamic') + + class Meta: + model = Account + fields = ["id", "username"] + + @staticmethod + def filter_dynamic(queryset, name, value): + _ids = list(queryset.values_list('id', flat=True)) + data_map = { + _id: account_secret_task_status.get(str(_id)).get(name) + for _id in _ids + } + matched = [_id for _id, v in data_map.items() if v == value] + return queryset.filter(id__in=matched) diff --git a/apps/accounts/serializers/automations/change_secret.py b/apps/accounts/serializers/automations/change_secret.py index e80246c38..8d8eac667 100644 --- a/apps/accounts/serializers/automations/change_secret.py +++ b/apps/accounts/serializers/automations/change_secret.py @@ -16,6 +16,7 @@ from assets.models import Asset from common.serializers.fields import LabeledChoiceField, ObjectRelatedField from common.utils import get_logger from .base import BaseAutomationSerializer +from ...utils import account_secret_task_status logger = get_logger(__file__) @@ -26,6 +27,7 @@ __all__ = [ 'ChangeSecretRecordBackUpSerializer', 'ChangeSecretUpdateAssetSerializer', 'ChangeSecretUpdateNodeSerializer', + 'ChangeSecretAccountSerializer' ] @@ -179,3 +181,24 @@ class ChangeSecretUpdateNodeSerializer(serializers.ModelSerializer): class Meta: model = ChangeSecretAutomation fields = ['id', 'nodes'] + + +class ChangeSecretAccountSerializer(serializers.ModelSerializer): + asset = ObjectRelatedField( + queryset=Asset.objects.all(), required=False, label=_("Asset") + ) + ttl = serializers.SerializerMethodField(label=_('TTL')) + meta = serializers.SerializerMethodField(label=_('Meta')) + + class Meta: + model = Account + fields = ['id', 'username', 'asset', 'meta', 'ttl'] + read_only_fields = fields + + @staticmethod + def get_meta(obj): + return account_secret_task_status.get(str(obj.id)) + + @staticmethod + def get_ttl(obj): + return account_secret_task_status.get_ttl(str(obj.id)) diff --git a/apps/accounts/tasks/push_account.py b/apps/accounts/tasks/push_account.py index 479b88c72..678e4a6fc 100644 --- a/apps/accounts/tasks/push_account.py +++ b/apps/accounts/tasks/push_account.py @@ -1,37 +1,108 @@ +from collections import defaultdict + from celery import shared_task from django.utils.translation import gettext_noop, gettext_lazy as _ -from accounts.const import AutomationTypes +from accounts.const import AutomationTypes, ChangeSecretAccountStatus from accounts.tasks.common import quickstart_automation_by_snapshot +from accounts.utils import account_secret_task_status from common.utils import get_logger +from orgs.utils import tmp_to_org logger = get_logger(__file__) __all__ = [ - 'push_accounts_to_assets_task', + 'push_accounts_to_assets_task', 'change_secret_accounts_to_assets_task' ] +def _process_accounts(account_ids, automation_model, default_name, automation_type, snapshot=None): + from accounts.models import Account + accounts = Account.objects.filter(id__in=account_ids) + if not accounts: + logger.warning( + "No accounts found for automation task %s with ids %s", + automation_type, account_ids + ) + return + + task_name = automation_model.generate_unique_name(gettext_noop(default_name)) + snapshot = snapshot or {} + snapshot.update({ + 'accounts': [str(a.id) for a in accounts], + 'assets': [str(a.asset_id) for a in accounts], + }) + + quickstart_automation_by_snapshot(task_name, automation_type, snapshot) + + @shared_task( queue="ansible", verbose_name=_('Push accounts to assets'), activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None), description=_( - "When creating or modifying an account requires account push, this task is executed" + "Whenever an account is created or modified and needs pushing to assets, run this task" ) ) def push_accounts_to_assets_task(account_ids, params=None): from accounts.models import PushAccountAutomation - from accounts.models import Account - - accounts = Account.objects.filter(id__in=account_ids) - task_name = gettext_noop("Push accounts to assets") - task_name = PushAccountAutomation.generate_unique_name(task_name) - - task_snapshot = { - 'accounts': [str(account.id) for account in accounts], - 'assets': [str(account.asset_id) for account in accounts], + snapshot = { 'params': params or {}, } + _process_accounts( + account_ids, + PushAccountAutomation, + _("Push accounts to assets"), + AutomationTypes.push_account, + snapshot=snapshot + ) - tp = AutomationTypes.push_account - quickstart_automation_by_snapshot(task_name, tp, task_snapshot) + +@shared_task( + queue="ansible", + verbose_name=_('Change secret accounts to assets'), + activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None), + description=_( + "When a secret on an account changes and needs pushing to assets, run this task" + ) +) +def change_secret_accounts_to_assets_task(account_ids, params=None, snapshot=None, trigger='manual'): + from accounts.models import ChangeSecretAutomation, Account + + manager = account_secret_task_status + + if trigger == 'delay': + for _id in manager.account_ids: + status = manager.get_status(_id) + ttl = manager.get_ttl(_id) + # Check if the account is in QUEUED status + if status == ChangeSecretAccountStatus.QUEUED and ttl <= 15: + account_ids.append(_id) + manager.set_status(_id, ChangeSecretAccountStatus.READY) + + if not account_ids: + return + + accounts = Account.objects.filter(id__in=account_ids) + if not accounts: + logger.warning( + "No accounts found for change secret automation task with ids %s", + account_ids + ) + return + + grouped_ids = defaultdict(lambda: defaultdict(list)) + for account in accounts: + grouped_ids[account.org_id][account.secret_type].append(str(account.id)) + + snapshot = snapshot or {} + for org_id, secret_map in grouped_ids.items(): + with tmp_to_org(org_id): + for secret_type, ids in secret_map.items(): + snapshot['secret_type'] = secret_type + _process_accounts( + ids, + ChangeSecretAutomation, + _("Change secret accounts to assets"), + AutomationTypes.change_secret, + snapshot=snapshot + ) diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py index 8f567a0d3..90670a1bd 100644 --- a/apps/accounts/urls.py +++ b/apps/accounts/urls.py @@ -17,6 +17,7 @@ router.register(r'account-template-secrets', api.AccountTemplateSecretsViewSet, router.register(r'account-backup-plans', api.BackupAccountViewSet, 'account-backup') router.register(r'account-backup-plan-executions', api.BackupAccountExecutionViewSet, 'account-backup-execution') router.register(r'change-secret-automations', api.ChangeSecretAutomationViewSet, 'change-secret-automation') +router.register(r'change-secret-status', api.ChangeSecretStatusViewSet, 'change-secret-status') router.register(r'change-secret-executions', api.ChangSecretExecutionViewSet, 'change-secret-execution') router.register(r'change-secret-records', api.ChangeSecretRecordViewSet, 'change-secret-record') router.register(r'gather-account-automations', api.DiscoverAccountsAutomationViewSet, 'gather-account-automation') diff --git a/apps/accounts/utils.py b/apps/accounts/utils.py index 9e2b84e74..7523a52f8 100644 --- a/apps/accounts/utils.py +++ b/apps/accounts/utils.py @@ -1,10 +1,11 @@ import copy +from django.conf import settings +from django.core.cache import cache from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from accounts.const import SecretType, DEFAULT_PASSWORD_RULES - from common.utils import ssh_key_gen, random_string from common.utils import validate_ssh_private_key, parse_ssh_private_key_str @@ -61,3 +62,80 @@ def validate_ssh_key(ssh_key, passphrase=None): if not valid: raise serializers.ValidationError(_("private key invalid or passphrase error")) return parse_ssh_private_key_str(ssh_key, passphrase) + + +class AccountSecretTaskStatus: + + def __init__( + self, + prefix='queue:change_secret:', + debounce_key='debounce:change_secret:task', + debounce_timeout=10, + queue_status_timeout=35, + default_timeout=3600, + delayed_task_countdown=20, + ): + self.prefix = prefix + self.debounce_key = debounce_key + self.debounce_timeout = debounce_timeout + self.queue_status_timeout = queue_status_timeout + self.default_timeout = default_timeout + self.delayed_task_countdown = delayed_task_countdown + self.enabled = getattr(settings, 'CHANGE_SECRET_AFTER_SESSION_END', False) + + def _key(self, identifier): + return f"{self.prefix}{identifier}" + + @property + def account_ids(self): + for key in cache.iter_keys(f"{self.prefix}*"): + yield key.split(':')[-1] + + def is_debounced(self): + return cache.add(self.debounce_key, True, self.debounce_timeout) + + def get_queue_key(self, identifier): + return self._key(identifier) + + def set_status( + self, + identifier, + status, + timeout=None, + metadata=None, + use_add=False + ): + if not self.enabled: + return + + key = self._key(identifier) + data = {"status": status} + if metadata: + data.update(metadata) + + if use_add: + return cache.set(key, data, timeout or self.queue_status_timeout) + + cache.set(key, data, timeout or self.default_timeout) + + def get(self, identifier): + return cache.get(self._key(identifier), {}) + + def get_status(self, identifier): + if not self.enabled: + return + + record = cache.get(self._key(identifier), {}) + return record.get("status") + + def get_ttl(self, identifier): + return cache.ttl(self._key(identifier)) + + def clear(self, identifier): + if not self.enabled: + return + + cache.delete(self._key(identifier)) + + +account_secret_task_status = AccountSecretTaskStatus() diff --git a/apps/acls/const.py b/apps/acls/const.py index e9e1b5096..5d958bc74 100644 --- a/apps/acls/const.py +++ b/apps/acls/const.py @@ -11,3 +11,4 @@ class ActionChoices(models.TextChoices): notify_and_warn = 'notify_and_warn', _('Prompt and warn') face_verify = 'face_verify', _('Face Verify') face_online = 'face_online', _('Face Online') + change_secret = 'change_secret', _('Change password') diff --git a/apps/acls/serializers/base.py b/apps/acls/serializers/base.py index 24a962822..3ce64594c 100644 --- a/apps/acls/serializers/base.py +++ b/apps/acls/serializers/base.py @@ -79,6 +79,8 @@ class ActionAclSerializer(serializers.Serializer): field_action._choices.pop(ActionChoices.face_online, None) for choice in self.Meta.action_choices_exclude: field_action._choices.pop(choice, None) + if not settings.CHANGE_SECRET_AFTER_SESSION_END: + field_action._choices.pop(ActionChoices.change_secret, None) class BaseACLSerializer(ActionAclSerializer, serializers.Serializer): diff --git a/apps/acls/serializers/command_acl.py b/apps/acls/serializers/command_acl.py index 11df8914c..2fcc7f8a8 100644 --- a/apps/acls/serializers/command_acl.py +++ b/apps/acls/serializers/command_acl.py @@ -33,7 +33,10 @@ class CommandFilterACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer) model = CommandFilterACL fields = BaseSerializer.Meta.fields + ['command_groups'] action_choices_exclude = [ - ActionChoices.notice, ActionChoices.face_verify, ActionChoices.face_online + ActionChoices.notice, + ActionChoices.face_verify, + ActionChoices.face_online, + ActionChoices.change_secret ] diff --git a/apps/acls/serializers/connect_method.py b/apps/acls/serializers/connect_method.py index 40f03e216..d58e92a2c 100644 --- a/apps/acls/serializers/connect_method.py +++ b/apps/acls/serializers/connect_method.py @@ -14,6 +14,10 @@ class ConnectMethodACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer) if i not in ['assets', 'accounts'] ] action_choices_exclude = BaseSerializer.Meta.action_choices_exclude + [ - ActionChoices.review, ActionChoices.accept, ActionChoices.notice, - ActionChoices.face_verify, ActionChoices.face_online + ActionChoices.review, + ActionChoices.accept, + ActionChoices.notice, + ActionChoices.face_verify, + ActionChoices.face_online, + ActionChoices.change_secret ] diff --git a/apps/acls/serializers/login_acl.py b/apps/acls/serializers/login_acl.py index 432306e4b..8d7f4eb25 100644 --- a/apps/acls/serializers/login_acl.py +++ b/apps/acls/serializers/login_acl.py @@ -22,7 +22,8 @@ class LoginACLSerializer(BaseUserACLSerializer, BulkOrgResourceModelSerializer): ActionChoices.warning, ActionChoices.notify_and_warn, ActionChoices.face_online, - ActionChoices.face_verify + ActionChoices.face_verify, + ActionChoices.change_secret ] def get_rules_serializer(self): diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index b25ca116f..cc7b20840 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -607,6 +607,7 @@ class Config(dict): 'SECURITY_CHECK_DIFFERENT_CITY_LOGIN': True, 'OLD_PASSWORD_HISTORY_LIMIT_COUNT': 5, 'CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED': True, + 'CHANGE_SECRET_AFTER_SESSION_END': False, 'USER_LOGIN_SINGLE_MACHINE_ENABLED': False, 'ONLY_ALLOW_EXIST_USER_AUTH': False, 'ONLY_ALLOW_AUTH_FROM_SOURCE': False, diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index ddb73a606..dc83f05db 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -141,6 +141,7 @@ WINDOWS_SKIP_ALL_MANUAL_PASSWORD = CONFIG.WINDOWS_SKIP_ALL_MANUAL_PASSWORD AUTH_EXPIRED_SECONDS = 60 * 10 CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED = CONFIG.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED +CHANGE_SECRET_AFTER_SESSION_END = CONFIG.CHANGE_SECRET_AFTER_SESSION_END DATETIME_DISPLAY_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/apps/settings/serializers/public.py b/apps/settings/serializers/public.py index da047edfd..3106beb9c 100644 --- a/apps/settings/serializers/public.py +++ b/apps/settings/serializers/public.py @@ -82,6 +82,7 @@ class PrivateSettingSerializer(PublicSettingSerializer): USER_DEFAULT_EXPIRED_DAYS = serializers.IntegerField() ASSET_PERMISSION_DEFAULT_EXPIRED_DAYS = serializers.IntegerField() PRIVACY_MODE = serializers.BooleanField() + CHANGE_SECRET_AFTER_SESSION_END = serializers.BooleanField() class ServerInfoSerializer(serializers.Serializer): diff --git a/apps/terminal/serializers/session.py b/apps/terminal/serializers/session.py index 924c11049..bde332261 100644 --- a/apps/terminal/serializers/session.py +++ b/apps/terminal/serializers/session.py @@ -1,9 +1,15 @@ +from django.conf import settings from django.utils.translation import gettext_lazy as _ from rest_framework import serializers +from accounts.const import ChangeSecretAccountStatus, SecretStrategy +from accounts.models import Account +from accounts.tasks import change_secret_accounts_to_assets_task +from accounts.utils import account_secret_task_status +from acls.models import LoginAssetACL from assets.models import Asset from common.serializers.fields import LabeledChoiceField -from common.utils import pretty_string +from common.utils import pretty_string, get_logger from orgs.mixins.serializers import BulkOrgResourceModelSerializer from terminal.session_lifecycle import lifecycle_events_map from users.models import User @@ -11,6 +17,8 @@ from .terminal import TerminalSmallSerializer from ..const import SessionType, SessionErrorReason from ..models import Session +logger = get_logger(__file__) + __all__ = [ 'SessionSerializer', 'SessionDisplaySerializer', 'ReplaySerializer', 'SessionJoinValidateSerializer', @@ -84,6 +92,47 @@ class SessionSerializer(BulkOrgResourceModelSerializer): raise serializers.ValidationError({field_name: error_message}) return instance + @staticmethod + def enqueue_change_secret_task(instance): + asset = Asset.objects.filter(id=instance.asset_id).first() + user = User.objects.filter(id=instance.user_id).first() + if not asset or not user: + logger.warning( + f"Invalid asset or user for change secret task: asset={instance.asset}, user={instance.user}" + ) + return + kwargs = {'user': user, 'asset': asset} + account_id = instance.account_id + + try: + account = Account.objects.get(id=account_id) + kwargs['account'] = account + except Account.DoesNotExist: + logger.warning(f"Account with id {account_id} does not exist for change secret task.") + return + acls = LoginAssetACL.filter_queryset(**kwargs) + acl = LoginAssetACL.get_match_rule_acls(user, instance.remote_addr, acls) + if not acl: + return + if not acl.is_action(acl.ActionChoices.change_secret): + return + + manager = account_secret_task_status + manager.set_status(account.id, ChangeSecretAccountStatus.QUEUED, use_add=True) + if manager.is_debounced(): + snapshot = { + 'check_conn_after_change': False, + 'secret_strategy': SecretStrategy.random, + } + change_secret_accounts_to_assets_task.apply_async( + args=[[]], # Pass an empty list as account_ids + kwargs={ + 'snapshot': snapshot, + 'trigger': 'delay', + }, + countdown=manager.delayed_task_countdown, + ) + def create(self, validated_data): user_id = validated_data.get('user_id') asset_id = validated_data.get('asset_id') @@ -107,6 +156,12 @@ class SessionSerializer(BulkOrgResourceModelSerializer): validated_data['asset'] = str(asset) return super().create(validated_data) + def update(self, instance, validated_data): + is_finished = validated_data.get('is_finished') + if settings.CHANGE_SECRET_AFTER_SESSION_END and is_finished and not instance.is_finished: + self.enqueue_change_secret_task(instance) + return super().update(instance, validated_data) + class SessionDisplaySerializer(SessionSerializer): command_amount = serializers.IntegerField(read_only=True, label=_('Command amount'))