diff --git a/apps/authentication.py b/apps/authentication.py new file mode 100644 index 000000000..48e986916 --- /dev/null +++ b/apps/authentication.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-节点管理(BlueKing-BK-NODEMAN) available. +Copyright (C) 2017-2022 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 https://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. +""" + +import logging + +from blueapps import metrics +from blueapps.account import get_user_model +from django.contrib.auth.backends import ModelBackend +from django.contrib.auth.models import AnonymousUser + +logger = logging.getLogger("component") + + +class ApiGatewayJWTUserModelBackend(ModelBackend): + """Get users by username""" + + def user_maker(self, bk_username): + user_model = get_user_model() + try: + user, _ = user_model.objects.get_or_create(defaults={"nickname": bk_username}, username=bk_username) + except Exception: + metrics.BLUEAPPS_USER_TOKEN_VERIFY_FAILED_TOTAL.labels( + hostname=metrics.HOSTNAME, token_type="bk_jwt", err="user_verify_err" + ).inc() + logger.exception(f"[{self.__class__.__name__}] Failed to get_or_create user -> {bk_username}.") + return None + else: + return user + + def make_anonymous_user(self, bk_username=None): + user = AnonymousUser() + user.username = bk_username # type: ignore + return user + + def authenticate(self, request, api_name, bk_username, verified, **credentials): + if not verified: + metrics.BLUEAPPS_USER_TOKEN_VERIFY_FAILED_TOTAL.labels( + hostname=metrics.HOSTNAME, token_type="bk_jwt", err="user_verify_err" + ).inc() + return self.make_anonymous_user(bk_username=bk_username) + return self.user_maker(bk_username) diff --git a/apps/middlewares.py b/apps/middlewares.py index 4fee0c706..a0098f040 100644 --- a/apps/middlewares.py +++ b/apps/middlewares.py @@ -15,6 +15,7 @@ import os import traceback +from apigw_manager.apigw.authentication import ApiGatewayJWTUserMiddleware from blueapps.account.models import User from blueapps.core.exceptions.base import BlueException @@ -227,3 +228,20 @@ def process_exception(self, request, exception): response.status_code = 500 return response + + +class ApiGatewayJWTUserInjectAppMiddleware(ApiGatewayJWTUserMiddleware): + def __call__(self, request): + # jwt_app 依赖于 ApiGatewayJWTAppMiddleware 注入 + jwt_app = getattr(request, "app", None) + if not jwt_app: + return super().__call__(request) + + # 和开发框架保持一致行为,如果通过应用认证并且开启 ESB 白名单,此时认为用户认证也通过 + use_esb_white_list = getattr(settings, "USE_ESB_WHITE_LIST", True) + if use_esb_white_list and jwt_app.verified: + # 如果 user 信息不存在,默认填充 bk_app_code 作为用户名 + request.jwt.payload["user"] = request.jwt.payload.get("user") or {"bk_username": jwt_app.bk_app_code} + request.jwt.payload["user"]["verified"] = True + + return super().__call__(request) diff --git a/apps/node_man/apps.py b/apps/node_man/apps.py index d1011162b..2a26e8048 100644 --- a/apps/node_man/apps.py +++ b/apps/node_man/apps.py @@ -8,14 +8,14 @@ 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. """ -import os - from blueapps.utils.esbclient import get_client_by_user from django.apps import AppConfig from django.conf import settings +from django.core.management import call_command from django.db import ProgrammingError, connection from common.log import logger +from env import constants as env_constants class ApiConfig(AppConfig): @@ -27,12 +27,14 @@ def ready(self): 初始化部分配置,主要目的是为了SaaS和后台共用部分配置 """ - from apps.node_man.models import GlobalSettings + # 判断 APIGW 的表是否存在,不存在先跳过 + from apigw_manager.apigw.models import Context - if GlobalSettings._meta.db_table not in connection.introspection.table_names(): - # 初次部署表不存在时跳过DB写入操作 - logger.info(f"{GlobalSettings._meta.db_table} not exists, skip fetch_esb_api_key before migrate.") + if Context._meta.db_table not in connection.introspection.table_names(): + # 初次部署表不存在时跳过 DB 写入操作 + logger.info(f"[ESB][JWT] {Context._meta.db_table} not exists, skip fetch_esb_api_key before migrate.") else: + logger.info(f"[ESB][JWT] {Context._meta.db_table} exist, start to fetch_esb_api_key.") self.fetch_esb_api_key() try: @@ -43,40 +45,43 @@ def ready(self): def fetch_esb_api_key(self): """ - 企业版获取JWT公钥并存储到全局配置中 + 获取JWT公钥并存储到全局配置中 """ - if hasattr(settings, "APIGW_PUBLIC_KEY") or os.environ.get("BKAPP_APIGW_CLOSE"): - return - from apps.node_man.models import GlobalSettings - try: - config = GlobalSettings.objects.filter(key=GlobalSettings.KeyEnum.APIGW_PUBLIC_KEY.value).first() - except ProgrammingError: - config = None - - if config: - # 从数据库取公钥,若存在,直接使用 - settings.APIGW_PUBLIC_KEY = config.v_json - message = "[ESB][JWT]get esb api public key success (from db cache)" - # flush=True 实时刷新输出 - logger.info(message) - else: - if settings.RUN_MODE == "DEVELOP": - return - - client = get_client_by_user(user_or_username=settings.SYSTEM_USE_API_ACCOUNT) - esb_result = client.esb.get_api_public_key() - if esb_result["result"]: - api_public_key = esb_result["data"]["public_key"] - settings.APIGW_PUBLIC_KEY = api_public_key - # 获取到公钥之后回写数据库 - GlobalSettings.objects.update_or_create( - key=GlobalSettings.KeyEnum.APIGW_PUBLIC_KEY.value, - defaults={"v_json": api_public_key}, - ) - logger.info("[ESB][JWT]get esb api public key success (from realtime api)") + # 当环境整体使用 APIGW 时,尝试通过 apigw-manager 获取 esb & apigw 公钥 + if settings.BKPAAS_MAJOR_VERSION == env_constants.BkPaaSVersion.V3.value: + try: + call_command("fetch_apigw_public_key") + except Exception: + logger.error("[ESB][JWT] fetch apigw public key error") else: - logger.error(f'[ESB][JWT]get esb api public key error:{esb_result["message"]}') + logger.info("[ESB][JWT] fetch apigw public key success") + + try: + call_command("fetch_esb_public_key") + except Exception: + logger.error("[ESB][JWT] fetch esb public key error") + else: + logger.info("[ESB][JWT] fetch esb public key success") + + client = get_client_by_user(user_or_username=settings.SYSTEM_USE_API_ACCOUNT) + esb_result = client.esb.get_api_public_key() + if not esb_result["result"]: + logger.error(f'[ESB][JWT] get esb api public key error:{esb_result["message"]}') + return + + from apigw_manager.apigw.helper import PublicKeyManager + + api_public_key = esb_result["data"]["public_key"] + if settings.RUN_VER == "ieod": + # ieod 环境需要额外注入 esb 公钥,从而支持 ESB & APIGW + PublicKeyManager().set("esb-ieod-clouds", api_public_key) + logger.info("[ESB][JWT] get esb api public key and save to esb-ieod-clouds") + elif settings.BKPAAS_MAJOR_VERSION != env_constants.BkPaaSVersion.V3.value: + # V2 环境没有 APIGW,手动注入 + PublicKeyManager().set("bk-esb", api_public_key) + PublicKeyManager().set("apigw", api_public_key) + logger.info("[ESB][JWT] get esb api public key and save to bk-esb & apigw") def init_settings(self): """ diff --git a/config/default.py b/config/default.py index 1c52ab465..a9c34c51c 100644 --- a/config/default.py +++ b/config/default.py @@ -34,6 +34,7 @@ BKAPP_IS_PAAS_DEPLOY = env.BKAPP_IS_PAAS_DEPLOY BKAPP_ENABLE_DHCP = env.BKAPP_ENABLE_DHCP BK_BACKEND_CONFIG = env.BK_BACKEND_CONFIG +BKPAAS_MAJOR_VERSION = env.BKPAAS_MAJOR_VERSION # =============================================================================== @@ -71,6 +72,8 @@ # django_prometheus "django_prometheus", "blueapps.opentelemetry.instrument_app", + # apigw + "apigw_manager.apigw", ) # 这里是默认的中间件,大部分情况下,不需要改动 @@ -89,8 +92,11 @@ "whitenoise.middleware.WhiteNoiseMiddleware", # Auth middleware # 'blueapps.account.middlewares.WeixinLoginRequiredMiddleware', - "blueapps.account.middlewares.BkJwtLoginRequiredMiddleware", + # "blueapps.account.middlewares.BkJwtLoginRequiredMiddleware", "blueapps.account.middlewares.LoginRequiredMiddleware", + "apigw_manager.apigw.authentication.ApiGatewayJWTGenericMiddleware", # JWT 认证 + "apigw_manager.apigw.authentication.ApiGatewayJWTAppMiddleware", # JWT 透传的应用信息 + "apps.middlewares.ApiGatewayJWTUserInjectAppMiddleware", # JWT 透传的用户信息 # exception middleware "blueapps.core.exceptions.middleware.AppExceptionMiddleware", # 自定义中间件 @@ -283,6 +289,8 @@ # ESB、APIGW 的域名,新增于PaaSV3,如果取不到该值,则使用 BK_PAAS_INNER_HOST # OVERWRITE 区分,BK_COMPONENT_API_URL 会被开发框架覆盖导致 PaaSV2 环境下为 None BK_COMPONENT_API_OVERWRITE_URL = env.BK_COMPONENT_API_URL +BK_APIGW_NAME = "bk-nodeman" +BK_API_URL_TMPL = env.BK_API_URL_TMPL BK_NODEMAN_HOST = env.BK_NODEMAN_HOST # 节点管理后台外网域名,用于构造文件导入导出的API URL @@ -353,6 +361,7 @@ # =============================================================================== AUTH_USER_MODEL = "account.User" AUTHENTICATION_BACKENDS = ( + "apps.authentication.ApiGatewayJWTUserModelBackend", "blueapps.account.backends.BkJwtBackend", "blueapps.account.backends.UserBackend", "django.contrib.auth.backends.ModelBackend", diff --git a/env/__init__.py b/env/__init__.py index 6a7348327..37e191107 100644 --- a/env/__init__.py +++ b/env/__init__.py @@ -29,7 +29,9 @@ "BKAPP_OTEL_BK_DATA_TOKEN", "BKAPP_OTEL_GRPC_URL", "BK_CC_HOST", + "BK_API_URL_TMPL", "ENVIRONMENT", + "BKPAAS_MAJOR_VERSION", # esb 访问地址 "BK_COMPONENT_API_URL", # 节点管理SaaS访问地址 @@ -93,3 +95,6 @@ # 第三方依赖 # =============================================================================== BK_CC_HOST = get_type_env(key="BK_CC_HOST", default="", _type=str) + +# APIGW API 地址模板,在 PaaS 3.0 部署的应用,可从环境变量中获取 BK_API_URL_TMPL +BK_API_URL_TMPL = get_type_env(key="BK_API_URL_TMPL", default=BK_COMPONENT_API_URL + "/api/{api_name}", _type=str) diff --git a/env/paas_version_diff/__init__.py b/env/paas_version_diff/__init__.py index c426bd592..31f3aa7b3 100644 --- a/env/paas_version_diff/__init__.py +++ b/env/paas_version_diff/__init__.py @@ -16,6 +16,8 @@ __all__ = [ # PaaS 部署环境,标准化为 stag / dev "ENVIRONMENT", + # PaaS 版本 + "BKPAAS_MAJOR_VERSION", # 是否为后台配置 "BK_BACKEND_CONFIG", # 后台是否为PaaS部署 diff --git a/requirements.txt b/requirements.txt index a76d20c2b..223ab495b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -102,3 +102,5 @@ opentelemetry-instrumentation-dbapi==0.30b1 opentelemetry-instrumentation-redis==0.30b1 opentelemetry-instrumentation-logging==0.30b1 opentelemetry-instrumentation-requests==0.30b1 + +apigw-manager[cryptography]==1.1.5