syncPermsService.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. package pub
  2. import (
  3. "context"
  4. "errors"
  5. "time"
  6. "perms-system-server/internal/consts"
  7. permModel "perms-system-server/internal/model/perm"
  8. "perms-system-server/internal/svc"
  9. "perms-system-server/internal/util"
  10. "github.com/zeromicro/go-zero/core/logx"
  11. "github.com/zeromicro/go-zero/core/stores/sqlx"
  12. "golang.org/x/crypto/bcrypt"
  13. )
  14. type SyncPermsResult struct {
  15. Added int64
  16. Updated int64
  17. Disabled int64
  18. }
  19. type SyncPermItem struct {
  20. Code string
  21. Name string
  22. Remark string
  23. }
  24. type SyncPermsError struct {
  25. Code int
  26. Message string
  27. }
  28. func (e *SyncPermsError) Error() string {
  29. return e.Message
  30. }
  31. func ExecuteSyncPerms(ctx context.Context, svcCtx *svc.ServiceContext, appKey, appSecret string, perms []SyncPermItem) (*SyncPermsResult, error) {
  32. product, err := svcCtx.SysProductModel.FindOneByAppKey(ctx, appKey)
  33. if err != nil {
  34. return nil, &SyncPermsError{Code: 401, Message: "无效的appKey"}
  35. }
  36. if err := bcrypt.CompareHashAndPassword([]byte(product.AppSecret), []byte(appSecret)); err != nil {
  37. return nil, &SyncPermsError{Code: 401, Message: "appSecret验证失败"}
  38. }
  39. if product.Status != consts.StatusEnabled {
  40. return nil, &SyncPermsError{Code: 403, Message: "产品已被禁用"}
  41. }
  42. if len(perms) == 0 {
  43. return nil, &SyncPermsError{Code: 400, Message: "权限列表不能为空"}
  44. }
  45. // 去重请求列表,避免同一笔同步里 codes 互相冲突。
  46. codes := make([]string, 0, len(perms))
  47. seen := make(map[string]bool, len(perms))
  48. dedupPerms := make([]SyncPermItem, 0, len(perms))
  49. for _, item := range perms {
  50. if seen[item.Code] {
  51. continue
  52. }
  53. seen[item.Code] = true
  54. codes = append(codes, item.Code)
  55. dedupPerms = append(dedupPerms, item)
  56. }
  57. now := time.Now().Unix()
  58. var added, updated, disabled int64
  59. // 同 tx 内先 SELECT ... FOR UPDATE 锁 sys_product 行,再在 tx 内读取 existing 并写入:
  60. // 把同一 product 的并发同步串行化,避免两次同步都认为 code X 不存在并并发 INSERT 撞
  61. // sys_perm UNIQUE(productCode, code) 拿 1062(见审计 H-3)。
  62. err = svcCtx.SysPermModel.TransactCtx(ctx, func(txCtx context.Context, session sqlx.Session) error {
  63. locked, err := svcCtx.SysProductModel.LockByCodeTx(txCtx, session, product.Code)
  64. if err != nil {
  65. if errors.Is(err, sqlx.ErrNotFound) {
  66. return &SyncPermsError{Code: 404, Message: "产品不存在"}
  67. }
  68. return err
  69. }
  70. // 审计 M-R10-1:事务外的 FindOneByAppKey 只是 cache/stale 读,无法感知"UpdateProduct
  71. // 与本同步并发到达、UpdateProduct 后提交"的时序(SyncPerms 先拿到 X 锁时,UpdateProduct 的
  72. // UPDATE 会排队等锁,SyncPerms 在锁内读到的 product.Status 仍是 Enabled 但真正提交顺序会
  73. // 让 product 行最终是 Disabled)。LockByCodeTx 已经拿到行 X 锁,这里对 locked.Status 再做一
  74. // 次事务内复核,任何已禁用的产品都不得继续写 sys_perm,修复"禁用后同一秒仍生成 perm diff"
  75. // 把审计告警带偏的问题。安全侧 loadPerms 仍会按 ProductStatus!=Enabled 返空,但运营/审计
  76. // 链路需要本处兜底避免假象。
  77. if locked.Status != consts.StatusEnabled {
  78. return &SyncPermsError{Code: 403, Message: "产品已被禁用"}
  79. }
  80. existingMap, err := svcCtx.SysPermModel.FindMapByProductCodeWithTx(txCtx, session, product.Code)
  81. if err != nil {
  82. return err
  83. }
  84. var toInsert []*permModel.SysPerm
  85. var toUpdate []*permModel.SysPerm
  86. for _, item := range dedupPerms {
  87. existing, ok := existingMap[item.Code]
  88. if !ok {
  89. toInsert = append(toInsert, &permModel.SysPerm{
  90. ProductCode: product.Code,
  91. Name: item.Name,
  92. Code: item.Code,
  93. Remark: item.Remark,
  94. Status: consts.StatusEnabled,
  95. CreateTime: now,
  96. UpdateTime: now,
  97. })
  98. added++
  99. continue
  100. }
  101. if existing.Name != item.Name || existing.Remark != item.Remark || existing.Status != consts.StatusEnabled {
  102. existing.Name = item.Name
  103. existing.Remark = item.Remark
  104. existing.Status = consts.StatusEnabled
  105. existing.UpdateTime = now
  106. toUpdate = append(toUpdate, existing)
  107. updated++
  108. }
  109. }
  110. if len(toInsert) > 0 {
  111. if insertErr := svcCtx.SysPermModel.BatchInsertWithTx(txCtx, session, toInsert); insertErr != nil {
  112. return insertErr
  113. }
  114. }
  115. if len(toUpdate) > 0 {
  116. if updateErr := svcCtx.SysPermModel.BatchUpdateWithTx(txCtx, session, toUpdate); updateErr != nil {
  117. return updateErr
  118. }
  119. }
  120. var disableErr error
  121. disabled, disableErr = svcCtx.SysPermModel.DisableNotInCodesWithTx(txCtx, session, product.Code, codes, now)
  122. return disableErr
  123. })
  124. if err != nil {
  125. var se *SyncPermsError
  126. if errors.As(err, &se) {
  127. return nil, se
  128. }
  129. // 第 6 轮测试报告 §9.5#3:H-3 已经通过 LockByCodeTx 把同一产品的同步串行化,理论上
  130. // sys_perm (productCode, code) UNIQUE 在事务内不可能再拿到 1062。若真的命中,说明:
  131. // (a) LockByCodeTx 没有生效(例如引擎/隔离级别被改);
  132. // (b) 有绕过本函数直接写 sys_perm 的代码路径被引入;
  133. // 任何一种都代表 H-3 的锁序契约失效,需要立即告警补回 409 重试契约。因此在这里落一条
  134. // 带 audit=mysql_error_1062 + table=sys_perm 的 ERROR 级日志,日志采集侧即可据此建
  135. // 指标与告警规则;对外仍然回通用 500 避免给客户端透传 DB 细节。
  136. if util.IsDuplicateEntryErr(err) {
  137. logx.WithContext(ctx).Errorw("sync perms hit 1062 under LockByCodeTx — H-3 contract regressed",
  138. logx.Field("audit", "mysql_error_1062"),
  139. logx.Field("table", "sys_perm"),
  140. logx.Field("productCode", product.Code),
  141. logx.Field("err", err.Error()),
  142. )
  143. }
  144. return nil, &SyncPermsError{Code: 500, Message: "同步权限事务失败"}
  145. }
  146. if added > 0 || updated > 0 || disabled > 0 {
  147. svcCtx.UserDetailsLoader.CleanByProduct(ctx, product.Code)
  148. }
  149. return &SyncPermsResult{
  150. Added: added,
  151. Updated: updated,
  152. Disabled: disabled,
  153. }, nil
  154. }