Skip to content

Commit

Permalink
feat(bkuser): idp cur api
Browse files Browse the repository at this point in the history
  • Loading branch information
nannan00 committed Nov 15, 2023
1 parent 4b4388a commit 94d8cc3
Show file tree
Hide file tree
Showing 10 changed files with 443 additions and 7 deletions.
6 changes: 2 additions & 4 deletions src/bk-user/bkuser/apis/login/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from rest_framework.response import Response

from bkuser.apps.data_source.models import DataSourceUser, LocalDataSourceIdentityInfo
from bkuser.apps.idp.data_models import DataSourceMatchRuleList, convert_match_rules_to_queryset_filter
from bkuser.apps.idp.data_models import convert_match_rules_to_queryset_filter
from bkuser.apps.idp.models import Idp
from bkuser.apps.tenant.models import Tenant, TenantUser
from bkuser.common.error_codes import error_codes
Expand Down Expand Up @@ -134,13 +134,11 @@ def post(self, request, *args, **kwargs):
# 一般社会化登录都得通过绑定匹配方式,比如QQ,用户得先绑定后才能使用QQ登录
# 直接匹配,一般是企业身份登录方式,
# 比如企业内部SAML2.0登录,认证后获取到的用户字段,能直接与数据源里的用户数据字段匹配
# 认证源与数据源的匹配规则
data_source_match_rules = DataSourceMatchRuleList.validate_python(idp.data_source_match_rules)
# 将规则转换为Django Queryset 过滤条件, 不同用户之间过滤逻辑是OR
conditions = [
condition
for userinfo in data["idp_users"]
if (condition := convert_match_rules_to_queryset_filter(data_source_match_rules, userinfo))
if (condition := convert_match_rules_to_queryset_filter(idp.data_source_match_rule_objs, userinfo))
]

# 查询数据源用户
Expand Down
10 changes: 10 additions & 0 deletions src/bk-user/bkuser/apis/web/idp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available.
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
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.
"""
182 changes: 182 additions & 0 deletions src/bk-user/bkuser/apis/web/idp/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available.
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
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.
"""
from typing import List, Dict, Any

from django.utils.translation import gettext_lazy as _
from drf_yasg.utils import swagger_serializer_method
from pydantic import ValidationError as PDValidationError
from rest_framework import serializers
from rest_framework.exceptions import ValidationError

from bkuser.apps.idp.constants import IdpStatus
from bkuser.apps.idp.models import Idp, IdpPlugin
from bkuser.apps.data_source.models import DataSource
from bkuser.apps.tenant.models import TenantUserCustomField, UserBuiltinField
from bkuser.idp_plugins.constants import BuiltinIdpPluginEnum
from bkuser.idp_plugins.base import get_plugin_cfg_cls
from bkuser.utils.pydantic import stringify_pydantic_error


class IdpPluginOutputSLZ(serializers.Serializer):
id = serializers.CharField(help_text="认证源插件唯一标识")
name = serializers.CharField(help_text="认证源插件名称")
description = serializers.CharField(help_text="认证源插件描述")
logo = serializers.CharField(help_text="认证源插件 Logo")


class IdpSearchInputSLZ(serializers.Serializer):
keyword = serializers.CharField(help_text="搜索关键字", required=False)


class IdpSearchOutputSLZ(serializers.Serializer):
id = serializers.CharField(help_text="认证源唯一标识")
name = serializers.CharField(help_text="认证源名称")
status = serializers.ChoiceField(help_text="认证源状态", choices=IdpStatus.get_choices())
updater = serializers.CharField(help_text="更新者")
updated_at = serializers.CharField(help_text="更新时间", source="updated_at_display")
plugin = IdpPluginOutputSLZ(help_text="认证源插件")
matched_data_sources = serializers.SerializerMethodField(help_text="匹配的数据源列表")

@swagger_serializer_method(
serializer_or_field=serializers.ListField(
help_text="匹配的数据源",
child=serializers.CharField(),
allow_empty=True,
)
)
def get_matched_data_sources(self, obj: Idp) -> List[str]:
data_source_name_map = self.context["data_source_name_map"]

return [
data_source_name_map[r.data_source_id]
for r in obj.data_source_match_rule_objs
if r.data_source_id in data_source_name_map
]


def _validate_duplicate_idp_name(name: str, tenant_id: str, idp_id: str = "") -> str:
"""校验IDP 是否重名"""
queryset = Idp.objects.filter(name=name, owner_tenant_id=tenant_id)
# 过滤掉自身名称
if idp_id:
queryset.exclude(id=idp_id)

if queryset.exists():
raise ValidationError(_("同名认证源已存在"))

return name


class FieldCompareRuleSLZ(serializers.Serializer):
source_field = serializers.CharField(help_text="认证源原始字段")
target_field = serializers.CharField(help_text="匹配的数据源字段")


class DataSourceMatchRuleSLZ(serializers.Serializer):
data_source_id = serializers.IntegerField(help_text="数据源 ID")
field_compare_rules = serializers.ListField(
help_text="字段比较规则", child=FieldCompareRuleSLZ(), allow_empty=False, min_length=1
)

def validate(self, attrs: Dict[str, Any]) -> Dict[str, Any]:
# 数据源是否当前租户的
tenant_id = self.context["tenant_id"]
if not DataSource.objects.filter(id=attrs["data_source_id"], owner_tenant_id=tenant_id).exists():
raise ValidationError(_("数据源必须是当前租户下的,{} 并不符合").format(attrs["data_source_id"]))

# 匹配的数据源字段必须是当前租户的用户字段,包括内建字段和自定义字段
builtin_fields = set(UserBuiltinField.objects.all().values_list("name", flat=True))
custom_fields = set(TenantUserCustomField.objects.filter(tenant_id=tenant_id).values_list("name", flat=True))
allowed_target_fields = builtin_fields | custom_fields

target_fields = {r.get("target_field") for r in attrs["field_compare_rules"]}
if not_found_fields := target_fields - allowed_target_fields:
raise ValidationError(_("匹配的数据源字段 {} 不属于用户自定义字段或内置字段").format(not_found_fields))

return attrs


class IdpCreateInputSLZ(serializers.Serializer):
name = serializers.CharField(help_text="认证源名称", max_length=128)
plugin_id = serializers.CharField(help_text="认证源插件 ID")
plugin_config = serializers.JSONField(help_text="认证源插件配置")
data_source_match_rules = serializers.ListField(
help_text="数据源匹配规则", child=DataSourceMatchRuleSLZ(), allow_empty=True, default=list
)

def validate_name(self, name: str) -> str:
return _validate_duplicate_idp_name(name, self.context["tenant_id"])

def validate_plugin_id(self, plugin_id: str) -> str:
if not IdpPlugin.objects.filter(id=plugin_id).exists():
raise ValidationError(_("认证源插件不存在"))

if plugin_id == BuiltinIdpPluginEnum.LOCAL:
raise ValidationError(_("不允许创建本地账密认证源"))

return plugin_id

def validate(self, attrs: Dict[str, Any]) -> Dict[str, Any]:
plugin_id = attrs["plugin_id"]

try:
cfg_cls = get_plugin_cfg_cls(plugin_id)
except NotImplementedError:
raise ValidationError(_("认证源插件 {} 不存在").format(plugin_id))

try:
attrs["plugin_config"] = cfg_cls(**attrs["plugin_config"]).model_dump()
except PDValidationError as e:
raise ValidationError(_("认证源插件配置不合法:{}").format(stringify_pydantic_error(e)))

return attrs


class IdpCreateOutputSLZ(serializers.Serializer):
id = serializers.CharField(help_text="认证源 ID")
callback_uri = serializers.CharField(help_text="回调地址")


class IdpRetrieveOutputSLZ(serializers.Serializer):
id = serializers.CharField(help_text="认证源唯一标识")
name = serializers.CharField(help_text="认证源名称")
owner_tenant_id = serializers.CharField(help_text="认证源所属租户 ID")
status = serializers.ChoiceField(help_text="认证源状态", choices=IdpStatus.get_choices())
plugin = IdpPluginOutputSLZ(help_text="认证源插件")
plugin_config = serializers.JSONField(help_text="认证源插件配置")
data_source_match_rules = serializers.JSONField(help_text="数据源匹配规则", default=list)
callback_uri = serializers.CharField(help_text="回调地址")


class IdpPartialUpdateInputSLZ(serializers.Serializer):
name = serializers.CharField(help_text="认证源名称")

def validate_name(self, name: str) -> str:
return _validate_duplicate_idp_name(name, self.context["tenant_id"], self.context["idp_id"])


class IdpUpdateInputSLZ(serializers.Serializer):
name = serializers.CharField(help_text="认证源名称", max_length=128)
plugin_config = serializers.JSONField(help_text="认证源插件配置")
data_source_match_rules = serializers.ListField(
help_text="数据源匹配规则", child=DataSourceMatchRuleSLZ(), allow_empty=True, default=list
)

def validate_name(self, name: str) -> str:
return _validate_duplicate_idp_name(name, self.context["tenant_id"], self.context["idp_id"])

def validate_plugin_config(self, plugin_config: Dict[str, Any]) -> Dict[str, Any]:
cfg_cls = get_plugin_cfg_cls(self.context["plugin_id"])

try:
return cfg_cls(**plugin_config).model_dump()
except PDValidationError as e:
raise ValidationError(_("认证源插件配置不合法:{}").format(stringify_pydantic_error(e)))
22 changes: 22 additions & 0 deletions src/bk-user/bkuser/apis/web/idp/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available.
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
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.
"""
from django.urls import path

from . import views

urlpatterns = [
# 认证源插件列表
path("plugins/", views.IdpPluginListApi.as_view(), name="idp_plugin.list"),
# 认证源创建/获取列表
path("", views.IdpListCreateApi.as_view(), name="idp.list_create"),
# 认证源获取/更新
path("<str:id>/", views.IdpRetrieveUpdateApi.as_view(), name="idp.retrieve_update"),
]
Loading

0 comments on commit 94d8cc3

Please sign in to comment.