mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-09-17 07:49:01 +00:00
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:
@@ -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):
|
||||
|
@@ -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'),
|
||||
),
|
||||
]
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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(
|
||||
|
@@ -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)
|
||||
|
@@ -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',
|
||||
|
@@ -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,
|
||||
|
@@ -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):
|
||||
|
@@ -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:
|
||||
|
@@ -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:
|
||||
|
Reference in New Issue
Block a user