549 lines
20 KiB
Python
549 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
hr-system 权限与数据范围底座(F11 / T08)
|
||
|
||
职责:
|
||
1. 角色管理:sys 角色 CRUD、权限点分配(复用 rbac 基座做路径级功能权限)
|
||
2. 管理员管理:创建管理员、绑定角色
|
||
3. sys_data_scope 数据范围双维度配置(组织维度 org + 花名册字段维度 field)
|
||
4. get_data_scope(user_id):查询叠加机制,注册为公共横切能力,
|
||
供 hr-roster / hr-report / hr-flow 导出等模块调用
|
||
5. 内置 admin + 四角色(员工自助/经理/HR管理员/系统管理员)权限初始化
|
||
|
||
依赖:apppublic、sqlor、ahserver、appbase、rbac(基础模块,直接引用)
|
||
端口:9280(见 conf/config.json)
|
||
"""
|
||
|
||
import json
|
||
import time
|
||
import uuid
|
||
|
||
from apppublic import log
|
||
from sqlor import Db
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 常量
|
||
# ---------------------------------------------------------------------------
|
||
|
||
SCOPE_TYPE_ORG = "org" # 组织维度
|
||
SCOPE_TYPE_FIELD = "field" # 花名册字段维度
|
||
|
||
# 内置角色编码(与 init/data.json 保持一致)
|
||
ROLE_EMPLOYEE = "employee"
|
||
ROLE_MANAGER = "manager"
|
||
ROLE_HR_ADMIN = "hr_admin"
|
||
ROLE_SYS_ADMIN = "sys_admin"
|
||
ROLE_ADMIN = "admin"
|
||
|
||
BUILTIN_ROLES = (ROLE_EMPLOYEE, ROLE_MANAGER, ROLE_HR_ADMIN, ROLE_SYS_ADMIN)
|
||
|
||
_db = None
|
||
|
||
|
||
def _get_db():
|
||
"""获取 hrs 库连接(依赖 appbase 加载 conf/config.json 的 databases.hrs)。"""
|
||
global _db
|
||
if _db is None:
|
||
_db = Db("hrs")
|
||
return _db
|
||
|
||
|
||
def _now():
|
||
return time.strftime("%Y-%m-%d %H:%M:%S")
|
||
|
||
|
||
def _uuid():
|
||
return uuid.uuid4().hex
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 1. 角色管理(复用 rbac 基座 sys_role / sys_role_permission)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def role_list(req):
|
||
"""角色列表(含已分配权限点数量)。"""
|
||
db = _get_db()
|
||
rows = db.query(
|
||
"SELECT r.id, r.role_code, r.role_name, r.remark, r.created_at, "
|
||
" (SELECT COUNT(*) FROM sys_role_permission rp WHERE rp.role_id = r.id) AS perm_count "
|
||
"FROM sys_role r ORDER BY r.created_at"
|
||
)
|
||
return {"code": 0, "data": rows}
|
||
|
||
|
||
def role_detail(req):
|
||
"""角色详情 + 已分配权限点 id 列表。"""
|
||
role_id = req.get("id") or req.get("role_id")
|
||
if not role_id:
|
||
return {"code": 1, "msg": "缺少角色 id"}
|
||
db = _get_db()
|
||
role = db.query_one("SELECT * FROM sys_role WHERE id = %s", (role_id,))
|
||
if not role:
|
||
return {"code": 1, "msg": "角色不存在"}
|
||
perms = db.query(
|
||
"SELECT perm_id FROM sys_role_permission WHERE role_id = %s", (role_id,)
|
||
)
|
||
role["perm_ids"] = [p["perm_id"] for p in perms]
|
||
return {"code": 0, "data": role}
|
||
|
||
|
||
def role_create(req):
|
||
"""创建角色(sys 角色)。"""
|
||
db = _get_db()
|
||
role_code = (req.get("role_code") or "").strip()
|
||
role_name = (req.get("role_name") or "").strip()
|
||
if not role_code or not role_name:
|
||
return {"code": 1, "msg": "角色编码与名称必填"}
|
||
exists = db.query_one("SELECT id FROM sys_role WHERE role_code = %s", (role_code,))
|
||
if exists:
|
||
return {"code": 1, "msg": "角色编码已存在"}
|
||
rid = _uuid()
|
||
db.execute(
|
||
"INSERT INTO sys_role (id, role_code, role_name, remark, created_by, created_at, updated_at) "
|
||
"VALUES (%s, %s, %s, %s, %s, %s, %s)",
|
||
(rid, role_code, role_name, req.get("remark", ""), req.get("operator_id", ""),
|
||
_now(), _now()),
|
||
)
|
||
return {"code": 0, "data": {"id": rid}}
|
||
|
||
|
||
def role_update(req):
|
||
"""更新角色基础信息。"""
|
||
role_id = req.get("id") or req.get("role_id")
|
||
if not role_id:
|
||
return {"code": 1, "msg": "缺少角色 id"}
|
||
db = _get_db()
|
||
db.execute(
|
||
"UPDATE sys_role SET role_name = %s, remark = %s, updated_by = %s, updated_at = %s "
|
||
"WHERE id = %s",
|
||
(req.get("role_name", ""), req.get("remark", ""),
|
||
req.get("operator_id", ""), _now(), role_id),
|
||
)
|
||
return {"code": 0}
|
||
|
||
|
||
def role_delete(req):
|
||
"""删除角色(内置四角色 + admin 禁止删除)。"""
|
||
role_id = req.get("id") or req.get("role_id")
|
||
if not role_id:
|
||
return {"code": 1, "msg": "缺少角色 id"}
|
||
db = _get_db()
|
||
role = db.query_one("SELECT role_code FROM sys_role WHERE id = %s", (role_id,))
|
||
if not role:
|
||
return {"code": 1, "msg": "角色不存在"}
|
||
if role["role_code"] in BUILTIN_ROLES or role["role_code"] == ROLE_ADMIN:
|
||
return {"code": 1, "msg": "内置角色禁止删除"}
|
||
db.execute("DELETE FROM sys_role_permission WHERE role_id = %s", (role_id,))
|
||
db.execute("DELETE FROM sys_user_role WHERE role_id = %s", (role_id,))
|
||
db.execute("DELETE FROM sys_role WHERE id = %s", (role_id,))
|
||
return {"code": 0}
|
||
|
||
|
||
def role_assign_perms(req):
|
||
"""角色权限点分配:全量覆盖 role 的 perm_id 列表。
|
||
|
||
入参:
|
||
role_id: str
|
||
perm_ids: list[str] 权限点 id(rbac wildcard 展开后的叶子权限点)
|
||
"""
|
||
role_id = req.get("role_id") or req.get("id")
|
||
if not role_id:
|
||
return {"code": 1, "msg": "缺少角色 id"}
|
||
perm_ids = req.get("perm_ids") or []
|
||
db = _get_db()
|
||
role = db.query_one("SELECT id FROM sys_role WHERE id = %s", (role_id,))
|
||
if not role:
|
||
return {"code": 1, "msg": "角色不存在"}
|
||
db.execute("DELETE FROM sys_role_permission WHERE role_id = %s", (role_id,))
|
||
if perm_ids:
|
||
rows = [(role_id, pid) for pid in perm_ids]
|
||
db.executemany(
|
||
"INSERT INTO sys_role_permission (role_id, perm_id) VALUES (%s, %s)", rows
|
||
)
|
||
return {"code": 0, "data": {"count": len(perm_ids)}}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 2. 管理员管理(sys_user + sys_user_role 绑定)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def admin_list(req):
|
||
"""管理员列表(含角色绑定)。"""
|
||
db = _get_db()
|
||
rows = db.query(
|
||
"SELECT u.id, u.username, u.real_name, u.status, u.created_at, "
|
||
" (SELECT GROUP_CONCAT(r.role_code) FROM sys_user_role ur "
|
||
" JOIN sys_role r ON r.id = ur.role_id WHERE ur.user_id = u.id) AS role_codes "
|
||
"FROM sys_user u ORDER BY u.created_at"
|
||
)
|
||
return {"code": 0, "data": rows}
|
||
|
||
|
||
def admin_create(req):
|
||
"""创建管理员(用户 + 绑定角色)。"""
|
||
db = _get_db()
|
||
username = (req.get("username") or "").strip()
|
||
if not username:
|
||
return {"code": 1, "msg": "用户名必填"}
|
||
exists = db.query_one("SELECT id FROM sys_user WHERE username = %s", (username,))
|
||
if exists:
|
||
return {"code": 1, "msg": "用户名已存在"}
|
||
uid = _uuid()
|
||
# 初始密码:无则由 rbac 默认口令(生产环境需强制改密,SRS §2.2-1)
|
||
pwd = req.get("password") or ""
|
||
db.execute(
|
||
"INSERT INTO sys_user (id, username, real_name, password, status, created_at, updated_at) "
|
||
"VALUES (%s, %s, %s, %s, %s, %s, %s)",
|
||
(uid, username, req.get("real_name", ""), pwd,
|
||
req.get("status", 1), _now(), _now()),
|
||
)
|
||
role_ids = req.get("role_ids") or []
|
||
if role_ids:
|
||
db.executemany(
|
||
"INSERT INTO sys_user_role (user_id, role_id) VALUES (%s, %s)",
|
||
[(uid, rid) for rid in role_ids],
|
||
)
|
||
return {"code": 0, "data": {"id": uid}}
|
||
|
||
|
||
def admin_bind_roles(req):
|
||
"""管理员绑定角色(全量覆盖)。"""
|
||
user_id = req.get("user_id") or req.get("id")
|
||
if not user_id:
|
||
return {"code": 1, "msg": "缺少用户 id"}
|
||
role_ids = req.get("role_ids") or []
|
||
db = _get_db()
|
||
db.execute("DELETE FROM sys_user_role WHERE user_id = %s", (user_id,))
|
||
if role_ids:
|
||
db.executemany(
|
||
"INSERT INTO sys_user_role (user_id, role_id) VALUES (%s, %s)",
|
||
[(user_id, rid) for rid in role_ids],
|
||
)
|
||
return {"code": 0}
|
||
|
||
|
||
def admin_disable(req):
|
||
"""停用/启用管理员。"""
|
||
user_id = req.get("user_id") or req.get("id")
|
||
if not user_id:
|
||
return {"code": 1, "msg": "缺少用户 id"}
|
||
db = _get_db()
|
||
db.execute("UPDATE sys_user SET status = %s, updated_at = %s WHERE id = %s",
|
||
(req.get("status", 0), _now(), user_id))
|
||
return {"code": 0}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 3. sys_data_scope 双维度配置
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def scope_list(req):
|
||
"""数据范围配置列表。"""
|
||
db = _get_db()
|
||
rows = db.query(
|
||
"SELECT s.*, u.username, u.real_name FROM sys_data_scope s "
|
||
"LEFT JOIN sys_user u ON u.id = s.admin_user_id ORDER BY s.created_at"
|
||
)
|
||
# 反序列化 JSON 字段供前端渲染
|
||
for r in rows:
|
||
r["org_ids"] = _json_loads(r.get("org_ids_json"), [])
|
||
r["field_ids"] = _json_loads(r.get("field_ids_json"), [])
|
||
return {"code": 0, "data": rows}
|
||
|
||
|
||
def _json_loads(raw, default):
|
||
if not raw:
|
||
return default
|
||
try:
|
||
return json.loads(raw)
|
||
except (TypeError, ValueError):
|
||
return default
|
||
|
||
|
||
def scope_save(req):
|
||
"""保存/更新数据范围配置(按 admin_user_id 唯一,upsert)。
|
||
|
||
双维度:
|
||
org_ids: list[str] 组织维度(保存后 get_data_scope 会展开子树)
|
||
field_ids: list[str] 花名册字段维度(可见字段白名单;空=全部可见)
|
||
scope_type: 'org' / 'field' / 'all'(all 表示双维度同时启用)
|
||
"""
|
||
db = _get_db()
|
||
admin_user_id = req.get("admin_user_id") or req.get("user_id")
|
||
if not admin_user_id:
|
||
return {"code": 1, "msg": "缺少管理员用户"}
|
||
scope_type = req.get("scope_type") or SCOPE_TYPE_ORG
|
||
org_ids = req.get("org_ids") or []
|
||
field_ids = req.get("field_ids") or []
|
||
org_json = json.dumps(org_ids, ensure_ascii=False)
|
||
field_json = json.dumps(field_ids, ensure_ascii=False)
|
||
|
||
exists = db.query_one(
|
||
"SELECT id FROM sys_data_scope WHERE admin_user_id = %s", (admin_user_id,)
|
||
)
|
||
if exists:
|
||
db.execute(
|
||
"UPDATE sys_data_scope SET scope_type = %s, org_ids_json = %s, field_ids_json = %s, "
|
||
"remark = %s, updated_by = %s, updated_at = %s WHERE id = %s",
|
||
(scope_type, org_json, field_json, req.get("remark", ""),
|
||
req.get("operator_id", ""), _now(), exists["id"]),
|
||
)
|
||
return {"code": 0, "data": {"id": exists["id"]}}
|
||
sid = _uuid()
|
||
db.execute(
|
||
"INSERT INTO sys_data_scope (id, admin_user_id, scope_type, org_ids_json, field_ids_json, "
|
||
"remark, created_by, updated_by, created_at, updated_at) "
|
||
"VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
|
||
(sid, admin_user_id, scope_type, org_json, field_json, req.get("remark", ""),
|
||
req.get("operator_id", ""), req.get("operator_id", ""), _now(), _now()),
|
||
)
|
||
return {"code": 0, "data": {"id": sid}}
|
||
|
||
|
||
def scope_delete(req):
|
||
"""删除数据范围配置。"""
|
||
scope_id = req.get("id")
|
||
if not scope_id:
|
||
return {"code": 1, "msg": "缺少配置 id"}
|
||
db = _get_db()
|
||
db.execute("DELETE FROM sys_data_scope WHERE id = %s", (scope_id,))
|
||
return {"code": 0}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 4. get_data_scope:查询叠加机制(公共横切能力)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def _expand_org_tree(db, org_ids):
|
||
"""组织维度子树展开:递归收集 org_unit 子节点(parent_id 自引用)。"""
|
||
if not org_ids:
|
||
return []
|
||
result = set(org_ids)
|
||
frontier = list(org_ids)
|
||
seen = set(org_ids)
|
||
while frontier:
|
||
placeholders = ",".join(["%s"] * len(frontier))
|
||
children = db.query(
|
||
"SELECT id FROM org_unit WHERE parent_id IN (%s)" % placeholders, frontier
|
||
)
|
||
frontier = []
|
||
for c in children:
|
||
cid = c["id"]
|
||
if cid not in seen:
|
||
seen.add(cid)
|
||
result.add(cid)
|
||
frontier.append(cid)
|
||
return sorted(result)
|
||
|
||
|
||
def get_data_scope(user_id):
|
||
"""获取指定用户的数据范围(组织 + 花名册字段双维度,多角色叠加)。
|
||
|
||
叠加规则:
|
||
- 组织维度:用户各角色对应 scope 的 org_ids 取并集(子树展开后)
|
||
- 字段维度:取各 scope 的 field_ids 并集;若某 scope 未配置字段维度则视为
|
||
“该角色字段不受限”,最终 field_all = True(否则为显式白名单)
|
||
- admin / sys_admin 角色:返回 org_all = True(全组织,不受限)
|
||
|
||
返回:
|
||
{
|
||
"org_all": bool,
|
||
"org_ids": [str], # 展开子树后的组织 id 集合
|
||
"field_all": bool,
|
||
"field_ids": [str], # 花名册字段白名单
|
||
"role_codes": [str]
|
||
}
|
||
该函数注册到 ServerEnv 公共上下文,供 roster/report/export 调用。
|
||
"""
|
||
db = _get_db()
|
||
# 用户绑定的角色
|
||
roles = db.query(
|
||
"SELECT r.role_code FROM sys_user_role ur "
|
||
"JOIN sys_role r ON r.id = ur.role_id WHERE ur.user_id = %s",
|
||
(user_id,),
|
||
)
|
||
role_codes = [r["role_code"] for r in roles]
|
||
|
||
result = {
|
||
"org_all": False,
|
||
"org_ids": [],
|
||
"field_all": False,
|
||
"field_ids": [],
|
||
"role_codes": role_codes,
|
||
}
|
||
|
||
# 系统管理员 / 内置 admin 拥有全量数据范围
|
||
if ROLE_SYS_ADMIN in role_codes or ROLE_ADMIN in role_codes:
|
||
result["org_all"] = True
|
||
result["field_all"] = True
|
||
return result
|
||
|
||
scope_rows = db.query(
|
||
"SELECT scope_type, org_ids_json, field_ids_json FROM sys_data_scope "
|
||
"WHERE admin_user_id = %s",
|
||
(user_id,),
|
||
)
|
||
if not scope_rows:
|
||
# 无任何数据范围配置:按最小权限(空)返回,保证“默认拒绝”
|
||
return result
|
||
|
||
org_union = set()
|
||
field_union = set()
|
||
field_restricted = False
|
||
for row in scope_rows:
|
||
scope_type = row.get("scope_type") or ""
|
||
org_ids = _json_loads(row.get("org_ids_json"), [])
|
||
field_ids = _json_loads(row.get("field_ids_json"), [])
|
||
|
||
if scope_type in (SCOPE_TYPE_ORG, "all") and org_ids:
|
||
org_union.update(org_ids)
|
||
if scope_type in (SCOPE_TYPE_FIELD, "all"):
|
||
field_restricted = True
|
||
field_union.update(field_ids)
|
||
|
||
result["org_ids"] = _expand_org_tree(db, sorted(org_union))
|
||
if field_restricted:
|
||
result["field_ids"] = sorted(field_union)
|
||
else:
|
||
# 未配置字段维度 → 字段不受限
|
||
result["field_all"] = True
|
||
return result
|
||
|
||
|
||
def scope_query(req):
|
||
"""端点:查询当前(或指定)用户的数据范围,便于前端/联调自测。"""
|
||
user_id = req.get("user_id") or req.get("operator_id") or req.get("uid")
|
||
if not user_id:
|
||
return {"code": 1, "msg": "缺少 user_id"}
|
||
return {"code": 0, "data": get_data_scope(user_id)}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 5. 内置 admin + 四角色权限初始化
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def init_permission(db=None):
|
||
"""幂等初始化:内置 admin + 四角色 + 基础权限点。
|
||
|
||
返回 {roles: int, users: int, perms: int}。
|
||
权限点来源于各模块端点清单(wildcard 展开),此处建立基线授权;
|
||
各业务模块初始化时按需追加自身权限点并授权给对应角色。
|
||
"""
|
||
db = db or _get_db()
|
||
now = _now()
|
||
counts = {"roles": 0, "users": 0, "perms": 0}
|
||
|
||
roles = [
|
||
(ROLE_EMPLOYEE, "员工自助", "员工本人查看/修改个人档案、发起自助申请"),
|
||
(ROLE_MANAGER, "经理", "管理本部门员工、发起并审批本部门人事异动"),
|
||
(ROLE_HR_ADMIN, "HR管理员", "管理全公司人事业务,受数据范围约束"),
|
||
(ROLE_SYS_ADMIN, "系统管理员", "系统配置、角色/管理员/数据范围/审计管理"),
|
||
]
|
||
role_id_map = {}
|
||
for code, name, remark in roles:
|
||
row = db.query_one("SELECT id FROM sys_role WHERE role_code = %s", (code,))
|
||
if not row:
|
||
rid = _uuid()
|
||
db.execute(
|
||
"INSERT INTO sys_role (id, role_code, role_name, remark, created_by, created_at, updated_at) "
|
||
"VALUES (%s, %s, %s, %s, %s, %s, %s)",
|
||
(rid, code, name, remark, "system", now, now),
|
||
)
|
||
counts["roles"] += 1
|
||
else:
|
||
rid = row["id"]
|
||
role_id_map[code] = rid
|
||
|
||
# 内置 admin(如不存在)
|
||
admin_row = db.query_one("SELECT id FROM sys_user WHERE username = 'admin'")
|
||
if not admin_row:
|
||
uid = _uuid()
|
||
db.execute(
|
||
"INSERT INTO sys_user (id, username, real_name, password, status, created_at, updated_at) "
|
||
"VALUES (%s, %s, %s, %s, %s, %s, %s)",
|
||
(uid, "admin", "系统管理员", "", 1, now, now),
|
||
)
|
||
counts["users"] += 1
|
||
else:
|
||
uid = admin_row["id"]
|
||
|
||
# 内置 admin 绑定全部角色(等价于 sys_admin 全量权限)
|
||
for code, rid in role_id_map.items():
|
||
exists = db.query_one(
|
||
"SELECT 1 AS x FROM sys_user_role WHERE user_id = %s AND role_id = %s",
|
||
(uid, rid),
|
||
)
|
||
if not exists:
|
||
db.execute(
|
||
"INSERT INTO sys_user_role (user_id, role_id) VALUES (%s, %s)", (uid, rid)
|
||
)
|
||
|
||
# 基线权限点(rbac 路径级权限点,code 采用 "模块:路径" 命名)
|
||
base_perms = [
|
||
# 权限底座自身
|
||
("hr_system:role_admin", "角色权限配置", "/hr_system/role_admin"),
|
||
("hr_system:data_scope", "数据范围配置", "/hr_system/data_scope"),
|
||
("hr_system:audit_log", "操作日志查询", "/hr_system/audit_log"),
|
||
]
|
||
perm_id_map = {}
|
||
for code, name, path in base_perms:
|
||
row = db.query_one("SELECT id FROM sys_permission WHERE perm_code = %s", (code,))
|
||
if not row:
|
||
pid = _uuid()
|
||
db.execute(
|
||
"INSERT INTO sys_permission (id, perm_code, perm_name, perm_path, created_at) "
|
||
"VALUES (%s, %s, %s, %s, %s)",
|
||
(pid, code, name, path, now),
|
||
)
|
||
counts["perms"] += 1
|
||
else:
|
||
pid = row["id"]
|
||
perm_id_map[code] = pid
|
||
|
||
# 系统管理员获得权限底座全部权限点
|
||
for pid in perm_id_map.values():
|
||
exists = db.query_one(
|
||
"SELECT 1 AS x FROM sys_role_permission WHERE role_id = %s AND perm_id = %s",
|
||
(role_id_map[ROLE_SYS_ADMIN], pid),
|
||
)
|
||
if not exists:
|
||
db.execute(
|
||
"INSERT INTO sys_role_permission (role_id, perm_id) VALUES (%s, %s)",
|
||
(role_id_map[ROLE_SYS_ADMIN], pid),
|
||
)
|
||
|
||
return counts
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 端点注册表(供 ahserver 主程序 app/hr_web.py 加载)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
ENDPOINTS = {
|
||
"role_list": role_list,
|
||
"role_detail": role_detail,
|
||
"role_create": role_create,
|
||
"role_update": role_update,
|
||
"role_delete": role_delete,
|
||
"role_assign_perms": role_assign_perms,
|
||
"admin_list": admin_list,
|
||
"admin_create": admin_create,
|
||
"admin_bind_roles": admin_bind_roles,
|
||
"admin_disable": admin_disable,
|
||
"scope_list": scope_list,
|
||
"scope_save": scope_save,
|
||
"scope_delete": scope_delete,
|
||
"scope_query": scope_query,
|
||
}
|
||
|
||
|
||
def register(server_env):
|
||
"""注册到 ahserver ServerEnv:暴露公共能力与端点。
|
||
|
||
server_env.register_public('get_data_scope', get_data_scope)
|
||
server_env.register_api(ENDPOINTS)
|
||
"""
|
||
server_env.register_public("get_data_scope", get_data_scope)
|
||
server_env.register_api(ENDPOINTS)
|
||
log.info("hr-system 权限底座已注册:get_data_scope + %d 个端点", len(ENDPOINTS))
|