mirror of https://github.com/jumpserver/jumpserver
perf: Change secret after successful login
parent
3991976a00
commit
b70fb58faf
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'))
|
||||
|
|
Loading…
Reference in New Issue