From a0895578596525fb62b3b49177850eaeba0fae8d Mon Sep 17 00:00:00 2001 From: nannan00 <17491932+nannan00@users.noreply.github.com> Date: Tue, 24 Dec 2024 19:52:45 +0800 Subject: [PATCH] feat(api): retrieve tenant user by id (#2022) --- src/bk-user/bkuser/apis/apigw/__init__.py | 16 +++++ .../bkuser/apis/apigw/authentications.py | 57 ++++++++++++++++ src/bk-user/bkuser/apis/apigw/permissions.py | 22 ++++++ src/bk-user/bkuser/apis/apigw/urls.py | 25 +++++++ src/bk-user/bkuser/apis/apigw/views.py | 45 +++++++++++++ src/bk-user/bkuser/settings.py | 26 +++---- src/bk-user/bkuser/urls.py | 8 ++- src/bk-user/tests/apis/apigw/__init__.py | 16 +++++ src/bk-user/tests/apis/apigw/conftest.py | 67 +++++++++++++++++++ .../tests/apis/apigw/test_tenant_user.py | 34 ++++++++++ 10 files changed, 302 insertions(+), 14 deletions(-) create mode 100644 src/bk-user/bkuser/apis/apigw/__init__.py create mode 100644 src/bk-user/bkuser/apis/apigw/authentications.py create mode 100644 src/bk-user/bkuser/apis/apigw/permissions.py create mode 100644 src/bk-user/bkuser/apis/apigw/urls.py create mode 100644 src/bk-user/bkuser/apis/apigw/views.py create mode 100644 src/bk-user/tests/apis/apigw/__init__.py create mode 100644 src/bk-user/tests/apis/apigw/conftest.py create mode 100644 src/bk-user/tests/apis/apigw/test_tenant_user.py diff --git a/src/bk-user/bkuser/apis/apigw/__init__.py b/src/bk-user/bkuser/apis/apigw/__init__.py new file mode 100644 index 000000000..95b0be489 --- /dev/null +++ b/src/bk-user/bkuser/apis/apigw/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - 用户管理 (bk-user) available. +# Copyright (C) 2017 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. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. diff --git a/src/bk-user/bkuser/apis/apigw/authentications.py b/src/bk-user/bkuser/apis/apigw/authentications.py new file mode 100644 index 000000000..3cf3a3a8a --- /dev/null +++ b/src/bk-user/bkuser/apis/apigw/authentications.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - 用户管理 (bk-user) available. +# Copyright (C) 2017 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. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. +from collections import namedtuple + +from django.conf import settings +from django.contrib.auth.models import AnonymousUser +from rest_framework import exceptions +from rest_framework.authentication import BaseAuthentication, get_authorization_header + +InnerBearerToken = namedtuple("InnerBearerToken", ["verified"]) + + +class InnerBearerTokenAuthentication(BaseAuthentication): + keyword = "Bearer" + + def __init__(self): + self.allowed_tokens = [settings.BK_APIGW_TO_BK_USER_INNER_BEARER_TOKEN] + + def authenticate(self, request): + auth = get_authorization_header(request).split() + if not auth or auth[0].lower() != self.keyword.lower().encode(): + return None + + if len(auth) == 1: + raise exceptions.AuthenticationFailed("Invalid token header. No credentials provided.") + if len(auth) > 2: # noqa: PLR2004 + raise exceptions.AuthenticationFailed("Invalid token header. Token string should not contain spaces.") + + try: + token = auth[1].decode() + except UnicodeError: + raise exceptions.AuthenticationFailed( + "Invalid token header. Token string should not contain invalid characters." + ) + + # Verify Bearer Token + if token not in self.allowed_tokens: + raise exceptions.AuthenticationFailed("Invalid token.") + + # Mark Verified Bearer Token + request.inner_bearer_token = InnerBearerToken(verified=True) + + return AnonymousUser(), None diff --git a/src/bk-user/bkuser/apis/apigw/permissions.py b/src/bk-user/bkuser/apis/apigw/permissions.py new file mode 100644 index 000000000..d449f9bde --- /dev/null +++ b/src/bk-user/bkuser/apis/apigw/permissions.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - 用户管理 (bk-user) available. +# Copyright (C) 2017 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. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. +from rest_framework.permissions import BasePermission + + +class IsInnerBearerTokenAuthenticated(BasePermission): + def has_permission(self, request, view): + return hasattr(request, "inner_bearer_token") and request.inner_bearer_token.verified diff --git a/src/bk-user/bkuser/apis/apigw/urls.py b/src/bk-user/bkuser/apis/apigw/urls.py new file mode 100644 index 000000000..58e6f000c --- /dev/null +++ b/src/bk-user/bkuser/apis/apigw/urls.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - 用户管理 (bk-user) available. +# Copyright (C) 2017 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. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. +from django.urls import path + +from . import views + +urlpatterns = [ + path( + "tenant-users//", views.TenantUserRetrieveApi.as_view(), name="apigw.tenant_user.retrieve" + ), +] diff --git a/src/bk-user/bkuser/apis/apigw/views.py b/src/bk-user/bkuser/apis/apigw/views.py new file mode 100644 index 000000000..0a8dea67e --- /dev/null +++ b/src/bk-user/bkuser/apis/apigw/views.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - 用户管理 (bk-user) available. +# Copyright (C) 2017 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. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. +from rest_framework import generics +from rest_framework.response import Response + +from bkuser.apps.tenant.models import TenantUser +from bkuser.common.error_codes import error_codes + +from .authentications import InnerBearerTokenAuthentication +from .permissions import IsInnerBearerTokenAuthenticated + + +class TenantUserRetrieveApi(generics.RetrieveAPIView): + """ + 查询用户信息 + Note: 网关内部接口对性能要求较高,所以不进行序列化,且查询必须按字段 + TODO:后续根据耗时统计进行 Cache 优化 + """ + + authentication_classes = [InnerBearerTokenAuthentication] + permission_classes = [IsInnerBearerTokenAuthenticated] + + def get(self, request, *args, **kwargs): + tenant_user_id = kwargs["tenant_user_id"] + + # [only] 用于减少查询字段,仅查询必要字段 + tenant_user = TenantUser.objects.filter(id=tenant_user_id).only("tenant_id").first() + if not tenant_user: + raise error_codes.OBJECT_NOT_FOUND.f(f"user({tenant_user_id}) not found", replace=True) + + return Response({"tenant_id": tenant_user.tenant_id}) diff --git a/src/bk-user/bkuser/settings.py b/src/bk-user/bkuser/settings.py index 16908d333..e15f190f7 100644 --- a/src/bk-user/bkuser/settings.py +++ b/src/bk-user/bkuser/settings.py @@ -148,14 +148,14 @@ # Static files (CSS, JavaScript, Images) STATIC_ROOT = BASE_DIR / "staticfiles" WHITENOISE_STATIC_PREFIX = "/staticfiles/" -# STATIC_URL 也可以是CDN地址 +# STATIC_URL 也可以是 CDN 地址 STATIC_URL = env.str("STATIC_URL", SITE_URL + "staticfiles/") # Media files (excel, pdf, ...) MEDIA_ROOT = BASE_DIR / "media" # cookie SESSION_COOKIE_NAME = "bkuser_sessionid" -SESSION_COOKIE_AGE = 60 * 60 * 24 # 1天 +SESSION_COOKIE_AGE = 60 * 60 * 24 # 1 天 # rest_framework REST_FRAMEWORK = { @@ -198,7 +198,7 @@ _BK_USER_IS_SPECIAL_PORT = _BK_USER_URL_PARSE_URL.port in [None, 80, 443] _BK_USER_SCHEME = _BK_USER_URL_PARSE_URL.scheme _BK_USER_URL_MD5_16BIT = hashlib.md5(BK_USER_URL.encode("utf-8")).hexdigest()[8:-8] -# 注意:Cookie Domain是不支持端口的 +# 注意:Cookie Domain 是不支持端口的 SESSION_COOKIE_DOMAIN = _BK_USER_HOSTNAME CSRF_COOKIE_DOMAIN = SESSION_COOKIE_DOMAIN CSRF_COOKIE_NAME = f"bkuser_csrftoken_{_BK_USER_URL_MD5_16BIT}" @@ -223,15 +223,17 @@ BK_LOGIN_PLAIN_URL = env.str("BK_LOGIN_PLAIN_URL", default=BK_LOGIN_URL.rstrip("/") + "/plain/") BK_LOGIN_PLAIN_WINDOW_WIDTH = env.int("BK_LOGIN_PLAIN_WINDOW_WIDTH", default=510) BK_LOGIN_PLAIN_WINDOW_HEIGHT = env.int("BK_LOGIN_PLAIN_WINDOW_HEIGHT", default=510) -# 登录回调地址参数Key +# 登录回调地址参数 Key BK_LOGIN_CALLBACK_URL_PARAM_KEY = env.str("BK_LOGIN_CALLBACK_URL_PARAM_KEY", default="c_url") -# 登录API URL +# 登录 API URL BK_LOGIN_API_URL = env.str("BK_LOGIN_API_URL", default="http://bk-login/login/") # bk esb api url BK_COMPONENT_API_URL = env.str("BK_COMPONENT_API_URL") # bk apigw url tmpl BK_API_URL_TMPL = env.str("BK_API_URL_TMPL") +# 与网关内部调用的认证 Token +BK_APIGW_TO_BK_USER_INNER_BEARER_TOKEN = env.str("BK_APIGW_TO_BK_USER_INNER_BEARER_TOKEN", default="") # 版本日志 VERSION_LOG_FILES_DIR = BASE_DIR / "version_log" @@ -249,11 +251,11 @@ # 连接 BROKER 超时时间 CELERY_BROKER_CONNECTION_TIMEOUT = 1 # 单位秒 -# CELERY 与 RabbitMQ 增加60秒心跳设置项 +# CELERY 与 RabbitMQ 增加 60 秒心跳设置项 CELERY_BROKER_HEARTBEAT = 60 # CELERY 并发数,默认为 2,可以通过环境变量或者 Procfile 设置 CELERY_WORKER_CONCURRENCY = env.int("CELERY_WORKER_CONCURRENCY", default=2) -# 与周期任务配置的定时相关UTC +# 与周期任务配置的定时相关 UTC CELERY_ENABLE_UTC = False # 任务结果存储 CELERY_RESULT_BACKEND = "django-db" @@ -355,7 +357,7 @@ # "SERIALIZER": "django_redis.serializers.pickle.PickleSerializer" # Redis 连接池配置 "CONNECTION_POOL_KWARGS": { - # redis-py 默认不会关闭连接, 可能会造成连接过多,导致 Redis 无法服务,因此需要设置最大值连接数 + # redis-py 默认不会关闭连接,可能会造成连接过多,导致 Redis 无法服务,因此需要设置最大值连接数 "max_connections": REDIS_MAX_CONNECTIONS }, }, @@ -606,14 +608,14 @@ def _build_file_handler(log_path: Path, filename: str, format: str) -> Dict: # 是否启用新建租户页面功能 ENABLE_CREATE_TENANT = env.bool("ENABLE_CREATE_TENANT", default=False) -# logo文件大小限制,单位为: KB +# logo 文件大小限制,单位为:KB MAX_LOGO_SIZE = env.int("MAX_LOGO_SIZE", 256) -# 数据源插件默认Logo,值为base64格式图片数据 +# 数据源插件默认 Logo,值为 base64 格式图片数据 DEFAULT_DATA_SOURCE_PLUGIN_LOGO = "" -# 租户默认Logo,值为base64格式图片数据 +# 租户默认 Logo,值为 base64 格式图片数据 DEFAULT_TENANT_LOGO = "" -# 数据源用户默认Logo,值为base64格式图片数据 +# 数据源用户默认 Logo,值为 base64 格式图片数据 DEFAULT_DATA_SOURCE_USER_LOGO = "" # 默认手机国际区号 DEFAULT_PHONE_COUNTRY_CODE = env.str("DEFAULT_PHONE_COUNTRY_CODE", default="86") diff --git a/src/bk-user/bkuser/urls.py b/src/bk-user/bkuser/urls.py index f23e777d0..056f8af85 100644 --- a/src/bk-user/bkuser/urls.py +++ b/src/bk-user/bkuser/urls.py @@ -25,16 +25,20 @@ from bkuser.common.views import VueTemplateView urlpatterns = [ - # 产品功能API + # 产品功能 API path("api/v3/web/", include("bkuser.apis.web.urls")), # 提供给登录服务使用的内部 API path("api/v3/login/", include("bkuser.apis.login.urls")), + # 提供给网关使用的内部 API + path("api/v3/apigw/", include("bkuser.apis.apigw.urls")), + # 对外开放的 API + # path("api/v3/open/", include("bkuser.apis.open_v3.urls")), # 兼容旧版本用户管理 OpenAPI # Q: 这里使用 api/v1、api/v2 而非 api/v1/open、api/v2/open # A: 为了保证 ESB 调用的兼容,只需修改 ESB 配置 bk_user host,不需要依赖 ESB 的版本发布 path("api/v1/", include("bkuser.apis.open_v1.urls")), path("api/v2/", include("bkuser.apis.open_v2.urls")), - # 用于监控相关的,比如ping/healthz/sentry/metrics/otel等等 + # 用于监控相关的,比如 ping/healthz/sentry/metrics/otel 等等 path("", include("bkuser.monitoring.urls")), ] diff --git a/src/bk-user/tests/apis/apigw/__init__.py b/src/bk-user/tests/apis/apigw/__init__.py new file mode 100644 index 000000000..95b0be489 --- /dev/null +++ b/src/bk-user/tests/apis/apigw/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - 用户管理 (bk-user) available. +# Copyright (C) 2017 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. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. diff --git a/src/bk-user/tests/apis/apigw/conftest.py b/src/bk-user/tests/apis/apigw/conftest.py new file mode 100644 index 000000000..92fd49f35 --- /dev/null +++ b/src/bk-user/tests/apis/apigw/conftest.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - 用户管理 (bk-user) available. +# Copyright (C) 2017 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. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. +from unittest import mock + +import pytest +from bkuser.apis.apigw.authentications import InnerBearerToken +from bkuser.apps.data_source.constants import DataSourceTypeEnum +from bkuser.apps.data_source.models import DataSource +from bkuser.apps.tenant.constants import TenantUserIdRuleEnum +from bkuser.apps.tenant.models import TenantUserIDGenerateConfig +from bkuser.plugins.local.models import LocalDataSourcePluginConfig +from django.contrib.auth.models import AnonymousUser +from rest_framework.test import APIClient + +from tests.test_utils.data_source import init_data_source_users_depts_and_relations +from tests.test_utils.tenant import sync_users_depts_to_tenant + + +def mock_token_authenticate(self, request): + request.inner_bearer_token = InnerBearerToken(verified=True) + return AnonymousUser(), None + + +@pytest.fixture +def apigw_api_client() -> APIClient: + client = APIClient() + + with mock.patch( + "bkuser.apis.apigw.authentications.InnerBearerTokenAuthentication.authenticate", new=mock_token_authenticate + ): + yield client + + +@pytest.fixture +def default_tenant_user_data(default_tenant, local_ds_plugin_cfg, local_ds_plugin) -> DataSource: + """默认租户的本地数据源数据""" + data_source = DataSource.objects.create( + owner_tenant_id=default_tenant.id, + type=DataSourceTypeEnum.REAL, + plugin=local_ds_plugin, + plugin_config=LocalDataSourcePluginConfig(**local_ds_plugin_cfg), + ) + init_data_source_users_depts_and_relations(data_source) + + # 设置租户用户生成规则表,让 tenant_user_id 与 数据源 username 一样 + TenantUserIDGenerateConfig.objects.create( + data_source=data_source, + target_tenant_id=default_tenant.id, + rule=TenantUserIdRuleEnum.USERNAME, + ) + + sync_users_depts_to_tenant(default_tenant, data_source) + return data_source diff --git a/src/bk-user/tests/apis/apigw/test_tenant_user.py b/src/bk-user/tests/apis/apigw/test_tenant_user.py new file mode 100644 index 000000000..e854a971c --- /dev/null +++ b/src/bk-user/tests/apis/apigw/test_tenant_user.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# TencentBlueKing is pleased to support the open source community by making +# 蓝鲸智云 - 用户管理 (bk-user) available. +# Copyright (C) 2017 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. +# +# We undertake not to change the open source license (MIT license) applicable +# to the current version of the project delivered to anyone in the future. +import pytest +from django.urls import reverse +from rest_framework import status + +pytestmark = pytest.mark.django_db + + +class TestTenantUserRetrieve: + def test_retrieve_tenant_user(self, apigw_api_client, default_tenant_user_data, default_tenant): + resp = apigw_api_client.get(reverse("apigw.tenant_user.retrieve", kwargs={"tenant_user_id": "zhangsan"})) + assert resp.status_code == status.HTTP_200_OK + assert resp.data["tenant_id"] == default_tenant.id + + def test_retrieve_tenant_user_not_found(self, apigw_api_client, default_tenant_user_data): + resp = apigw_api_client.get( + reverse("apigw.tenant_user.retrieve", kwargs={"tenant_user_id": "zhangsan_not_found"}) + ) + assert resp.status_code == status.HTTP_404_NOT_FOUND