diff --git a/src/api/bkuser_core/categories/plugins/ldap/adaptor.py b/src/api/bkuser_core/categories/plugins/ldap/adaptor.py index d43298b99..2d74eb151 100644 --- a/src/api/bkuser_core/categories/plugins/ldap/adaptor.py +++ b/src/api/bkuser_core/categories/plugins/ldap/adaptor.py @@ -8,6 +8,7 @@ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ +import logging from dataclasses import dataclass from typing import Any, Dict, List, NamedTuple, Optional @@ -16,62 +17,69 @@ from django.utils.encoding import force_str from ldap3.utils import dn as dn_utils +logger = logging.getLogger(__name__) + @dataclass class ProfileFieldMapper: """从 ldap 对象属性中获取用户字段""" config_loader: ConfigProvider - setting_field_map: dict - - def get_field(self, user_meta: Dict[str, List[bytes]], field_name: str, raise_exception: bool = False) -> str: - """根据字段映射关系, 从 ldap 中获取 `field_name` 的值""" - try: - setting_name = self.setting_field_map[field_name] - except KeyError: - if raise_exception: - raise ValueError("该用户字段没有在配置中有对应项,无法同步") + embed_fields = [ + "username", + "display_name", + "email", + "telephone", + ] + + def get_value(self, field_name: str, user_meta: Dict[str, List[bytes]], remain_raw: bool = False) -> Any: + """通过 field_name 从 ldap 数据中获取具体值""" + # 1. 从目录配置中获取 字段名 + ldap_field_name = self.config_loader.get(field_name) + if not ldap_field_name: + logger.info("no config[%s] in configs of category", field_name) return "" - try: - ldap_field_name = self.config_loader[setting_name] - except KeyError: - if raise_exception: - raise ValueError(f"用户目录配置中缺失字段 {setting_name}") + # 2. 通过字段名,获取具体值 + if ldap_field_name not in user_meta or not user_meta[ldap_field_name]: + logger.info("field[%s] is missing in raw attributes of user data from ldap", field_name) return "" - try: - if user_meta[ldap_field_name]: - return force_str(user_meta[ldap_field_name][0]) + # 3. 类似 memberOf 字段,将会返回原始列表 + if remain_raw: + return user_meta[ldap_field_name] - return "" - except KeyError: - if raise_exception: - raise ValueError(f"搜索数据中没有对应的字段 {ldap_field_name}") - return "" + return force_str(user_meta[ldap_field_name][0]) + + def get_values(self, user_meta: Dict[str, List[bytes]]) -> Dict[str, Any]: + """根据字段映射关系, 从 ldap 中获取 `field_name` 的值""" + + values = {} + for field_name in self.embed_fields: + values.update({field_name: self.get_value(field_name, user_meta)}) + + return values def get_user_attributes(self) -> list: """获取远端属性名列表""" - return [self.config_loader[x] for x in self.setting_field_map.values() if self.config_loader.get(x)] + return [self.config_loader[x] for x in self.embed_fields if self.config_loader.get(x)] def user_adapter( code: str, user_meta: Dict[str, Any], field_mapper: ProfileFieldMapper, restrict_types: List[str] ) -> LdapUserProfile: - groups = user_meta["attributes"].get(field_mapper.config_loader["user_member_of"], []) + + groups = field_mapper.get_value("user_member_of", user_meta["raw_attributes"], True) or [] return LdapUserProfile( - username=field_mapper.get_field(user_meta=user_meta["raw_attributes"], field_name="username"), - email=field_mapper.get_field(user_meta=user_meta["raw_attributes"], field_name="email"), - telephone=field_mapper.get_field(user_meta=user_meta["raw_attributes"], field_name="telephone"), - display_name=field_mapper.get_field(user_meta=user_meta["raw_attributes"], field_name="display_name"), + **field_mapper.get_values(user_meta["raw_attributes"]), code=code, # TODO: 完成转换 departments 的逻辑 departments=[ # 根据约定, dn 中除去第一个成分以外的部分即为用户所在的部门, 因此需要取 [1:] list(reversed(parse_dn_value_list(user_meta["dn"], restrict_types)[1:])), # 用户与用户组之间的关系 - *[list(reversed(parse_dn_value_list(group, restrict_types))) for group in groups], + *[list(reversed(parse_dn_value_list(force_str(group), restrict_types))) for group in groups], ], ) diff --git a/src/api/bkuser_core/categories/plugins/ldap/login.py b/src/api/bkuser_core/categories/plugins/ldap/login.py index 1e48384de..3983606c2 100644 --- a/src/api/bkuser_core/categories/plugins/ldap/login.py +++ b/src/api/bkuser_core/categories/plugins/ldap/login.py @@ -11,7 +11,6 @@ from bkuser_core.categories.plugins.ldap.adaptor import ProfileFieldMapper from bkuser_core.categories.plugins.ldap.client import LDAPClient from bkuser_core.categories.plugins.ldap.exceptions import FetchUserMetaInfoFailed -from bkuser_core.categories.plugins.ldap.syncer import SETTING_FIELD_MAP from bkuser_core.user_settings.loader import ConfigProvider from django.utils.encoding import force_str @@ -19,7 +18,7 @@ class LoginHandler: @staticmethod def fetch_username(field_fetcher, user_info: dict) -> str: - return force_str(field_fetcher.get_field(user_meta=user_info["raw_attributes"], field_name="username")) + return force_str(field_fetcher.get_value(field_name="username", user_meta=user_info["raw_attributes"])) @staticmethod def fetch_dn(user_info: dict) -> str: @@ -29,7 +28,7 @@ def check(self, profile, password): category_id = profile.category_id config_loader = ConfigProvider(category_id=category_id) client = LDAPClient(config_loader) - field_fetcher = ProfileFieldMapper(config_loader, SETTING_FIELD_MAP) + field_fetcher = ProfileFieldMapper(config_loader) users = client.search( object_class=config_loader["user_class"], diff --git a/src/api/bkuser_core/categories/plugins/ldap/syncer.py b/src/api/bkuser_core/categories/plugins/ldap/syncer.py index c37b5a371..e80c7422a 100644 --- a/src/api/bkuser_core/categories/plugins/ldap/syncer.py +++ b/src/api/bkuser_core/categories/plugins/ldap/syncer.py @@ -29,14 +29,6 @@ logger = logging.getLogger(__name__) -SETTING_FIELD_MAP = { - "username": "username", - "display_name": "display_name", - "email": "email", - "telephone": "telephone", - "user_member_of": "user_member_of", -} - @dataclass class LDAPFetcher(Fetcher): @@ -46,7 +38,7 @@ class LDAPFetcher(Fetcher): def __post_init__(self): self.client = LDAPClient(self.config_loader) - self.field_mapper = ProfileFieldMapper(config_loader=self.config_loader, setting_field_map=SETTING_FIELD_MAP) + self.field_mapper = ProfileFieldMapper(config_loader=self.config_loader) self._data: Tuple[List, List, List] = None def fetch(self): diff --git a/src/api/bkuser_core/categories/serializers.py b/src/api/bkuser_core/categories/serializers.py index cf2ca3f7d..e0eb5ee62 100644 --- a/src/api/bkuser_core/categories/serializers.py +++ b/src/api/bkuser_core/categories/serializers.py @@ -99,6 +99,7 @@ class CategoryTestFetchDataSerializer(Serializer): user_filter = CharField() organization_class = CharField() user_group_filter = CharField(required=False, allow_blank=True, allow_null=True) + user_member_of = CharField(required=False, allow_blank=True, allow_null=True) class SyncTaskSerializer(Serializer): diff --git a/src/api/bkuser_core/profiles/views.py b/src/api/bkuser_core/profiles/views.py index f756d181e..833ae0a62 100644 --- a/src/api/bkuser_core/profiles/views.py +++ b/src/api/bkuser_core/profiles/views.py @@ -615,35 +615,39 @@ def login(self, request): ) logger.exception("check profile<%s> failed", profile.username) raise error_codes.PASSWORD_ERROR - else: - # 密码状态校验:初始密码未修改 - if config_loader.get("force_reset_first_login") and profile.password_update_time is None: - create_profile_log( - profile=profile, - operation="LogIn", - request=request, - params={"is_success": False, "reason": LogInFailReason.SHOULD_CHANGE_INITIAL_PASSWORD.value}, - ) - raise error_codes.SHOULD_CHANGE_INITIAL_PASSWORD.format( - data=self._generate_reset_passwd_url_with_token(profile) - ) + # 密码状态校验:初始密码未修改 + # 暂时跳过判断 admin,考虑在 login 模块未升级替换时,admin 可以在 SaaS 配置中关掉该特性 + if ( + not profile.is_superuser + and config_loader.get("force_reset_first_login") + and profile.password_update_time is None + ): + create_profile_log( + profile=profile, + operation="LogIn", + request=request, + params={"is_success": False, "reason": LogInFailReason.SHOULD_CHANGE_INITIAL_PASSWORD.value}, + ) - # 密码状态校验:密码过期 - valid_period = datetime.timedelta(days=profile.password_valid_days) - if ( - profile.password_valid_days > 0 - and ((profile.password_update_time or profile.latest_password_update_time) + valid_period) - < time_aware_now - ): - create_profile_log( - profile=profile, - operation="LogIn", - request=request, - params={"is_success": False, "reason": LogInFailReason.EXPIRED_PASSWORD.value}, - ) + raise error_codes.SHOULD_CHANGE_INITIAL_PASSWORD.format( + data=self._generate_reset_passwd_url_with_token(profile) + ) + + # 密码状态校验:密码过期 + valid_period = datetime.timedelta(days=profile.password_valid_days) + if ( + profile.password_valid_days > 0 + and ((profile.password_update_time or profile.latest_password_update_time) + valid_period) < time_aware_now + ): + create_profile_log( + profile=profile, + operation="LogIn", + request=request, + params={"is_success": False, "reason": LogInFailReason.EXPIRED_PASSWORD.value}, + ) - raise error_codes.PASSWORD_EXPIRED.format(data=self._generate_reset_passwd_url_with_token(profile)) + raise error_codes.PASSWORD_EXPIRED.format(data=self._generate_reset_passwd_url_with_token(profile)) create_profile_log(profile=profile, operation="LogIn", request=request, params={"is_success": True}) return Response(data=local_serializers.ProfileSerializer(profile, context={"request": request}).data) diff --git a/src/api/bkuser_core/tests/categories/plugins/conftest.py b/src/api/bkuser_core/tests/categories/plugins/conftest.py index 57a466b99..bd3ace51c 100644 --- a/src/api/bkuser_core/tests/categories/plugins/conftest.py +++ b/src/api/bkuser_core/tests/categories/plugins/conftest.py @@ -11,7 +11,6 @@ import pytest from bkuser_core.categories.plugins.base import DBSyncManager, SyncContext from bkuser_core.categories.plugins.ldap.adaptor import ProfileFieldMapper -from bkuser_core.categories.plugins.ldap.syncer import SETTING_FIELD_MAP @pytest.fixture() @@ -44,7 +43,7 @@ def ldap_config(): @pytest.fixture() def profile_field_mapper(ldap_config): - return ProfileFieldMapper(config_loader=ldap_config, setting_field_map=SETTING_FIELD_MAP) + return ProfileFieldMapper(config_loader=ldap_config) @pytest.fixture diff --git a/src/api/bkuser_core/user_settings/migrations/0007_auto_20210703_2020.py b/src/api/bkuser_core/user_settings/migrations/0007_auto_20210703_2020.py index df7ee73b8..09b5cfec3 100644 --- a/src/api/bkuser_core/user_settings/migrations/0007_auto_20210703_2020.py +++ b/src/api/bkuser_core/user_settings/migrations/0007_auto_20210703_2020.py @@ -22,7 +22,8 @@ def forwards_func(apps, schema_editor): new_setting_meta_map = {} for category_type in need_connect_types: meta = SettingMeta.objects.create( - namespace=SettingsEnableNamespaces.CONNECTION.value, + namespace=SettingsEnableNamespaces.FIELDS.value, + region="group", category_type=category_type, required=False, **dict(key="user_member_of", example="memberOf", default="memberOf") @@ -46,7 +47,8 @@ def backwards_func(apps, schema_editor): SettingMeta = apps.get_model("user_settings", "SettingMeta") need_connect_types = [CategoryType.MAD.value, CategoryType.LDAP.value] meta = SettingMeta.objects.filter( - namespace=SettingsEnableNamespaces.CONNECTION.value, + namespace=SettingsEnableNamespaces.FIELDS.value, + region="group", category_type__in=need_connect_types, key="user_member_of", ) diff --git a/src/pages/src/components/catalog/operation/SetField.vue b/src/pages/src/components/catalog/operation/SetField.vue index 65b98d902..0d80ba377 100644 --- a/src/pages/src/components/catalog/operation/SetField.vue +++ b/src/pages/src/components/catalog/operation/SetField.vue @@ -112,62 +112,64 @@ - -
-
-
-
- {{$t('用户扩展字段')}} -
- - {{expandExtensional ? $t('收起') : $t('展开')}} -
-
-
- -
- -
- {{$t('蓝鲸用户管理字段')}} - -
- -
- = -
- -
- {{$t('对应字段1') + catalogName + $t('对应字段2')}} - - - - - - - - - - -
-
-
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
- {{$t('用户组字段')}} + {{$t('用户组字段')}} + ({{$t('用户组配置描述')}}) +
{{expandGroup ? $t('收起') : $t('展开')}} @@ -193,20 +195,31 @@ @hasError="handleHasError" /> - + --> - + --> + + +
@@ -381,6 +394,13 @@ export default { > .title { font-weight: bold; + + > .namespace-description { + margin-top: 8px; + font-size: 12px; + font-weight: lighter; + color: $fontLight; + } } > .collapse-group { diff --git a/src/pages/src/language/lang/zh.js b/src/pages/src/language/lang/zh.js index 0d05f438d..2720ea57a 100644 --- a/src/pages/src/language/lang/zh.js +++ b/src/pages/src/language/lang/zh.js @@ -130,6 +130,8 @@ export default { 对应字段1: '对应', 对应字段2: '目录字段', 用户组对象类: '用户组对象类', + 用户组配置描述: '如果不需要使用用户组,请将以下字段留空', + 用户组关联字段描述: '某些 LDAP 服务(如 OpenDJ)需要填写成 isMemberOf', 用户组对象过滤: '用户组对象过滤', 用户组名字段: '用户组名字段', 用户组描述字段: '用户组描述字段',