setUserPermsLogic.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. package user
  2. import (
  3. "context"
  4. "fmt"
  5. "strings"
  6. "time"
  7. "perms-system-server/internal/consts"
  8. authHelper "perms-system-server/internal/logic/auth"
  9. "perms-system-server/internal/middleware"
  10. "perms-system-server/internal/model/userperm"
  11. "perms-system-server/internal/response"
  12. "perms-system-server/internal/svc"
  13. "perms-system-server/internal/types"
  14. "github.com/zeromicro/go-zero/core/logx"
  15. "github.com/zeromicro/go-zero/core/stores/sqlx"
  16. )
  17. type SetUserPermsLogic struct {
  18. logx.Logger
  19. ctx context.Context
  20. svcCtx *svc.ServiceContext
  21. }
  22. func NewSetUserPermsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SetUserPermsLogic {
  23. return &SetUserPermsLogic{
  24. Logger: logx.WithContext(ctx),
  25. ctx: ctx,
  26. svcCtx: svcCtx,
  27. }
  28. }
  29. // SetUserPerms 设置用户个性化权限。对指定用户在当前产品下做权限全量覆盖,支持 ALLOW(附加)和 DENY(拒绝)两种效果,用于角色权限之外的细粒度调整。
  30. func (l *SetUserPermsLogic) SetUserPerms(req *types.SetPermsReq) error {
  31. targetUser, err := l.svcCtx.SysUserModel.FindOne(l.ctx, req.UserId)
  32. if err != nil {
  33. return response.ErrNotFound("用户不存在")
  34. }
  35. productCode := middleware.GetProductCode(l.ctx)
  36. if err := authHelper.RequireProductAdminFor(l.ctx, productCode); err != nil {
  37. return err
  38. }
  39. product, err := l.svcCtx.SysProductModel.FindOneByCode(l.ctx, productCode)
  40. if err != nil {
  41. return response.ErrNotFound("产品不存在")
  42. }
  43. if product.Status != consts.StatusEnabled {
  44. return response.ErrBadRequest("产品已被禁用,无法设置权限")
  45. }
  46. if err := authHelper.CheckManageAccess(l.ctx, l.svcCtx, req.UserId, productCode, authHelper.WithPrefetchedTarget(targetUser)); err != nil {
  47. return err
  48. }
  49. member, memberErr := l.svcCtx.SysProductMemberModel.FindOneByProductCodeUserId(l.ctx, productCode, req.UserId)
  50. if memberErr != nil {
  51. return response.ErrBadRequest("目标用户不是当前产品的成员")
  52. }
  53. if member.Status != consts.StatusEnabled {
  54. return response.ErrBadRequest("目标用户的成员资格已被禁用")
  55. }
  56. for _, p := range req.Perms {
  57. if p.Effect != consts.PermEffectAllow && p.Effect != consts.PermEffectDeny {
  58. return response.ErrBadRequest("effect值无效,仅支持 ALLOW 和 DENY")
  59. }
  60. }
  61. perms := req.Perms
  62. if len(perms) > 0 {
  63. seen := make(map[int64]string, len(perms))
  64. uniquePerms := make([]types.UserPermItem, 0, len(perms))
  65. for _, p := range perms {
  66. if prev, ok := seen[p.PermId]; ok {
  67. if prev != p.Effect {
  68. return response.ErrBadRequest("同一权限ID不能同时为 ALLOW 和 DENY")
  69. }
  70. continue
  71. }
  72. seen[p.PermId] = p.Effect
  73. uniquePerms = append(uniquePerms, p)
  74. }
  75. perms = uniquePerms
  76. }
  77. if len(perms) > 0 {
  78. permIds := make([]int64, 0, len(perms))
  79. for _, p := range perms {
  80. permIds = append(permIds, p.PermId)
  81. }
  82. dbPerms, err := l.svcCtx.SysPermModel.FindByIds(l.ctx, permIds)
  83. if err != nil {
  84. return err
  85. }
  86. if len(dbPerms) != len(perms) {
  87. return response.ErrBadRequest("包含无效的权限ID")
  88. }
  89. for _, p := range dbPerms {
  90. if p.ProductCode != productCode {
  91. return response.ErrBadRequest("不能设置其他产品的权限")
  92. }
  93. if p.Status != consts.StatusEnabled {
  94. return response.ErrBadRequest(fmt.Sprintf("权限 %s 已被禁用,无法设置", p.Code))
  95. }
  96. }
  97. }
  98. if err := l.svcCtx.SysUserPermModel.TransactCtx(l.ctx, func(ctx context.Context, session sqlx.Session) error {
  99. if err := l.svcCtx.SysUserPermModel.DeleteByUserIdForProductTx(ctx, session, req.UserId, productCode); err != nil {
  100. return err
  101. }
  102. if len(perms) == 0 {
  103. return nil
  104. }
  105. now := time.Now().Unix()
  106. data := make([]*userperm.SysUserPerm, 0, len(perms))
  107. permIds := make([]int64, 0, len(perms))
  108. for _, p := range perms {
  109. data = append(data, &userperm.SysUserPerm{
  110. UserId: req.UserId,
  111. PermId: p.PermId,
  112. Effect: p.Effect,
  113. CreateTime: now,
  114. UpdateTime: now,
  115. })
  116. permIds = append(permIds, p.PermId)
  117. }
  118. if err := l.svcCtx.SysUserPermModel.BatchInsertWithTx(ctx, session, data); err != nil {
  119. return err
  120. }
  121. // 审计 L-4:事务末对 sys_perm 的 status 再做一次 COUNT 复核,把 "入事务前 FindByIds
  122. // 校验通过 → 事务外某次 SyncPermissions 把这些 permId 置为 DISABLED → 我们仍旧把脏
  123. // user_perm 写进去" 的 TOCTOU 窗口收紧到零。校验失败主动返回错误触发事务回滚,外层
  124. // 映射为 409(数据被并发改动,前端建议重试)。
  125. // 复核手法:不在这里加 FOR SHARE(会被 SyncPerms 的 LockByCodeTx X 锁阻塞,增大 SyncPerms 尾延迟);
  126. // COUNT 只读 sys_perm 最新可见版本即可:
  127. // - 同一事务读到的是事务开始时的快照(InnoDB RR),若 SyncPermissions 已提交,
  128. // COUNT 结果会反映 DISABLED 行,与 BatchInsertWithTx 真实落盘行数对不上 → 回滚。
  129. // - 若 SyncPermissions 与本事务重叠但尚未提交,则两边按常规 gap lock 规则互不可见,
  130. // 本次依然按事务开始时的 Enabled 快照落盘;SyncPermissions 提交后其 UPDATE 会
  131. // 覆盖 sys_perm.status,本 user_perm 行在 loadPerms JOIN sys_perm.status = ? 时
  132. // 自动被过滤,与 "脏行不生效" 的不变式一致。
  133. placeholders := make([]string, len(permIds))
  134. args := make([]interface{}, 0, len(permIds)+2)
  135. for i, id := range permIds {
  136. placeholders[i] = "?"
  137. args = append(args, id)
  138. }
  139. args = append(args, productCode, consts.StatusEnabled)
  140. countQuery := fmt.Sprintf(
  141. "SELECT COUNT(*) FROM sys_perm WHERE id IN (%s) AND `productCode` = ? AND `status` = ?",
  142. strings.Join(placeholders, ","),
  143. )
  144. var enabled int64
  145. if err := session.QueryRowCtx(ctx, &enabled, countQuery, args...); err != nil {
  146. return err
  147. }
  148. if enabled != int64(len(permIds)) {
  149. return response.ErrConflict("部分权限在提交时已被禁用,请刷新后重试")
  150. }
  151. return nil
  152. }); err != nil {
  153. return err
  154. }
  155. l.svcCtx.UserDetailsLoader.Del(l.ctx, req.UserId, productCode)
  156. return nil
  157. }