perf: 修改翻译

pull/13291/head
ibuler 2024-05-23 19:00:28 +08:00
parent 714b4ef7f4
commit 32ef4c79da
7 changed files with 1731 additions and 1575 deletions

View File

@ -9,26 +9,34 @@ from common.serializers import ResourceLabelsMixin
from common.serializers.fields import EncryptedField, LabeledChoiceField
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
__all__ = ['AuthValidateMixin', 'BaseAccountSerializer']
__all__ = ["AuthValidateMixin", "BaseAccountSerializer"]
class AuthValidateMixin(serializers.Serializer):
secret_type = LabeledChoiceField(
choices=SecretType.choices, label=_('Secret type'), default='password'
choices=SecretType.choices, label=_("Secret type"), default="password"
)
secret = EncryptedField(
label=_('Secret'), required=False, max_length=40960, allow_blank=True,
allow_null=True, write_only=True,
label=_("Secret"),
required=False,
max_length=40960,
allow_blank=True,
allow_null=True,
write_only=True,
)
passphrase = serializers.CharField(
allow_blank=True, allow_null=True, required=False, max_length=512,
write_only=True, label=_('Passphrase')
allow_blank=True,
allow_null=True,
required=False,
max_length=512,
write_only=True,
label=_("Passphrase"),
)
@staticmethod
def handle_secret(secret, secret_type, passphrase=None):
if not secret:
return ''
return ""
if secret_type == SecretType.PASSWORD:
validate_password_for_ansible(secret)
return secret
@ -40,17 +48,15 @@ class AuthValidateMixin(serializers.Serializer):
return secret
def clean_auth_fields(self, validated_data):
secret_type = validated_data.get('secret_type')
passphrase = validated_data.get('passphrase')
secret = validated_data.pop('secret', None)
validated_data['secret'] = self.handle_secret(
secret, secret_type, passphrase
)
for field in ('secret',):
secret_type = validated_data.get("secret_type")
passphrase = validated_data.get("passphrase")
secret = validated_data.pop("secret", None)
validated_data["secret"] = self.handle_secret(secret, secret_type, passphrase)
for field in ("secret",):
value = validated_data.get(field)
if not value:
validated_data.pop(field, None)
validated_data.pop('passphrase', None)
validated_data.pop("passphrase", None)
def create(self, validated_data):
self.clean_auth_fields(validated_data)
@ -61,23 +67,34 @@ class AuthValidateMixin(serializers.Serializer):
return super().update(instance, validated_data)
class BaseAccountSerializer(AuthValidateMixin, ResourceLabelsMixin, BulkOrgResourceModelSerializer):
class BaseAccountSerializer(
AuthValidateMixin, ResourceLabelsMixin, BulkOrgResourceModelSerializer
):
class Meta:
model = BaseAccount
fields_mini = ['id', 'name', 'username']
fields_mini = ["id", "name", "username"]
fields_small = fields_mini + [
'secret_type', 'secret', 'passphrase',
'privileged', 'is_active', 'spec_info',
"secret_type",
"secret",
"passphrase",
"privileged",
"is_active",
"spec_info",
]
fields_other = ['created_by', 'date_created', 'date_updated', 'comment']
fields = fields_small + fields_other + ['labels']
fields_other = ["created_by", "date_created", "date_updated", "comment"]
fields = fields_small + fields_other + ["labels"]
read_only_fields = [
'spec_info', 'date_verified', 'created_by', 'date_created',
"spec_info",
"date_verified",
"created_by",
"date_created",
]
extra_kwargs = {
'spec_info': {'label': _('Spec info')},
'username': {'help_text': _(
"Tip: If no username is required for authentication, fill in `null`, "
"If AD account, like `username@domain`"
)},
"spec_info": {"label": _("Spec info")},
"username": {
"help_text": _(
"* If no username is required for authentication, enter null. "
"For AD accounts, use the format username@domain."
)
},
}

View File

@ -902,8 +902,8 @@ msgstr ""
#: accounts/serializers/account/base.py:80
msgid ""
"Tip: If no username is required for authentication, fill in `null`, If AD "
"account, like `username@domain`"
"* If no username is required for authentication, enter null. "
"For AD accounts, use the format username@domain."
msgstr ""
#: accounts/serializers/account/template.py:13

View File

@ -928,8 +928,8 @@ msgstr "特別情報"
#: accounts/serializers/account/base.py:80
msgid ""
"Tip: If no username is required for authentication, fill in `null`, If AD "
"account, like `username@domain`"
"* If no username is required for authentication, enter null. "
"For AD accounts, use the format username@domain."
msgstr ""
"ヒント: 認証にユーザー名が必要ない場合は、`null`を入力します。ADアカウントの"
"場合は、`username@domain`のようになります。"

View File

@ -920,8 +920,8 @@ msgstr "特殊信息"
#: accounts/serializers/account/base.py:80
msgid ""
"Tip: If no username is required for authentication, fill in `null`, If AD "
"account, like `username@domain`"
"* If no username is required for authentication, enter null. "
"For AD accounts, use the format username@domain."
msgstr ""
"提示: 如果认证时不需要用户名,可填写为 null, 如果是 AD 账号,格式为 "
"username@domain"

File diff suppressed because it is too large Load Diff

View File

@ -22,18 +22,22 @@ from rest_framework.exceptions import PermissionDenied
from common.db import fields, models as jms_models
from common.utils import (
date_expired_default, get_logger, lazyproperty,
random_string, bulk_create_with_signal
date_expired_default,
get_logger,
lazyproperty,
random_string,
bulk_create_with_signal,
)
from labels.mixins import LabeledMixin
from orgs.utils import current_org
from rbac.const import Scope
from rbac.models import RoleBinding
from ..signals import (
post_user_change_password, post_user_leave_org, pre_user_leave_org
)
from ..signals import post_user_change_password, post_user_leave_org, pre_user_leave_org
__all__ = ['User', 'UserPasswordHistory', ]
__all__ = [
"User",
"UserPasswordHistory",
]
logger = get_logger(__file__)
@ -48,12 +52,12 @@ class AuthMixin:
set_password: Callable
save: Callable
history_passwords: models.Manager
sect_cache_tpl = 'user_sect_{}'
sect_cache_tpl = "user_sect_{}"
id: str
@property
def password_raw(self):
raise AttributeError('Password raw is not a readable attribute')
raise AttributeError("Password raw is not a readable attribute")
#: Use this attr to set user object password, example
#: user = User(username='example', password_raw='password', ...)
@ -88,8 +92,9 @@ class AuthMixin:
def is_history_password(self, password):
allow_history_password_count = settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT
history_passwords = self.history_passwords.all() \
.order_by('-date_created')[:int(allow_history_password_count)]
history_passwords = self.history_passwords.all().order_by("-date_created")[
: int(allow_history_password_count)
]
for history_password in history_passwords:
if check_password(password, history_password.password):
@ -99,8 +104,8 @@ class AuthMixin:
def is_public_key_valid(self):
"""
Check if the user's ssh public key is valid.
This function is used in base.html.
Check if the user's ssh public key is valid.
This function is used in base.html.
"""
if self.public_key:
return True
@ -110,7 +115,7 @@ class AuthMixin:
def public_key_obj(self):
class PubKey(object):
def __getattr__(self, item):
return ''
return ""
if self.public_key:
try:
@ -124,11 +129,11 @@ class AuthMixin:
def get_public_key_hash_md5(self):
if not callable(self.public_key_obj.hash_md5):
return ''
return ""
try:
return self.public_key_obj.hash_md5()
except:
return ''
return ""
def reset_password(self, new_password):
self.set_password(new_password)
@ -139,7 +144,8 @@ class AuthMixin:
def date_password_expired(self):
interval = settings.SECURITY_PASSWORD_EXPIRATION_TIME
date_expired = self.date_password_last_updated + timezone.timedelta(
days=int(interval))
days=int(interval)
)
return date_expired
@property
@ -165,7 +171,7 @@ class AuthMixin:
key_obj = sshpubkeys.SSHKey(key)
return key_obj.hash_md5()
except Exception as e:
return ''
return ""
def check_public_key(self, key):
if not self.public_key:
@ -178,10 +184,11 @@ class AuthMixin:
def cache_login_password_if_need(self, password):
from common.utils import signer
if not settings.CACHE_LOGIN_PASSWORD_ENABLED:
return
backend = getattr(self, 'backend', '')
if backend.lower().find('ldap') < 0:
backend = getattr(self, "backend", "")
if backend.lower().find("ldap") < 0:
return
if not password:
return
@ -194,12 +201,13 @@ class AuthMixin:
def get_cached_password_if_has(self):
from common.utils import signer
if not settings.CACHE_LOGIN_PASSWORD_ENABLED:
return ''
return ""
key = self.sect_cache_tpl.format(self.id)
secret = cache.get(key)
if not secret:
return ''
return ""
password = signer.unsign(secret)
return password
@ -215,6 +223,7 @@ class RoleManager(models.Manager):
@lazyproperty
def role_binding_cls(self):
from rbac.models import SystemRoleBinding, OrgRoleBinding
if self.scope == Scope.org:
return OrgRoleBinding
else:
@ -223,6 +232,7 @@ class RoleManager(models.Manager):
@lazyproperty
def role_cls(self):
from rbac.models import SystemRole, OrgRole
if self.scope == Scope.org:
return OrgRole
else:
@ -232,7 +242,7 @@ class RoleManager(models.Manager):
def display(self):
roles = sorted(list(self.all()), key=lambda r: r.scope)
roles_display = [role.display_name for role in roles]
return ', '.join(roles_display)
return ", ".join(roles_display)
@property
def role_bindings(self):
@ -273,25 +283,27 @@ class RoleManager(models.Manager):
return
roles = self._clean_roles(roles)
old_ids = self.role_bindings.values_list('role', flat=True)
old_ids = self.role_bindings.values_list("role", flat=True)
need_adds = [r for r in roles if r.id not in old_ids]
items = []
for role in need_adds:
kwargs = {'role': role, 'user': self.user, 'scope': self.scope}
kwargs = {"role": role, "user": self.user, "scope": self.scope}
if self.scope == Scope.org:
if current_org.is_root():
continue
else:
kwargs['org_id'] = current_org.id
kwargs["org_id"] = current_org.id
items.append(self.role_binding_cls(**kwargs))
try:
result = bulk_create_with_signal(self.role_binding_cls, items, ignore_conflicts=True)
result = bulk_create_with_signal(
self.role_binding_cls, items, ignore_conflicts=True
)
self.user.expire_users_rbac_perms_cache()
return result
except Exception as e:
logger.error('\tCreate role binding error: {}'.format(e))
logger.error("\tCreate role binding error: {}".format(e))
def set(self, roles, clear=False):
if clear:
@ -300,7 +312,7 @@ class RoleManager(models.Manager):
return
role_ids = set([r.id for r in roles])
old_ids = self.role_bindings.values_list('role', flat=True)
old_ids = self.role_bindings.values_list("role", flat=True)
old_ids = set(old_ids)
del_ids = old_ids - role_ids
@ -324,12 +336,14 @@ class RoleManager(models.Manager):
@property
def builtin_role(self):
from rbac.builtin import BuiltinRole
return BuiltinRole
class OrgRoleManager(RoleManager):
def __init__(self, *args, **kwargs):
from rbac.const import Scope
self.scope = Scope.org
super().__init__(*args, **kwargs)
@ -337,6 +351,7 @@ class OrgRoleManager(RoleManager):
class SystemRoleManager(RoleManager):
def __init__(self, *args, **kwargs):
from rbac.const import Scope
self.scope = Scope.system
super().__init__(*args, **kwargs)
@ -364,8 +379,8 @@ class RoleMixin:
id: str
_org_roles = None
_system_roles = None
PERM_CACHE_KEY = 'USER_PERMS_ROLES_{}_{}'
PERM_ORG_KEY = 'USER_PERMS_ORG_{}'
PERM_CACHE_KEY = "USER_PERMS_ROLES_{}_{}"
PERM_ORG_KEY = "USER_PERMS_ORG_{}"
_is_superuser = None
_update_superuser = False
@ -383,39 +398,43 @@ class RoleMixin:
@lazyproperty
def console_orgs(self):
return self.cached_orgs.get('console_orgs', [])
return self.cached_orgs.get("console_orgs", [])
@lazyproperty
def audit_orgs(self):
return self.cached_orgs.get('audit_orgs', [])
return self.cached_orgs.get("audit_orgs", [])
@lazyproperty
def workbench_orgs(self):
return self.cached_orgs.get('workbench_orgs', [])
return self.cached_orgs.get("workbench_orgs", [])
@lazyproperty
def joined_orgs(self):
from rbac.models import RoleBinding
return RoleBinding.get_user_joined_orgs(self)
@lazyproperty
def cached_orgs(self):
from rbac.models import RoleBinding
key = self.PERM_ORG_KEY.format(self.id)
data = cache.get(key)
if data:
return data
console_orgs = RoleBinding.get_user_has_the_perm_orgs('rbac.view_console', self)
audit_orgs = RoleBinding.get_user_has_the_perm_orgs('rbac.view_audit', self)
workbench_orgs = RoleBinding.get_user_has_the_perm_orgs('rbac.view_workbench', self)
console_orgs = RoleBinding.get_user_has_the_perm_orgs("rbac.view_console", self)
audit_orgs = RoleBinding.get_user_has_the_perm_orgs("rbac.view_audit", self)
workbench_orgs = RoleBinding.get_user_has_the_perm_orgs(
"rbac.view_workbench", self
)
if settings.LIMIT_SUPER_PRIV:
audit_orgs = list(set(audit_orgs) - set(console_orgs))
data = {
'console_orgs': console_orgs,
'audit_orgs': audit_orgs,
'workbench_orgs': workbench_orgs,
"console_orgs": console_orgs,
"audit_orgs": audit_orgs,
"workbench_orgs": workbench_orgs,
}
cache.set(key, data, 60 * 60)
return data
@ -428,9 +447,9 @@ class RoleMixin:
return data
data = {
'org_roles': self.org_roles.all(),
'system_roles': self.system_roles.all(),
'perms': self.get_all_permissions(),
"org_roles": self.org_roles.all(),
"system_roles": self.system_roles.all(),
"perms": self.get_all_permissions(),
}
cache.set(key, data, 60 * 60)
return data
@ -438,27 +457,29 @@ class RoleMixin:
@lazyproperty
def orgs_roles(self):
orgs_roles = defaultdict(set)
rbs = RoleBinding.objects_raw.filter(user=self, scope='org').prefetch_related('role', 'org')
rbs = RoleBinding.objects_raw.filter(user=self, scope="org").prefetch_related(
"role", "org"
)
for rb in rbs:
orgs_roles[rb.org_name].add(str(rb.role.display_name))
return orgs_roles
def expire_rbac_perms_cache(self):
key = self.PERM_CACHE_KEY.format(self.id, '*')
key = self.PERM_CACHE_KEY.format(self.id, "*")
cache.delete_pattern(key)
key = self.PERM_ORG_KEY.format(self.id)
cache.delete(key)
@classmethod
def expire_users_rbac_perms_cache(cls):
key = cls.PERM_CACHE_KEY.format('*', '*')
key = cls.PERM_CACHE_KEY.format("*", "*")
cache.delete_pattern(key)
key = cls.PERM_ORG_KEY.format('*')
key = cls.PERM_ORG_KEY.format("*")
cache.delete_pattern(key)
@lazyproperty
def perms(self):
return self.cached_role_and_perms['perms']
return self.cached_role_and_perms["perms"]
@property
def is_superuser(self):
@ -469,6 +490,7 @@ class RoleMixin:
return self._is_superuser
from rbac.builtin import BuiltinRole
ids = [str(r.id) for r in self.system_roles.all()]
yes = BuiltinRole.system_admin.id in ids
self._is_superuser = yes
@ -486,6 +508,7 @@ class RoleMixin:
@lazyproperty
def is_org_admin(self):
from rbac.builtin import BuiltinRole
if self.is_superuser:
return True
ids = [str(r.id) for r in self.org_roles.all()]
@ -500,14 +523,18 @@ class RoleMixin:
def is_staff(self, value):
pass
service_account_email_suffix = '@local.domain'
service_account_email_suffix = "@local.domain"
@classmethod
def create_service_account(cls, name, email, comment):
app = cls.objects.create(
username=name, name=name, email=email,
comment=comment, is_first_login=False,
created_by='System', is_service_account=True,
username=name,
name=name,
email=email,
comment=comment,
is_first_login=False,
created_by="System",
is_service_account=True,
)
access_key = app.create_access_key()
return app, access_key
@ -523,12 +550,14 @@ class RoleMixin:
@classmethod
def get_super_admins(cls):
from rbac.models import Role, RoleBinding
system_admin = Role.BuiltinRole.system_admin.get_role()
return RoleBinding.get_role_users(system_admin)
@classmethod
def get_org_admins(cls):
from rbac.models import Role, RoleBinding
org_admin = Role.BuiltinRole.org_admin.get_role()
return RoleBinding.get_role_users(org_admin)
@ -560,16 +589,17 @@ class RoleMixin:
def get_all_permissions(self):
from rbac.models import RoleBinding
perms = RoleBinding.get_user_perms(self)
if settings.LIMIT_SUPER_PRIV and 'view_console' in perms:
if settings.LIMIT_SUPER_PRIV and "view_console" in perms:
perms = [p for p in perms if p != "view_audit"]
return perms
class TokenMixin:
CACHE_KEY_USER_RESET_PASSWORD_PREFIX = "_KEY_USER_RESET_PASSWORD_{}"
email = ''
email = ""
id = None
@property
@ -578,11 +608,13 @@ class TokenMixin:
def create_private_token(self):
from authentication.models import PrivateToken
token, created = PrivateToken.objects.get_or_create(user=self)
return token
def delete_private_token(self):
from authentication.models import PrivateToken
PrivateToken.objects.filter(user=self).delete()
def refresh_private_token(self):
@ -592,18 +624,18 @@ class TokenMixin:
def create_bearer_token(self, request=None):
expiration = settings.TOKEN_EXPIRATION or 3600
if request:
remote_addr = request.META.get('REMOTE_ADDR', '')
remote_addr = request.META.get("REMOTE_ADDR", "")
else:
remote_addr = '0.0.0.0'
remote_addr = "0.0.0.0"
if not isinstance(remote_addr, bytes):
remote_addr = remote_addr.encode("utf-8")
remote_addr = base64.b16encode(remote_addr) # .replace(b'=', '')
cache_key = '%s_%s' % (self.id, remote_addr)
cache_key = "%s_%s" % (self.id, remote_addr)
token = cache.get(cache_key)
if not token:
token = random_string(36)
cache.set(token, self.id, expiration)
cache.set('%s_%s' % (self.id, remote_addr), token, expiration)
cache.set("%s_%s" % (self.id, remote_addr), token, expiration)
date_expired = timezone.now() + timezone.timedelta(seconds=expiration)
return token, date_expired
@ -621,7 +653,7 @@ class TokenMixin:
def generate_reset_token(self):
token = random_string(50)
key = self.CACHE_KEY_USER_RESET_PASSWORD_PREFIX.format(token)
cache.set(key, {'id': self.id, 'email': self.email}, 3600)
cache.set(key, {"id": self.id, "email": self.email}, 3600)
return token
@classmethod
@ -633,8 +665,8 @@ class TokenMixin:
if not value:
return None
try:
user_id = value.get('id', '')
email = value.get('email', '')
user_id = value.get("id", "")
email = value.get("email", "")
user = cls.objects.get(id=user_id, email=email)
return user
except (AttributeError, cls.DoesNotExist) as e:
@ -649,11 +681,11 @@ class TokenMixin:
class MFAMixin:
mfa_level = 0
otp_secret_key = ''
otp_secret_key = ""
MFA_LEVEL_CHOICES = (
(0, _('Disable')),
(1, _('Enable')),
(2, _("Force enable")),
(0, _("Disabled")),
(1, _("Enabled")),
(2, _("Force enabled")),
)
is_org_admin: bool
username: str
@ -728,39 +760,43 @@ class JSONFilterMixin:
from orgs.utils import current_org
kwargs = {}
if name == 'system_roles':
kwargs['scope'] = 'system'
elif name == 'org_roles':
kwargs['scope'] = 'org'
if name == "system_roles":
kwargs["scope"] = "system"
elif name == "org_roles":
kwargs["scope"] = "org"
if not current_org.is_root():
kwargs['org_id'] = current_org.id
kwargs["org_id"] = current_org.id
else:
return None
bindings = RoleBinding.objects.filter(**kwargs, role__in=value)
if match == 'm2m_all':
user_id = bindings.values('user_id').annotate(count=Count('user_id', distinct=True)) \
.filter(count=len(value)).values_list('user_id', flat=True)
if match == "m2m_all":
user_id = (
bindings.values("user_id")
.annotate(count=Count("user_id", distinct=True))
.filter(count=len(value))
.values_list("user_id", flat=True)
)
else:
user_id = bindings.values_list('user_id', flat=True)
user_id = bindings.values_list("user_id", flat=True)
return models.Q(id__in=user_id)
class Source(models.TextChoices):
local = 'local', _('Local')
ldap = 'ldap', 'LDAP/AD'
openid = 'openid', 'OpenID'
radius = 'radius', 'Radius'
cas = 'cas', 'CAS'
saml2 = 'saml2', 'SAML2'
oauth2 = 'oauth2', 'OAuth2'
wecom = 'wecom', _('WeCom')
dingtalk = 'dingtalk', _('DingTalk')
feishu = 'feishu', _('FeiShu')
lark = 'lark', _('Lark')
slack = 'slack', _('Slack')
custom = 'custom', 'Custom'
local = "local", _("Local")
ldap = "ldap", "LDAP/AD"
openid = "openid", "OpenID"
radius = "radius", "Radius"
cas = "cas", "CAS"
saml2 = "saml2", "SAML2"
oauth2 = "oauth2", "OAuth2"
wecom = "wecom", _("WeCom")
dingtalk = "dingtalk", _("DingTalk")
feishu = "feishu", _("FeiShu")
lark = "lark", _("Lark")
slack = "slack", _("Slack")
custom = "custom", "Custom"
class SourceMixin:
@ -773,43 +809,21 @@ class SourceMixin:
settings.AUTH_BACKEND_MODEL,
settings.AUTH_BACKEND_PUBKEY,
],
Source.ldap: [
settings.AUTH_BACKEND_LDAP
],
Source.ldap: [settings.AUTH_BACKEND_LDAP],
Source.openid: [
settings.AUTH_BACKEND_OIDC_PASSWORD,
settings.AUTH_BACKEND_OIDC_CODE
settings.AUTH_BACKEND_OIDC_CODE,
],
Source.radius: [
settings.AUTH_BACKEND_RADIUS
],
Source.cas: [
settings.AUTH_BACKEND_CAS
],
Source.saml2: [
settings.AUTH_BACKEND_SAML2
],
Source.oauth2: [
settings.AUTH_BACKEND_OAUTH2
],
Source.wecom: [
settings.AUTH_BACKEND_WECOM
],
Source.feishu: [
settings.AUTH_BACKEND_FEISHU
],
Source.lark: [
settings.AUTH_BACKEND_LARK
],
Source.slack: [
settings.AUTH_BACKEND_SLACK
],
Source.dingtalk: [
settings.AUTH_BACKEND_DINGTALK
],
Source.custom: [
settings.AUTH_BACKEND_CUSTOM
]
Source.radius: [settings.AUTH_BACKEND_RADIUS],
Source.cas: [settings.AUTH_BACKEND_CAS],
Source.saml2: [settings.AUTH_BACKEND_SAML2],
Source.oauth2: [settings.AUTH_BACKEND_OAUTH2],
Source.wecom: [settings.AUTH_BACKEND_WECOM],
Source.feishu: [settings.AUTH_BACKEND_FEISHU],
Source.lark: [settings.AUTH_BACKEND_LARK],
Source.slack: [settings.AUTH_BACKEND_SLACK],
Source.dingtalk: [settings.AUTH_BACKEND_DINGTALK],
Source.custom: [settings.AUTH_BACKEND_CUSTOM],
}
@classmethod
@ -826,7 +840,7 @@ class SourceMixin:
cls.Source.feishu: settings.AUTH_FEISHU,
cls.Source.slack: settings.AUTH_SLACK,
cls.Source.dingtalk: settings.AUTH_DINGTALK,
cls.Source.custom: settings.AUTH_CUSTOM
cls.Source.custom: settings.AUTH_CUSTOM,
}
return [str(k) for k, v in mapper.items() if v]
@ -842,7 +856,9 @@ class SourceMixin:
def get_source_choices(cls):
if cls._source_choices:
return cls._source_choices
used = cls.objects.values_list('source', flat=True).order_by('source').distinct()
used = (
cls.objects.values_list("source", flat=True).order_by("source").distinct()
)
enabled_sources = cls.get_sources_enabled()
_choices = []
for k, v in cls.Source.choices:
@ -866,78 +882,105 @@ class SourceMixin:
return self.SOURCE_BACKEND_MAPPING.get(self.source, [])
class User(AuthMixin, SourceMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin, JSONFilterMixin, AbstractUser):
class User(
AuthMixin,
SourceMixin,
TokenMixin,
RoleMixin,
MFAMixin,
LabeledMixin,
JSONFilterMixin,
AbstractUser,
):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
username = models.CharField(
max_length=128, unique=True, verbose_name=_('Username')
)
name = models.CharField(max_length=128, verbose_name=_('Name'))
email = models.EmailField(
max_length=128, unique=True, verbose_name=_('Email')
)
username = models.CharField(max_length=128, unique=True, verbose_name=_("Username"))
name = models.CharField(max_length=128, verbose_name=_("Name"))
email = models.EmailField(max_length=128, unique=True, verbose_name=_("Email"))
groups = models.ManyToManyField(
'users.UserGroup', related_name='users',
blank=True, verbose_name=_('User group')
"users.UserGroup",
related_name="users",
blank=True,
verbose_name=_("User group"),
)
role = models.CharField(
default='User', max_length=10,
blank=True, verbose_name=_('Role')
default="User", max_length=10, blank=True, verbose_name=_("Role")
)
is_service_account = models.BooleanField(default=False, verbose_name=_("Is service account"))
avatar = models.ImageField(
upload_to="avatar", null=True, verbose_name=_('Avatar')
is_service_account = models.BooleanField(
default=False, verbose_name=_("Is service account")
)
avatar = models.ImageField(upload_to="avatar", null=True, verbose_name=_("Avatar"))
wechat = fields.EncryptCharField(
max_length=128, blank=True, verbose_name=_('Wechat')
max_length=128, blank=True, verbose_name=_("Wechat")
)
phone = fields.EncryptCharField(
max_length=128, blank=True, null=True, verbose_name=_('Phone')
max_length=128, blank=True, null=True, verbose_name=_("Phone")
)
mfa_level = models.SmallIntegerField(
default=0, choices=MFAMixin.MFA_LEVEL_CHOICES, verbose_name=_('MFA')
default=0, choices=MFAMixin.MFA_LEVEL_CHOICES, verbose_name=_("MFA")
)
otp_secret_key = fields.EncryptCharField(
max_length=128, blank=True, null=True, verbose_name=_('OTP secret key')
max_length=128, blank=True, null=True, verbose_name=_("OTP secret key")
)
# Todo: Auto generate key, let user download
private_key = fields.EncryptTextField(
blank=True, null=True, verbose_name=_('Private key')
blank=True, null=True, verbose_name=_("Private key")
)
public_key = fields.EncryptTextField(
blank=True, null=True, verbose_name=_('Public key')
blank=True, null=True, verbose_name=_("Public key")
)
comment = models.TextField(
blank=True, null=True, verbose_name=_('Comment')
)
is_first_login = models.BooleanField(default=True, verbose_name=_('Is first login'))
comment = models.TextField(blank=True, null=True, verbose_name=_("Comment"))
is_first_login = models.BooleanField(default=True, verbose_name=_("Is first login"))
date_expired = models.DateTimeField(
default=date_expired_default, blank=True, null=True,
db_index=True, verbose_name=_('Date expired')
default=date_expired_default,
blank=True,
null=True,
db_index=True,
verbose_name=_("Date expired"),
)
created_by = models.CharField(
max_length=30, default="", blank=True, verbose_name=_("Created by")
)
updated_by = models.CharField(
max_length=30, default="", blank=True, verbose_name=_("Updated by")
)
created_by = models.CharField(max_length=30, default='', blank=True, verbose_name=_('Created by'))
updated_by = models.CharField(max_length=30, default='', blank=True, verbose_name=_('Updated by'))
date_password_last_updated = models.DateTimeField(
auto_now_add=True, blank=True, null=True,
verbose_name=_('Date password last updated')
auto_now_add=True,
blank=True,
null=True,
verbose_name=_("Date password last updated"),
)
need_update_password = models.BooleanField(
default=False, verbose_name=_('Need update password')
default=False, verbose_name=_("Need update password")
)
source = models.CharField(
max_length=30, default=Source.local,
choices=Source.choices, verbose_name=_('Source')
max_length=30,
default=Source.local,
choices=Source.choices,
verbose_name=_("Source"),
)
wecom_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('WeCom'))
dingtalk_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('DingTalk'))
feishu_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('FeiShu'))
lark_id = models.CharField(null=True, default=None, max_length=128, verbose_name='Lark')
slack_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('Slack'))
date_api_key_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date api key used'))
date_updated = models.DateTimeField(auto_now=True, verbose_name=_('Date updated'))
wecom_id = models.CharField(
null=True, default=None, max_length=128, verbose_name=_("WeCom")
)
dingtalk_id = models.CharField(
null=True, default=None, max_length=128, verbose_name=_("DingTalk")
)
feishu_id = models.CharField(
null=True, default=None, max_length=128, verbose_name=_("FeiShu")
)
lark_id = models.CharField(
null=True, default=None, max_length=128, verbose_name="Lark"
)
slack_id = models.CharField(
null=True, default=None, max_length=128, verbose_name=_("Slack")
)
date_api_key_last_used = models.DateTimeField(
null=True, blank=True, verbose_name=_("Date api key used")
)
date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated"))
DATE_EXPIRED_WARNING_DAYS = 5
def __str__(self):
return '{0.name}({0.username})'.format(self)
return "{0.name}({0.username})".format(self)
@classmethod
def get_queryset(cls):
@ -949,7 +992,7 @@ class User(AuthMixin, SourceMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin
@property
def secret_key(self):
instance = self.preferences.filter(name='secret_key').first()
instance = self.preferences.filter(name="secret_key").first()
if not instance:
return
return instance.decrypt_value
@ -966,7 +1009,7 @@ class User(AuthMixin, SourceMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin
return bool(self.otp_secret_key)
def get_absolute_url(self):
return reverse('users:user-detail', args=(self.id,))
return reverse("users:user-detail", args=(self.id,))
@property
def is_expired(self):
@ -1002,16 +1045,16 @@ class User(AuthMixin, SourceMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin
def set_required_attr_if_need(self):
if not self.name:
self.name = self.username
if not self.email or '@' not in self.email:
email = '{}@{}'.format(self.username, settings.EMAIL_SUFFIX)
if '@' in self.username:
if not self.email or "@" not in self.email:
email = "{}@{}".format(self.username, settings.EMAIL_SUFFIX)
if "@" in self.username:
email = self.username
self.email = email
def save(self, *args, **kwargs):
self.set_required_attr_if_need()
if self.username == 'admin':
self.role = 'Admin'
if self.username == "admin":
self.role = "Admin"
self.is_active = True
return super().save(*args, **kwargs)
@ -1040,12 +1083,14 @@ class User(AuthMixin, SourceMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin
def unblock_login(self):
from users.utils import LoginBlockUtil, MFABlockUtils
LoginBlockUtil.unblock_user(self.username)
MFABlockUtils.unblock_user(self.username)
@property
def login_blocked(self):
from users.utils import LoginBlockUtil, MFABlockUtils
if LoginBlockUtil.is_user_block(self.username):
return True
if MFABlockUtils.is_user_block(self.username):
@ -1053,37 +1098,40 @@ class User(AuthMixin, SourceMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin
return False
def delete(self, using=None, keep_parents=False):
if self.pk == 1 or self.username == 'admin':
raise PermissionDenied(_('Can not delete admin user'))
if self.pk == 1 or self.username == "admin":
raise PermissionDenied(_("Can not delete admin user"))
return super(User, self).delete(using=using, keep_parents=keep_parents)
class Meta:
ordering = ['username']
ordering = ["username"]
verbose_name = _("User")
unique_together = (
('dingtalk_id',),
('wecom_id',),
('feishu_id',),
('lark_id',),
('slack_id',),
("dingtalk_id",),
("wecom_id",),
("feishu_id",),
("lark_id",),
("slack_id",),
)
permissions = [
('invite_user', _('Can invite user')),
('remove_user', _('Can remove user')),
('match_user', _('Can match user')),
("invite_user", _("Can invite user")),
("remove_user", _("Can remove user")),
("match_user", _("Can match user")),
]
#: Use this method initial user
@classmethod
def initial(cls):
from .group import UserGroup
user = cls(username='admin',
email='admin@jumpserver.org',
name=_('Administrator'),
password_raw='admin',
role='Admin',
comment=_('Administrator is the super user of system'),
created_by=_('System'))
user = cls(
username="admin",
email="admin@jumpserver.org",
name=_("Administrator"),
password_raw="admin",
role="Admin",
comment=_("Administrator is the super user of system"),
created_by=_("System"),
)
user.save()
user.groups.add(UserGroup.initial())
@ -1096,12 +1144,18 @@ class User(AuthMixin, SourceMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin
class UserPasswordHistory(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
password = models.CharField(max_length=128)
user = models.ForeignKey("users.User", related_name='history_passwords',
on_delete=jms_models.CASCADE_SIGNAL_SKIP, verbose_name=_('User'))
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
user = models.ForeignKey(
"users.User",
related_name="history_passwords",
on_delete=jms_models.CASCADE_SIGNAL_SKIP,
verbose_name=_("User"),
)
date_created = models.DateTimeField(
auto_now_add=True, verbose_name=_("Date created")
)
def __str__(self):
return f'{self.user} set at {self.date_created}'
return f"{self.user} set at {self.date_created}"
def __repr__(self):
return self.__str__()

View File

@ -3,12 +3,16 @@
from functools import partial
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from common.serializers import ResourceLabelsMixin, CommonBulkModelSerializer
from common.serializers.fields import (
EncryptedField, ObjectRelatedField, LabeledChoiceField, PhoneField
EncryptedField,
ObjectRelatedField,
LabeledChoiceField,
PhoneField,
)
from common.utils import pretty_string, get_logger
from common.validators import PhoneValidator
@ -40,18 +44,29 @@ def default_org_roles():
class RolesSerializerMixin(serializers.Serializer):
system_roles = ObjectRelatedField(
queryset=Role.system_roles, attrs=('id', 'display_name'),
label=_("System roles"), many=True, default=default_system_roles,
help_text=_("System roles are roles at the system level, and they will take effect across all organizations")
queryset=Role.system_roles,
attrs=("id", "display_name"),
label=_("System roles"),
many=True,
default=default_system_roles,
help_text=_(
"System roles are roles at the system level, and they will take effect across all organizations"
),
)
org_roles = ObjectRelatedField(
queryset=Role.org_roles, attrs=('id', 'display_name', 'name'),
label=_("Org roles"), many=True, required=False,
queryset=Role.org_roles,
attrs=("id", "display_name", "name"),
label=_("Org roles"),
many=True,
required=False,
default=default_org_roles,
help_text=_(
"Org roles are roles at the organization level, and they will only take effect within current organization")
"Org roles are roles at the organization level, and they will only take effect within current organization"
),
)
orgs_roles = serializers.JSONField(
read_only=True, label=_("Organizations and roles")
)
orgs_roles = serializers.JSONField(read_only=True, label=_("Organizations and roles"))
def pop_roles_if_need(self, fields):
request = self.context.get("request")
@ -67,7 +82,12 @@ class RolesSerializerMixin(serializers.Serializer):
OrgRoleBinding: ["org_roles", "orgs_roles"],
}
update_actions = ("partial_bulk_update", "bulk_update", "partial_update", "update")
update_actions = (
"partial_bulk_update",
"bulk_update",
"partial_update",
"update",
)
action = view.action or "list"
if action in update_actions:
action = "create"
@ -87,7 +107,9 @@ class RolesSerializerMixin(serializers.Serializer):
return fields
class UserSerializer(RolesSerializerMixin, ResourceLabelsMixin, CommonBulkModelSerializer):
class UserSerializer(
RolesSerializerMixin, ResourceLabelsMixin, CommonBulkModelSerializer
):
password_strategy = LabeledChoiceField(
choices=PasswordStrategy.choices,
default=PasswordStrategy.email,
@ -102,16 +124,33 @@ class UserSerializer(RolesSerializerMixin, ResourceLabelsMixin, CommonBulkModelS
login_blocked = serializers.BooleanField(read_only=True, label=_("Login blocked"))
is_expired = serializers.BooleanField(read_only=True, label=_("Is expired"))
is_valid = serializers.BooleanField(read_only=True, label=_("Is valid"))
is_otp_secret_key_bound = serializers.BooleanField(read_only=True, label=_("Is OTP bound"))
is_superuser = serializers.BooleanField(read_only=True, label=_("Super Administrator"))
is_org_admin = serializers.BooleanField(read_only=True, label=_("Organization Administrator"))
can_public_key_auth = serializers.BooleanField(
source="can_use_ssh_key_login", label=_("Can public key authentication"),
read_only=True
is_otp_secret_key_bound = serializers.BooleanField(
read_only=True, label=_("Is OTP bound")
)
is_superuser = serializers.BooleanField(
read_only=True, label=_("Super Administrator")
)
is_org_admin = serializers.BooleanField(
read_only=True, label=_("Organization Administrator")
)
can_public_key_auth = serializers.BooleanField(
source="can_use_ssh_key_login",
label=_("Can public key authentication"),
read_only=True,
)
password = EncryptedField(
label=_("Password"),
required=False,
allow_blank=True,
allow_null=True,
max_length=1024,
)
password = EncryptedField(label=_("Password"), required=False, allow_blank=True, allow_null=True, max_length=1024, )
phone = PhoneField(
validators=[PhoneValidator()], required=False, allow_blank=True, allow_null=True, label=_("Phone")
validators=[PhoneValidator()],
required=False,
allow_blank=True,
allow_null=True,
label=_("Phone"),
)
custom_m2m_fields = {
"system_roles": [BuiltinRole.system_user],
@ -124,31 +163,58 @@ class UserSerializer(RolesSerializerMixin, ResourceLabelsMixin, CommonBulkModelS
fields_mini = ["id", "name", "username"]
# 只能写的字段, 这个虽然无法在框架上生效,但是更多对我们是提醒
fields_write_only = [
"password", "public_key",
"password",
"public_key",
]
# xpack 包含的字段
fields_xpack = ["wecom_id", "dingtalk_id", "feishu_id", "lark_id", "slack_id"]
# small 指的是 不需要计算的直接能从一张表中获取到的数据
fields_small = fields_mini + fields_write_only + [
"email", "wechat", "phone", "mfa_level", "source",
*fields_xpack, "created_by", "updated_by", "comment", # 通用字段
]
fields_small = (
fields_mini
+ fields_write_only
+ [
"email",
"wechat",
"phone",
"mfa_level",
"source",
*fields_xpack,
"created_by",
"updated_by",
"comment", # 通用字段
]
)
fields_date = [
"date_expired", "date_joined", "last_login",
"date_updated", "date_api_key_last_used",
"date_expired",
"date_joined",
"last_login",
"date_updated",
"date_api_key_last_used",
]
fields_bool = [
"is_superuser", "is_org_admin",
"is_service_account", "is_valid",
"is_expired", "is_active", # 布尔字段
"is_otp_secret_key_bound", "can_public_key_auth",
"mfa_enabled", "need_update_password",
"is_superuser",
"is_org_admin",
"is_service_account",
"is_valid",
"is_expired",
"is_active", # 布尔字段
"is_otp_secret_key_bound",
"can_public_key_auth",
"mfa_enabled",
"need_update_password",
]
# 包含不太常用的字段,可以没有
fields_verbose = fields_small + fields_date + fields_bool + [
"mfa_force_enabled", "is_first_login",
"date_password_last_updated", "avatar_url",
]
fields_verbose = (
fields_small
+ fields_date
+ fields_bool
+ [
"mfa_force_enabled",
"is_first_login",
"date_password_last_updated",
"avatar_url",
]
)
# 外键的字段
fields_fk = []
# 多对多字段
@ -159,18 +225,25 @@ class UserSerializer(RolesSerializerMixin, ResourceLabelsMixin, CommonBulkModelS
fields_unexport = ["avatar_url", "is_service_account"]
read_only_fields = [
"date_joined", "last_login", "created_by",
"is_first_login", *fields_xpack, "date_api_key_last_used",
"date_joined",
"last_login",
"created_by",
"is_first_login",
*fields_xpack,
"date_api_key_last_used",
]
fields_only_root_org = ["orgs_roles"]
disallow_self_update_fields = ["is_active", "system_roles", "org_roles"]
extra_kwargs = {
"name": {
"help_text": _("Fullname of user"),
"help_text": _("Full name"),
},
"username": {
"help_text": _("Login username"),
},
"email": {
"help_text": _("Email address"),
},
"password": {
"write_only": True,
"required": False,
@ -179,13 +252,18 @@ class UserSerializer(RolesSerializerMixin, ResourceLabelsMixin, CommonBulkModelS
},
"groups": {
"label": _("Groups"),
"help_text": _("User groups to join"),
},
"is_superuser": {
"label": _("Superuser")
"source": {
"help_text": _(
"User source identifies where the user was created, which could be AD or other sources."
"There are security settings that can restrict users to log in to the system only from the sources."
),
},
"is_superuser": {"label": _("Superuser")},
"public_key": {"write_only": True},
"is_first_login": {"label": _("Is first login"), "read_only": True},
"is_active": {"label": _("Is active")},
"is_active": {"label": _("Is active"), "help_text": ""},
"is_valid": {"label": _("Is valid")},
"is_service_account": {"label": _("Is service account")},
"is_org_admin": {"label": _("Is org admin")},
@ -194,7 +272,10 @@ class UserSerializer(RolesSerializerMixin, ResourceLabelsMixin, CommonBulkModelS
"created_by": {"read_only": True, "allow_blank": True},
"role": {"default": "User"},
"is_otp_secret_key_bound": {"label": _("Is OTP bound")},
'mfa_level': {'label': _("MFA level")},
"mfa_level": {
"label": _("MFA level"),
"help_text": _("Multi-Factor Authentication"),
},
}
def __init__(self, *args, **kwargs):
@ -216,9 +297,10 @@ class UserSerializer(RolesSerializerMixin, ResourceLabelsMixin, CommonBulkModelS
source = self.fields.get("source")
if not source:
return
open_source = ['local', 'ldap', 'cas']
# if not settings.XPACK_ENABLED:
choices = {k: v for k, v in source.choices.items() if k in open_source}
open_source = ["local", "ldap", "cas"]
choices = dict(source.choices)
if not settings.XPACK_ENABLED:
choices = {k: v for k, v in choices.items() if k in open_source}
source.choices = list(choices.items())
def validate_password(self, password):
@ -306,7 +388,7 @@ class UserSerializer(RolesSerializerMixin, ResourceLabelsMixin, CommonBulkModelS
@classmethod
def setup_eager_loading(cls, queryset):
queryset = queryset.prefetch_related('groups', 'labels', 'labels__label')
queryset = queryset.prefetch_related("groups", "labels", "labels__label")
return queryset
@ -330,7 +412,9 @@ class InviteSerializer(RolesSerializerMixin, serializers.Serializer):
queryset=User.get_nature_users(),
many=True,
label=_("Users"),
help_text=_("For security, only a partial of users is displayed. You can search for more"),
help_text=_(
"* For security, only a partial of users is displayed. You can search for more"
),
)
system_roles = None
@ -344,6 +428,7 @@ class ServiceAccountSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
from authentication.serializers import AccessKeyCreateSerializer
self.fields["access_key"] = AccessKeyCreateSerializer(read_only=True)
def get_username(self):