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

View File

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

View File

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

View File

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