mirror of https://github.com/jumpserver/jumpserver
perf: 修改翻译
parent
714b4ef7f4
commit
32ef4c79da
|
@ -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."
|
||||
)
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`のようになります。"
|
||||
|
|
|
@ -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
|
@ -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__()
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue