Skip to content

Commit

Permalink
feat(backend): 权限修改支持会签模式 TencentBlueKing#6751
Browse files Browse the repository at this point in the history
  • Loading branch information
iSecloud committed Nov 7, 2024
1 parent f504bee commit 1cd2fc8
Show file tree
Hide file tree
Showing 30 changed files with 1,122 additions and 389 deletions.
3 changes: 3 additions & 0 deletions dbm-ui/backend/configuration/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class SystemSettingsEnum(str, StructuredEnum):
BKM_DBM_TOKEN = EnumField("BKM_DBM_TOKEN", _("监控数据源token"))
BKM_DBM_REPORT = EnumField("BKM_DBM_REPORT", _("mysql/redis-监控自定义上报: dataid/token"))
FREE_BK_MODULE_ID = EnumField("FREE_BK_MODULE_ID", _("业务空闲模块ID"))
VIRTUAL_USERS = EnumField("VIRTUAL_USERS", _("平台调用的虚拟账号列表"))
# 主机默认统一转移到 DBM 业务下托管,若业务 ID 属于这个列表,则转移到对应的业务下
INDEPENDENT_HOSTING_BIZS = EnumField("INDEPENDENT_HOSTING_BIZS", _("独立托管机器的业务列表"))
BF_WHITELIST_BIZS = EnumField("BF_WHITELIST_BIZS", _("BF业务白名单"))
Expand Down Expand Up @@ -211,6 +212,8 @@ class BizSettingsEnum(str, StructuredEnum):
[SystemSettingsEnum.AFFINITY, "list", [], _("环境的容灾要求")],
[SystemSettingsEnum.SYSTEM_MSG_TYPE, "list", ["weixin", "mail"], _("系统消息通知方式")],
[SystemSettingsEnum.PADDING_PROXY_CLUSTER_LIST, "list", [], _("补全proxy的集群域名列表")],
[SystemSettingsEnum.PADDING_PROXY_CLUSTER_LIST, "list", [], _("补全proxy的集群域名列表")],
[SystemSettingsEnum.VIRTUAL_USERS, "list", [], _("平台调用的虚拟账户列表")],
]

# 环境配置项 是否支持DNS解析 pulsar flow used
Expand Down
18 changes: 17 additions & 1 deletion dbm-ui/backend/db_services/dbpermission/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@ class FormatType(str, StructuredEnum):
CLUSTER = EnumField("cluster", _("cluster"))


class RuleActionType(str, StructuredEnum):
"""权限操作类型"""

AUTH = EnumField("auth", _("授权"))
CREATE = EnumField("create", _("创建"))
CHANGE = EnumField("change", _("修改"))
DELETE = EnumField("delete", _("删除"))


class AuthorizeExcelHeader(str, StructuredEnum):
"""授权excel的头部信息"""

Expand All @@ -117,6 +126,14 @@ class AuthorizeExcelHeader(str, StructuredEnum):
ERROR = EnumField("错误信息/提示信息", _("错误信息/提示信息"))


# 授权字段和授权excel头的映射
AUTHORIZE_KEY__EXCEL_FIELD_MAP = {
"user": AuthorizeExcelHeader.USER,
"access_dbs": AuthorizeExcelHeader.ACCESS_DBS,
"source_ips": AuthorizeExcelHeader.SOURCE_IPS,
"target_instances": AuthorizeExcelHeader.TARGET_INSTANCES,
}

# 授权数据过期时间
AUTHORIZE_DATA_EXPIRE_TIME = 60 * 60 * 6

Expand All @@ -126,5 +143,4 @@ class AuthorizeExcelHeader(str, StructuredEnum):
# 账号名称最大长度
MAX_ACCOUNT_LENGTH = 31


DPRIV_PARAMETER_MAP = {"account_type": "cluster_type", "rule_ids": "ids", "privilege": "privs", "access_db": "dbname"}
23 changes: 20 additions & 3 deletions dbm-ui/backend/db_services/dbpermission/db_account/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,19 @@
from backend.components.mysql_priv_manager.client import DBPrivManagerApi
from backend.core.encrypt.constants import AsymmetricCipherConfigType
from backend.core.encrypt.handlers import AsymmetricHandler
from backend.db_services.dbpermission.constants import DPRIV_PARAMETER_MAP, AccountType
from backend.db_services.dbpermission.constants import DPRIV_PARAMETER_MAP, AccountType, RuleActionType
from backend.db_services.dbpermission.db_account.dataclass import (
AccountMeta,
AccountPrivMeta,
AccountRuleMeta,
AccountUserMeta,
)
from backend.db_services.dbpermission.db_account.signals import create_account_signal
from backend.db_services.dbpermission.db_authorize.models import DBRuleActionLog
from backend.db_services.mysql.open_area.models import TendbOpenAreaConfig
from backend.db_services.mysql.permission.exceptions import DBPermissionBaseException
from backend.ticket.constants import TicketStatus, TicketType
from backend.ticket.models import Ticket
from backend.utils.excel import ExcelHandler

logger = logging.getLogger("root")
Expand Down Expand Up @@ -63,6 +66,17 @@ def _decrypt_password(password: str) -> str:

def _format_account_rules(self, account_rules_list: Dict) -> Dict:
"""格式化账号权限列表信息"""
# 查询规则变更的单据,补充规则正在运行的状态
try:
ticket_type = getattr(TicketType, f"{self.account_type.upper()}_ACCOUNT_RULE_CHANGE")
priv_tickets = Ticket.objects.filter(status=TicketStatus.RUNNING, ticket_type=ticket_type)
rule__action_map = {}
for t in priv_tickets:
rule__action_map[t.details["rule_id"]] = {"action": t.details["action"], "ticket_id": t.id}
except (AttributeError, Exception):
rule__action_map = {}

# 格式化账号规则的字段信息
for account_rules in account_rules_list["items"]:
account_rules["account"]["account_id"] = account_rules["account"].pop("id")

Expand All @@ -74,6 +88,7 @@ def _format_account_rules(self, account_rules_list: Dict) -> Dict:
rule["rule_id"] = rule.pop("id")
rule["access_db"] = rule.pop("dbname")
rule["privilege"] = rule.pop("priv")
rule["priv_ticket"] = rule__action_map.get(rule["rule_id"], {})

return account_rules_list

Expand Down Expand Up @@ -147,6 +162,7 @@ def add_account_rule(self, account_rule: AccountRuleMeta) -> Optional[Any]:
"dbname": account_rule.access_db,
}
)
# DBRuleActionLog.create_log(account_rule, self.operator, action=RuleActionType.CHANGE)
return resp

def query_account_rules(self, account_rule: AccountRuleMeta):
Expand Down Expand Up @@ -211,7 +227,6 @@ def modify_account_rule(self, account_rule: AccountRuleMeta) -> Optional[Any]:
- 修改账号规则
:param account_rule: 账号规则元信息
"""

resp = DBPrivManagerApi.modify_account_rule(
{
"bk_biz_id": self.bk_biz_id,
Expand All @@ -223,6 +238,7 @@ def modify_account_rule(self, account_rule: AccountRuleMeta) -> Optional[Any]:
"priv": account_rule.privilege,
}
)
DBRuleActionLog.create_log(account_rule, self.operator, action=RuleActionType.CHANGE)
return resp

def delete_account_rule(self, account_rule: AccountRuleMeta) -> Optional[Any]:
Expand All @@ -233,7 +249,7 @@ def delete_account_rule(self, account_rule: AccountRuleMeta) -> Optional[Any]:
# 如果账号规则与其他地方耦合,需要进行判断
config = TendbOpenAreaConfig.objects.filter(related_authorize__contains=[account_rule.rule_id])
if config.exists():
raise DBPermissionBaseException(_("当前授权规则已被开区模板{}引用,不允许删除").format(config.first().name))
raise DBPermissionBaseException(_("当前规则已被开区模板{}引用,不允许删除").format(config.first().config_name))

resp = DBPrivManagerApi.delete_account_rule(
{
Expand All @@ -243,6 +259,7 @@ def delete_account_rule(self, account_rule: AccountRuleMeta) -> Optional[Any]:
"id": [account_rule.rule_id],
}
)
DBRuleActionLog.create_log(account_rule, self.operator, action=RuleActionType.DELETE)
return resp

@classmethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,24 +190,21 @@ class RuleTypeSerializer(serializers.Serializer):
)

account_id = serializers.IntegerField(help_text=_("账号ID"))
access_db = serializers.CharField(help_text=_("访问DB"))
privilege = RuleTypeSerializer(help_text=_("授权规则"))
access_db = serializers.CharField(help_text=_("访问DB"), required=False)
privilege = RuleTypeSerializer(help_text=_("授权规则"), required=False)
account_type = serializers.ChoiceField(help_text=_("账号类型"), choices=AccountType.get_choices())

class Meta:
swagger_schema_fields = {"example": mock_data.ADD_MYSQL_ACCOUNT_RULE_REQUEST}


class ModifyMySQLAccountRuleSerializer(AddAccountRuleSerializer):
class ModifyAccountRuleSerializer(AddAccountRuleSerializer):
rule_id = serializers.IntegerField(help_text=_("规则ID"))

class Meta:
swagger_schema_fields = {"example": mock_data.MODIFY_MYSQL_ACCOUNT_RULE_REQUEST}


class DeleteAccountRuleSerializer(serializers.Serializer):
rule_id = serializers.IntegerField(help_text=_("规则ID"))
account_type = serializers.ChoiceField(help_text=_("账号类型"), choices=AccountType.get_choices())

class DeleteAccountRuleSerializer(ModifyAccountRuleSerializer):
class Meta:
swagger_schema_fields = {"example": mock_data.DELETE_MYSQL_ACCOUNT_RULE_REQUEST}
6 changes: 3 additions & 3 deletions dbm-ui/backend/db_services/dbpermission/db_account/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
FilterAccountRulesSerializer,
FilterPrivSerializer,
ListAccountRulesSerializer,
ModifyMySQLAccountRuleSerializer,
ModifyAccountRuleSerializer,
PageAccountRulesSerializer,
QueryAccountRulesSerializer,
UpdateAccountPasswordSerializer,
Expand Down Expand Up @@ -177,9 +177,9 @@ def query_account_rules(self, request, bk_biz_id):
)

@common_swagger_auto_schema(
operation_summary=_("修改账号规则"), request_body=ModifyMySQLAccountRuleSerializer(), tags=[SWAGGER_TAG]
operation_summary=_("修改账号规则"), request_body=ModifyAccountRuleSerializer(), tags=[SWAGGER_TAG]
)
@action(methods=["POST"], detail=False, serializer_class=ModifyMySQLAccountRuleSerializer)
@action(methods=["POST"], detail=False, serializer_class=ModifyAccountRuleSerializer)
def modify_account_rule(self, request, bk_biz_id):
return self._view_common_handler(
request=request,
Expand Down
49 changes: 44 additions & 5 deletions dbm-ui/backend/db_services/dbpermission/db_authorize/dataclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@
from backend.constants import IP_RE_PATTERN
from backend.db_meta.enums import ClusterEntryType
from backend.db_meta.models import ClusterEntry
from backend.db_services.dbpermission.db_authorize.models import AuthorizeRecord
from backend.db_services.dbpermission.constants import (
AUTHORIZE_KEY__EXCEL_FIELD_MAP,
EXCEL_DIVIDER,
AuthorizeExcelHeader,
)
from backend.flow.plugins.components.collections.common.base_service import BaseService
from backend.ticket.constants import FlowType
from backend.ticket.models import Flow


@dataclass
Expand Down Expand Up @@ -48,12 +55,35 @@ def from_dict(cls, init_data: Dict) -> "AuthorizeMeta":
@classmethod
def from_excel_data(cls, excel_data: Dict, cluster_type: str) -> "AuthorizeMeta":
"""从权限excel数据解析为AuthorizeMeta, 每个集群类型自己实现"""
raise NotImplementedError
return cls(
user=excel_data[AuthorizeExcelHeader.USER],
access_dbs=excel_data[AuthorizeExcelHeader.ACCESS_DBS].split(EXCEL_DIVIDER),
target_instances=excel_data[AuthorizeExcelHeader.TARGET_INSTANCES].split(EXCEL_DIVIDER),
source_ips=ExcelAuthorizeMeta.format_ip(excel_data.get(AuthorizeExcelHeader.SOURCE_IPS)),
cluster_type=cluster_type,
)

@classmethod
def serializer_record_data(cls, record_queryset: List[AuthorizeRecord]) -> List[Dict]:
def serializer_record_data(cls, ticket_id: int) -> List[Dict]:
"""将授权记录进行序列化"""
raise NotImplementedError

def __format(_data):
if isinstance(_data, (list, dict)):
return "\n".join(_data)
return _data

# 目前授权只有:授权单据和开区单据,仅一个inner flow,可以直接取first
flow = Flow.objects.filter(ticket_id=ticket_id, flow_type=FlowType.INNER_FLOW).first()
authorize_results = BaseService.get_flow_output(flow)["data"].get("authorize_results", [])
field_map = AUTHORIZE_KEY__EXCEL_FIELD_MAP

record_data_list = []
for index, info in enumerate(flow.details["ticket_data"]["rules_set"]):
data = {field_map[field]: __format(value) for field, value in info.items() if field in field_map}
data.update({AuthorizeExcelHeader.ERROR: authorize_results[index]})
record_data_list.append(data)

return record_data_list


@dataclass
Expand All @@ -73,11 +103,20 @@ def from_dict(cls, init_data: Dict) -> "ExcelAuthorizeMeta":

@classmethod
def format_ip(cls, ips: str):
if not ips:
return []
# 编译捕获ip:port的正则表达式(注意用?:取消分组)
ip_pattern = re.compile(IP_RE_PATTERN)
return ip_pattern.findall(ips)

@classmethod
def serialize_excel_data(cls, data: Dict) -> Dict:
"""将数据解析为权限excel data类的数据, 每个集群类型自己实现"""
raise NotImplementedError
excel_data = {
AuthorizeExcelHeader.USER: data["user"],
AuthorizeExcelHeader.TARGET_INSTANCES: EXCEL_DIVIDER.join(data["target_instances"]),
AuthorizeExcelHeader.ACCESS_DBS: EXCEL_DIVIDER.join([rule["dbname"] for rule in data["account_rules"]]),
AuthorizeExcelHeader.SOURCE_IPS: EXCEL_DIVIDER.join(cls.format_ip(str(data.get("source_ips")))),
AuthorizeExcelHeader.ERROR: data["message"],
}
return excel_data
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
from backend.db_meta.enums import ClusterType
from backend.db_services.dbpermission.constants import AUTHORIZE_DATA_EXPIRE_TIME, AccountType
from backend.db_services.dbpermission.db_authorize.dataclass import AuthorizeMeta, ExcelAuthorizeMeta
from backend.db_services.dbpermission.db_authorize.models import AuthorizeRecord
from backend.utils.cache import data_cache
from backend.utils.excel import ExcelHandler

Expand Down Expand Up @@ -195,8 +194,7 @@ def get_authorize_info_excel(self, excel_authorize: ExcelAuthorizeMeta) -> HttpR
# 单据走来的excel下载
if excel_authorize.ticket_id:
excel_name = "authorize_results.xlsx"
record_queryset = AuthorizeRecord.get_authorize_records_by_ticket(excel_authorize.ticket_id)
excel_data_dict__list = self.authorize_meta.serializer_record_data(record_queryset)
excel_data_dict__list = self.authorize_meta.serializer_record_data(excel_authorize.ticket_id)

# 授权ID走来的excel下载
if excel_authorize.authorize_uid:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Generated by Django 3.2.25 on 2024-09-02 03:06

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("db_authorize", "0001_initial"),
]

operations = [
migrations.CreateModel(
name="DBRuleActionLog",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("operator", models.CharField(max_length=32, verbose_name="操作人")),
("record_time", models.DateTimeField(auto_now=True, verbose_name="记录时间")),
("account_id", models.IntegerField(verbose_name="账号ID")),
("rule_id", models.IntegerField(verbose_name="权限规则ID")),
(
"action_type",
models.CharField(
choices=[("auth", "授权"), ("create", "创建"), ("change", "修改"), ("delete", "删除")],
max_length=32,
verbose_name="操作类型",
),
),
],
options={
"verbose_name": "权限变更记录",
"verbose_name_plural": "权限变更记录",
"ordering": ["-record_time"],
},
),
migrations.AddIndex(
model_name="dbruleactionlog",
index=models.Index(fields=["rule_id"], name="db_authoriz_rule_id_35a12d_idx"),
),
migrations.AddIndex(
model_name="dbruleactionlog",
index=models.Index(fields=["account_id"], name="db_authoriz_account_06c480_idx"),
),
]
46 changes: 45 additions & 1 deletion dbm-ui/backend/db_services/dbpermission/db_authorize/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,18 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _

from backend.bk_web.constants import LEN_SHORT
from backend.components import UserManagerApi
from backend.configuration.constants import SystemSettingsEnum
from backend.configuration.models import SystemSettings
from backend.db_services.dbpermission.constants import RuleActionType
from backend.db_services.dbpermission.db_account.dataclass import AccountRuleMeta
from backend.ticket.models import Ticket


class AuthorizeRecord(models.Model):
"""
授权记录
授权记录 TODO: 请不要使用此表,以后会删除
"""

ticket = models.ForeignKey(Ticket, help_text=_("关联工单"), related_name="authorize_records", on_delete=models.CASCADE)
Expand All @@ -39,3 +45,41 @@ class Meta:
@classmethod
def get_authorize_records_by_ticket(cls, ticket_id: int):
return cls.objects.filter(ticket__pk=ticket_id)


class DBRuleActionLog(models.Model):
"""
权限变更记录:包括授权、修改、删除、创建
"""

operator = models.CharField(_("操作人"), max_length=LEN_SHORT)
record_time = models.DateTimeField(_("记录时间"), auto_now=True)
account_id = models.IntegerField(_("账号ID"))
rule_id = models.IntegerField(_("权限规则ID"))
action_type = models.CharField(_("操作类型"), choices=RuleActionType.get_choices(), max_length=LEN_SHORT)

class Meta:
verbose_name = _("权限变更记录")
verbose_name_plural = _("权限变更记录")
ordering = ["-record_time"]
indexes = [
models.Index(fields=["rule_id"]),
models.Index(fields=["account_id"]),
]

@classmethod
def create_log(cls, account_rule: AccountRuleMeta, operator, action):
log = DBRuleActionLog.objects.create(
rule_id=account_rule.rule_id, account_id=account_rule.account_id, operator=operator, action_type=action
)
return log

@classmethod
def get_notifiers(cls, rule_id):
"""获取通知人列表,目前只过滤有授权记录的人"""
users = cls.objects.filter(rule_id=rule_id, action_type=RuleActionType.AUTH).values_list("operator", flat=True)
# 过滤离职的和虚拟账户
virtual_users = SystemSettings.get_setting_value(key=SystemSettingsEnum.VIRTUAL_USERS, default=[])
real_users = UserManagerApi.list_users({"fields": "username", "exact_lookups": ",".join(users)})["results"]
real_users = [user["username"] for user in real_users]
return list(set(real_users) - set(virtual_users))
Loading

0 comments on commit 1cd2fc8

Please sign in to comment.