diff --git a/apps/accounts/api/account/template.py b/apps/accounts/api/account/template.py index 11675368f..0aecb5143 100644 --- a/apps/accounts/api/account/template.py +++ b/apps/accounts/api/account/template.py @@ -1,4 +1,6 @@ from django_filters import rest_framework as drf_filters +from rest_framework.decorators import action +from rest_framework.response import Response from accounts import serializers from accounts.models import AccountTemplate @@ -38,8 +40,20 @@ class AccountTemplateViewSet(OrgBulkModelViewSet): filterset_class = AccountTemplateFilterSet search_fields = ('username', 'name') serializer_classes = { - 'default': serializers.AccountTemplateSerializer + 'default': serializers.AccountTemplateSerializer, } + rbac_perms = { + 'su_from_account_templates': 'accounts.view_accounttemplate', + } + + @action(methods=['get'], detail=False, url_path='su-from-account-templates') + def su_from_account_templates(self, request, *args, **kwargs): + pk = request.query_params.get('template_id') + template = AccountTemplate.objects.filter(pk=pk).first() + templates = AccountTemplate.get_su_from_account_templates(template) + templates = self.filter_queryset(templates) + serializer = self.get_serializer(templates, many=True) + return Response(data=serializer.data) class AccountTemplateSecretsViewSet(RecordViewLogMixin, AccountTemplateViewSet): diff --git a/apps/accounts/migrations/0011_auto_20230506_1443.py b/apps/accounts/migrations/0011_auto_20230506_1443.py new file mode 100644 index 000000000..3460376bd --- /dev/null +++ b/apps/accounts/migrations/0011_auto_20230506_1443.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.17 on 2023-05-06 06:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0010_gatheraccountsautomation_is_sync_account'), + ] + + operations = [ + migrations.AddField( + model_name='accounttemplate', + name='su_from', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='su_to', to='accounts.accounttemplate', verbose_name='Su from'), + ), + migrations.AlterField( + model_name='changesecretautomation', + name='ssh_key_change_strategy', + field=models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (Replace only keys pushed by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy'), + ), + migrations.AlterField( + model_name='pushaccountautomation', + name='ssh_key_change_strategy', + field=models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (Replace only keys pushed by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy'), + ), + ] diff --git a/apps/accounts/models/account.py b/apps/accounts/models/account.py index de89db545..30eb853e3 100644 --- a/apps/accounts/models/account.py +++ b/apps/accounts/models/account.py @@ -1,5 +1,5 @@ from django.db import models -from django.db.models import Count +from django.db.models import Count, Q from django.utils import timezone from django.utils.translation import gettext_lazy as _ from simple_history.models import HistoricalRecords @@ -108,6 +108,11 @@ class Account(AbsConnectivity, BaseAccount): class AccountTemplate(BaseAccount): + su_from = models.ForeignKey( + 'self', related_name='su_to', null=True, + on_delete=models.SET_NULL, verbose_name=_("Su from") + ) + class Meta: verbose_name = _('Account template') unique_together = ( @@ -118,6 +123,21 @@ class AccountTemplate(BaseAccount): ('change_accounttemplatesecret', _('Can change asset account template secret')), ] + @classmethod + def get_su_from_account_templates(cls, instance=None): + if not instance: + return cls.objects.all() + return cls.objects.exclude(Q(id=instance.id) | Q(su_from=instance)) + + def get_su_from_account(self, asset): + su_from = self.su_from + if su_from and asset.platform.su_enabled: + account = asset.accounts.filter( + username=su_from.username, + secret_type=su_from.secret_type + ).first() + return account + def __str__(self): return self.username diff --git a/apps/accounts/serializers/account/account.py b/apps/accounts/serializers/account/account.py index c76cb1788..45f7e26f3 100644 --- a/apps/accounts/serializers/account/account.py +++ b/apps/accounts/serializers/account/account.py @@ -91,7 +91,7 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer): self._template = template # Set initial data from template - ignore_fields = ['id', 'date_created', 'date_updated', 'org_id'] + ignore_fields = ['id', 'date_created', 'date_updated', 'su_from', 'org_id'] field_names = [ field.name for field in template._meta.fields if field.name not in ignore_fields @@ -151,6 +151,7 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer): template = self._template if template is None: return + validated_data['source'] = Source.TEMPLATE validated_data['source_id'] = str(template.id) @@ -238,6 +239,9 @@ class AssetAccountBulkSerializerResultSerializer(serializers.Serializer): class AssetAccountBulkSerializer( AccountCreateUpdateSerializerMixin, AuthValidateMixin, serializers.ModelSerializer ): + su_from_username = serializers.CharField( + max_length=128, required=False, write_only=True, allow_null=True, label=_("Su from") + ) assets = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, many=True, label=_('Assets')) class Meta: @@ -245,7 +249,7 @@ class AssetAccountBulkSerializer( fields = [ 'name', 'username', 'secret', 'secret_type', 'privileged', 'is_active', 'comment', 'template', - 'on_invalid', 'push_now', 'assets', + 'on_invalid', 'push_now', 'assets', 'su_from_username' ] extra_kwargs = { 'name': {'required': False}, @@ -293,8 +297,20 @@ class AssetAccountBulkSerializer( raise serializers.ValidationError(_('Account already exists')) return instance, True, 'created' + def generate_su_from_data(self, validated_data): + template = self._template + asset = validated_data['asset'] + su_from = validated_data.get('su_from') + su_from_username = validated_data.pop('su_from_username', None) + if template: + su_from = template.get_su_from_account() + elif su_from_username: + su_from = asset.accounts.filter(username=su_from_username).first() + validated_data['su_from'] = su_from + def perform_create(self, vd, handler): lookup = self.get_filter_lookup(vd) + self.generate_su_from_data(vd) try: instance, changed, state = handler(vd, lookup) except IntegrityError: diff --git a/apps/accounts/serializers/account/template.py b/apps/accounts/serializers/account/template.py index bbf83bd89..937c69d36 100644 --- a/apps/accounts/serializers/account/template.py +++ b/apps/accounts/serializers/account/template.py @@ -1,7 +1,9 @@ +from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from accounts.models import AccountTemplate, Account from common.serializers import SecretReadableMixin +from common.serializers.fields import ObjectRelatedField from .base import BaseAccountSerializer @@ -9,9 +11,14 @@ class AccountTemplateSerializer(BaseAccountSerializer): is_sync_account = serializers.BooleanField(default=False, write_only=True) _is_sync_account = False + su_from = ObjectRelatedField( + required=False, queryset=AccountTemplate.objects, allow_null=True, + allow_empty=True, label=_('Su from'), attrs=('id', 'name', 'username') + ) + class Meta(BaseAccountSerializer.Meta): model = AccountTemplate - fields = BaseAccountSerializer.Meta.fields + ['is_sync_account'] + fields = BaseAccountSerializer.Meta.fields + ['is_sync_account', 'su_from'] def sync_accounts_secret(self, instance, diff): if not self._is_sync_account or 'secret' not in diff: diff --git a/apps/assets/migrations/0117_alter_baseautomation_params.py b/apps/assets/migrations/0117_alter_baseautomation_params.py new file mode 100644 index 000000000..1fc93bdd3 --- /dev/null +++ b/apps/assets/migrations/0117_alter_baseautomation_params.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.17 on 2023-05-06 06:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0116_auto_20230418_1726'), + ] + + operations = [ + migrations.AlterField( + model_name='baseautomation', + name='params', + field=models.JSONField(default=dict, verbose_name='Parameters'), + ), + ] diff --git a/apps/perms/serializers/permission.py b/apps/perms/serializers/permission.py index 0e5f3b80a..c6f770b8a 100644 --- a/apps/perms/serializers/permission.py +++ b/apps/perms/serializers/permission.py @@ -109,6 +109,7 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): if condition in username_secret_type_dict: continue account_data = {key: getattr(template, key) for key in account_attribute} + account_data['su_from'] = template.get_su_from_account(asset) account_data['name'] = f"{account_data['name']}-{_('Account template')}" need_create_accounts.append(Account(**{'asset_id': asset.id, **account_data})) return Account.objects.bulk_create(need_create_accounts)