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 rest_framework.response import Response
|
||||||
|
|
||||||
from accounts import serializers
|
from accounts import serializers
|
||||||
from accounts.const import AutomationTypes, ChangeSecretRecordStatusChoice
|
from accounts.const import (
|
||||||
from accounts.filters import ChangeSecretRecordFilterSet
|
AutomationTypes, ChangeSecretRecordStatusChoice
|
||||||
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord
|
)
|
||||||
|
from accounts.filters import ChangeSecretRecordFilterSet, ChangeSecretStatusFilterSet
|
||||||
|
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord, Account
|
||||||
from accounts.tasks import execute_automation_record_task
|
from accounts.tasks import execute_automation_record_task
|
||||||
|
from accounts.utils import account_secret_task_status
|
||||||
from authentication.permissions import UserConfirmation, ConfirmType
|
from authentication.permissions import UserConfirmation, ConfirmType
|
||||||
from common.permissions import IsValidLicense
|
from common.permissions import IsValidLicense
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
|
||||||
|
@ -23,7 +26,7 @@ __all__ = [
|
||||||
'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet',
|
'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet',
|
||||||
'ChangSecretExecutionViewSet', 'ChangSecretAssetsListApi',
|
'ChangSecretExecutionViewSet', 'ChangSecretAssetsListApi',
|
||||||
'ChangSecretRemoveAssetApi', 'ChangSecretAddAssetApi',
|
'ChangSecretRemoveAssetApi', 'ChangSecretAddAssetApi',
|
||||||
'ChangSecretNodeAddRemoveApi'
|
'ChangSecretNodeAddRemoveApi', 'ChangeSecretStatusViewSet'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -154,3 +157,24 @@ class ChangSecretAddAssetApi(AutomationAddAssetApi):
|
||||||
class ChangSecretNodeAddRemoveApi(AutomationNodeAddRemoveApi):
|
class ChangSecretNodeAddRemoveApi(AutomationNodeAddRemoveApi):
|
||||||
model = ChangeSecretAutomation
|
model = ChangeSecretAutomation
|
||||||
serializer_class = serializers.ChangeSecretUpdateNodeSerializer
|
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 django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.automations.methods import platform_automation_methods
|
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.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.automations.base.manager import BasePlaybookManager
|
||||||
from assets.const import HostTypes
|
from assets.const import HostTypes
|
||||||
from common.db.utils import safe_atomic_db_connection
|
from common.db.utils import safe_atomic_db_connection
|
||||||
|
@ -132,10 +133,24 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
|
||||||
for account in accounts:
|
for account in accounts:
|
||||||
h = deepcopy(host)
|
h = deepcopy(host)
|
||||||
h['name'] += '(' + account.username + ')' # To distinguish different accounts
|
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:
|
try:
|
||||||
h = self.gen_account_inventory(account, asset, h, path_dir)
|
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:
|
except Exception as e:
|
||||||
h['error'] = str(e)
|
h['error'] = str(e)
|
||||||
|
self.clear_account_queue_status(account)
|
||||||
|
|
||||||
inventory_hosts.append(h)
|
inventory_hosts.append(h)
|
||||||
|
|
||||||
return inventory_hosts
|
return inventory_hosts
|
||||||
|
@ -144,6 +159,10 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
|
||||||
def save_record(recorder):
|
def save_record(recorder):
|
||||||
recorder.save(update_fields=['error', 'status', 'date_finished'])
|
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):
|
def on_host_success(self, host, result):
|
||||||
recorder = self.name_recorder_mapper.get(host)
|
recorder = self.name_recorder_mapper.get(host)
|
||||||
if not recorder:
|
if not recorder:
|
||||||
|
@ -173,6 +192,7 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
|
||||||
with safe_atomic_db_connection():
|
with safe_atomic_db_connection():
|
||||||
account.save(update_fields=['secret', 'date_updated', 'date_change_secret', 'change_secret_status'])
|
account.save(update_fields=['secret', 'date_updated', 'date_change_secret', 'change_secret_status'])
|
||||||
self.save_record(recorder)
|
self.save_record(recorder)
|
||||||
|
self.clear_account_queue_status(account)
|
||||||
|
|
||||||
def on_host_error(self, host, error, result):
|
def on_host_error(self, host, error, result):
|
||||||
recorder = self.name_recorder_mapper.get(host)
|
recorder = self.name_recorder_mapper.get(host)
|
||||||
|
@ -201,3 +221,4 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
|
||||||
with safe_atomic_db_connection():
|
with safe_atomic_db_connection():
|
||||||
account.save(update_fields=['change_secret_status', 'date_change_secret', 'date_updated'])
|
account.save(update_fields=['change_secret_status', 'date_change_secret', 'date_updated'])
|
||||||
self.save_record(recorder)
|
self.save_record(recorder)
|
||||||
|
self.clear_account_queue_status(account)
|
||||||
|
|
|
@ -17,7 +17,7 @@ __all__ = [
|
||||||
'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity',
|
'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity',
|
||||||
'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice',
|
'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice',
|
||||||
'PushAccountActionChoice', 'AccountBackupType', 'ChangeSecretRecordStatusChoice',
|
'PushAccountActionChoice', 'AccountBackupType', 'ChangeSecretRecordStatusChoice',
|
||||||
'GatherAccountDetailField'
|
'GatherAccountDetailField', 'ChangeSecretAccountStatus'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -117,6 +117,12 @@ class ChangeSecretRecordStatusChoice(models.TextChoices):
|
||||||
pending = 'pending', _('Pending')
|
pending = 'pending', _('Pending')
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeSecretAccountStatus(models.TextChoices):
|
||||||
|
QUEUED = 'queued', _('Queued')
|
||||||
|
READY = 'ready', _('Ready')
|
||||||
|
PROCESSING = 'processing', _('Processing')
|
||||||
|
|
||||||
|
|
||||||
class GatherAccountDetailField(models.TextChoices):
|
class GatherAccountDetailField(models.TextChoices):
|
||||||
can_login = 'can_login', _('Can login')
|
can_login = 'can_login', _('Can login')
|
||||||
superuser = 'superuser', _('Superuser')
|
superuser = 'superuser', _('Superuser')
|
||||||
|
|
|
@ -17,6 +17,7 @@ from common.utils.timezone import local_zero_hour, local_now
|
||||||
from .const.automation import ChangeSecretRecordStatusChoice
|
from .const.automation import ChangeSecretRecordStatusChoice
|
||||||
from .models import Account, GatheredAccount, ChangeSecretRecord, PushSecretRecord, IntegrationApplication, \
|
from .models import Account, GatheredAccount, ChangeSecretRecord, PushSecretRecord, IntegrationApplication, \
|
||||||
AutomationExecution
|
AutomationExecution
|
||||||
|
from .utils import account_secret_task_status
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
@ -242,3 +243,26 @@ class PushAccountRecordFilterSet(SecretRecordMixin, UUIDFilterMixin, BaseFilterS
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PushSecretRecord
|
model = PushSecretRecord
|
||||||
fields = ["id", "status", "asset_id", "execution_id"]
|
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.serializers.fields import LabeledChoiceField, ObjectRelatedField
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from .base import BaseAutomationSerializer
|
from .base import BaseAutomationSerializer
|
||||||
|
from ...utils import account_secret_task_status
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@ __all__ = [
|
||||||
'ChangeSecretRecordBackUpSerializer',
|
'ChangeSecretRecordBackUpSerializer',
|
||||||
'ChangeSecretUpdateAssetSerializer',
|
'ChangeSecretUpdateAssetSerializer',
|
||||||
'ChangeSecretUpdateNodeSerializer',
|
'ChangeSecretUpdateNodeSerializer',
|
||||||
|
'ChangeSecretAccountSerializer'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -179,3 +181,24 @@ class ChangeSecretUpdateNodeSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ChangeSecretAutomation
|
model = ChangeSecretAutomation
|
||||||
fields = ['id', 'nodes']
|
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 celery import shared_task
|
||||||
from django.utils.translation import gettext_noop, gettext_lazy as _
|
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.tasks.common import quickstart_automation_by_snapshot
|
||||||
|
from accounts.utils import account_secret_task_status
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
|
from orgs.utils import tmp_to_org
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
__all__ = [
|
__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(
|
@shared_task(
|
||||||
queue="ansible",
|
queue="ansible",
|
||||||
verbose_name=_('Push accounts to assets'),
|
verbose_name=_('Push accounts to assets'),
|
||||||
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None),
|
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None),
|
||||||
description=_(
|
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):
|
def push_accounts_to_assets_task(account_ids, params=None):
|
||||||
from accounts.models import PushAccountAutomation
|
from accounts.models import PushAccountAutomation
|
||||||
from accounts.models import Account
|
snapshot = {
|
||||||
|
|
||||||
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],
|
|
||||||
'params': params or {},
|
'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-plans', api.BackupAccountViewSet, 'account-backup')
|
||||||
router.register(r'account-backup-plan-executions', api.BackupAccountExecutionViewSet, 'account-backup-execution')
|
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-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-executions', api.ChangSecretExecutionViewSet, 'change-secret-execution')
|
||||||
router.register(r'change-secret-records', api.ChangeSecretRecordViewSet, 'change-secret-record')
|
router.register(r'change-secret-records', api.ChangeSecretRecordViewSet, 'change-secret-record')
|
||||||
router.register(r'gather-account-automations', api.DiscoverAccountsAutomationViewSet, 'gather-account-automation')
|
router.register(r'gather-account-automations', api.DiscoverAccountsAutomationViewSet, 'gather-account-automation')
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.cache import cache
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.const import SecretType, DEFAULT_PASSWORD_RULES
|
from accounts.const import SecretType, DEFAULT_PASSWORD_RULES
|
||||||
|
|
||||||
from common.utils import ssh_key_gen, random_string
|
from common.utils import ssh_key_gen, random_string
|
||||||
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str
|
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:
|
if not valid:
|
||||||
raise serializers.ValidationError(_("private key invalid or passphrase error"))
|
raise serializers.ValidationError(_("private key invalid or passphrase error"))
|
||||||
return parse_ssh_private_key_str(ssh_key, passphrase)
|
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')
|
notify_and_warn = 'notify_and_warn', _('Prompt and warn')
|
||||||
face_verify = 'face_verify', _('Face Verify')
|
face_verify = 'face_verify', _('Face Verify')
|
||||||
face_online = 'face_online', _('Face Online')
|
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)
|
field_action._choices.pop(ActionChoices.face_online, None)
|
||||||
for choice in self.Meta.action_choices_exclude:
|
for choice in self.Meta.action_choices_exclude:
|
||||||
field_action._choices.pop(choice, None)
|
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):
|
class BaseACLSerializer(ActionAclSerializer, serializers.Serializer):
|
||||||
|
|
|
@ -33,7 +33,10 @@ class CommandFilterACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer)
|
||||||
model = CommandFilterACL
|
model = CommandFilterACL
|
||||||
fields = BaseSerializer.Meta.fields + ['command_groups']
|
fields = BaseSerializer.Meta.fields + ['command_groups']
|
||||||
action_choices_exclude = [
|
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']
|
if i not in ['assets', 'accounts']
|
||||||
]
|
]
|
||||||
action_choices_exclude = BaseSerializer.Meta.action_choices_exclude + [
|
action_choices_exclude = BaseSerializer.Meta.action_choices_exclude + [
|
||||||
ActionChoices.review, ActionChoices.accept, ActionChoices.notice,
|
ActionChoices.review,
|
||||||
ActionChoices.face_verify, ActionChoices.face_online
|
ActionChoices.accept,
|
||||||
|
ActionChoices.notice,
|
||||||
|
ActionChoices.face_verify,
|
||||||
|
ActionChoices.face_online,
|
||||||
|
ActionChoices.change_secret
|
||||||
]
|
]
|
||||||
|
|
|
@ -22,7 +22,8 @@ class LoginACLSerializer(BaseUserACLSerializer, BulkOrgResourceModelSerializer):
|
||||||
ActionChoices.warning,
|
ActionChoices.warning,
|
||||||
ActionChoices.notify_and_warn,
|
ActionChoices.notify_and_warn,
|
||||||
ActionChoices.face_online,
|
ActionChoices.face_online,
|
||||||
ActionChoices.face_verify
|
ActionChoices.face_verify,
|
||||||
|
ActionChoices.change_secret
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_rules_serializer(self):
|
def get_rules_serializer(self):
|
||||||
|
|
|
@ -607,6 +607,7 @@ class Config(dict):
|
||||||
'SECURITY_CHECK_DIFFERENT_CITY_LOGIN': True,
|
'SECURITY_CHECK_DIFFERENT_CITY_LOGIN': True,
|
||||||
'OLD_PASSWORD_HISTORY_LIMIT_COUNT': 5,
|
'OLD_PASSWORD_HISTORY_LIMIT_COUNT': 5,
|
||||||
'CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED': True,
|
'CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED': True,
|
||||||
|
'CHANGE_SECRET_AFTER_SESSION_END': False,
|
||||||
'USER_LOGIN_SINGLE_MACHINE_ENABLED': False,
|
'USER_LOGIN_SINGLE_MACHINE_ENABLED': False,
|
||||||
'ONLY_ALLOW_EXIST_USER_AUTH': False,
|
'ONLY_ALLOW_EXIST_USER_AUTH': False,
|
||||||
'ONLY_ALLOW_AUTH_FROM_SOURCE': 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
|
AUTH_EXPIRED_SECONDS = 60 * 10
|
||||||
|
|
||||||
CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED = CONFIG.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED
|
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'
|
DATETIME_DISPLAY_FORMAT = '%Y-%m-%d %H:%M:%S'
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,7 @@ class PrivateSettingSerializer(PublicSettingSerializer):
|
||||||
USER_DEFAULT_EXPIRED_DAYS = serializers.IntegerField()
|
USER_DEFAULT_EXPIRED_DAYS = serializers.IntegerField()
|
||||||
ASSET_PERMISSION_DEFAULT_EXPIRED_DAYS = serializers.IntegerField()
|
ASSET_PERMISSION_DEFAULT_EXPIRED_DAYS = serializers.IntegerField()
|
||||||
PRIVACY_MODE = serializers.BooleanField()
|
PRIVACY_MODE = serializers.BooleanField()
|
||||||
|
CHANGE_SECRET_AFTER_SESSION_END = serializers.BooleanField()
|
||||||
|
|
||||||
|
|
||||||
class ServerInfoSerializer(serializers.Serializer):
|
class ServerInfoSerializer(serializers.Serializer):
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
|
from django.conf import settings
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
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 assets.models import Asset
|
||||||
from common.serializers.fields import LabeledChoiceField
|
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 orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
from terminal.session_lifecycle import lifecycle_events_map
|
from terminal.session_lifecycle import lifecycle_events_map
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
@ -11,6 +17,8 @@ from .terminal import TerminalSmallSerializer
|
||||||
from ..const import SessionType, SessionErrorReason
|
from ..const import SessionType, SessionErrorReason
|
||||||
from ..models import Session
|
from ..models import Session
|
||||||
|
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'SessionSerializer', 'SessionDisplaySerializer',
|
'SessionSerializer', 'SessionDisplaySerializer',
|
||||||
'ReplaySerializer', 'SessionJoinValidateSerializer',
|
'ReplaySerializer', 'SessionJoinValidateSerializer',
|
||||||
|
@ -84,6 +92,47 @@ class SessionSerializer(BulkOrgResourceModelSerializer):
|
||||||
raise serializers.ValidationError({field_name: error_message})
|
raise serializers.ValidationError({field_name: error_message})
|
||||||
return instance
|
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):
|
def create(self, validated_data):
|
||||||
user_id = validated_data.get('user_id')
|
user_id = validated_data.get('user_id')
|
||||||
asset_id = validated_data.get('asset_id')
|
asset_id = validated_data.get('asset_id')
|
||||||
|
@ -107,6 +156,12 @@ class SessionSerializer(BulkOrgResourceModelSerializer):
|
||||||
validated_data['asset'] = str(asset)
|
validated_data['asset'] = str(asset)
|
||||||
return super().create(validated_data)
|
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):
|
class SessionDisplaySerializer(SessionSerializer):
|
||||||
command_amount = serializers.IntegerField(read_only=True, label=_('Command amount'))
|
command_amount = serializers.IntegerField(read_only=True, label=_('Command amount'))
|
||||||
|
|
Loading…
Reference in New Issue