"""授权查询与计算,权限每次从数据库解析,不采用 JWT 中的角色作为依据。""" from sqlalchemy import select from sqlalchemy.orm import object_session from app.core.exceptions import BadRequestError, ForbiddenError from app.models import ( AuthorizationSetting, Permission, Role, RolePermission, User, UserRole, ) from app.services.permission_catalog import CATALOG, PAYROLL_WRITE_PERMISSIONS def setting(session, *, lock=False): statement = select(AuthorizationSetting).where(AuthorizationSetting.id == 1) if lock: statement = statement.with_for_update().execution_options(populate_existing=True) row = session.scalar(statement) if row is None: raise ForbiddenError("授权配置尚未初始化,请先完成数据库迁移") return row def role_codes(session, user_id, *, for_update=False): statement = select(Role.role_code).join(UserRole).where( UserRole.user_id == user_id, Role.status == 1 ) if for_update: statement = statement.with_for_update() return list(session.scalars(statement)) def calculate(session, user, *, roles=None, overrides=None): """用户继承启用角色权限的并集;管理员固定全权限,操作依赖模块查看。""" if overrides: raise BadRequestError("已改为角色授权,请在角色管理中配置权限") if user is None or user.status != 1: return {"permissions": [], "sources": {}, "roles": []} roles = roles if roles is not None else role_codes(session, user.id) if "admin" in roles: return {"permissions": sorted(CATALOG), "sources": { code: {"roles": ["admin"], "override": None, "effective": True} for code in CATALOG }, "roles": roles} inherited = {} for code, role in session.execute( select(Permission.perm_code, Role.role_code) .join(RolePermission, RolePermission.permission_id == Permission.id) .join(Role, Role.id == RolePermission.role_id) .where(Role.role_code.in_(roles), Role.status == 1, Permission.status == 1) ): inherited.setdefault(code, []).append(role) allowed = set(inherited) allowed &= set(CATALOG) allowed = {c for c in allowed if not CATALOG[c]["admin_only"]} allowed = {c for c in allowed if CATALOG[c]["view_code"] in allowed} sources = {c: {"roles": inherited.get(c, []), "override": None, "effective": c in allowed} for c in CATALOG} return {"permissions": sorted(allowed), "sources": sources, "roles": roles} def is_maker(session, user): """兼容旧会话字段:现表示账号具备工资写权限,不再查询全局绑定。""" return bool(set(calculate(session, user)["permissions"]) & PAYROLL_WRITE_PERMISSIONS) def require_permission(session, user, code, *, check_maker=True): item = CATALOG.get(code) if item is None or code not in calculate(session, user)["permissions"]: label = f"{item['module_name']}:{item['name']}" if item else "此操作" raise ForbiddenError(f"当前账号没有「{label}」权限,请联系管理员授权") return user def session_info(session, user): result = calculate(session, user) maker = bool(set(result["permissions"]) & PAYROLL_WRITE_PERMISSIONS) return {"permissions": result["permissions"], "authorization_revision": setting(session).revision, "is_payroll_maker": maker, "can_write_payroll": maker, "maker_required_permissions": [], "roles": result["roles"]} def user_session(user): session = object_session(user) if isinstance(user, User) else None if session is None: raise ForbiddenError("账号状态不可用,请重新登录") return session