Skip to content

Commit 3953f17

Browse files
committed
feat(backend): 全租户cmdb、cmsi、用户管理改造 #16093
1 parent 9cc8d4d commit 3953f17

File tree

14 files changed

+324
-33
lines changed

14 files changed

+324
-33
lines changed

dbm-ui/backend/components/base.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from backend.components.domains import ESB_PREFIX
3131
from backend.components.exception import DataAPIException
3232
from backend.configuration.models.system import SystemSettings
33-
from backend.exceptions import ApiError, ApiRequestError, ApiResultError, AppBaseException
33+
from backend.exceptions import ApiError, ApiRequestError, ApiResultError, AppBaseException, ValidationError
3434
from backend.utils.local import local
3535

3636
logger = logging.getLogger("root")
@@ -413,6 +413,39 @@ def _set_cache(self, cache_key, data):
413413
"""
414414
cache.set(cache_key, data, self.cache_time)
415415

416+
def _get_cached_admin_username(self):
417+
"""
418+
获取缓存的租户管理员用户名
419+
"""
420+
if not env.ENABLE_MULTI_TENANT_MODE:
421+
return env.DEFAULT_USERNAME
422+
423+
login_name = "bk_admin"
424+
cache_key = f"dbm:tenant_admin_username:{env.BK_TENANT_ID}:{login_name}"
425+
bk_username = cache.get(cache_key, "")
426+
if not bk_username:
427+
try:
428+
from backend.components.usermanage.client import UserManagerApi
429+
430+
params = {"lookup_field": "login_name", "lookups": login_name}
431+
# 接口返回不规范(无result/code字段),使用raw=True绕过DataAPI的统一校验
432+
res = UserManagerApi.batch_lookup_virtual_user(params, use_admin=False, use_param_user=True, raw=True)
433+
data = res.get("data")
434+
if isinstance(data, list) and data:
435+
bk_username = data[0].get("bk_username") or data[0].get("username")
436+
elif isinstance(data, dict) and data:
437+
bk_username = data.get("bk_username") or data.get("username")
438+
if bk_username:
439+
cache.set(cache_key, bk_username, 60 * 60 * 24)
440+
else:
441+
raise ValidationError(_("获取租户管理员账号失败: 未能从响应中获取用户名"))
442+
except Exception as e:
443+
error_msg = _("获取租户管理员账号失败: {error}").format(error=str(e))
444+
logger.error(error_msg)
445+
raise ValidationError(error_msg)
446+
447+
return bk_username
448+
416449
def _set_session_headers(self, session, local_request, headers: Dict, params: Dict, use_admin: bool = False):
417450
"""
418451
设置session的headers
@@ -430,17 +463,23 @@ def _set_session_headers(self, session, local_request, headers: Dict, params: Di
430463
# 增加鉴权信息
431464
if not isinstance(params, dict):
432465
return
466+
467+
bk_token = session.headers.get("bk_token", "")
468+
if not bk_token and local_request and hasattr(local_request, "COOKIES"):
469+
bk_token = local_request.COOKIES.get("bk_token", "")
470+
433471
bkapi_auth_headers = {
434472
"bk_app_code": params.pop("bk_app_code", env.APP_CODE),
435473
"bk_app_secret": params.pop("bk_app_secret", env.SECRET_KEY),
436474
"bk_username": params.get("bk_username", "Anonymous"),
475+
"bk_token": bk_token,
437476
}
438477
if use_admin:
439478
# 使用管理员/平台身份调用接口
440-
bkapi_auth_headers["bk_username"] = env.DEFAULT_USERNAME
479+
bkapi_auth_headers["bk_username"] = self._get_cached_admin_username()
441480
elif self.is_backend_request(local_request) and not self.use_param_user:
442481
# 后台调用(且不明确用户),使用管理员/平台身份调用接口
443-
bkapi_auth_headers["bk_username"] = env.DEFAULT_USERNAME
482+
bkapi_auth_headers["bk_username"] = self._get_cached_admin_username()
444483
elif local_request and local_request.COOKIES:
445484
# 根据不同环境,传递认证信息
446485
bkapi_auth_headers["bk_username"] = local_request.user.username

dbm-ui/backend/components/cmsi/client.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,57 @@ class _CmsiApi(BaseApi):
1919
MODULE = _("消息管理")
2020
BASE = CMSI_APIGW_DOMAIN
2121

22+
# 消息类型映射定义
23+
@property
24+
def MSG_TYPE_MAP(self):
25+
from ...core.notify.constants import MsgType
26+
27+
return {
28+
MsgType.VOICE.value: "send_voice",
29+
MsgType.SMS.value: "send_sms",
30+
MsgType.WEIXIN.value: "send_weixin",
31+
MsgType.MAIL.value: "send_mail",
32+
}
33+
2234
def __init__(self):
23-
self.send_msg = self.generate_data_api(
24-
method="POST",
25-
url="send_msg/",
26-
description=_("通用消息发送"),
27-
)
2835
self.get_msg_type = self.generate_data_api(
2936
method="GET",
30-
url="get_msg_type/",
37+
url="channels/",
3138
description=_("查询通知类型"),
3239
)
40+
self.send_voice = self.generate_data_api(
41+
method="POST",
42+
url="send_voice/",
43+
description=_("语音通知"),
44+
)
45+
self.send_sms = self.generate_data_api(
46+
method="POST",
47+
url="send_sms/",
48+
description=_("短信通知"),
49+
)
50+
self.send_weixin = self.generate_data_api(
51+
method="POST",
52+
url="send_weixin/",
53+
description=_("微信通知"),
54+
)
55+
self.send_mail = self.generate_data_api(
56+
method="POST",
57+
url="send_mail/",
58+
description=_("邮件通知"),
59+
)
60+
61+
def get_msg_map(self):
62+
return {msg_type: getattr(self, method_name) for msg_type, method_name in self.MSG_TYPE_MAP.items()}
63+
64+
def send_msg(self, params):
65+
msg_type = params.pop("msg_type")
66+
if not msg_type:
67+
raise (_("消息类型(msg_type)不能为空"))
68+
69+
msg_map = self.get_msg_map()
70+
if msg_type not in msg_map:
71+
raise (_("不支持的消息类型: {}").format(msg_type))
72+
return msg_map[msg_type](params)
3373

3474

3575
CmsiApi = _CmsiApi()

dbm-ui/backend/components/domains.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111

1212
from backend import env
1313

14+
# esb
1415
ESB_PREFIX = "/api/c/compapi/v2/"
15-
1616
ESB_DOMAIN_TPL = "{}{}{{}}/".format(env.BK_COMPONENT_API_URL, ESB_PREFIX)
1717

18+
1819
# 优先取环境变量的配置,若未配置对应的环境变量,则取paas默认的esb地址
1920
CC_APIGW_DOMAIN = env.CC_APIGW_DOMAIN or ESB_DOMAIN_TPL.format("cc")
2021
GSE_APIGW_DOMAIN = env.GSE_APIGW_DOMAIN or ESB_DOMAIN_TPL.format("gse")

dbm-ui/backend/components/usermanage/client.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,23 @@ class _UserManageApi(BaseApi):
2020
BASE = USER_MANAGE_APIGW_DOMAIN
2121

2222
def __init__(self):
23+
is_esb = self.is_esb()
2324
self.list_users = self.generate_data_api(
2425
method="GET",
25-
url="list_users/",
26+
url="list_users/" if is_esb else "tenant/users/",
2627
description=_("获取所有用户"),
2728
cache_time=300,
2829
)
2930
self.retrieve_user = self.generate_data_api(
3031
method="GET",
31-
url="retrieve_user/",
32+
url="retrieve_user/" if is_esb else "tenant/users/{bk_username}/",
3233
description=_("获取单个用户"),
3334
)
35+
self.batch_lookup_virtual_user = self.generate_data_api(
36+
method="GET",
37+
url="tenant/virtual-users/-/lookup/",
38+
description=_("获取租户的管理员用户"),
39+
)
3440

3541

3642
UserManagerApi = _UserManageApi()

dbm-ui/backend/configuration/views/profile.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from rest_framework.decorators import action
1414
from rest_framework.response import Response
1515

16+
from backend import env
1617
from backend.bk_web import viewsets
1718
from backend.bk_web.swagger import common_swagger_auto_schema
1819
from backend.configuration.constants import ProfileLabel
@@ -48,6 +49,7 @@ def get_profile(self, request, *args, **kwargs):
4849
"profile": list(profile),
4950
"is_superuser": request.user.is_superuser,
5051
"is_dba": DBAdministrator.is_dba(request.user.username),
52+
"tenant_id": env.BK_TENANT_ID,
5153
}
5254
)
5355

dbm-ui/backend/configuration/views/system.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,10 @@ def environ(self, request):
130130
if not env.ENABLE_EXTERNAL_PROXY and not env.ENABLE_OPEN_EXTERNAL_PROXY:
131131
envs.update(
132132
{
133-
"CC_IDLE_MODULE_ID": CcManage(env.DBA_APP_BK_BIZ_ID, "").get_biz_internal_module(
134-
env.DBA_APP_BK_BIZ_ID
135-
)[IDLE_HOST_MODULE]["bk_module_id"],
133+
"CC_IDLE_MODULE_ID": CcManage(env.DBA_APP_BK_BIZ_ID, "")
134+
.get_biz_internal_module(env.DBA_APP_BK_BIZ_ID)
135+
.get(IDLE_HOST_MODULE, {})
136+
.get("bk_module_id"),
136137
"BK_COMPONENT_API_URL": env.BK_COMPONENT_API_URL,
137138
"BK_CMDB_URL": env.BK_CMDB_URL,
138139
"BK_NODEMAN_URL": env.BK_NODEMAN_URL,

dbm-ui/backend/core/notify/handlers.py

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -180,18 +180,72 @@ class CmsiHandler(BaseNotifyHandler):
180180
def get_msg_type(cls):
181181
return [s["type"] for s in CmsiApi.get_msg_type()]
182182

183+
def _build_mail_params(self, **kwargs):
184+
"""构建邮件参数"""
185+
params = {
186+
"title": self.title,
187+
"content": self.content.replace("\n", "<br>"), # 邮件换行用<br>
188+
"is_content_base64": False,
189+
}
190+
params.update(kwargs) # 更新额外参数(sender, cc__username等)
191+
return params
192+
193+
def _build_sms_params(self, **kwargs):
194+
"""构建短信参数"""
195+
return {
196+
"content": f"{self.title}\n{self.content}", # 短信合并标题和内容
197+
"is_content_base64": False,
198+
}
199+
200+
def _build_voice_params(self, **kwargs):
201+
"""构建语音参数"""
202+
return {
203+
"auto_read_message": f"{self.title}\n{self.content}", # 语音播报内容
204+
}
205+
206+
def _build_weixin_params(self, **kwargs):
207+
"""构建微信参数"""
208+
return {
209+
"message_data": {
210+
"heading": self.title,
211+
"message": self.content,
212+
"is_message_base64": False,
213+
}
214+
}
215+
216+
def _build_default_params(self, **kwargs):
217+
"""构建默认参数(保留 RTX、企微机器人等功能的参数)"""
218+
params = {
219+
"title": self.title,
220+
"content": self.content,
221+
}
222+
params.update(kwargs)
223+
return params
224+
183225
def _cmsi_send_msg(self, msg_type: str, **kwargs):
184226
"""
227+
统一处理所有消息类型的发送逻辑
185228
@param msg_type: 发送类型
186229
@param kwargs: 额外参数
187230
"""
188231
msg_info = {
189232
"msg_type": msg_type,
190-
"receiver__username": ",".join(self.receivers),
191-
"title": self.title,
192-
"content": self.content,
233+
"receiver__username": self.receivers, # 接收者列表
193234
}
194-
msg_info.update(kwargs)
235+
236+
# 策略映射:根据消息类型选择对应的参数构建方法
237+
param_builders_map = {
238+
MsgType.MAIL.value: self._build_mail_params,
239+
MsgType.SMS.value: self._build_sms_params,
240+
MsgType.VOICE.value: self._build_voice_params,
241+
MsgType.WEIXIN.value: self._build_weixin_params,
242+
}
243+
244+
# 获取对应的参数构建器,如果没有则使用默认构建器
245+
builder = param_builders_map.get(msg_type, self._build_default_params)
246+
msg_info.update(builder(**kwargs))
247+
248+
# 调用统一的send_msg接口
195249
CmsiApi.send_msg(msg_info)
196250

197251
def send_mail(self, sender: str = None, cc: list = None):
@@ -201,12 +255,10 @@ def send_mail(self, sender: str = None, cc: list = None):
201255
"""
202256
kwargs = {}
203257
if sender:
204-
kwargs.update(sender=sender)
258+
kwargs["sender"] = sender
205259
if cc:
206-
kwargs.update(cc__username=",".join(cc))
207-
# 邮件的换行要用<br>的html
208-
self.content = self.content.replace("\n", "<br>")
209-
self._cmsi_send_msg(MsgType.MAIL, **kwargs)
260+
kwargs["cc__username"] = cc
261+
self._cmsi_send_msg(MsgType.MAIL.value, **kwargs)
210262

211263
def send_voice(self):
212264
"""发送语音消息"""
@@ -222,8 +274,6 @@ def send_rtx(self):
222274

223275
def send_sms(self):
224276
"""发送短信消息"""
225-
# 短信消息没有标题参数,直接把标题和内容放在一起
226-
self.content = f"{self.title}\n{self.content}"
227277
self._cmsi_send_msg(MsgType.SMS.value)
228278

229279
def send_wecom_robot(self):

dbm-ui/backend/flow/utils/cc_manage.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ def get_or_create_set_module(
140140
def get_biz_internal_module(bk_biz_id: int):
141141
"""获取业务下的内置模块"""
142142
biz_internal_module = ResourceQueryHelper.get_biz_internal_module(bk_biz_id)
143+
if not biz_internal_module or not biz_internal_module.get("module"):
144+
logger.warning(f"biz_internal_module is empty, bk_biz_id: {bk_biz_id}")
145+
return {}
143146
module_type__module = {module["default"]: module for module in biz_internal_module["module"]}
144147
return module_type__module
145148

dbm-ui/backend/iam_app/handlers/permission.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def __init__(self, username: str = "", request=None):
6767
@classmethod
6868
def get_iam_client(cls):
6969
tenant_id = getattr(env, "BK_TENANT_ID", "")
70-
if settings.BK_IAM_SKIP:
70+
if env.BK_IAM_SKIP:
7171
return DummyIAM(env.APP_CODE, env.SECRET_KEY, env.BK_IAM_APIGATEWAY, tenant_id)
7272
return IAM(env.APP_CODE, env.SECRET_KEY, env.BK_IAM_APIGATEWAY, tenant_id)
7373

dbm-ui/backend/tests/components/test_base.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,13 @@ def test_call_raise_exception_on_failure(self, mock_session):
134134
mock_resp = MagicMock()
135135
mock_resp.status_code = 200
136136
mock_resp.json.return_value = {"result": False, "message": "error", "code": 1}
137-
mock_session.return_value.request.return_value = mock_resp
137+
138+
# 设置完整的mock session
139+
mock_session_instance = MagicMock()
140+
mock_session_instance.request.return_value = mock_resp
141+
mock_session_instance.headers = {}
142+
mock_session_instance.cookies = {}
143+
mock_session.return_value = mock_session_instance
138144

139145
api = DataAPI(method="GET", base="http://test.com", url="/api/test", module="test")
140146
with pytest.raises(ApiResultError):

0 commit comments

Comments
 (0)