access.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. package auth
  2. import (
  3. "context"
  4. "errors"
  5. "math"
  6. "strings"
  7. "perms-system-server/internal/consts"
  8. "perms-system-server/internal/loaders"
  9. "perms-system-server/internal/middleware"
  10. userModel "perms-system-server/internal/model/user"
  11. "perms-system-server/internal/response"
  12. "perms-system-server/internal/svc"
  13. "github.com/zeromicro/go-zero/core/stores/sqlx"
  14. )
  15. func memberTypePriority(memberType string) int {
  16. switch memberType {
  17. case consts.MemberTypeSuperAdmin:
  18. return 0
  19. case consts.MemberTypeAdmin:
  20. return 1
  21. case consts.MemberTypeDeveloper:
  22. return 2
  23. case consts.MemberTypeMember:
  24. return 3
  25. default:
  26. return math.MaxInt32
  27. }
  28. }
  29. // ManageAccessOption 给 CheckManageAccess 的可选参数;主要用来传递调用方已经拿到的目标用户对象,
  30. // 避免 checkDeptHierarchy 内部再做一次 FindOne(targetUserId)(见审计 M-5)。
  31. type ManageAccessOption func(*manageAccessOpts)
  32. type manageAccessOpts struct {
  33. prefetchedTarget *userModel.SysUser
  34. }
  35. // WithPrefetchedTarget 供调用方透传已获取的目标用户数据。仅在 target.Id == targetUserId 时有效,
  36. // 调用方负责保证一致性;不一致时该选项被忽略,回落到普通 FindOne。
  37. func WithPrefetchedTarget(target *userModel.SysUser) ManageAccessOption {
  38. return func(o *manageAccessOpts) {
  39. o.prefetchedTarget = target
  40. }
  41. }
  42. // CheckManageAccess 检查当前操作者是否有权管理目标用户。
  43. // 规则:
  44. // 1. SUPER_ADMIN 完全豁免
  45. // 2. 操作自己豁免
  46. // 3. 部门检查:目标用户须在操作者本部门或下级子部门(ADMIN 豁免)
  47. // 4. 权限级别检查:操作者的级别必须严格高于目标用户
  48. // - 先比 memberType 优先级(SUPER_ADMIN > ADMIN > DEVELOPER > MEMBER)
  49. // - 同 memberType 时比 permsLevel(数值越小权限越高)
  50. func CheckManageAccess(ctx context.Context, svcCtx *svc.ServiceContext, targetUserId int64, productCode string, opts ...ManageAccessOption) error {
  51. caller := middleware.GetUserDetails(ctx)
  52. if caller == nil {
  53. return response.ErrUnauthorized("未登录")
  54. }
  55. if caller.IsSuperAdmin {
  56. return nil
  57. }
  58. if caller.UserId == targetUserId {
  59. return nil
  60. }
  61. options := &manageAccessOpts{}
  62. for _, opt := range opts {
  63. opt(options)
  64. }
  65. prefetched := options.prefetchedTarget
  66. if prefetched != nil && prefetched.Id != targetUserId {
  67. prefetched = nil
  68. }
  69. if err := checkDeptHierarchy(ctx, svcCtx, caller, targetUserId, prefetched); err != nil {
  70. return err
  71. }
  72. return checkPermLevel(ctx, svcCtx, caller, targetUserId, productCode)
  73. }
  74. // CheckMemberTypeAssignment 检查操作者是否有权分配指定的 memberType。
  75. func CheckMemberTypeAssignment(ctx context.Context, assignedType string) error {
  76. caller := middleware.GetUserDetails(ctx)
  77. if caller == nil {
  78. return response.ErrUnauthorized("未登录")
  79. }
  80. if caller.IsSuperAdmin {
  81. return nil
  82. }
  83. if caller.MemberType == "" {
  84. return response.ErrForbidden("缺少产品成员上下文")
  85. }
  86. if memberTypePriority(caller.MemberType) >= memberTypePriority(assignedType) {
  87. return response.ErrForbidden("无权分配该成员类型,不能分配与自己同级或更高级别的类型")
  88. }
  89. return nil
  90. }
  91. // RequireSuperAdmin 要求当前操作者必须是超级管理员。
  92. func RequireSuperAdmin(ctx context.Context) error {
  93. caller := middleware.GetUserDetails(ctx)
  94. if caller == nil {
  95. return response.ErrUnauthorized("未登录")
  96. }
  97. if !caller.IsSuperAdmin {
  98. return response.ErrForbidden("仅超级管理员可执行此操作")
  99. }
  100. return nil
  101. }
  102. // RequireProductAdminFor 要求当前操作者是超级管理员或指定产品的管理员。
  103. func RequireProductAdminFor(ctx context.Context, targetProductCode string) error {
  104. caller := middleware.GetUserDetails(ctx)
  105. if caller == nil {
  106. return response.ErrUnauthorized("未登录")
  107. }
  108. if caller.IsSuperAdmin {
  109. return nil
  110. }
  111. if caller.MemberType == consts.MemberTypeAdmin && caller.ProductCode == targetProductCode {
  112. return nil
  113. }
  114. return response.ErrForbidden("仅超级管理员或该产品的管理员可执行此操作")
  115. }
  116. // GuardRoleLevelAssignable 校验调用者能否把 rolePermsLevel 这一等级的角色分配给他人。
  117. // 约束:"只能分配严格低于自身的等级"(数字更大 = 更低),与 checkPermLevel 的 ">=" 拦截口径对齐,
  118. // 避免调用者把下属拉到与自己平级后彻底失去管控(见审计 H-3)。
  119. // 拥有产品全权(SuperAdmin / ADMIN / DEVELOPER)的调用者直接放行。
  120. //
  121. // 授权依据直接走 DB 强一致查询,而不是 caller 的 UD 缓存:
  122. // 超管刚把 caller 从高级降到中级时,如果 UD 缓存的 Clean 因为 Redis 抖动失败,caller 的缓存里还
  123. // 是旧的高 MinPermsLevel;缓存 TTL 窗口内 caller 仍可凭旧级别批量分配超出当前权限的角色
  124. // (审计 M-3 TOCTOU + 缓存失效延迟)。这里按"最小代价避开缓存"的原则,只在 assignment 决策点
  125. // 打一条 FindMinPermsLevelByUserIdAndProductCode 走 NoCache 查询。
  126. func GuardRoleLevelAssignable(ctx context.Context, svcCtx *svc.ServiceContext, caller *loaders.UserDetails, rolePermsLevel int64) error {
  127. if HasFullProductPerms(caller) {
  128. return nil
  129. }
  130. if caller == nil {
  131. return response.ErrForbidden("您没有可分配的角色等级")
  132. }
  133. freshLevel, err := svcCtx.SysRoleModel.FindMinPermsLevelByUserIdAndProductCode(ctx, caller.UserId, caller.ProductCode)
  134. if err != nil {
  135. if errors.Is(err, sqlx.ErrNotFound) {
  136. return response.ErrForbidden("您没有可分配的角色等级")
  137. }
  138. // 其他错误走 fail-close,避免 DB 抖动被同化为"无角色 = 最低级"放行超权分配。
  139. return response.NewCodeError(500, "校验可分配角色等级失败,请稍后重试")
  140. }
  141. if rolePermsLevel <= freshLevel {
  142. return response.ErrForbidden("不能分配权限级别高于自身的角色(含同级)")
  143. }
  144. return nil
  145. }
  146. // HasFullProductPerms 判断调用者是否拥有当前产品的全部权限(无需做 permsLevel 校验)。
  147. // SuperAdmin / ADMIN / DEVELOPER 均视为全权;loadPerms 对此三者走全权分支。
  148. // 所有依赖"调用者已拥有全权"的短路逻辑应复用此函数,变更只需改一处。
  149. func HasFullProductPerms(caller *loaders.UserDetails) bool {
  150. if caller == nil {
  151. return false
  152. }
  153. return caller.IsSuperAdmin ||
  154. caller.MemberType == consts.MemberTypeAdmin ||
  155. caller.MemberType == consts.MemberTypeDeveloper
  156. }
  157. // CheckAddMemberAccess 校验 caller 是否有权把 target 作为**新成员**拉进产品。与 CheckManageAccess 的
  158. // 差异:
  159. // 1. 不做 memberType / permsLevel 比对——AddMember 时 target 还不是成员,checkPermLevel 的
  160. // FindOneByProductCodeUserId 必定落空报 403,整个流程对产品 ADMIN 直接不可用;
  161. // 2. 对产品 ADMIN 也强制执行部门链校验(checkDeptHierarchy 的 ADMIN bypass 不生效),防止
  162. // ADMIN 从部门树外 / HR / 财务把人强拉入自己的产品(见审计 H-3);
  163. // 3. SuperAdmin 仍完全豁免,自改自加场景由上层业务规则另行屏蔽。
  164. //
  165. // 调用方应先走 RequireProductAdminFor 保证 caller 本身有"添加成员"的基线权限,再调这一步过部门链。
  166. func CheckAddMemberAccess(ctx context.Context, svcCtx *svc.ServiceContext, target *userModel.SysUser) error {
  167. caller := middleware.GetUserDetails(ctx)
  168. if caller == nil {
  169. return response.ErrUnauthorized("未登录")
  170. }
  171. if caller.IsSuperAdmin {
  172. return nil
  173. }
  174. if target == nil {
  175. return response.ErrBadRequest("缺少目标用户信息")
  176. }
  177. if caller.UserId == target.Id {
  178. return nil
  179. }
  180. // 不走 checkDeptHierarchy 的 ADMIN 分支:ADMIN bypass 设计本意是"product ADMIN 对产品内既有
  181. // 成员有全面管理权",但 AddMember 还没把 target 拉进产品,这里的 ADMIN bypass 会变成一个
  182. // "随手拉部门树外的人进来"的漏洞(审计 H-3)。
  183. if caller.DeptId == 0 || caller.DeptPath == "" {
  184. return response.ErrForbidden("您未归属任何部门,无权添加产品成员")
  185. }
  186. if target.DeptId == 0 {
  187. return response.ErrForbidden("目标用户未归属部门,仅超管可将其添加为成员")
  188. }
  189. targetDept, err := svcCtx.SysDeptModel.FindOne(ctx, target.DeptId)
  190. if err != nil {
  191. return response.ErrForbidden("无法校验目标用户部门")
  192. }
  193. if !strings.HasPrefix(targetDept.Path, caller.DeptPath) {
  194. return response.ErrForbidden("无权将其他部门的用户添加为产品成员")
  195. }
  196. return nil
  197. }
  198. // ValidateStatusChange 校验状态变更的合法性(不允许自改状态、不允许冻结超管)。
  199. // UpdateUser 和 UpdateUserStatus 共用此函数以确保校验逻辑一致。
  200. //
  201. // 返回校验通过时对应的目标用户对象,方便调用方透传给 CheckManageAccess 的 WithPrefetchedTarget
  202. // 选项和后续业务使用,避免同一请求内重复 FindOne(见审计 M-5)。
  203. func ValidateStatusChange(ctx context.Context, svcCtx *svc.ServiceContext, callerId, targetUserId int64) (*userModel.SysUser, error) {
  204. if callerId == targetUserId {
  205. return nil, response.ErrBadRequest("不能修改自己的状态")
  206. }
  207. target, err := svcCtx.SysUserModel.FindOne(ctx, targetUserId)
  208. if err != nil {
  209. return nil, response.ErrNotFound("用户不存在")
  210. }
  211. if target.IsSuperAdmin == consts.IsSuperAdminYes {
  212. return nil, response.ErrForbidden("不能修改超级管理员的状态")
  213. }
  214. return target, nil
  215. }
  216. func checkDeptHierarchy(ctx context.Context, svcCtx *svc.ServiceContext, caller *loaders.UserDetails, targetUserId int64, prefetchedTarget *userModel.SysUser) error {
  217. if caller.MemberType == consts.MemberTypeAdmin {
  218. return nil
  219. }
  220. // TODO(L-6): H-4 落地之后,新建 MEMBER/DEVELOPER 不会再出现 DeptId=0;但迁移/老数据里仍可能存在
  221. // "MemberType!=ADMIN 且 DeptId=0" 的幽灵账号,此处一律 403 会让这类账号失去任何管理能力
  222. // (包括原本可以由 checkPermLevel 通过的 product-admin-downward 操作)。运维应补一次 data fix
  223. // 把这类账号归入默认部门;若未来需要放宽,可在此允许"管理自己"或跳过部门链校验直接交由
  224. // checkPermLevel 判定。
  225. if caller.DeptId == 0 {
  226. return response.ErrForbidden("您未归属任何部门,无权管理其他用户")
  227. }
  228. if caller.DeptPath == "" {
  229. return response.ErrForbidden("您的部门信息异常,无法执行此操作")
  230. }
  231. target := prefetchedTarget
  232. if target == nil {
  233. t, err := svcCtx.SysUserModel.FindOne(ctx, targetUserId)
  234. if err != nil {
  235. return response.ErrNotFound("目标用户不存在")
  236. }
  237. target = t
  238. }
  239. if target.DeptId == 0 {
  240. return response.ErrForbidden("目标用户未归属部门,仅超管或产品管理员可管理")
  241. }
  242. targetDept, err := svcCtx.SysDeptModel.FindOne(ctx, target.DeptId)
  243. if err != nil {
  244. return response.ErrForbidden("无权操作")
  245. }
  246. if !strings.HasPrefix(targetDept.Path, caller.DeptPath) {
  247. return response.ErrForbidden("无权管理其他部门的用户")
  248. }
  249. return nil
  250. }
  251. func checkPermLevel(ctx context.Context, svcCtx *svc.ServiceContext, caller *loaders.UserDetails, targetUserId int64, productCode string) error {
  252. if productCode == "" {
  253. return response.ErrBadRequest("缺少产品上下文,无法进行权限级别判定")
  254. }
  255. targetMember, err := svcCtx.SysProductMemberModel.FindOneByProductCodeUserId(ctx, productCode, targetUserId)
  256. if err != nil {
  257. return response.ErrForbidden("目标用户不是当前产品的成员,无法执行管理操作")
  258. }
  259. targetMemberType := targetMember.MemberType
  260. callerPri := memberTypePriority(caller.MemberType)
  261. targetPri := memberTypePriority(targetMemberType)
  262. if callerPri > targetPri {
  263. return response.ErrForbidden("无权管理权限级别高于您的用户")
  264. }
  265. if callerPri < targetPri {
  266. return nil
  267. }
  268. // memberType 相同,比较 permsLevel
  269. targetLevel, err := svcCtx.SysRoleModel.FindMinPermsLevelByUserIdAndProductCode(ctx, targetUserId, productCode)
  270. if err != nil {
  271. // 区分"无角色 → 等价最低等级"与"DB 抖动 → 未知":只有 ErrNotFound 语义的场景才允许
  272. // 降级为 MaxInt64 放行管辖;其余错误一律视作不确定,fail-close 返回 500,避免 DB 抖动
  273. // 被同化成"目标无角色"造成越权放行(见审计 L-4)。
  274. if !errors.Is(err, sqlx.ErrNotFound) {
  275. return response.NewCodeError(500, "校验权限级别失败,请稍后重试")
  276. }
  277. targetLevel = math.MaxInt64
  278. }
  279. if caller.MinPermsLevel >= targetLevel {
  280. return response.ErrForbidden("无权管理权限级别高于或等于您的用户")
  281. }
  282. return nil
  283. }