From 2bf7ad75b347f59c10d3166e0cd5d8e79b54c918 Mon Sep 17 00:00:00 2001 From: iSecloud <869820505@qq.com> Date: Wed, 18 Sep 2024 16:12:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(backend):=20=E6=8F=90=E4=BE=9B=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=88=86=E9=85=8Diam=E6=9D=83=E9=99=90=E7=BB=99DBA?= =?UTF-8?q?=E7=9A=84=E8=84=9A=E6=9C=AC=20#6988?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dbm-ui/backend/env/__init__.py | 1 + dbm-ui/backend/iam_app/dataclass/__init__.py | 115 +++++++++++++--- dbm-ui/backend/iam_app/handlers/client.py | 66 ++++++++++ dbm-ui/backend/iam_app/handlers/permission.py | 3 +- .../iam_app/migration_json_files/initial.json | 123 +++++++++++++++++- dbm-ui/backend/iam_app/serializers.py | 6 + dbm-ui/backend/iam_app/views/views.py | 22 ++++ 7 files changed, 317 insertions(+), 19 deletions(-) create mode 100644 dbm-ui/backend/iam_app/handlers/client.py diff --git a/dbm-ui/backend/env/__init__.py b/dbm-ui/backend/env/__init__.py index 431ade5513..bbd2070fb5 100644 --- a/dbm-ui/backend/env/__init__.py +++ b/dbm-ui/backend/env/__init__.py @@ -69,6 +69,7 @@ BK_IAM_API_VERSION = get_type_env(key="BK_IAM_API_VERSION", _type=str, default="v1") IAM_APP_URL = get_type_env(key="IAM_APP_URL", _type=str, default="https://iam.example.com") BK_IAM_RESOURCE_API_HOST = get_type_env(key="BK_IAM_RESOURCE_API_HOST", _type=str, default="https://bkdbm.example.com") +BK_IAM_GRADE_MANAGER_ID = get_type_env(key="BK_IAM_GRADE_MANAGER_ID", _type=int, default=0) # APIGW 相关配置 BK_APIGATEWAY_DOMAIN = get_type_env(key="BK_APIGATEWAY_DOMAIN", _type=str, default=BK_COMPONENT_API_URL) diff --git a/dbm-ui/backend/iam_app/dataclass/__init__.py b/dbm-ui/backend/iam_app/dataclass/__init__.py index e0df9523b0..396aef1a98 100644 --- a/dbm-ui/backend/iam_app/dataclass/__init__.py +++ b/dbm-ui/backend/iam_app/dataclass/__init__.py @@ -9,18 +9,28 @@ specific language governing permissions and limitations under the License. """ import json +import logging import os +import re +import time from collections import defaultdict from typing import Any, Dict, List from django.conf import settings from django.utils.translation import ugettext as _ -from ...env import BK_IAM_SYSTEM_ID +from backend import env +from backend.db_meta.models import AppCache + from ..constans import CommonActionLabel +from ..exceptions import BaseIAMError +from ..handlers.client import IAM +from ..handlers.permission import Permission from .actions import _all_actions from .resources import ResourceEnum, ResourceMeta, _all_resources, _extra_instance_selections +logger = logging.getLogger("root") + IAM_SYSTEM_DEFINITION = { "operation": "upsert_system", "data": { @@ -48,7 +58,7 @@ def generate_iam_migration_json(json_name: str = ""): # 获取资源的json内容 for resource in _all_resources.values(): - if resource.system_id != BK_IAM_SYSTEM_ID: + if resource.system_id != env.BK_IAM_SYSTEM_ID: continue iam_resources.append(resource.to_json()) @@ -90,7 +100,7 @@ def generate_iam_migration_json(json_name: str = ""): continue related_resource = action.related_resource_types[0] # 不关联跨系统和特殊资源(dbtype) - if related_resource.system_id != BK_IAM_SYSTEM_ID: + if related_resource.system_id != env.BK_IAM_SYSTEM_ID: continue if related_resource in [ResourceEnum.DBTYPE]: continue @@ -149,7 +159,7 @@ def generate_iam_migration_json(json_name: str = ""): iam_json_content.append({"operation": "upsert_resource_creator_actions", "data": iam_resource_creator_actions}) # 获取dbm在iam完整的注册json - dbm_iam_json = {"system_id": BK_IAM_SYSTEM_ID, "operations": iam_json_content} + dbm_iam_json = {"system_id": env.BK_IAM_SYSTEM_ID, "operations": iam_json_content} json_name = json_name or "initial—tmp.json" iam_migrate_json_path = os.path.join(settings.BASE_DIR, f"backend/iam_app/migration_json_files/{json_name}") @@ -157,25 +167,22 @@ def generate_iam_migration_json(json_name: str = ""): f.write(json.dumps(dbm_iam_json, ensure_ascii=False, indent=4)) -def generate_iam_biz_maintain_json(label: str = CommonActionLabel.BIZ_MAINTAIN, json_name: str = ""): - """ - 根据dataclass的定义自动生成业务运维的用户组迁移json - """ +def generate_resource_topo_auth(res_actions: list, bk_biz_id: int = None, bk_biz_name=None): + bk_biz_id = bk_biz_id or "{{biz_id}}" + bk_biz_name = bk_biz_name or "{{biz_name}}" def get_resource_path_info(resource: ResourceMeta): if ResourceEnum.BUSINESS not in [resource, resource.parent]: paths = [] else: - paths = [[{"system": "bk_cmdb", "type": "biz", "id": "{{biz_id}}", "name": "{{biz_name}}"}]] + paths = [[{"system": "bk_cmdb", "type": "biz", "id": bk_biz_id, "name": bk_biz_name}]] return {"system": resource.system_id, "type": resource.id, "paths": paths} resources__actions_map: Dict[str, List[str]] = defaultdict(list) - biz_maintain_migrate_content: List[Dict[str, Any]] = [] + resource_topo_auth_content: List[Dict[str, Any]] = [] # 聚合相同资源的动作 - for action in _all_actions.values(): - if label not in action.common_labels: - continue + for action in res_actions: resource_ids = ",".join([resource.id for resource in action.related_resource_types]) resources__actions_map[resource_ids].append(action.id) @@ -189,12 +196,90 @@ def get_resource_path_info(resource: ResourceMeta): resource_infos = [get_resource_path_info(resource) for resource in resource_metas] # 生成action的迁移信息 action_infos = [{"id": id} for id in action_ids] - biz_maintain_migrate_content.append( - {"system": BK_IAM_SYSTEM_ID, "actions": action_infos, "resources": resource_infos} + resource_topo_auth_content.append( + {"system": env.BK_IAM_SYSTEM_ID, "actions": action_infos, "resources": resource_infos} ) + return resource_topo_auth_content + + +def generate_iam_biz_maintain_json(label: str = CommonActionLabel.BIZ_MAINTAIN, json_name: str = ""): + """ + 根据dataclass的定义自动生成业务运维的用户组迁移json + """ + res_actions = [action for action in _all_actions.values() if label in action.common_labels] + biz_maintain_migrate_content = generate_resource_topo_auth(res_actions) + # 生成json文件 json_name = json_name or "biz_maintain_migrate.json" migrate_json_path = os.path.join(settings.BASE_DIR, f"backend/iam_app/migration_json_files/{json_name}") with open(migrate_json_path, "w+") as f: f.write(json.dumps(biz_maintain_migrate_content, ensure_ascii=False, indent=4)) + + +def assign_auth_to_group(iam: IAM, biz: AppCache, group_id): + """ + 给单个用户组分配权限,这里的权限固定是DBA权限 + """ + biz_actions = [action for action in _all_actions.values() if action.group not in [_("全局设置"), _("资源管理")]] + auth_contents = generate_resource_topo_auth(biz_actions, bk_biz_id=biz.bk_biz_id, bk_biz_name=biz.bk_biz_name) + for auth_info in auth_contents: + ok, message, data = iam._client.grant_user_group_actions(env.BK_IAM_SYSTEM_ID, group_id, data=auth_info) + if not ok: + raise BaseIAMError(_("用户组添加授权失败,错误信息: {}").format(message)) + + +def assign_auth_to_dba(bk_biz_id: int, group_name: str, members: list): + """ + 给DBA分配iam权限,具体是: + 创建用户组 ---> 给用户组分配权限 ---> 成员加入用户组 + """ + biz = AppCache.objects.get(bk_biz_id=bk_biz_id) + manager_id = env.BK_IAM_GRADE_MANAGER_ID + iam = Permission.get_iam_client() + + # 创建用户组 + group_data = {"groups": [{"name": group_name, "description": group_name}]} + ok, message, data = iam._client.create_user_groups(env.BK_IAM_SYSTEM_ID, manager_id, data=group_data) + if not ok: + raise BaseIAMError(_("创建用户组失败,错误信息: {}").format(message)) + + # 对用户组分配权限,动作不包含资源管理和全局设置 + group_id = data[0] + assign_auth_to_group(iam, biz, data[0]) + + # 对用户组添加成员,默认过期时间是1年 + expired_at = int(time.time() + 60 * 60 * 24 * 30 * 12) + members = [{"type": "user", "id": member} for member in members] + add_members_data = {"members": members, "expired_at": expired_at} + ok, message, data = iam._client.add_user_group_members(env.BK_IAM_SYSTEM_ID, group_id, data=add_members_data) + if not ok: + raise BaseIAMError(_("用户组添加成员{}失败,错误信息: {}").format(members, message)) + + +def flush_groups_auth(): + """ + 刷新存量用户组权限 + """ + iam = Permission.get_iam_client() + manager_id = env.BK_IAM_GRADE_MANAGER_ID + + # 查询包含DBA名称的用户组,默认不超过500 + params = {"name": "DBA", "page_size": 500, "page": 1} + ok, message, data = iam._client.query_user_groups(env.BK_IAM_SYSTEM_ID, manager_id, data=params) + if not ok: + raise BaseIAMError(_("用户组查询失败,错误信息: {}").format(message)) + # 匹配{biz_name}_DBA(#{biz_id})这样格式的用户组 + dba_group_pattern = re.compile(r"^.*?_DBA\(#[0-9]*\)$") + dba_groups = [group for group in data["results"] if dba_group_pattern.match(group["name"])] + + # 刷新权限 + for group in dba_groups: + try: + # 提取用户组ID和业务 + group_id, name = group["id"], group["name"] + biz = AppCache.objects.get(bk_biz_name=name.split("_DBA")[0]) + # 分配权限 + assign_auth_to_group(iam, biz, group_id) + except Exception as e: + raise BaseIAMError(_("用户组{}刷新失败,错误信息:{}").format(group, e)) diff --git a/dbm-ui/backend/iam_app/handlers/client.py b/dbm-ui/backend/iam_app/handlers/client.py new file mode 100644 index 0000000000..86ef52469f --- /dev/null +++ b/dbm-ui/backend/iam_app/handlers/client.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available. +Copyright (C) 2017-2023 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. +""" + +from iam import IAM as BaseIAM +from iam.api.client import Client as IAMClient +from iam.api.http import http_get, http_post, http_put + + +class Client(IAMClient): + """补充一些额外的api请求""" + + # 创建用户组 + def create_user_groups(self, system_id, grade_manager_id, data): + path = "/api/v2/open/management/systems/{system_id}/grade_managers/{grade_manager_id}/groups/".format( + system_id=system_id, grade_manager_id=grade_manager_id + ) + ok, message, data = self._call_iam_api(http_post, path, data) + return ok, message, data + + # 用户组授权 + def grant_user_group_actions(self, system_id, group_id, data): + path = "/api/v2/open/management/systems/{system_id}/groups/{group_id}/policies".format( + system_id=system_id, group_id=group_id + ) + ok, message, data = self._call_iam_api(http_post, path, data) + return ok, message, data + + # 添加用户组成员 + def add_user_group_members(self, system_id, group_id, data): + path = "/api/v2/open/management/systems/{system_id}/groups/{group_id}/members".format( + system_id=system_id, group_id=group_id + ) + ok, message, data = self._call_iam_api(http_post, path, data) + return ok, message, data + + # 查询用户组 + def query_user_groups(self, system_id, grade_manager_id, data): + path = "/api/v2/open/management/systems/{system_id}/grade_managers/{grade_manager_id}/groups".format( + system_id=system_id, grade_manager_id=grade_manager_id + ) + ok, message, data = self._call_iam_api(http_get, path, data) + return ok, message, data + + # 更新用户组名字和描述 + def update_user_groups(self, system_id, group_id, data): + path = "/api/v2/open/management/systems/{system_id}/groups/{group_id}/".format( + system_id=system_id, group_id=group_id + ) + ok, message, data = self._call_iam_api(http_put, path, data) + return ok, message, data + + +class IAM(BaseIAM): + def __init__( + self, app_code, app_secret, bk_iam_host=None, bk_paas_host=None, bk_apigateway_url=None, api_version="v2" + ): + super().__init__(app_code, app_secret, bk_iam_host, bk_paas_host, bk_apigateway_url, api_version) + self._client = Client(app_code, app_secret, bk_iam_host, bk_paas_host, bk_apigateway_url) diff --git a/dbm-ui/backend/iam_app/handlers/permission.py b/dbm-ui/backend/iam_app/handlers/permission.py index 8223cf6ca3..d1e9a5c92c 100644 --- a/dbm-ui/backend/iam_app/handlers/permission.py +++ b/dbm-ui/backend/iam_app/handlers/permission.py @@ -17,7 +17,7 @@ from blueapps.account.models import User from django.conf import settings from django.utils.translation import ugettext as _ -from iam import IAM, DummyIAM, MultiActionRequest, ObjectSet, Request, Resource, Subject, make_expression +from iam import DummyIAM, MultiActionRequest, ObjectSet, Request, Resource, Subject, make_expression from iam.apply.models import ( ActionWithoutResources, ActionWithResources, @@ -36,6 +36,7 @@ from backend.iam_app.dataclass.actions import ActionEnum, ActionMeta, _all_actions from backend.iam_app.dataclass.resources import ResourceEnum, ResourceMeta, _all_resources from backend.iam_app.exceptions import ActionNotExistError, GetSystemInfoError, PermissionDeniedError +from backend.iam_app.handlers.client import IAM from backend.utils.local import local logger = logging.getLogger("root") diff --git a/dbm-ui/backend/iam_app/migration_json_files/initial.json b/dbm-ui/backend/iam_app/migration_json_files/initial.json index 6d385817ab..c48b09689a 100644 --- a/dbm-ui/backend/iam_app/migration_json_files/initial.json +++ b/dbm-ui/backend/iam_app/migration_json_files/initial.json @@ -8165,7 +8165,7 @@ "version": 1, "hidden": false, "group": "Redis", - "subgroup": "工具箱", + "subgroup": "集群维护", "description": "Redis集群域名重命名", "description_en": "REDIS_CLUSTER_RENAME_DOMAIN" } @@ -8199,6 +8199,64 @@ "description_en": "REDIS_CLUSTER_MAXMEMORY_SET" } }, + { + "operation": "upsert_action", + "data": { + "id": "redis_cluster_load_modules", + "name": "Redis 集群加载modules", + "name_en": "REDIS_CLUSTER_LOAD_MODULES", + "type": "execute", + "related_resource_types": [ + { + "system_id": "bk_dbm", + "id": "redis", + "selection_mode": "all", + "related_instance_selections": [ + { + "system_id": "bk_dbm", + "id": "redis_list" + } + ] + } + ], + "related_actions": [], + "version": 1, + "hidden": false, + "group": "Redis", + "subgroup": "工具箱", + "description": "Redis 集群加载modules", + "description_en": "REDIS_CLUSTER_LOAD_MODULES" + } + }, + { + "operation": "upsert_action", + "data": { + "id": "redis_tendisplus_lightning_data", + "name": "Tendisplus闪电导入数据", + "name_en": "REDIS_TENDISPLUS_LIGHTNING_DATA", + "type": "execute", + "related_resource_types": [ + { + "system_id": "bk_dbm", + "id": "redis", + "selection_mode": "all", + "related_instance_selections": [ + { + "system_id": "bk_dbm", + "id": "redis_list" + } + ] + } + ], + "related_actions": [], + "version": 1, + "hidden": false, + "group": "Redis", + "subgroup": "集群维护", + "description": "Tendisplus闪电导入数据", + "description_en": "REDIS_TENDISPLUS_LIGHTNING_DATA" + } + }, { "operation": "upsert_action", "data": { @@ -9417,6 +9475,35 @@ "description_en": "MONGODB_CUTOFF" } }, + { + "operation": "upsert_action", + "data": { + "id": "mongodb_import", + "name": "MongoDB 数据导入", + "name_en": "MONGODB_IMPORT", + "type": "execute", + "related_resource_types": [ + { + "system_id": "bk_dbm", + "id": "mongodb", + "selection_mode": "all", + "related_instance_selections": [ + { + "system_id": "bk_dbm", + "id": "mongodb_list" + } + ] + } + ], + "related_actions": [], + "version": 1, + "hidden": false, + "group": "MongoDB", + "subgroup": "集群维护", + "description": "MongoDB 数据导入", + "description_en": "MONGODB_IMPORT" + } + }, { "operation": "upsert_action", "data": { @@ -10432,6 +10519,12 @@ }, { "id": "redis_cluster_add_slave" + }, + { + "id": "redis_cluster_rename_domain" + }, + { + "id": "redis_tendisplus_lightning_data" } ] }, @@ -10473,10 +10566,10 @@ "id": "redis_cluster_reinstall_dbmon" }, { - "id": "redis_cluster_rename_domain" + "id": "redis_cluster_maxmemory_set" }, { - "id": "redis_cluster_maxmemory_set" + "id": "redis_cluster_load_modules" } ] } @@ -10809,6 +10902,9 @@ { "id": "mongodb_cutoff" }, + { + "id": "mongodb_import" + }, { "id": "mongodb_restore" }, @@ -11705,6 +11801,12 @@ { "id": "redis_cluster_maxmemory_set" }, + { + "id": "redis_cluster_load_modules" + }, + { + "id": "redis_tendisplus_lightning_data" + }, { "id": "kafka_scale_up" }, @@ -11831,6 +11933,9 @@ { "id": "mongodb_cutoff" }, + { + "id": "mongodb_import" + }, { "id": "mongodb_restore" }, @@ -12539,6 +12644,14 @@ { "id": "redis_cluster_maxmemory_set", "required": true + }, + { + "id": "redis_cluster_load_modules", + "required": true + }, + { + "id": "redis_tendisplus_lightning_data", + "required": true } ] }, @@ -12823,6 +12936,10 @@ "id": "mongodb_cutoff", "required": true }, + { + "id": "mongodb_import", + "required": true + }, { "id": "mongodb_restore", "required": true diff --git a/dbm-ui/backend/iam_app/serializers.py b/dbm-ui/backend/iam_app/serializers.py index 90549d333a..2a9a7ee7fd 100644 --- a/dbm-ui/backend/iam_app/serializers.py +++ b/dbm-ui/backend/iam_app/serializers.py @@ -61,3 +61,9 @@ class Meta: class CheckAllowedResSerializer(serializers.Serializer): class Meta: swagger_schema_fields = {"example": mock_data.ACTION_CHECK_ALLOWED} + + +class AssignAuthToDBASerializer(serializers.Serializer): + bk_biz_id = serializers.IntegerField(help_text=_("业务ID")) + group_name = serializers.CharField(help_text=_("组名")) + members = serializers.ListField(help_text=_("成员列表"), child=serializers.CharField()) diff --git a/dbm-ui/backend/iam_app/views/views.py b/dbm-ui/backend/iam_app/views/views.py index e5f0a7a419..8e515a19e7 100644 --- a/dbm-ui/backend/iam_app/views/views.py +++ b/dbm-ui/backend/iam_app/views/views.py @@ -15,8 +15,10 @@ from backend.bk_web import viewsets from backend.bk_web.swagger import common_swagger_auto_schema +from backend.iam_app.dataclass import assign_auth_to_dba, flush_groups_auth from backend.iam_app.handlers.permission import Permission from backend.iam_app.serializers import ( + AssignAuthToDBASerializer, CheckAllowedResSerializer, GetApplyDataResSerializer, IamActionResourceRequestSerializer, @@ -97,3 +99,23 @@ def simple_get_apply_data(self, request, *args, **kwargs): resources = client.batch_make_resource_instance(self.validated_data["resources"]) apply_data, apply_url = client.get_apply_data([self.validated_data["action_id"]], [resources]) return Response({"permission": apply_data, "apply_url": apply_url}) + + @common_swagger_auto_schema( + operation_summary=_("自动分配权限给DBA"), + request_body=AssignAuthToDBASerializer(), + tags=[SWAGGER_TAG], + ) + @action(detail=False, methods=["POST"], serializer_class=AssignAuthToDBASerializer) + def assign_auth_to_dba(self, request, *args, **kwargs): + data = self.validated_data + assign_auth_to_dba(bk_biz_id=data["bk_biz_id"], group_name=data["group_name"], members=data["members"]) + return Response(_("权限分配成功!")) + + @common_swagger_auto_schema( + operation_summary=_("存量用户组刷新权限"), + tags=[SWAGGER_TAG], + ) + @action(detail=False, methods=["POST"]) + def flush_groups_auth(self, request, *args, **kwargs): + flush_groups_auth() + return Response(_("存量用户组权限刷新成功!"))