guardRoleLevelAssignable_freshRead_audit_test.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. package auth
  2. import (
  3. "context"
  4. "errors"
  5. "testing"
  6. "perms-system-server/internal/consts"
  7. "perms-system-server/internal/loaders"
  8. "perms-system-server/internal/response"
  9. "perms-system-server/internal/testutil/mocks"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/stretchr/testify/require"
  12. "github.com/zeromicro/go-zero/core/stores/sqlx"
  13. "go.uber.org/mock/gomock"
  14. )
  15. // ---------------------------------------------------------------------------
  16. // 覆盖目标:审计 M-3 修复 —— GuardRoleLevelAssignable 必须每次走 DB 强一致查询,
  17. // 绝不能信任 caller(loaders.UserDetails)里可能已经 stale 的 MinPermsLevel 缓存。
  18. //
  19. // TOCTOU 场景:
  20. // 1. caller 原先是 permsLevel=5 的高阶成员。
  21. // 2. 超管把 caller 的高阶角色摘掉,现在 DB 里 MinPermsLevel=100(低阶)。
  22. // 3. UD 缓存还没被 Clean(Redis 抖动 / TTL 窗口内),caller.MinPermsLevel=5 是旧值。
  23. // 4. caller 此刻尝试分配 permsLevel=50 的角色 —— 若信缓存(5 vs 50)会**误放行**;
  24. // 修复后走 DB(100 vs 50),必须 403 拦截。
  25. // ---------------------------------------------------------------------------
  26. // TC-0930: M-3 —— stale caller.MinPermsLevel 不得影响判定,rolePermsLevel <= freshLevel 必须 403。
  27. func TestGuardRoleLevelAssignable_StaleCallerCache_FreshDBRejects(t *testing.T) {
  28. ctrl := gomock.NewController(t)
  29. t.Cleanup(ctrl.Finish)
  30. const productCode = "m3_pc_stale"
  31. const callerId = int64(1001)
  32. mockRole := mocks.NewMockSysRoleModel(ctrl)
  33. // 关键:DB 强一致返回 100(被降级后的真实等级)。
  34. mockRole.EXPECT().
  35. FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), callerId, productCode).
  36. Return(int64(100), nil).
  37. Times(1)
  38. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole})
  39. caller := &loaders.UserDetails{
  40. UserId: callerId,
  41. Username: "m3_stale_caller",
  42. MemberType: consts.MemberTypeMember,
  43. ProductCode: productCode,
  44. Status: consts.StatusEnabled,
  45. MinPermsLevel: 5,
  46. }
  47. err := GuardRoleLevelAssignable(context.Background(), svcCtx, caller, 50)
  48. require.Error(t, err, "stale 缓存(5) 下试图分配 permsLevel=50,信缓存会放行;走 DB(100) 必须 403")
  49. var ce *response.CodeError
  50. require.True(t, errors.As(err, &ce))
  51. assert.Equal(t, 403, ce.Code(), "M-3:拒绝分配高于自身 fresh 等级的角色 → 403")
  52. }
  53. // TC-0931: M-3 —— 同级(rolePermsLevel == freshLevel)也要拦截,保持与 checkPermLevel 的 ">=" 对齐。
  54. func TestGuardRoleLevelAssignable_SameLevel_Rejected(t *testing.T) {
  55. ctrl := gomock.NewController(t)
  56. t.Cleanup(ctrl.Finish)
  57. const productCode = "m3_pc_same"
  58. const callerId = int64(1002)
  59. mockRole := mocks.NewMockSysRoleModel(ctrl)
  60. mockRole.EXPECT().
  61. FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), callerId, productCode).
  62. Return(int64(50), nil).
  63. Times(1)
  64. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole})
  65. caller := &loaders.UserDetails{
  66. UserId: callerId,
  67. Username: "m3_same_caller",
  68. MemberType: consts.MemberTypeMember,
  69. ProductCode: productCode,
  70. Status: consts.StatusEnabled,
  71. }
  72. err := GuardRoleLevelAssignable(context.Background(), svcCtx, caller, 50)
  73. require.Error(t, err, "与自身同级不允许分配,否则会让下属获得与上级等效的权力")
  74. var ce *response.CodeError
  75. require.True(t, errors.As(err, &ce))
  76. assert.Equal(t, 403, ce.Code())
  77. assert.Contains(t, ce.Error(), "同级")
  78. }
  79. // TC-0932: M-3 —— rolePermsLevel 严格低于 freshLevel(数值更大)时放行。
  80. func TestGuardRoleLevelAssignable_StrictlyLower_Allowed(t *testing.T) {
  81. ctrl := gomock.NewController(t)
  82. t.Cleanup(ctrl.Finish)
  83. const productCode = "m3_pc_ok"
  84. const callerId = int64(1003)
  85. mockRole := mocks.NewMockSysRoleModel(ctrl)
  86. mockRole.EXPECT().
  87. FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), callerId, productCode).
  88. Return(int64(50), nil).
  89. Times(1)
  90. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole})
  91. caller := &loaders.UserDetails{
  92. UserId: callerId,
  93. Username: "m3_ok_caller",
  94. MemberType: consts.MemberTypeMember,
  95. ProductCode: productCode,
  96. Status: consts.StatusEnabled,
  97. }
  98. err := GuardRoleLevelAssignable(context.Background(), svcCtx, caller, 80)
  99. require.NoError(t, err, "permsLevel=80 严格低于 freshLevel=50(数值更大 = 更低权限)应放行")
  100. }
  101. // TC-0933: M-3 —— SuperAdmin 完全豁免,不触发任何 DB 查询。
  102. func TestGuardRoleLevelAssignable_SuperAdmin_BypassNoDBCall(t *testing.T) {
  103. ctrl := gomock.NewController(t)
  104. t.Cleanup(ctrl.Finish)
  105. mockRole := mocks.NewMockSysRoleModel(ctrl)
  106. // 预期 0 次调用:SuperAdmin 必须短路返回,不能浪费 DB RTT。
  107. mockRole.EXPECT().
  108. FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), gomock.Any(), gomock.Any()).
  109. Times(0)
  110. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole})
  111. caller := &loaders.UserDetails{
  112. UserId: 1, Username: "root", IsSuperAdmin: true, Status: consts.StatusEnabled,
  113. }
  114. err := GuardRoleLevelAssignable(context.Background(), svcCtx, caller, 1)
  115. require.NoError(t, err, "SuperAdmin 必须放行任何 permsLevel")
  116. }
  117. // TC-0934: M-3 —— 产品 ADMIN 拥有全权,豁免 DB 查询。
  118. func TestGuardRoleLevelAssignable_ProductAdmin_BypassNoDBCall(t *testing.T) {
  119. ctrl := gomock.NewController(t)
  120. t.Cleanup(ctrl.Finish)
  121. mockRole := mocks.NewMockSysRoleModel(ctrl)
  122. mockRole.EXPECT().
  123. FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), gomock.Any(), gomock.Any()).
  124. Times(0)
  125. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole})
  126. caller := &loaders.UserDetails{
  127. UserId: 2, Username: "pa", MemberType: consts.MemberTypeAdmin,
  128. ProductCode: "p1", Status: consts.StatusEnabled,
  129. }
  130. err := GuardRoleLevelAssignable(context.Background(), svcCtx, caller, 1)
  131. require.NoError(t, err, "产品 ADMIN 属于全权角色,必须豁免等级校验")
  132. }
  133. // TC-0935: M-3 —— DEVELOPER 同样享有全权豁免。
  134. func TestGuardRoleLevelAssignable_Developer_BypassNoDBCall(t *testing.T) {
  135. ctrl := gomock.NewController(t)
  136. t.Cleanup(ctrl.Finish)
  137. mockRole := mocks.NewMockSysRoleModel(ctrl)
  138. mockRole.EXPECT().
  139. FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), gomock.Any(), gomock.Any()).
  140. Times(0)
  141. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole})
  142. caller := &loaders.UserDetails{
  143. UserId: 3, Username: "dev", MemberType: consts.MemberTypeDeveloper,
  144. ProductCode: "p1", Status: consts.StatusEnabled,
  145. }
  146. err := GuardRoleLevelAssignable(context.Background(), svcCtx, caller, 1)
  147. require.NoError(t, err)
  148. }
  149. // TC-0936: M-3 —— caller 在 DB 里**无任何角色**(ErrNotFound),必须 403,不能默认为 MaxInt64 放行。
  150. // 这里的语义是"没有可分配的角色等级":一个 MEMBER 连自己都没角色,自然不能分配角色给别人。
  151. func TestGuardRoleLevelAssignable_CallerHasNoRole_Rejected(t *testing.T) {
  152. ctrl := gomock.NewController(t)
  153. t.Cleanup(ctrl.Finish)
  154. const productCode = "m3_pc_noRole"
  155. const callerId = int64(1004)
  156. mockRole := mocks.NewMockSysRoleModel(ctrl)
  157. mockRole.EXPECT().
  158. FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), callerId, productCode).
  159. Return(int64(0), sqlx.ErrNotFound).
  160. Times(1)
  161. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole})
  162. caller := &loaders.UserDetails{
  163. UserId: callerId, Username: "m3_no_role", MemberType: consts.MemberTypeMember,
  164. ProductCode: productCode, Status: consts.StatusEnabled,
  165. }
  166. err := GuardRoleLevelAssignable(context.Background(), svcCtx, caller, 99)
  167. require.Error(t, err, "caller 无任何角色时必须拒绝,否则会被误判为 MaxInt64 最低级从而放行任何 permsLevel")
  168. var ce *response.CodeError
  169. require.True(t, errors.As(err, &ce))
  170. assert.Equal(t, 403, ce.Code())
  171. assert.Contains(t, ce.Error(), "没有可分配的角色等级")
  172. }
  173. // TC-0937: M-3 —— DB 抖动(非 ErrNotFound)必须 fail-close 返回 500,不得降级为"无角色 → 放行"。
  174. func TestGuardRoleLevelAssignable_DBError_FailCloseWith500(t *testing.T) {
  175. ctrl := gomock.NewController(t)
  176. t.Cleanup(ctrl.Finish)
  177. const productCode = "m3_pc_dbErr"
  178. const callerId = int64(1005)
  179. mockRole := mocks.NewMockSysRoleModel(ctrl)
  180. mockRole.EXPECT().
  181. FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), callerId, productCode).
  182. Return(int64(0), errors.New("driver: bad connection")).
  183. Times(1)
  184. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole})
  185. caller := &loaders.UserDetails{
  186. UserId: callerId, Username: "m3_db_err", MemberType: consts.MemberTypeMember,
  187. ProductCode: productCode, Status: consts.StatusEnabled,
  188. }
  189. err := GuardRoleLevelAssignable(context.Background(), svcCtx, caller, 10)
  190. require.Error(t, err)
  191. var ce *response.CodeError
  192. require.True(t, errors.As(err, &ce))
  193. assert.Equal(t, 500, ce.Code(),
  194. "M-3:DB 非 ErrNotFound 错误必须 fail-close 500,不能被伪装成 ErrNotFound → 放行超权分配")
  195. }
  196. // TC-0938: M-3 —— nil caller 防御:理论上无登录上下文绝不该进入此函数,防御性路径必须 403 而非 panic。
  197. func TestGuardRoleLevelAssignable_NilCaller_Rejected(t *testing.T) {
  198. ctrl := gomock.NewController(t)
  199. t.Cleanup(ctrl.Finish)
  200. mockRole := mocks.NewMockSysRoleModel(ctrl)
  201. mockRole.EXPECT().
  202. FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), gomock.Any(), gomock.Any()).
  203. Times(0)
  204. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole})
  205. err := GuardRoleLevelAssignable(context.Background(), svcCtx, nil, 10)
  206. require.Error(t, err, "nil caller 必须被拦截,杜绝隐式放行")
  207. var ce *response.CodeError
  208. require.True(t, errors.As(err, &ce))
  209. assert.Equal(t, 403, ce.Code())
  210. }