hr-system/apps/hr_permission.py

549 lines
20 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 -*-
"""
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))