Skip to content

Commit

Permalink
Merge pull request TencentBlueKing#182 from IMBlues/development
Browse files Browse the repository at this point in the history
refactor: allow no user group config when syncing ldap
  • Loading branch information
IMBlues authored Dec 2, 2021
2 parents e2bcf7a + fa0a9ec commit 376c726
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 123 deletions.
66 changes: 37 additions & 29 deletions src/api/bkuser_core/categories/plugins/ldap/adaptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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],
],
)

Expand Down
5 changes: 2 additions & 3 deletions src/api/bkuser_core/categories/plugins/ldap/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@
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


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:
Expand All @@ -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"],
Expand Down
10 changes: 1 addition & 9 deletions src/api/bkuser_core/categories/plugins/ldap/syncer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
1 change: 1 addition & 0 deletions src/api/bkuser_core/categories/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
56 changes: 30 additions & 26 deletions src/api/bkuser_core/profiles/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions src/api/bkuser_core/tests/categories/plugins/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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",
)
Expand Down
Loading

0 comments on commit 376c726

Please sign in to comment.