# -*- coding: utf-8 -*- # 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, ) from common.utils import pretty_string, get_logger from common.validators import PhoneValidator from orgs.utils import current_org from rbac.builtin import BuiltinRole from rbac.models import OrgRoleBinding, SystemRoleBinding, Role from rbac.permissions import RBACPermission from ..const import PasswordStrategy from ..models import User __all__ = [ "UserSerializer", "MiniUserSerializer", "InviteSerializer", "ServiceAccountSerializer", "UserRetrieveSerializer", ] logger = get_logger(__file__) def default_system_roles(): return [BuiltinRole.system_user.get_role()] def default_org_roles(): return [BuiltinRole.org_user.get_role()] 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" ), ) org_roles = ObjectRelatedField( 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" ), ) orgs_roles = serializers.JSONField( read_only=True, label=_("Organizations and roles") ) def pop_roles_if_need(self, fields): request = self.context.get("request") view = self.context.get("view") if not all([request, view, hasattr(view, "action")]): return fields if request.user.is_anonymous: return fields model_cls_field_mapper = { SystemRoleBinding: ["system_roles"], OrgRoleBinding: ["org_roles", "orgs_roles"], } update_actions = ( "partial_bulk_update", "bulk_update", "partial_update", "update", ) action = view.action or "list" if action in update_actions: action = "create" for model_cls, fields_names in model_cls_field_mapper.items(): perms = RBACPermission.parse_action_model_perms(action, model_cls) if request.user.has_perms(perms): continue # 没有权限就去掉 for field_name in fields_names: fields.pop(field_name, None) return fields def get_fields(self): fields = super().get_fields() self.pop_roles_if_need(fields) return fields class UserSerializer( RolesSerializerMixin, ResourceLabelsMixin, CommonBulkModelSerializer ): password_strategy = LabeledChoiceField( choices=PasswordStrategy.choices, default=PasswordStrategy.email, allow_null=True, required=False, label=_("Password setting"), ) mfa_enabled = serializers.BooleanField(read_only=True, label=_("MFA enabled")) mfa_force_enabled = serializers.BooleanField( read_only=True, label=_("MFA force enabled") ) 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, ) 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"), ) custom_m2m_fields = { "system_roles": [BuiltinRole.system_user], "org_roles": [BuiltinRole.org_user], } class Meta: model = User # mini 是指能识别对象的最小单元 fields_mini = ["id", "name", "username"] # 只能写的字段, 这个虽然无法在框架上生效,但是更多对我们是提醒 fields_write_only = [ "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_date = [ "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", ] # 包含不太常用的字段,可以没有 fields_verbose = ( fields_small + fields_date + fields_bool + [ "mfa_force_enabled", "is_first_login", "date_password_last_updated", "avatar_url", ] ) # 外键的字段 fields_fk = [] # 多对多字段 fields_m2m = ["groups", "system_roles", "org_roles", "orgs_roles", "labels"] # 在serializer 上定义的字段 fields_custom = ["login_blocked", "password_strategy"] fields = fields_verbose + fields_fk + fields_m2m + fields_custom 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", ] fields_only_root_org = ["orgs_roles"] disallow_self_update_fields = ["is_active", "system_roles", "org_roles"] extra_kwargs = { "name": { "help_text": _("Full name"), }, "username": { "help_text": _("Login username"), }, "email": { "help_text": _("Email address"), }, "password": { "write_only": True, "required": False, "allow_null": True, "allow_blank": True, }, "groups": { "label": _("Groups"), "help_text": _("User groups to join"), }, "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"), "help_text": ""}, "is_valid": {"label": _("Is valid")}, "is_service_account": {"label": _("Is service account")}, "is_org_admin": {"label": _("Is org admin")}, "is_expired": {"label": _("Is expired")}, "avatar_url": {"label": _("Avatar url")}, "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"), "help_text": _("Multi-Factor Authentication"), }, } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.update_source_choices() def get_fields(self): fields = super().get_fields() self.pop_fields_if_need(fields) return fields def pop_fields_if_need(self, fields): # pop only root org fields if not current_org.is_root(): for f in self.Meta.fields_only_root_org: fields.pop(f, None) def update_source_choices(self): source = self.fields.get("source") if not source: return 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): password_strategy = self.initial_data.get("password_strategy") if self.instance is None and password_strategy != PasswordStrategy.custom: # 创建用户,使用邮件设置密码 return if self.instance and not password: # 更新用户, 未设置密码 return return password @staticmethod def change_password_to_raw(attrs): password = attrs.pop("password", None) if password: attrs["password_raw"] = password return attrs @staticmethod def clean_auth_fields(attrs): for field in ("password", "public_key"): value = attrs.get(field) if not value: attrs.pop(field, None) return attrs def check_disallow_self_update_fields(self, attrs): request = self.context.get("request") if not request or not request.user.is_authenticated: return attrs if not self.instance: return attrs if request.user.id != self.instance.id: return attrs disallow_fields = set(list(attrs.keys())) & set( self.Meta.disallow_self_update_fields ) if not disallow_fields: return attrs # 用户自己不能更新自己的一些字段 logger.debug("Disallow update self fields: %s", disallow_fields) for field in disallow_fields: attrs.pop(field, None) return attrs def validate(self, attrs): attrs = self.check_disallow_self_update_fields(attrs) attrs = self.change_password_to_raw(attrs) attrs = self.clean_auth_fields(attrs) attrs.pop("password_strategy", None) return attrs def save_and_set_custom_m2m_fields(self, validated_data, save_handler, created): m2m_values = {} for f, default_roles in self.custom_m2m_fields.items(): roles = validated_data.pop(f, None) if created and not roles: roles = [ Role.objects.filter(id=role.id).first() for role in default_roles ] m2m_values[f] = roles instance = save_handler(validated_data) for field_name, value in m2m_values.items(): if value is None: continue field = getattr(instance, field_name) field.set(value) return instance def update(self, instance, validated_data): save_handler = partial(super().update, instance) instance = self.save_and_set_custom_m2m_fields( validated_data, save_handler, created=False ) return instance def create(self, validated_data): save_handler = super().create instance = self.save_and_set_custom_m2m_fields( validated_data, save_handler, created=True ) return instance @classmethod def setup_eager_loading(cls, queryset): queryset = queryset.prefetch_related("groups", "labels", "labels__label") return queryset class UserRetrieveSerializer(UserSerializer): login_confirm_settings = serializers.PrimaryKeyRelatedField( read_only=True, source="login_confirm_setting.reviewers", many=True ) class Meta(UserSerializer.Meta): fields = UserSerializer.Meta.fields + ["login_confirm_settings"] class MiniUserSerializer(serializers.ModelSerializer): class Meta: model = User fields = UserSerializer.Meta.fields_mini class InviteSerializer(RolesSerializerMixin, serializers.Serializer): users = serializers.PrimaryKeyRelatedField( 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" ), ) system_roles = None class ServiceAccountSerializer(serializers.ModelSerializer): class Meta: model = User fields = ["id", "name", "access_key", "comment"] read_only_fields = ["access_key"] 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): return self.initial_data.get("name") def get_email(self): name = self.initial_data.get("name") name_max_length = 128 - len(User.service_account_email_suffix) name = pretty_string(name, max_length=name_max_length, ellipsis_str="-") return "{}{}".format(name, User.service_account_email_suffix) def validate_name(self, name): email = self.get_email() username = self.get_username() if self.instance: users = User.objects.exclude(id=self.instance.id) else: users = User.objects.all() if users.filter(email=email) or users.filter(username=username): raise serializers.ValidationError(_("name not unique"), code="unique") return name def create(self, validated_data): name = validated_data["name"] email = self.get_email() comment = validated_data.get("comment", "") user, ak = User.create_service_account(name, email, comment) return user