# -*- 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))