Files
iAOP/core/template-console/rbac.py
T

311 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""⑤.7 配置台三级 RBAC 权限模型 —— issue #62 / PRD ⑤.7。
配置台面向**多角色协作**:实施工程师配模板,行业工程师调参数,运维/管理者
发布上线。直接对所有人开放写权限会带来误改与不可溯源风险。本模块用三级
RBAC(基于角色的访问控制)锁定"谁能对哪类配置做什么",并把每次权限判定
的**理由**一并返回,对齐 PRD「可解释可溯源」。
三级角色(由低到高,后者继承前者全部权限):
- ``readonly`` (只读):查看配置 / 预览 / 历史版本,不可写;
- ``engineer`` (行业工程师):只读权限 + 编辑/校验/导入配置(模型超参 /
RAG / 布局 / 点位字典),但**不能发布与回滚**;
- ``admin`` (管理员):工程师权限 + 发布 / 回滚 / 推送内核 / 用户管理。
权限判定核心为 ``has_permission(user, resource, action)``,返回
``PermissionDecision``(allow + reason),便于配置台前端把"为什么拒绝"
直接展示给操作者,而不是一个干瘪的 403。
零运行时依赖:仅用 dataclass / Enum / 标准库。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, List, Optional, Set
# ---------------------------------------------------------------------------
# 权限维度:资源 × 动作
# ---------------------------------------------------------------------------
class Resource(str, Enum):
"""配置台可管控的资源(对齐 #63~#67 子模块)。"""
POINT_DICT = "point_dict" # 点位字典(#63)
MODEL_PARAM = "model_param" # 模型超参配置(#64)
RAG_CONFIG = "rag_config" # RAG 知识库配置(#64)
LAYOUT = "layout" # 驾驶舱布局配置(#64/#65)
PREVIEW = "preview" # 预览(#65)
RELEASE = "release" # 版本发布/回滚(#66)
PUSH = "push" # 配置推送内核(#67)
USER = "user" # 用户/角色管理
class Action(str, Enum):
"""对资源可执行的动作。"""
VIEW = "view" # 查看 / 预览 / 列表
EDIT = "edit" # 新增 / 修改 / 删除 / 导入 / 校验
PUBLISH = "publish" # 发布版本 / 回滚 / 推送内核
MANAGE = "manage" # 用户与角色管理
class RoleKind(str, Enum):
"""三级角色枚举(值即配置资产中的角色标识)。"""
READONLY = "readonly"
ENGINEER = "engineer"
ADMIN = "admin"
# 各资源的「写」动作等价集合:EDIT 含新增/修改/删除/导入/校验。
# PUBLISH 含发布/回滚/推送。这样配置台前端只需关心粗粒度动作。
_WRITE_ACTIONS: Set[Action] = {Action.EDIT, Action.PUBLISH, Action.MANAGE}
# ---------------------------------------------------------------------------
# 权限模型
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class Permission:
"""一条权限授予(角色 → 资源 → 动作)。
``meaning`` 解释该权限的业务含义,用于审计日志与配置台权限矩阵展示。
注意:权限**匹配**基于 ``resource:action``(资源×动作),与授予角色无关——
这正是角色继承能生效的关键(admin 继承 engineer 的 edit,匹配键相同)。
``role`` 仅作为审计元数据,记录"是谁授予的"。
"""
role: RoleKind
resource: Resource
action: Action
meaning: str = ""
def key(self) -> str:
"""权限匹配键(资源:动作)—— 角色继承据此累计。"""
return f"{self.resource.value}:{self.action.value}"
def audit_key(self) -> str:
"""审计唯一键(角色/资源/动作三元组,含授予者)。"""
return f"{self.role.value}:{self.resource.value}:{self.action.value}"
@dataclass
class Role:
"""一个角色:权限集合 + 继承的父角色。"""
kind: RoleKind
label: str # 中文展示名
permissions: List[Permission] = field(default_factory=list)
inherits: Optional[RoleKind] = None # 继承的低一级角色
description: str = "" # 角色职责说明(可解释性)
def permission_keys(self) -> Set[str]:
"""本角色直接授予的权限键集合。"""
return {p.key() for p in self.permissions}
@dataclass
class User:
"""配置台用户。"""
username: str
role: RoleKind
display_name: str = ""
# 可选资源级收窄:即便角色允许,列表中的资源也会被额外限制为只读。
# 用于"只允许工程师改某几类配置"的细粒度场景。
restricted_to_readonly: List[Resource] = field(default_factory=list)
@dataclass
class PermissionDecision:
"""``has_permission`` 的判定结果(带理由,可解释)。"""
allow: bool
reason: str # 人类可读的判定理由(允许/拒绝原因)
role: RoleKind
resource: Resource
action: Action
source: str = "explicit" # explicit(本角色直接授予)/ inherited(继承自父角色)
# ---------------------------------------------------------------------------
# 角色注册表:三级权限矩阵(对齐 PRD ⑤.7「三级 RBAC」)
# ---------------------------------------------------------------------------
def _build_role_registry() -> Dict[RoleKind, Role]:
"""构建三级角色及其权限矩阵。
权限设计依据(PRD ⑤.7):
- readonly:可查看所有配置/预览/历史,但不能改、不能发;
- engineer:在 readonly 基础上,可编辑/校验/导入四类业务配置,
但**发布/回滚/推送/用户管理仍归 admin**(避免未经评审上线);
- admin:在 engineer 基础上,可发布/回滚/推送 + 管理用户角色。
"""
ro = Role(
kind=RoleKind.READONLY,
label="只读",
description="实施/运维只读角色:查看配置、预览、历史版本,不可写。",
permissions=[
Permission(RoleKind.READONLY, Resource.POINT_DICT, Action.VIEW,
"查看点位字典与校验报告"),
Permission(RoleKind.READONLY, Resource.MODEL_PARAM, Action.VIEW,
"查看模型超参配置"),
Permission(RoleKind.READONLY, Resource.RAG_CONFIG, Action.VIEW,
"查看 RAG 知识库配置"),
Permission(RoleKind.READONLY, Resource.LAYOUT, Action.VIEW,
"查看驾驶舱布局配置"),
Permission(RoleKind.READONLY, Resource.PREVIEW, Action.VIEW,
"查看配置预览"),
Permission(RoleKind.READONLY, Resource.RELEASE, Action.VIEW,
"查看历史发布版本"),
],
)
engineer = Role(
kind=RoleKind.ENGINEER,
label="行业工程师",
inherits=RoleKind.READONLY,
description="行业工程师:编辑/校验/导入业务配置,但不能发布与推送。",
permissions=[
Permission(RoleKind.ENGINEER, Resource.POINT_DICT, Action.EDIT,
"导入/编辑/校验点位字典 CSV"),
Permission(RoleKind.ENGINEER, Resource.MODEL_PARAM, Action.EDIT,
"调整模型超参配置"),
Permission(RoleKind.ENGINEER, Resource.RAG_CONFIG, Action.EDIT,
"编辑 RAG 知识库配置"),
Permission(RoleKind.ENGINEER, Resource.LAYOUT, Action.EDIT,
"编辑驾驶舱布局配置"),
Permission(RoleKind.ENGINEER, Resource.PREVIEW, Action.VIEW,
"预览配置效果(编辑后必看)"),
],
)
admin = Role(
kind=RoleKind.ADMIN,
label="管理员",
inherits=RoleKind.ENGINEER,
description="管理员:在工程师基础上负责发布/回滚/推送与用户管理。",
permissions=[
Permission(RoleKind.ADMIN, Resource.RELEASE, Action.PUBLISH,
"发布新版本与回滚到历史版本"),
Permission(RoleKind.ADMIN, Resource.PUSH, Action.PUBLISH,
"把已发布配置推送给内核"),
Permission(RoleKind.ADMIN, Resource.USER, Action.MANAGE,
"管理用户与角色分配"),
Permission(RoleKind.ADMIN, Resource.POINT_DICT, Action.PUBLISH,
"确认点位字典上线(审批环节)"),
Permission(RoleKind.ADMIN, Resource.MODEL_PARAM, Action.PUBLISH,
"确认模型超参上线"),
Permission(RoleKind.ADMIN, Resource.LAYOUT, Action.PUBLISH,
"确认布局上线"),
],
)
return {RoleKind.READONLY: ro, RoleKind.ENGINEER: engineer, RoleKind.ADMIN: admin}
_ROLES: Dict[RoleKind, Role] = _build_role_registry()
def get_role(kind: RoleKind) -> Role:
"""获取角色定义。"""
return _ROLES[kind]
def all_roles() -> List[Role]:
"""全部角色(按权限由低到高)。"""
return [_ROLES[RoleKind.READONLY], _ROLES[RoleKind.ENGINEER], _ROLES[RoleKind.ADMIN]]
def effective_permissions(kind: RoleKind) -> Set[str]:
"""角色有效权限键(含继承链)。
继承解析:admin 继承 engineer 继承 readonly,递归向上累计权限键。
"""
role = _ROLES[kind]
keys: Set[str] = set(role.permission_keys())
if role.inherits is not None:
keys |= effective_permissions(role.inherits)
return keys
# ---------------------------------------------------------------------------
# 判定 API
# ---------------------------------------------------------------------------
def has_permission(
user: User,
resource: Resource,
action: Action,
) -> PermissionDecision:
"""判定用户对某资源执行某动作是否被允许(带理由)。
判定顺序:
1. 计算角色有效权限(含继承),命中即允许并标注来源(本角色/继承);
2. 命中后若该资源在用户 ``restricted_to_readonly`` 列表且动作是写动作,
则降级拒绝(细粒度收窄);
3. 未命中则拒绝,理由标注缺失的权限三元组。
Args:
user: 配置台用户;
resource: 目标资源;
action: 目标动作。
Returns:
PermissionDecision:allow + reason(可直接展示给操作者)。
"""
target = f"{resource.value}:{action.value}"
eff = effective_permissions(user.role)
# 细粒度收窄:即便角色允许,特定资源也被限制为只读
if resource in user.restricted_to_readonly and action in _WRITE_ACTIONS:
return PermissionDecision(
allow=False,
reason=(f"用户 '{user.username}' 对资源 '{resource.value}' 被收窄为只读,"
f"禁止执行 '{action.value}' 动作"),
role=user.role, resource=resource, action=action, source="restricted",
)
if target in eff:
# 判定来源:本角色直接授予 or 继承自父角色
own = get_role(user.role).permission_keys()
source = "explicit" if target in own else "inherited"
src_label = "本角色直接授予" if source == "explicit" else "继承自低级角色"
return PermissionDecision(
allow=True,
reason=(f"用户 '{user.username}'({get_role(user.role).label})"
f"允许对 '{resource.value}' 执行 '{action.value}'({src_label})"),
role=user.role, resource=resource, action=action, source=source,
)
return PermissionDecision(
allow=False,
reason=(f"用户 '{user.username}'({get_role(user.role).label})缺少权限 "
f"{user.role.value}:{resource.value}:{action.value};"
f"该动作需更高角色或审批"),
role=user.role, resource=resource, action=action, source="denied",
)
def can_publish(user: User) -> bool:
"""便捷判定:用户是否具备发布(发布/回滚/推送)能力。"""
return has_permission(user, Resource.RELEASE, Action.PUBLISH).allow
def user_summary(user: User) -> Dict[str, object]:
"""用户权限概览(供配置台用户卡片/审计日志展示)。"""
role = get_role(user.role)
return {
"username": user.username,
"display_name": user.display_name or user.username,
"role": user.role.value,
"role_label": role.label,
"description": role.description,
"effective_permission_count": len(effective_permissions(user.role)),
"restricted_to_readonly": [r.value for r in user.restricted_to_readonly],
}