Files

710 lines
24 KiB
Python

from typing import Optional, List, Dict, Any, Tuple
from sqlalchemy import select, delete as sa_delete
from sqlalchemy.ext.asyncio import AsyncSession
from zq_smart_table.permission.model import (
SmartTableRole, SmartTableCollaborator, SmartTableFieldPerm, SmartTableRowRule,
SYSTEM_CAPABILITIES, ROLE_PRIORITY,
)
from zq_smart_table.model import SmartTable
from utils.context import get_current_user_info_from_context
SUPERADMIN_CAPABILITIES = {k: True for k in SYSTEM_CAPABILITIES["owner"]}
class PermissionService:
"""多维表格权限判定服务"""
# ─── 系统预置角色 ───
@classmethod
async def ensure_system_roles(cls, db: AsyncSession, table_id: str) -> Dict[str, str]:
"""确保表拥有 4 个系统预置角色,返回 {role_type: role_id}"""
result = await db.execute(
select(SmartTableRole).where(
SmartTableRole.table_id == table_id,
SmartTableRole.is_system == True, # noqa: E712
SmartTableRole.is_deleted == False, # noqa: E712
)
)
existing = {r.role_type: r for r in result.scalars().all()}
role_map: Dict[str, str] = {}
names = {"owner": "所有者", "manager": "管理者", "editor": "编辑者", "viewer": "只读者"}
for rt, caps in SYSTEM_CAPABILITIES.items():
if rt in existing:
role_map[rt] = existing[rt].id
else:
role = SmartTableRole(
table_id=table_id,
name=names[rt],
role_type=rt,
capabilities=caps,
is_system=True,
)
db.add(role)
await db.flush()
role_map[rt] = role.id
await db.commit()
return role_map
# ─── 初始化所有者 ───
@classmethod
async def init_table_owner(cls, db: AsyncSession, table_id: str, user_id: str) -> None:
"""创建表时自动将创建者设为所有者"""
role_map = await cls.ensure_system_roles(db, table_id)
owner_role_id = role_map["owner"]
existing = await db.execute(
select(SmartTableCollaborator).where(
SmartTableCollaborator.table_id == table_id,
SmartTableCollaborator.subject_type == "user",
SmartTableCollaborator.subject_id == user_id,
SmartTableCollaborator.is_deleted == False, # noqa: E712
)
)
if existing.scalar_one_or_none():
return
collab = SmartTableCollaborator(
table_id=table_id,
subject_type="user",
subject_id=user_id,
role_id=owner_role_id,
)
db.add(collab)
await db.commit()
# ─── 获取有效角色 ───
@classmethod
async def get_effective_role(
cls, db: AsyncSession, table_id: str,
user_id: str, dept_id: Optional[str] = None,
role_ids: Optional[List[str]] = None,
is_superuser: bool = False,
) -> Optional[Dict[str, Any]]:
"""
获取用户对某表的有效角色。
优先级: superadmin > user直接 > dept > system_role
多个匹配时取权限最高的角色。
"""
if is_superuser:
return {
"role_type": "superadmin",
"role_name": "超级管理员",
"capabilities": SUPERADMIN_CAPABILITIES,
"role_id": None,
}
# 查同表创建者(兜底:如果 collaborator 记录缺失但用户是表的创建者)
table = await db.execute(
select(SmartTable).where(SmartTable.id == table_id, SmartTable.is_deleted == False) # noqa: E712
)
table_obj = table.scalar_one_or_none()
if table_obj and table_obj.sys_creator_id == user_id:
role_map = await cls.ensure_system_roles(db, table_id)
owner_role_id = role_map["owner"]
return {
"role_type": "owner",
"role_name": "所有者",
"capabilities": dict(SYSTEM_CAPABILITIES["owner"]),
"role_id": owner_role_id,
}
# 查询所有匹配的协作者记录
conditions = [
SmartTableCollaborator.table_id == table_id,
SmartTableCollaborator.is_deleted == False, # noqa: E712
]
subject_conditions = [
(SmartTableCollaborator.subject_type == "user") & (SmartTableCollaborator.subject_id == user_id)
]
if dept_id:
subject_conditions.append(
(SmartTableCollaborator.subject_type == "dept") & (SmartTableCollaborator.subject_id == dept_id)
)
if role_ids:
for rid in role_ids:
subject_conditions.append(
(SmartTableCollaborator.subject_type == "role") & (SmartTableCollaborator.subject_id == rid)
)
from sqlalchemy import or_
conditions.append(or_(*subject_conditions))
result = await db.execute(
select(SmartTableCollaborator).where(*conditions)
)
collabs = list(result.scalars().all())
if not collabs:
return None
# 加载对应角色,取优先级最高的
best_collab = None
best_role = None
best_priority = -1
for c in collabs:
role_result = await db.execute(
select(SmartTableRole).where(
SmartTableRole.id == c.role_id,
SmartTableRole.is_deleted == False, # noqa: E712
)
)
role = role_result.scalar_one_or_none()
if not role:
continue
# user 直接授权优先级额外 +10
priority = ROLE_PRIORITY.get(role.role_type, 50)
if c.subject_type == "user":
priority += 10
if priority > best_priority:
best_priority = priority
best_collab = c
best_role = role
if not best_role:
return None
return {
"role_type": best_role.role_type,
"role_name": best_role.name,
"capabilities": dict(best_role.capabilities) if best_role.capabilities else {},
"role_id": best_role.id,
}
# ─── 能力检查 ───
@classmethod
async def check_capability(
cls, db: AsyncSession, table_id: str, capability: str,
) -> bool:
"""检查当前用户是否具有某项能力"""
user_info = get_current_user_info_from_context()
if not user_info:
return False
role_info = await cls.get_effective_role(
db, table_id,
user_id=user_info.get("user_id", ""),
dept_id=user_info.get("dept_id"),
role_ids=user_info.get("role_ids", []),
is_superuser=user_info.get("is_superuser", False),
)
if not role_info:
return False
return role_info["capabilities"].get(capability, False)
@classmethod
async def require_capability(
cls, db: AsyncSession, table_id: str, capability: str,
) -> Dict[str, Any]:
"""检查能力,无权限则抛 403"""
from fastapi import HTTPException
user_info = get_current_user_info_from_context()
if not user_info:
raise HTTPException(status_code=401, detail="未登录")
role_info = await cls.get_effective_role(
db, table_id,
user_id=user_info.get("user_id", ""),
dept_id=user_info.get("dept_id"),
role_ids=user_info.get("role_ids", []),
is_superuser=user_info.get("is_superuser", False),
)
if not role_info:
raise HTTPException(status_code=403, detail="无权访问此表")
if not role_info["capabilities"].get(capability, False):
raise HTTPException(status_code=403, detail=f"无权执行此操作({capability})")
return role_info
@classmethod
async def require_table_access(
cls, db: AsyncSession, table_id: str,
) -> Dict[str, Any]:
"""检查表访问权限(至少 viewer),无权限则抛 403"""
from fastapi import HTTPException
user_info = get_current_user_info_from_context()
if not user_info:
raise HTTPException(status_code=401, detail="未登录")
role_info = await cls.get_effective_role(
db, table_id,
user_id=user_info.get("user_id", ""),
dept_id=user_info.get("dept_id"),
role_ids=user_info.get("role_ids", []),
is_superuser=user_info.get("is_superuser", False),
)
if not role_info:
raise HTTPException(status_code=403, detail="无权访问此表")
return role_info
# ─── 列权限 ───
@classmethod
async def get_field_permissions(
cls, db: AsyncSession, table_id: str, role_id: str,
) -> Dict[str, str]:
"""获取某角色的列权限映射: {field_id: access}"""
result = await db.execute(
select(SmartTableFieldPerm).where(
SmartTableFieldPerm.table_id == table_id,
SmartTableFieldPerm.role_id == role_id,
SmartTableFieldPerm.is_deleted == False, # noqa: E712
)
)
return {fp.field_id: fp.access for fp in result.scalars().all()}
@classmethod
async def get_accessible_field_ids(
cls, db: AsyncSession, table_id: str, role_id: Optional[str],
mode: str = "read",
) -> Optional[List[str]]:
"""
获取角色可访问的字段ID列表。
mode="read" 返回 access != "hidden" 的字段
mode="write" 返回 access == "write" 的字段
如果没有配置列权限,返回 None 表示不限制。
"""
if not role_id:
return None
perms = await cls.get_field_permissions(db, table_id, role_id)
if not perms:
return None
if mode == "write":
return [fid for fid, acc in perms.items() if acc == "write"]
else:
return [fid for fid, acc in perms.items() if acc != "hidden"]
@classmethod
async def batch_set_field_permissions(
cls, db: AsyncSession, table_id: str, role_id: str,
permissions: List[Dict[str, str]],
) -> None:
"""批量设置列权限"""
await db.execute(
sa_delete(SmartTableFieldPerm).where(
SmartTableFieldPerm.table_id == table_id,
SmartTableFieldPerm.role_id == role_id,
)
)
for p in permissions:
fp = SmartTableFieldPerm(
table_id=table_id,
role_id=role_id,
field_id=p["field_id"],
access=p.get("access", "write"),
)
db.add(fp)
await db.commit()
# ─── 行权限 ───
@classmethod
async def get_row_rules(
cls, db: AsyncSession, table_id: str, role_id: str,
) -> List[SmartTableRowRule]:
result = await db.execute(
select(SmartTableRowRule).where(
SmartTableRowRule.table_id == table_id,
SmartTableRowRule.role_id == role_id,
SmartTableRowRule.is_deleted == False, # noqa: E712
)
)
return list(result.scalars().all())
@classmethod
async def upsert_row_rule(
cls, db: AsyncSession, table_id: str, role_id: str,
rule_type: str, mode: str, conditions: List[Dict[str, Any]],
) -> SmartTableRowRule:
"""创建或更新行权限规则"""
result = await db.execute(
select(SmartTableRowRule).where(
SmartTableRowRule.table_id == table_id,
SmartTableRowRule.role_id == role_id,
SmartTableRowRule.rule_type == rule_type,
SmartTableRowRule.is_deleted == False, # noqa: E712
)
)
rule = result.scalar_one_or_none()
if rule:
rule.mode = mode
rule.conditions = conditions
else:
rule = SmartTableRowRule(
table_id=table_id,
role_id=role_id,
rule_type=rule_type,
mode=mode,
conditions=conditions,
)
db.add(rule)
await db.commit()
await db.refresh(rule)
return rule
# ─── 协作者管理 ───
@classmethod
async def get_collaborators(
cls, db: AsyncSession, table_id: str,
) -> List[SmartTableCollaborator]:
result = await db.execute(
select(SmartTableCollaborator).where(
SmartTableCollaborator.table_id == table_id,
SmartTableCollaborator.is_deleted == False, # noqa: E712
).order_by(SmartTableCollaborator.sys_create_datetime)
)
return list(result.scalars().all())
@classmethod
async def add_collaborator(
cls, db: AsyncSession, table_id: str,
subject_type: str, subject_id: str, role_id: str,
) -> SmartTableCollaborator:
existing = await db.execute(
select(SmartTableCollaborator).where(
SmartTableCollaborator.table_id == table_id,
SmartTableCollaborator.subject_type == subject_type,
SmartTableCollaborator.subject_id == subject_id,
SmartTableCollaborator.is_deleted == False, # noqa: E712
)
)
collab = existing.scalar_one_or_none()
if collab:
collab.role_id = role_id
else:
collab = SmartTableCollaborator(
table_id=table_id,
subject_type=subject_type,
subject_id=subject_id,
role_id=role_id,
)
db.add(collab)
await db.commit()
await db.refresh(collab)
return collab
@classmethod
async def update_collaborator_role(
cls, db: AsyncSession, collab_id: str, role_id: str,
) -> Optional[SmartTableCollaborator]:
result = await db.execute(
select(SmartTableCollaborator).where(
SmartTableCollaborator.id == collab_id,
SmartTableCollaborator.is_deleted == False, # noqa: E712
)
)
collab = result.scalar_one_or_none()
if not collab:
return None
collab.role_id = role_id
await db.commit()
await db.refresh(collab)
return collab
@classmethod
async def remove_collaborator(
cls, db: AsyncSession, collab_id: str,
) -> bool:
result = await db.execute(
select(SmartTableCollaborator).where(
SmartTableCollaborator.id == collab_id,
SmartTableCollaborator.is_deleted == False, # noqa: E712
)
)
collab = result.scalar_one_or_none()
if not collab:
return False
collab.is_deleted = True
await db.commit()
return True
# ─── 角色管理 ───
@classmethod
async def get_roles(
cls, db: AsyncSession, table_id: str,
) -> List[SmartTableRole]:
result = await db.execute(
select(SmartTableRole).where(
SmartTableRole.table_id == table_id,
SmartTableRole.is_deleted == False, # noqa: E712
).order_by(SmartTableRole.sys_create_datetime)
)
return list(result.scalars().all())
@classmethod
async def create_custom_role(
cls, db: AsyncSession, table_id: str,
name: str, capabilities: Dict[str, bool],
) -> SmartTableRole:
role = SmartTableRole(
table_id=table_id,
name=name,
role_type="custom",
capabilities=capabilities,
is_system=False,
)
db.add(role)
await db.commit()
await db.refresh(role)
return role
@classmethod
async def update_role(
cls, db: AsyncSession, role_id: str,
name: Optional[str] = None, capabilities: Optional[Dict[str, bool]] = None,
) -> Optional[SmartTableRole]:
result = await db.execute(
select(SmartTableRole).where(
SmartTableRole.id == role_id,
SmartTableRole.is_deleted == False, # noqa: E712
)
)
role = result.scalar_one_or_none()
if not role:
return None
if name is not None:
role.name = name
if capabilities is not None:
role.capabilities = capabilities
await db.commit()
await db.refresh(role)
return role
@classmethod
async def delete_role(
cls, db: AsyncSession, role_id: str,
) -> bool:
result = await db.execute(
select(SmartTableRole).where(
SmartTableRole.id == role_id,
SmartTableRole.is_system == False, # noqa: E712
SmartTableRole.is_deleted == False, # noqa: E712
)
)
role = result.scalar_one_or_none()
if not role:
return False
role.is_deleted = True
await db.commit()
return True
# ─── 获取 my-permission ───
@classmethod
async def get_my_permission(
cls, db: AsyncSession, table_id: str,
) -> Dict[str, Any]:
"""获取当前用户对某表的完整权限信息"""
user_info = get_current_user_info_from_context()
if not user_info:
return {
"role_type": "none",
"role_name": "无权限",
"capabilities": {},
"field_permissions": {},
"row_view_mode": "none",
"row_edit_mode": "none",
}
role_info = await cls.get_effective_role(
db, table_id,
user_id=user_info.get("user_id", ""),
dept_id=user_info.get("dept_id"),
role_ids=user_info.get("role_ids", []),
is_superuser=user_info.get("is_superuser", False),
)
if not role_info:
return {
"role_type": "none",
"role_name": "无权限",
"capabilities": {},
"field_permissions": {},
"row_view_mode": "none",
"row_edit_mode": "none",
}
role_id = role_info.get("role_id")
field_perms: Dict[str, str] = {}
row_view_mode = "all"
row_edit_mode = "all"
if role_id:
field_perms = await cls.get_field_permissions(db, table_id, role_id)
row_rules = await cls.get_row_rules(db, table_id, role_id)
for rr in row_rules:
if rr.rule_type == "view":
row_view_mode = rr.mode
elif rr.rule_type == "edit":
row_edit_mode = rr.mode
out: Dict[str, Any] = {
"role_type": role_info["role_type"],
"role_name": role_info["role_name"],
"capabilities": role_info["capabilities"],
"field_permissions": field_perms,
"row_view_mode": row_view_mode,
"row_edit_mode": row_edit_mode,
}
# 文档无列/行数据维度,列权限与行规则不对文档内容生效;归一化返回值以免与多维表格混淆
tbl_row = await db.execute(
select(SmartTable).where(SmartTable.id == table_id, SmartTable.is_deleted == False) # noqa: E712
)
tbl_obj = tbl_row.scalar_one_or_none()
if tbl_obj and getattr(tbl_obj, "type", "table") == "document":
out["field_permissions"] = {}
out["row_view_mode"] = "all"
out["row_edit_mode"] = "all"
return out
@classmethod
async def require_multidimensional_table(cls, db: AsyncSession, table_id: str) -> None:
"""列权限、行权限仅适用于多维表格(type=table),文档页应使用协作者角色与能力位。"""
from fastapi import HTTPException
result = await db.execute(
select(SmartTable).where(SmartTable.id == table_id, SmartTable.is_deleted == False) # noqa: E712
)
tbl = result.scalar_one_or_none()
if not tbl:
raise HTTPException(status_code=404, detail="表不存在")
if getattr(tbl, "type", "table") == "document":
raise HTTPException(
status_code=400,
detail="文档不支持列权限与行权限配置,请通过协作者角色控制访问",
)
# ─── 行权限过滤 ───
@classmethod
async def build_row_filter_conditions(
cls, db: AsyncSession, table_id: str, role_id: Optional[str],
user_id: str, rule_type: str = "view",
) -> list:
"""
根据行权限规则构建 SQLAlchemy 过滤条件列表。
rule_type: "view""edit"
返回空列表表示不限制。
"""
from zq_smart_table.model import SmartRecord
from app.db_compat import json_extract
if not role_id:
return []
row_rules = await cls.get_row_rules(db, table_id, role_id)
rule = next((r for r in row_rules if r.rule_type == rule_type), None)
if not rule or rule.mode == "all":
return []
if rule.mode == "creator_only":
return [SmartRecord.sys_creator_id == user_id]
if rule.mode == "conditions" and rule.conditions:
from sqlalchemy import or_, and_
conds = []
for c in rule.conditions:
fid = c.get("field_id")
op = c.get("operator", "equals")
val = c.get("value")
if not fid:
continue
col = json_extract(SmartRecord.values, fid)
if op == "equals":
conds.append(col == str(val) if val is not None else col == None) # noqa: E711
elif op == "contains":
conds.append(col.ilike(f"%{val}%") if val else col == col)
elif op == "isEmpty":
conds.append(or_(col == None, col == "")) # noqa: E711
elif op == "isNotEmpty":
conds.append(and_(col != None, col != "")) # noqa: E711
elif op == "greaterThan":
from sqlalchemy import cast, String
conds.append(cast(col, String) > str(val))
elif op == "lessThan":
from sqlalchemy import cast, String
conds.append(cast(col, String) < str(val))
return conds
return []
@classmethod
async def check_row_edit_permission(
cls, db: AsyncSession, table_id: str, record, role_id: Optional[str], user_id: str,
) -> bool:
"""检查当前用户是否有权编辑指定记录(基于行编辑权限规则)"""
if not role_id:
return True
row_rules = await cls.get_row_rules(db, table_id, role_id)
rule = next((r for r in row_rules if r.rule_type == "edit"), None)
if not rule or rule.mode == "all":
return True
if rule.mode == "creator_only":
return getattr(record, "sys_creator_id", None) == user_id
if rule.mode == "conditions" and rule.conditions:
from app.db_compat import json_extract
values = record.values or {}
for c in rule.conditions:
fid = c.get("field_id")
op = c.get("operator", "equals")
val = c.get("value")
if not fid:
continue
cell_val = str(values.get(fid, ""))
if op == "equals" and cell_val != str(val):
return False
if op == "contains" and str(val or "") not in cell_val:
return False
if op == "isEmpty" and cell_val != "":
return False
if op == "isNotEmpty" and cell_val == "":
return False
return True
return True
# ─── 过滤记录值(去掉不可见字段) ───
@classmethod
def filter_record_values(
cls, values: Dict[str, Any], accessible_fields: Optional[List[str]],
) -> Dict[str, Any]:
"""过滤记录中不可见的字段值"""
if accessible_fields is None:
return values
return {k: v for k, v in values.items() if k in accessible_fields}