Skip to content

Commit

Permalink
feat(bkuser): idp cur api (#1392)
Browse files Browse the repository at this point in the history
  • Loading branch information
nannan00 authored Nov 17, 2023
1 parent f4445a3 commit 6bb768c
Show file tree
Hide file tree
Showing 27 changed files with 789 additions and 22 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/bk-login.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ jobs:
- name: Format & Lint with ruff
run: |
pip install ruff==0.1.4
ruff format src/bk-login --config=src/bk-login/pyproject.toml
ruff src/bk-login --config=src/bk-login/pyproject.toml
ruff format src/bk-login --config=src/bk-login/pyproject.toml --no-cache
ruff src/bk-login --config=src/bk-login/pyproject.toml --no-cache
- name: Lint with mypy
run: |
pip install mypy==1.6.1 types-requests==2.31.0.2 types-setuptools==57.4.18 types-dataclasses==0.1.7 types-redis==3.5.18 types-PyMySQL==1.1.0.1 types-six==0.1.9 types-toml==0.1.5 types-pytz==2023.3.0.0 types-urllib3==1.26.25.14
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/bk-user.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ jobs:
- name: Format & Lint with ruff
run: |
pip install ruff==0.1.4
ruff format src/bk-user --config=src/bk-user/pyproject.toml
ruff src/bk-user --config=src/bk-user/pyproject.toml
ruff format src/bk-user --config=src/bk-user/pyproject.toml --no-cache
ruff src/bk-user --config=src/bk-user/pyproject.toml --no-cache
- name: Lint with mypy
run: |
pip install mypy==1.6.1 types-requests==2.31.0.2 types-setuptools==57.4.18 types-dataclasses==0.1.7 types-redis==3.5.18 types-PyMySQL==1.1.0.1 types-six==0.1.9 types-toml==0.1.5 types-pytz==2023.3.0.0 types-urllib3==1.26.25.14
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/idp-plugins.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ jobs:
- name: Format & Lint with ruff
run: |
pip install ruff==0.1.4
ruff format src/idp-plugins --config=src/idp-plugins/pyproject.toml
ruff src/idp-plugins --config=src/idp-plugins/pyproject.toml
ruff format src/idp-plugins --config=src/idp-plugins/pyproject.toml --no-cache
ruff src/idp-plugins --config=src/idp-plugins/pyproject.toml --no-cache
- name: Lint with mypy
run: |
pip install mypy==1.6.1 types-requests==2.31.0.2 types-setuptools==57.4.18 types-dataclasses==0.1.7 types-redis==3.5.18 types-PyMySQL==1.1.0.1 types-six==0.1.9 types-toml==0.1.5 types-pytz==2023.3.0.0 types-urllib3==1.26.25.14
Expand Down
12 changes: 6 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ repos:
name: ruff-formatter
language: python
types: [python]
entry: ruff format --config=src/bk-user/pyproject.toml --force-exclude
entry: ruff format --config=src/bk-user/pyproject.toml --no-cache
files: src/bk-user/
- id: ruff
name: ruff
language: python
types: [python]
entry: ruff --config=src/bk-user/pyproject.toml --force-exclude --fix
entry: ruff --config=src/bk-user/pyproject.toml --fix --no-cache
files: src/bk-user/
- id: mypy
name: mypy
Expand All @@ -80,13 +80,13 @@ repos:
name: ruff-formatter
language: python
types: [python]
entry: ruff format --config=src/bk-login/pyproject.toml --force-exclude
entry: ruff format --config=src/bk-login/pyproject.toml --no-cache
files: src/bk-login/
- id: ruff
name: ruff
language: python
types: [python]
entry: ruff --config=src/bk-login/pyproject.toml --force-exclude --fix
entry: ruff --config=src/bk-login/pyproject.toml --fix --no-cache
files: src/bk-login/
- id: mypy
name: mypy
Expand All @@ -105,13 +105,13 @@ repos:
name: ruff-formatter
language: python
types: [python]
entry: ruff format --config=src/bk-plugins/pyproject.toml --force-exclude
entry: ruff format --config=src/bk-plugins/pyproject.toml --no-cache
files: src/bk-plugins/
- id: ruff
name: ruff
language: python
types: [python]
entry: ruff --config=src/idp-plugins/pyproject.toml --force-exclude --fix
entry: ruff --config=src/idp-plugins/pyproject.toml --fix --no-cache
files: src/idp-plugins/
- id: mypy
name: mypy
Expand Down
1 change: 1 addition & 0 deletions src/bk-login/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ exclude = [
"venv",
"*/migrations/*",
]
force-exclude = true

[tool.ruff.mccabe]
# Unlike Flake8, default to a complexity level of 10.
Expand Down
12 changes: 12 additions & 0 deletions src/bk-user/bkuser/apis/login/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ class TenantListOutputSLZ(serializers.Serializer):
name = serializers.CharField(help_text="租户名称")
logo = serializers.CharField(help_text="租户 Logo")

class Meta:
ref_name = "login.TenantListOutputSLZ"


class TenantRetrieveOutputSLZ(TenantListOutputSLZ):
...
Expand All @@ -57,6 +60,9 @@ class IdpPluginOutputSLZ(serializers.Serializer):
id = serializers.CharField(help_text="认证源插件 ID")
name = serializers.CharField(help_text="认证源插件名称")

class Meta:
ref_name = "login.IdpPluginOutputSLZ"


class IdpListOutputSLZ(serializers.Serializer):
id = serializers.CharField(help_text="认证源 ID")
Expand All @@ -69,6 +75,9 @@ class IdpRetrieveOutputSLZ(IdpListOutputSLZ):
owner_tenant_id = serializers.CharField(help_text="归属的租户 ID")
plugin_config = serializers.JSONField(help_text="认证源插件配置")

class Meta:
ref_name = "login.IdpRetrieveOutputSLZ"


class TenantUserMatchInputSLZ(serializers.Serializer):
idp_users = serializers.ListField(
Expand All @@ -92,3 +101,6 @@ class TenantUserRetrieveOutputSLZ(serializers.Serializer):
time_zone = serializers.CharField(help_text="时区")

tenant_id = serializers.CharField(help_text="用户所在租户 ID")

class Meta:
ref_name = "login.TenantUserRetrieveOutputSLZ"
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.
"""
186 changes: 186 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,186 @@
# -*- 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 Any, Dict, List

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.data_source.models import DataSource
from bkuser.apps.idp.constants import IdpStatus
from bkuser.apps.idp.models import Idp, IdpPlugin
from bkuser.apps.tenant.models import UserBuiltinField
from bkuser.idp_plugins.base import get_plugin_cfg_cls
from bkuser.idp_plugins.constants import BuiltinIdpPluginEnum
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 = 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))
if not_found_fields := target_fields - builtin_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)))
24 changes: 24 additions & 0 deletions src/bk-user/bkuser/apis/web/idp/swagger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- 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 Dict

from drf_yasg import openapi

from bkuser.idp_plugins.base import list_plugin_cls
from bkuser.utils.pydantic import gen_openapi_schema


def get_idp_plugin_cfg_schema_map() -> Dict[str, openapi.Schema]:
"""获取认证插件配置类 JsonSchema 映射表"""
return {
f"plugin_config:{plugin_cls.id}": gen_openapi_schema(plugin_cls.config_class)
for plugin_cls in list_plugin_cls()
}
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 6bb768c

Please sign in to comment.