From b301ee47cf9d55204f3566af4b3cd8411a7209f9 Mon Sep 17 00:00:00 2001 From: nannan00 <17491932+nannan00@users.noreply.github.com> Date: Sat, 28 Dec 2024 18:01:57 +0800 Subject: [PATCH] Ft login support inner bearer token (#2027) --- src/bk-login/bklogin/common/middlewares.py | 96 ++++++++++++++----- src/bk-login/bklogin/open_apis/mixins.py | 28 ++++-- src/bk-login/bklogin/open_apis/urls.py | 13 ++- src/bk-login/bklogin/open_apis/views.py | 22 ++++- src/bk-login/bklogin/settings.py | 4 + src/bk-login/poetry.lock | 8 +- src/bk-login/pyproject.toml | 2 +- src/bk-login/requirements.txt | 2 +- src/bk-login/requirements_dev.txt | 2 +- src/bk-login/tests/open_apis/conftest.py | 10 +- src/bk-user/bkuser/auth/backends.py | 65 +++---------- src/bk-user/bkuser/component/login.py | 10 +- src/bk-user/bkuser/settings.py | 2 +- src/bk-user/bkuser/urls.py | 22 ++++- .../en/batch_query_user_display_name.md | 8 +- .../zh/batch_query_user_display_name.md | 10 +- src/bk-user/tests/apis/open_v3/test_tenant.py | 37 ------- src/bk-user/tests/apis/open_v3/test_user.py | 64 +++++++++++++ 18 files changed, 252 insertions(+), 153 deletions(-) create mode 100644 src/bk-user/tests/apis/open_v3/test_user.py diff --git a/src/bk-login/bklogin/common/middlewares.py b/src/bk-login/bklogin/common/middlewares.py index 21f2b963e..ed5c2b8f5 100644 --- a/src/bk-login/bklogin/common/middlewares.py +++ b/src/bk-login/bklogin/common/middlewares.py @@ -16,7 +16,9 @@ # to the current version of the project delivered to anyone in the future. import json import logging +from collections import namedtuple +from django.conf import settings from django.db import connections from sentry_sdk import capture_exception @@ -39,7 +41,7 @@ def process_exception(self, request, exception): """ 对异常进行处理,使用蓝鲸 Http API 协议响应 """ - error = _handle_exception(request, exception) + error = self._handle_exception(request, exception) # 根据蓝鲸新版 HTTP API 协议,处理响应数据 # 开发者关注的,能自助排查,快速定位问题 @@ -56,31 +58,79 @@ def process_exception(self, request, exception): status=error.status_code, ) + def _handle_exception(self, request, exc) -> APIError: + """统一处理异常,并转换成 APIError""" + if isinstance(exc, APIError): + # 回滚事务 + self._set_rollback() + return exc + + # 非预期内的异常(1)记录日志(2)推送到 sentry (3) 以系统异常响应 + logger.exception( + "catch unexpected error, request url->[%s], request method->[%s] request params->[%s]", + request.path, + request.method, + json.dumps(getattr(request, request.method, None)), + ) + + # 推送异常到 sentry + capture_exception(exc) + + # Note: 系统异常不暴露异常详情信息,避免敏感信息泄露 + return error_codes.SYSTEM_ERROR + + @staticmethod + def _set_rollback(): + """DB 事务回滚""" + for db in connections.all(): + if db.settings_dict["ATOMIC_REQUESTS"] and db.in_atomic_block: + db.set_rollback(True) + + +class InnerBearerTokenMiddleware: + keyword = "Bearer" + InnerBearerToken = namedtuple("InnerBearerToken", ["verified"]) + + def __init__(self, get_response): + self.get_response = get_response + + self.allowed_token = [settings.BK_APIGW_TO_BK_USER_INNER_BEARER_TOKEN] + + def __call__(self, request): + # 从请求头中获取 InnerBearerToken + auth = request.META.get("HTTP_AUTHORIZATION", "").split() + + if not auth or auth[0].lower() != self.keyword.lower() or len(auth) != 2: # noqa: PLR2004 + return self.get_response(request) + + token = auth[1] + # 验证 InnerBearerToken 是否合法 + if token not in self.allowed_token: + return self.get_response(request) -def _handle_exception(request, exc) -> APIError: - """统一处理异常,并转换成 APIError""" - if isinstance(exc, APIError): - # 回滚事务 - _set_rollback() - return exc + # 设置 InnerBearerToken + request.inner_bearer_token = self.InnerBearerToken(verified=True) - # 非预期内的异常(1)记录日志(2)推送到 sentry (3) 以系统异常响应 - logger.exception( - "catch unexpected error, request url->[%s], request method->[%s] request params->[%s]", - request.path, - request.method, - json.dumps(getattr(request, request.method, None)), - ) + return self.get_response(request) - # 推送异常到 sentry - capture_exception(exc) - # Note: 系统异常不暴露异常详情信息,避免敏感信息泄露 - return error_codes.SYSTEM_ERROR +class BkUserAppMiddleware: + app_code_header = "HTTP_X_BK_APP_CODE" + app_secret_header = "HTTP_X_BK_APP_SECRET" + def __init__(self, get_response): + self.get_response = get_response -def _set_rollback(): - """DB 事务回滚""" - for db in connections.all(): - if db.settings_dict["ATOMIC_REQUESTS"] and db.in_atomic_block: - db.set_rollback(True) + def __call__(self, request): + # 从请求头中获取 app_code / app_secret + app_code = request.META.get(self.app_code_header) + app_secret = request.META.get(self.app_secret_header) + + # 校验是否 BkUser App + if (app_code, app_secret) != (settings.BK_USER_APP_CODE, settings.BK_USER_APP_SECRET): + return self.get_response(request) + + # 设置 BkUser App 标记 + request.bk_user_app_verified = True + + return self.get_response(request) diff --git a/src/bk-login/bklogin/open_apis/mixins.py b/src/bk-login/bklogin/open_apis/mixins.py index 8714e1c8e..9a813c1f6 100644 --- a/src/bk-login/bklogin/open_apis/mixins.py +++ b/src/bk-login/bklogin/open_apis/mixins.py @@ -20,15 +20,31 @@ class APIGatewayAppVerifiedMixin: """校验来源 APIGateway JWT 的应用是否认证""" - # FIXME (nan): 待讨论清楚网关本身的用户认证与登录如何认证后再去除 - skip_app_verified = False + def dispatch(self, request, *args, **kwargs): # type: ignore + app = getattr(request, "app", None) + if app and app.verified: + return super().dispatch(request, *args, **kwargs) # type: ignore + + raise error_codes.UNAUTHENTICATED.f("the api must be verify app from api gateway") + + +class InnerBearerTokenVerifiedMixin: + """校验来源内部请求的 Bearer Token 是否认证""" def dispatch(self, request, *args, **kwargs): - if self.skip_app_verified: + token = getattr(request, "inner_bearer_token", None) + if token and token.verified: return super().dispatch(request, *args, **kwargs) # type: ignore - app = getattr(request, "app", None) - if app and app.verified: + raise error_codes.UNAUTHENTICATED.f("the api must be verify inner bearer token") + + +class BkUserAppVerifiedMixin: + """校验来源内部 Bk User App 的请求""" + + def dispatch(self, request, *args, **kwargs): + bk_user_app_verified = getattr(request, "bk_user_app_verified", False) + if bk_user_app_verified: return super().dispatch(request, *args, **kwargs) # type: ignore - raise error_codes.UNAUTHENTICATED.f("the api must be verify app from api gateway") + raise error_codes.UNAUTHENTICATED.f("the api must be verify from bk user app") diff --git a/src/bk-login/bklogin/open_apis/urls.py b/src/bk-login/bklogin/open_apis/urls.py index 6ff191a55..ecad05fe3 100644 --- a/src/bk-login/bklogin/open_apis/urls.py +++ b/src/bk-login/bklogin/open_apis/urls.py @@ -28,18 +28,21 @@ path("api/v3/is_login/", compatibility_views.TokenIntrospectCompatibilityApi.as_view(api_version="v3")), path("api/v3/get_user/", compatibility_views.UserRetrieveCompatibilityApi.as_view(api_version="v3")), # Note: 新的 OpenAPI 后面统一接入 APIGateway,不支持直接调用 - # 同时只提供给 APIGateway 做用户认证的接口与通用 OpenAPI 区分开 + # 通用 OpenAPI path("api/v3/open/bk-tokens/verify/", views.TokenVerifyApi.as_view(), name="v3_open.bk_token.verify"), path( "api/v3/open/bk-tokens/userinfo/", views.TokenUserInfoRetrieveApi.as_view(), name="v3_open.bk_token.userinfo_retrieve", ), - path("api/v3/apigw/bk-tokens/verify/", views.TokenVerifyApi.as_view(skip_app_verified=True)), - # FIXME (nan): 临时兼容用户管理 SaaS 本地开发的登录 - path("api/v3/bkuser/bk-tokens/verify/", views.TokenVerifyApi.as_view(skip_app_verified=True)), + # 提供给 apigw 的内部 API + path( + "api/v3/apigw/bk-tokens/verify/", views.TokenVerifyApiByBearerAuth.as_view(), name="v3_apigw.bk_token.verify" + ), + # 提供给 bkuser 的内部 API path( "api/v3/bkuser/bk-tokens/userinfo/", - views.TokenUserInfoRetrieveApi.as_view(skip_app_verified=True), + views.TokenUserInfoRetrieveApiByBkUserAppAuth.as_view(), + name="v3_bkuser.bk_token.userinfo_retrieve", ), ] diff --git a/src/bk-login/bklogin/open_apis/views.py b/src/bk-login/bklogin/open_apis/views.py index 4718f9f36..e9982b6f4 100644 --- a/src/bk-login/bklogin/open_apis/views.py +++ b/src/bk-login/bklogin/open_apis/views.py @@ -22,10 +22,10 @@ from bklogin.common.response import APISuccessResponse from bklogin.component.bk_user import api as bk_user_api -from .mixins import APIGatewayAppVerifiedMixin +from .mixins import APIGatewayAppVerifiedMixin, BkUserAppVerifiedMixin, InnerBearerTokenVerifiedMixin -class TokenVerifyApi(APIGatewayAppVerifiedMixin, View): +class TokenVerifyApiBase(View): """Token 解析""" def get(self, request, *args, **kwargs): @@ -42,7 +42,15 @@ def get(self, request, *args, **kwargs): return APISuccessResponse(data={"bk_username": user.id, "tenant_id": user.tenant_id}) -class TokenUserInfoRetrieveApi(APIGatewayAppVerifiedMixin, View): +class TokenVerifyApi(APIGatewayAppVerifiedMixin, TokenVerifyApiBase): + """Token 解析,请求需经过验证 网关 JWT App 认证""" + + +class TokenVerifyApiByBearerAuth(InnerBearerTokenVerifiedMixin, TokenVerifyApiBase): + """Token 解析,请求需经过验证 内部 Bearer Token 认证""" + + +class TokenUserInfoRetrieveApiBase(View): """Token 用户信息解析""" def get(self, request, *args, **kwargs): @@ -65,3 +73,11 @@ def get(self, request, *args, **kwargs): "time_zone": user.time_zone, } ) + + +class TokenUserInfoRetrieveApi(APIGatewayAppVerifiedMixin, TokenUserInfoRetrieveApiBase): + """Token 用户信息解析,请求需经过验证 网关 JWT App 认证""" + + +class TokenUserInfoRetrieveApiByBkUserAppAuth(BkUserAppVerifiedMixin, TokenUserInfoRetrieveApiBase): + """Token 用户信息解析,请求需来着用户管理的 App 认证""" diff --git a/src/bk-login/bklogin/settings.py b/src/bk-login/bklogin/settings.py index c6a3db570..11c5543af 100644 --- a/src/bk-login/bklogin/settings.py +++ b/src/bk-login/bklogin/settings.py @@ -71,6 +71,8 @@ "bklogin.common.middlewares.ExceptionHandlerMiddleware", "apigw_manager.apigw.authentication.ApiGatewayJWTGenericMiddleware", "apigw_manager.apigw.authentication.ApiGatewayJWTAppMiddleware", + "bklogin.common.middlewares.InnerBearerTokenMiddleware", + "bklogin.common.middlewares.BkUserAppMiddleware", "django_prometheus.middleware.PrometheusAfterMiddleware", ] @@ -212,7 +214,9 @@ # bk apigw url tmpl BK_API_URL_TMPL = env.str("BK_API_URL_TMPL", default="") BK_APIGW_NAME = env.str("BK_APIGW_NAME", default="bk-login") +# 是否开启同步网关 API ENABLE_SYNC_APIGW = env.bool("ENABLE_SYNC_APIGW", default=False) +BK_APIGW_TO_BK_USER_INNER_BEARER_TOKEN = env.str("BK_APIGW_TO_BK_USER_INNER_BEARER_TOKEN", default="") # footer / logo / title 等全局配置存储的共享仓库地址 BK_SHARED_RES_URL = env.str("BK_SHARED_RES_URL", default="") diff --git a/src/bk-login/poetry.lock b/src/bk-login/poetry.lock index 69de5145c..db06b44c8 100644 --- a/src/bk-login/poetry.lock +++ b/src/bk-login/poetry.lock @@ -18,12 +18,12 @@ reference = "tencent" [[package]] name = "apigw-manager" -version = "4.0.0" +version = "4.0.1" description = "The SDK for managing blueking gateway resource." optional = false python-versions = ">=3.8,<3.13" files = [ - {file = "apigw_manager-4.0.0-py3-none-any.whl", hash = "sha256:e70dafc869204e6f872bc8641f86ed86a2e3153a15dc07ebd3fb3a5fcf626b93"}, + {file = "apigw_manager-4.0.1-py3-none-any.whl", hash = "sha256:b6881fbf1f174d187b34f2b3983d0ade8ba6b457b3068f7a95b68bc7cc58ac86"}, ] [package.dependencies] @@ -470,7 +470,6 @@ files = [ {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, @@ -481,7 +480,6 @@ files = [ {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, @@ -2762,4 +2760,4 @@ reference = "tencent" [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.12" -content-hash = "9f22ae0d6ce70ac27a928b3079566b031326094f2e036f9f060f4bc4ae04e513" +content-hash = "a71c21fcd2c99600d1c717d8b5bd965bd2d068f64edb83ebce8e653710e700ad" diff --git a/src/bk-login/pyproject.toml b/src/bk-login/pyproject.toml index c8791253c..0f1afc3aa 100644 --- a/src/bk-login/pyproject.toml +++ b/src/bk-login/pyproject.toml @@ -39,7 +39,7 @@ opentelemetry-instrumentation-celery = "0.46b0" opentelemetry-instrumentation-logging = "0.46b0" bk-notice-sdk = "1.3.2" pyjwt = {version = "2.10.1", extras = ["cryptography"]} -apigw-manager = {version = "4.0.0", extras = ["cryptography"]} +apigw-manager = {version = "4.0.1", extras = ["cryptography"]} [tool.poetry.group.dev.dependencies] ruff = "^0.7.1" diff --git a/src/bk-login/requirements.txt b/src/bk-login/requirements.txt index 4120fbaa6..e805ba114 100644 --- a/src/bk-login/requirements.txt +++ b/src/bk-login/requirements.txt @@ -1,7 +1,7 @@ --index-url https://mirrors.tencent.com/pypi/simple annotated-types==0.7.0 ; python_version >= "3.11" and python_version < "3.12" -apigw-manager[cryptography]==4.0.0 ; python_version >= "3.11" and python_version < "3.12" +apigw-manager[cryptography]==4.0.1 ; python_version >= "3.11" and python_version < "3.12" asgiref==3.8.1 ; python_version >= "3.11" and python_version < "3.12" bk-crypto-python-sdk==2.0.0 ; python_version >= "3.11" and python_version < "3.12" bk-notice-sdk==1.3.2 ; python_version >= "3.11" and python_version < "3.12" diff --git a/src/bk-login/requirements_dev.txt b/src/bk-login/requirements_dev.txt index 6af7e712d..36f4a6aaa 100644 --- a/src/bk-login/requirements_dev.txt +++ b/src/bk-login/requirements_dev.txt @@ -1,7 +1,7 @@ --index-url https://mirrors.tencent.com/pypi/simple annotated-types==0.7.0 ; python_version >= "3.11" and python_version < "3.12" -apigw-manager[cryptography]==4.0.0 ; python_version >= "3.11" and python_version < "3.12" +apigw-manager[cryptography]==4.0.1 ; python_version >= "3.11" and python_version < "3.12" asgiref==3.8.1 ; python_version >= "3.11" and python_version < "3.12" bk-crypto-python-sdk==2.0.0 ; python_version >= "3.11" and python_version < "3.12" bk-notice-sdk==1.3.2 ; python_version >= "3.11" and python_version < "3.12" diff --git a/src/bk-login/tests/open_apis/conftest.py b/src/bk-login/tests/open_apis/conftest.py index aa086a6f3..9ff3e7b14 100644 --- a/src/bk-login/tests/open_apis/conftest.py +++ b/src/bk-login/tests/open_apis/conftest.py @@ -14,15 +14,21 @@ # # 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 os import pytest from django.test import Client +from django.test.utils import override_settings @pytest.fixture def open_api_client() -> Client: client = Client() - with mock.patch("bklogin.open_apis.mixins.APIGatewayAppVerifiedMixin.skip_app_verified", return_value=True): + # Set new environment variables + os.environ["APIGW_MANAGER_DUMMY_GATEWAY_NAME"] = "bk-login" + os.environ["APIGW_MANAGER_DUMMY_PAYLOAD_APP_CODE"] = "app_code" + os.environ["APIGW_MANAGER_DUMMY_PAYLOAD_USERNAME"] = "username" + + with override_settings(BK_APIGW_JWT_PROVIDER_CLS="apigw_manager.apigw.providers.DummyEnvPayloadJWTProvider"): yield client diff --git a/src/bk-user/bkuser/auth/backends.py b/src/bk-user/bkuser/auth/backends.py index a95ca4289..c63ce947f 100644 --- a/src/bk-user/bkuser/auth/backends.py +++ b/src/bk-user/bkuser/auth/backends.py @@ -15,11 +15,10 @@ # 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 logging -import traceback +from typing import Dict, Tuple from django.contrib.auth import get_user_model from django.contrib.auth.backends import BaseBackend -from django.db import IntegrityError from bkuser.component import login @@ -33,47 +32,28 @@ def authenticate(self, request=None, bk_token=None): if not bk_token: return None - verify_result, username = self.verify_bk_token(bk_token) + result, user_info = self.get_user_info(bk_token) # 判断 bk_token 是否验证通过,不通过则返回 None - if not verify_result: + if not result: return None user_model = get_user_model() + username = user_info["username"] - try: - user, _ = user_model.objects.get_or_create(username=username) - get_user_info_result, user_info = self.get_user_info(bk_token) - # 判断是否获取到用户信息,获取不到则返回 None - if not get_user_info_result: - return None - user.set_property(key="language", value=user_info.get("language", "")) - user.set_property(key="time_zone", value=user_info.get("time_zone", "")) - user.set_property(key="tenant_id", value=user_info.get("tenant_id", "")) - user.set_property(key="display_name", value=user_info.get("display_name", "")) + user, _ = user_model.objects.get_or_create(username=username) + user.set_property(key="language", value=user_info["language"]) + user.set_property(key="time_zone", value=user_info["time_zone"]) + user.set_property(key="tenant_id", value=user_info["tenant_id"]) + user.set_property(key="display_name", value=user_info["display_name"]) - return user - - except IntegrityError: - logger.exception(traceback.format_exc()) - logger.exception("get_or_create UserModel fail or update_or_create UserProperty") - return None - except Exception: # pylint: disable=broad-except - logger.exception(traceback.format_exc()) - logger.exception("Auto create & update UserModel fail") - return None + return user @staticmethod - def get_user_info(bk_token): + def get_user_info(bk_token: str) -> Tuple[bool, Dict]: """ 请求平台 ESB 接口获取用户信息 - @param bk_token: bk_token - @type bk_token: str - @return:True, { - 'username': 'test', - 'language': 'zh-cn', - 'time_zone': 'Asia/Shanghai', - } - @rtype: bool,dict + :param bk_token: 用户登录凭证 + :return: 是否获取成功,用户信息 """ try: data = login.get_user_info(bk_token) @@ -82,27 +62,10 @@ def get_user_info(bk_token): return False, {} user_info = { - "username": data.get("bk_username", ""), + "username": data["bk_username"], "language": data.get("language", ""), "time_zone": data.get("time_zone", ""), "tenant_id": data.get("tenant_id", ""), "display_name": data.get("display_name", ""), } return True, user_info - - @staticmethod - def verify_bk_token(bk_token): - """ - 请求 VERIFY_URL,认证 bk_token 是否正确 - @param bk_token: "_FrcQiMNevOD05f8AY0tCynWmubZbWz86HslzmOqnhk" - @type bk_token: str - @return: False,None True,username - @rtype: bool,None/str - """ - try: - data = login.verify_bk_token(bk_token) - except Exception: # pylint: disable=broad-except - logger.warning("Abnormal error in verify_bk_token...", exc_info=True) - return False, None - - return True, data["bk_username"] diff --git a/src/bk-user/bkuser/component/login.py b/src/bk-user/bkuser/component/login.py index 9457851f6..be2a4d5a5 100644 --- a/src/bk-user/bkuser/component/login.py +++ b/src/bk-user/bkuser/component/login.py @@ -28,7 +28,7 @@ logger = logging.getLogger("component") -# FIXME: 后续登录 OpenAPI 接入 APIGateway 需重新调整 +# Note: 用户管理模块的调用登录接口,不经过 APIGateway,避免循环依赖 def _call_login_api(http_func, url_path, **kwargs): request_id = local.request_id @@ -38,6 +38,8 @@ def _call_login_api(http_func, url_path, **kwargs): { "Content-Type": "application/json", "X-Request-Id": request_id, + "X-Bk-App-Code": settings.BK_APP_CODE, + "X-Bk-App-Secret": settings.BK_APP_SECRET, } ) @@ -62,12 +64,6 @@ def _call_login_api(http_func, url_path, **kwargs): return resp_data["data"] -def verify_bk_token(bk_token: str): - """验证 bk_token""" - url_path = "api/v3/bkuser/bk-tokens/verify/" - return _call_login_api(http_get, url_path, params={"bk_token": bk_token}) - - def get_user_info(bk_token: str): """ 获取用户信息 diff --git a/src/bk-user/bkuser/settings.py b/src/bk-user/bkuser/settings.py index 052df25b5..e5b3e02a4 100644 --- a/src/bk-user/bkuser/settings.py +++ b/src/bk-user/bkuser/settings.py @@ -684,4 +684,4 @@ def _build_file_handler(log_path: Path, filename: str, format: str) -> Dict: ORGANIZATION_BATCH_OPERATION_API_LIMIT = env.int("ORGANIZATION_BATCH_OPERATION_API_LIMIT", 100) # 限制 bk_username 批量查询 display_name 的数量上限,避免性能问题 -BATCH_QUERY_USER_DISPLAY_NAME_BY_BK_USERNAME_LIMIT = env.int("BATCH_QUERY_USER_DISPLAY_NAME_BY_BK_USERNAME_LIMIT", 50) +BATCH_QUERY_USER_DISPLAY_NAME_BY_BK_USERNAME_LIMIT = env.int("BATCH_QUERY_USER_DISPLAY_NAME_BY_BK_USERNAME_LIMIT", 100) diff --git a/src/bk-user/bkuser/urls.py b/src/bk-user/bkuser/urls.py index 868c687f0..2d424c6b1 100644 --- a/src/bk-user/bkuser/urls.py +++ b/src/bk-user/bkuser/urls.py @@ -52,7 +52,7 @@ if settings.SWAGGER_ENABLE: schema_view = get_schema_view( openapi.Info( - title="BK-User API", + title="BK-User Web API", default_version="vx", description="BK-User API Document", terms_of_service="http://bk-user.bking.com", @@ -61,6 +61,7 @@ ), public=False, permission_classes=[permissions.IsAuthenticated], + urlconf="bkuser.apis.web.urls", ) urlpatterns += [ path("swagger/", schema_view.without_ui(cache_timeout=0), name="schema-json"), @@ -68,6 +69,25 @@ path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"), ] + open_schema_view = get_schema_view( + openapi.Info( + title="BK-User Open API", + default_version="vx", + description="BK-User API Document", + terms_of_service="http://bk-user.bking.com", + contact=openapi.Contact(email="blueking@tencent.com"), + license=openapi.License(name="BSD License"), + ), + public=True, + permission_classes=[permissions.AllowAny], + urlconf="bkuser.apis.open_v3.urls", + ) + urlpatterns += [ + path("open/swagger/", open_schema_view.without_ui(cache_timeout=0), name="open-schema-json"), + path("open/swagger/", open_schema_view.with_ui("swagger", cache_timeout=0), name="open-schema-swagger-ui"), + path("open/redoc/", open_schema_view.with_ui("redoc", cache_timeout=0), name="open-schema-redoc"), + ] + # static file urlpatterns += [ diff --git a/src/bk-user/support-files/apidocs/en/batch_query_user_display_name.md b/src/bk-user/support-files/apidocs/en/batch_query_user_display_name.md index ad965cc2c..747a861cb 100644 --- a/src/bk-user/support-files/apidocs/en/batch_query_user_display_name.md +++ b/src/bk-user/support-files/apidocs/en/batch_query_user_display_name.md @@ -4,9 +4,9 @@ Batch query user's display_name ### Parameters -| Name | Type | Required | Description | -|--------------|--------|----------|-----------------------------------------------------------------------------------------------| -| bk_usernames | string | Yes | Blueking unique identifier, multiple identifiers are separated by commas, and the limit is 50 | +| Name | Type | Required | Description | +|--------------|--------|----------|------------------------------------------------------------------------------------------------| +| bk_usernames | string | Yes | Blueking unique identifier, multiple identifiers are separated by commas, and the limit is 100 | ### Request Example @@ -56,7 +56,7 @@ bk_usernames=7idwx3b7nzk6xigs,0wngfim3uzhadh1w { "error": { "code": "INVALID_ARGUMENT", - "message": "Arguments Validation Failed: bk_usernames: This field must contain at most 50 objects." + "message": "Arguments Validation Failed: bk_usernames: This field must contain at most 100 objects." } } ``` diff --git a/src/bk-user/support-files/apidocs/zh/batch_query_user_display_name.md b/src/bk-user/support-files/apidocs/zh/batch_query_user_display_name.md index a556dba93..90c983344 100644 --- a/src/bk-user/support-files/apidocs/zh/batch_query_user_display_name.md +++ b/src/bk-user/support-files/apidocs/zh/batch_query_user_display_name.md @@ -4,9 +4,9 @@ ### 输入参数 -| 参数名称 | 参数类型 | 必选 | 描述 | -|--------------|--------|----|-------------------------| -| bk_usernames | string | 是 | 蓝鲸唯一标识,多个以逗号分隔,限制数量为 50 | +| 参数名称 | 参数类型 | 必选 | 描述 | +|--------------|--------|----|--------------------------| +| bk_usernames | string | 是 | 蓝鲸唯一标识,多个以逗号分隔,限制数量为 100 | ### 请求示例 @@ -46,7 +46,7 @@ bk_usernames=7idwx3b7nzk6xigs,0wngfim3uzhadh1w { "error": { "code": "INVALID_ARGUMENT", - "message": "参数校验不通过: bk_usernames: 该字段不能为空。" + "message": "参数校验不通过:bk_usernames: 该字段不能为空。" } } ``` @@ -56,7 +56,7 @@ bk_usernames=7idwx3b7nzk6xigs,0wngfim3uzhadh1w { "error": { "code": "INVALID_ARGUMENT", - "message": "参数校验不通过: bk_usernames: 至多包含 50 个对象。" + "message": "参数校验不通过:bk_usernames: 至多包含 100 个对象。" } } ``` diff --git a/src/bk-user/tests/apis/open_v3/test_tenant.py b/src/bk-user/tests/apis/open_v3/test_tenant.py index 2a3858f87..d51304ded 100644 --- a/src/bk-user/tests/apis/open_v3/test_tenant.py +++ b/src/bk-user/tests/apis/open_v3/test_tenant.py @@ -15,7 +15,6 @@ # 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 bkuser.apps.tenant.models import TenantUser from django.urls import reverse from rest_framework import status @@ -29,39 +28,3 @@ def test_standard(self, api_client, default_tenant, random_tenant): assert resp.data["count"] == 2 assert {t["id"] for t in resp.data["results"]} == {default_tenant.id, random_tenant.id} assert set(resp.data["results"][0].keys()) == {"id", "name", "status"} - - -@pytest.mark.usefixtures("_init_tenant_users_depts") -class TestTenantUserDisplayNameList: - def test_standard(self, api_client): - zhangsan_id = TenantUser.objects.get(data_source_user__code="zhangsan").id - lisi_id = TenantUser.objects.get(data_source_user__code="lisi").id - resp = api_client.get( - reverse("open_v3.tenant_user.display_name.list"), data={"bk_usernames": ",".join([zhangsan_id, lisi_id])} - ) - - assert resp.status_code == status.HTTP_200_OK - assert len(resp.data) == 2 - assert {t["bk_username"] for t in resp.data} == {zhangsan_id, lisi_id} - assert {t["display_name"] for t in resp.data} == {"张三", "李四"} - - def test_with_invalid_bk_usernames(self, api_client): - zhangsan_id = TenantUser.objects.get(data_source_user__code="zhangsan").id - resp = api_client.get( - reverse("open_v3.tenant_user.display_name.list"), data={"bk_usernames": ",".join([zhangsan_id, "invalid"])} - ) - - assert resp.status_code == status.HTTP_200_OK - assert len(resp.data) == 1 - assert resp.data[0]["bk_username"] == zhangsan_id - assert resp.data[0]["display_name"] == "张三" - - def test_with_no_bk_usernames(self, api_client): - resp = api_client.get(reverse("open_v3.tenant_user.display_name.list"), data={"bk_usernames": ""}) - assert resp.status_code == status.HTTP_400_BAD_REQUEST - - def test_with_invalid_length(self, api_client): - resp = api_client.get( - reverse("open_v3.tenant_user.display_name.list"), data={"bk_usernames": ",".join(map(str, range(1, 52)))} - ) - assert resp.status_code == status.HTTP_400_BAD_REQUEST diff --git a/src/bk-user/tests/apis/open_v3/test_user.py b/src/bk-user/tests/apis/open_v3/test_user.py new file mode 100644 index 000000000..338fc2720 --- /dev/null +++ b/src/bk-user/tests/apis/open_v3/test_user.py @@ -0,0 +1,64 @@ +# -*- 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 bkuser.apps.tenant.models import TenantUser +from django.conf import settings +from django.urls import reverse +from rest_framework import status + +pytestmark = pytest.mark.django_db + + +@pytest.mark.usefixtures("_init_tenant_users_depts") +class TestTenantUserDisplayNameList: + def test_standard(self, api_client): + zhangsan_id = TenantUser.objects.get(data_source_user__code="zhangsan").id + lisi_id = TenantUser.objects.get(data_source_user__code="lisi").id + resp = api_client.get( + reverse("open_v3.tenant_user.display_name.list"), data={"bk_usernames": ",".join([zhangsan_id, lisi_id])} + ) + + assert resp.status_code == status.HTTP_200_OK + assert len(resp.data) == 2 + assert {t["bk_username"] for t in resp.data} == {zhangsan_id, lisi_id} + assert {t["display_name"] for t in resp.data} == {"张三", "李四"} + + def test_with_invalid_bk_usernames(self, api_client): + zhangsan_id = TenantUser.objects.get(data_source_user__code="zhangsan").id + resp = api_client.get( + reverse("open_v3.tenant_user.display_name.list"), data={"bk_usernames": ",".join([zhangsan_id, "invalid"])} + ) + + assert resp.status_code == status.HTTP_200_OK + assert len(resp.data) == 1 + assert resp.data[0]["bk_username"] == zhangsan_id + assert resp.data[0]["display_name"] == "张三" + + def test_with_no_bk_usernames(self, api_client): + resp = api_client.get(reverse("open_v3.tenant_user.display_name.list"), data={"bk_usernames": ""}) + assert resp.status_code == status.HTTP_400_BAD_REQUEST + + def test_with_invalid_length(self, api_client): + resp = api_client.get( + reverse("open_v3.tenant_user.display_name.list"), + data={ + "bk_usernames": ",".join( + map(str, range(1, settings.BATCH_QUERY_USER_DISPLAY_NAME_BY_BK_USERNAME_LIMIT + 2)) + ) + }, + ) + assert resp.status_code == status.HTTP_400_BAD_REQUEST