Skip to content

Commit

Permalink
Ft login support inner bearer token (#2027)
Browse files Browse the repository at this point in the history
  • Loading branch information
nannan00 authored Dec 28, 2024
1 parent 7b2493f commit b301ee4
Show file tree
Hide file tree
Showing 18 changed files with 252 additions and 153 deletions.
96 changes: 73 additions & 23 deletions src/bk-login/bklogin/common/middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 协议,处理响应数据
# 开发者关注的,能自助排查,快速定位问题
Expand All @@ -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)
28 changes: 22 additions & 6 deletions src/bk-login/bklogin/open_apis/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
13 changes: 8 additions & 5 deletions src/bk-login/bklogin/open_apis/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
),
]
22 changes: 19 additions & 3 deletions src/bk-login/bklogin/open_apis/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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 认证"""
4 changes: 4 additions & 0 deletions src/bk-login/bklogin/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]

Expand Down Expand Up @@ -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="")
Expand Down
8 changes: 3 additions & 5 deletions src/bk-login/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/bk-login/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/bk-login/requirements.txt
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/bk-login/requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
10 changes: 8 additions & 2 deletions src/bk-login/tests/open_apis/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit b301ee4

Please sign in to comment.