perf: 修改账号生成 (#11591)

* perf: 修改账号生成

* perf: 修改账号模版支持策略

* perf: 修改特殊字符数量

* perf: 修改 model 继承

* perf: 修改顺序

* perf: 修改 requirements

* perf: 修改翻译

* perf: 修改随机生成密码

* perf: 修改密钥生成

* perf: 修复 bug

---------

Co-authored-by: ibuler <ibuler@qq.com>
This commit is contained in:
fit2bot
2023-09-19 10:59:33 +08:00
committed by GitHub
parent 0b30f5cf88
commit e6fe7c489e
16 changed files with 432 additions and 326 deletions

View File

@@ -4,11 +4,13 @@ from django.utils.translation import gettext_lazy as _
from assets.const import Connectivity
from common.db.fields import TreeChoices
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~'
DEFAULT_PASSWORD_LENGTH = 30
DEFAULT_PASSWORD_RULES = {
'length': DEFAULT_PASSWORD_LENGTH,
'symbol_set': string_punctuation
'uppercase': True,
'lowercase': True,
'digit': True,
'symbol': True,
}
__all__ = [
@@ -41,8 +43,8 @@ class AutomationTypes(models.TextChoices):
class SecretStrategy(models.TextChoices):
custom = 'specific', _('Specific password')
random = 'random', _('Random')
custom = 'specific', _('Specific secret')
random = 'random', _('Random generate')
class SSHKeyStrategy(models.TextChoices):

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.1.10 on 2023-09-18 08:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0015_auto_20230825_1120'),
]
operations = [
migrations.AddField(
model_name='accounttemplate',
name='password_rules',
field=models.JSONField(default=dict, verbose_name='Password rules'),
),
]

View File

@@ -1,5 +1,5 @@
from .account import *
from .automations import *
from .base import *
from .template import *
from .virtual import *
from .account import * # noqa
from .base import * # noqa
from .automations import * # noqa
from .template import * # noqa
from .virtual import * # noqa

View File

@@ -1,14 +1,13 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from accounts.const import SecretStrategy, SSHKeyStrategy, SecretType
from accounts.models import Account
from accounts.const import SSHKeyStrategy
from accounts.models import Account, SecretWithRandomMixin
from accounts.tasks import execute_account_automation_task
from assets.models.automations import (
BaseAutomation as AssetBaseAutomation,
AutomationExecution as AssetAutomationExecution
)
from common.db import fields
__all__ = ['AccountBaseAutomation', 'AutomationExecution', 'ChangeSecretMixin']
@@ -49,27 +48,11 @@ class AutomationExecution(AssetAutomationExecution):
return manager.run()
class ChangeSecretRuleMixin(models.Model):
secret_strategy = models.CharField(
choices=SecretStrategy.choices, max_length=16,
default=SecretStrategy.custom, verbose_name=_('Secret strategy')
)
password_rules = models.JSONField(default=dict, verbose_name=_('Password rules'))
class ChangeSecretMixin(SecretWithRandomMixin):
ssh_key_change_strategy = models.CharField(
choices=SSHKeyStrategy.choices, max_length=16,
default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy')
)
class Meta:
abstract = True
class ChangeSecretMixin(ChangeSecretRuleMixin):
secret_type = models.CharField(
choices=SecretType.choices, max_length=16,
default=SecretType.PASSWORD, verbose_name=_('Secret type')
)
secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
get_all_assets: callable # get all assets
class Meta:

View File

@@ -17,9 +17,9 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation):
def create_nonlocal_accounts(self, usernames, asset):
secret_type = self.secret_type
account_usernames = asset.accounts.filter(secret_type=self.secret_type).values_list(
'username', flat=True
)
account_usernames = asset.accounts \
.filter(secret_type=self.secret_type) \
.values_list('username', flat=True)
create_usernames = set(usernames) - set(account_usernames)
create_account_objs = [
Account(

View File

@@ -8,8 +8,10 @@ from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
from accounts.const import SecretType
from accounts.const import SecretType, SecretStrategy
from accounts.models.mixins import VaultModelMixin, VaultManagerMixin, VaultQuerySetMixin
from accounts.utils import SecretGenerator
from common.db import fields
from common.utils import (
ssh_key_string_to_obj, ssh_key_gen, get_logger,
random_string, lazyproperty, parse_ssh_public_key_str, is_openssh_format_key
@@ -29,6 +31,35 @@ class BaseAccountManager(VaultManagerMixin, OrgManager):
return self.get_queryset().active()
class SecretWithRandomMixin(models.Model):
secret_type = models.CharField(
choices=SecretType.choices, max_length=16,
default=SecretType.PASSWORD, verbose_name=_('Secret type')
)
secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
secret_strategy = models.CharField(
choices=SecretStrategy.choices, max_length=16,
default=SecretStrategy.custom, verbose_name=_('Secret strategy')
)
password_rules = models.JSONField(default=dict, verbose_name=_('Password rules'))
class Meta:
abstract = True
@lazyproperty
def secret_generator(self):
return SecretGenerator(
self.secret_strategy, self.secret_type,
self.password_rules,
)
def get_secret(self):
if self.secret_strategy == 'random':
return self.secret_generator.get_secret()
else:
return self.secret
class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
name = models.CharField(max_length=128, verbose_name=_("Name"))
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)

View File

@@ -4,22 +4,16 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from .account import Account
from .base import BaseAccount
from .base import BaseAccount, SecretWithRandomMixin
__all__ = ['AccountTemplate', ]
from ..const import SecretStrategy
class AccountTemplate(BaseAccount):
class AccountTemplate(BaseAccount, SecretWithRandomMixin):
su_from = models.ForeignKey(
'self', related_name='su_to', null=True,
on_delete=models.SET_NULL, verbose_name=_("Su from")
)
secret_strategy = models.CharField(
choices=SecretStrategy.choices, max_length=16,
default=SecretStrategy.custom, verbose_name=_('Secret strategy')
)
auto_push = models.BooleanField(default=False, verbose_name=_('Auto push'))
platforms = models.ManyToManyField(
'assets.Platform', related_name='account_templates',

View File

@@ -73,6 +73,22 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
name = name + '_' + uuid.uuid4().hex[:4]
initial_data['name'] = name
@staticmethod
def get_template_attr_for_account(template):
# Set initial data from template
field_names = [
'username', 'secret', 'secret_type', 'privileged', 'is_active'
]
attrs = {}
for name in field_names:
value = getattr(template, name, None)
if value is None:
continue
attrs[name] = value
attrs['secret'] = template.get_secret()
return attrs
def from_template_if_need(self, initial_data):
if isinstance(initial_data, str):
return
@@ -89,20 +105,7 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
raise serializers.ValidationError({'template': 'Template not found'})
self._template = template
# Set initial data from template
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
]
field_names = [name if name != '_secret' else 'secret' for name in field_names]
attrs = {}
for name in field_names:
value = getattr(template, name, None)
if value is None:
continue
attrs[name] = value
attrs = self.get_template_attr_for_account(template)
initial_data.update(attrs)
initial_data.update({
'source': Source.TEMPLATE,

View File

@@ -7,10 +7,19 @@ from common.serializers.fields import ObjectRelatedField
from .base import BaseAccountSerializer
class PasswordRulesSerializer(serializers.Serializer):
length = serializers.IntegerField(min_value=8, max_value=30, default=16, label=_('Password length'))
lowercase = serializers.BooleanField(default=True, label=_('Lowercase'))
uppercase = serializers.BooleanField(default=True, label=_('Uppercase'))
digit = serializers.BooleanField(default=True, label=_('Digit'))
symbol = serializers.BooleanField(default=True, label=_('Special symbol'))
class AccountTemplateSerializer(BaseAccountSerializer):
is_sync_account = serializers.BooleanField(default=False, write_only=True)
_is_sync_account = False
password_rules = PasswordRulesSerializer(required=False, label=_('Password rules'))
su_from = ObjectRelatedField(
required=False, queryset=AccountTemplate.objects, allow_null=True,
allow_empty=True, label=_('Su from'), attrs=('id', 'name', 'username')
@@ -19,17 +28,20 @@ class AccountTemplateSerializer(BaseAccountSerializer):
class Meta(BaseAccountSerializer.Meta):
model = AccountTemplate
fields = BaseAccountSerializer.Meta.fields + [
'secret_strategy',
'secret_strategy', 'password_rules',
'auto_push', 'push_params', 'platforms',
'is_sync_account', 'su_from'
]
extra_kwargs = {
'secret_strategy': {'help_text': _('Secret generation strategy for account creation')},
'auto_push': {'help_text': _('Whether to automatically push the account to the asset')},
'platforms': {'help_text': _(
'Associated platform, you can configure push parameters. '
'If not associated, default parameters will be used'
)},
'platforms': {
'help_text': _(
'Associated platform, you can configure push parameters. '
'If not associated, default parameters will be used'
),
'required': False
},
}
def sync_accounts_secret(self, instance, diff):

View File

@@ -4,14 +4,13 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from accounts.const import (
AutomationTypes, DEFAULT_PASSWORD_RULES,
SecretType, SecretStrategy, SSHKeyStrategy
AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy
)
from accounts.models import (
Account, ChangeSecretAutomation,
ChangeSecretRecord, AutomationExecution
)
from accounts.serializers import AuthValidateMixin
from accounts.serializers import AuthValidateMixin, PasswordRulesSerializer
from assets.models import Asset
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
from common.utils import get_logger
@@ -42,7 +41,7 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
ssh_key_change_strategy = LabeledChoiceField(
choices=SSHKeyStrategy.choices, required=False, label=_('SSH Key strategy')
)
password_rules = serializers.DictField(default=DEFAULT_PASSWORD_RULES)
password_rules = PasswordRulesSerializer(required=False, label=_('Password rules'))
secret_type = LabeledChoiceField(choices=get_secret_types(), required=True, label=_('Secret type'))
class Meta:

View File

@@ -1,3 +1,5 @@
import copy
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
@@ -18,9 +20,19 @@ class SecretGenerator:
return private_key
def generate_password(self):
length = int(self.password_rules.get('length', 0))
length = length if length else DEFAULT_PASSWORD_RULES['length']
return random_string(length, special_char=True)
password_rules = self.password_rules
if not password_rules or not isinstance(password_rules, dict):
password_rules = {}
rules = copy.deepcopy(DEFAULT_PASSWORD_RULES)
rules.update(password_rules)
rules = {
'length': rules['length'],
'lower': rules['lowercase'],
'upper': rules['uppercase'],
'digit': rules['digit'],
'special_char': rules['symbol']
}
return random_string(**rules)
def get_secret(self):
if self.secret_type == SecretType.SSH_KEY: