Skip to content

Commit

Permalink
feature: 支持 ESB / APIGW 双公钥 (closed TencentBlueKing#1369)
Browse files Browse the repository at this point in the history
  • Loading branch information
ZhuoZhuoCrayon committed Feb 23, 2023
1 parent 4d86a15 commit 8ca979e
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 38 deletions.
49 changes: 49 additions & 0 deletions apps/authentication.py
Original file line number Diff line number Diff line change
@@ -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)
18 changes: 18 additions & 0 deletions apps/middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
79 changes: 42 additions & 37 deletions apps/node_man/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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:
Expand All @@ -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):
"""
Expand Down
11 changes: 10 additions & 1 deletion config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


# ===============================================================================
Expand Down Expand Up @@ -71,6 +72,8 @@
# django_prometheus
"django_prometheus",
"blueapps.opentelemetry.instrument_app",
# apigw
"apigw_manager.apigw",
)

# 这里是默认的中间件,大部分情况下,不需要改动
Expand All @@ -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",
# 自定义中间件
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions env/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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访问地址
Expand Down Expand Up @@ -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)
2 changes: 2 additions & 0 deletions env/paas_version_diff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
__all__ = [
# PaaS 部署环境,标准化为 stag / dev
"ENVIRONMENT",
# PaaS 版本
"BKPAAS_MAJOR_VERSION",
# 是否为后台配置
"BK_BACKEND_CONFIG",
# 后台是否为PaaS部署
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 8ca979e

Please sign in to comment.