access_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. package auth
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "math"
  7. "math/rand"
  8. "testing"
  9. "time"
  10. "perms-system-server/internal/consts"
  11. "perms-system-server/internal/loaders"
  12. deptModel "perms-system-server/internal/model/dept"
  13. "perms-system-server/internal/model/productmember"
  14. userModel "perms-system-server/internal/model/user"
  15. "perms-system-server/internal/response"
  16. "perms-system-server/internal/svc"
  17. "perms-system-server/internal/testutil"
  18. "perms-system-server/internal/testutil/ctxhelper"
  19. "github.com/stretchr/testify/assert"
  20. "github.com/stretchr/testify/require"
  21. "github.com/zeromicro/go-zero/core/stores/sqlx"
  22. )
  23. // =====================================================================
  24. // RequireSuperAdmin
  25. // =====================================================================
  26. // TC-0461: 超管通过
  27. func TestRequireSuperAdmin_SuperAdmin(t *testing.T) {
  28. err := RequireSuperAdmin(ctxhelper.SuperAdminCtx())
  29. assert.NoError(t, err)
  30. }
  31. // TC-0462: ADMIN → 403 "仅超级管理员"
  32. func TestRequireSuperAdmin_Admin(t *testing.T) {
  33. err := RequireSuperAdmin(ctxhelper.AdminCtx("p1"))
  34. require.Error(t, err)
  35. var ce *response.CodeError
  36. require.True(t, errors.As(err, &ce))
  37. assert.Equal(t, 403, ce.Code())
  38. assert.Contains(t, ce.Error(), "仅超级管理员")
  39. }
  40. // TC-0463: MEMBER → 403
  41. func TestRequireSuperAdmin_Member(t *testing.T) {
  42. err := RequireSuperAdmin(ctxhelper.MemberCtx("p1"))
  43. require.Error(t, err)
  44. var ce *response.CodeError
  45. require.True(t, errors.As(err, &ce))
  46. assert.Equal(t, 403, ce.Code())
  47. }
  48. // TC-0464: 无 UserDetails → 401 "未登录"
  49. func TestRequireSuperAdmin_NoUserDetails(t *testing.T) {
  50. err := RequireSuperAdmin(context.Background())
  51. require.Error(t, err)
  52. var ce *response.CodeError
  53. require.True(t, errors.As(err, &ce))
  54. assert.Equal(t, 401, ce.Code())
  55. assert.Contains(t, ce.Error(), "未登录")
  56. }
  57. // =====================================================================
  58. // RequireProductAdmin
  59. // =====================================================================
  60. // TC-0465: SuperAdmin → nil
  61. func TestRequireProductAdminFor_SuperAdmin(t *testing.T) {
  62. err := RequireProductAdminFor(ctxhelper.SuperAdminCtx(), "p1")
  63. assert.NoError(t, err)
  64. }
  65. // TC-0466: ADMIN → nil (same product)
  66. func TestRequireProductAdminFor_Admin(t *testing.T) {
  67. err := RequireProductAdminFor(ctxhelper.AdminCtx("p1"), "p1")
  68. assert.NoError(t, err)
  69. }
  70. // TC-0467: DEVELOPER → 403
  71. func TestRequireProductAdminFor_Developer(t *testing.T) {
  72. err := RequireProductAdminFor(ctxhelper.DeveloperCtx("p1"), "p1")
  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. }
  78. // TC-0468: MEMBER → 403
  79. func TestRequireProductAdminFor_Member(t *testing.T) {
  80. err := RequireProductAdminFor(ctxhelper.MemberCtx("p1"), "p1")
  81. require.Error(t, err)
  82. var ce *response.CodeError
  83. require.True(t, errors.As(err, &ce))
  84. assert.Equal(t, 403, ce.Code())
  85. }
  86. // TC-0469: 无 UserDetails → 401
  87. func TestRequireProductAdminFor_NoUserDetails(t *testing.T) {
  88. err := RequireProductAdminFor(context.Background(), "p1")
  89. require.Error(t, err)
  90. var ce *response.CodeError
  91. require.True(t, errors.As(err, &ce))
  92. assert.Equal(t, 401, ce.Code())
  93. assert.Contains(t, ce.Error(), "未登录")
  94. }
  95. // TC-0555: ADMIN 跨产品被拒绝
  96. func TestRequireProductAdminFor_AdminCrossProduct(t *testing.T) {
  97. err := RequireProductAdminFor(ctxhelper.AdminCtx("p1"), "other_product")
  98. require.Error(t, err)
  99. var ce *response.CodeError
  100. require.True(t, errors.As(err, &ce))
  101. assert.Equal(t, 403, ce.Code())
  102. }
  103. // =====================================================================
  104. // CheckMemberTypeAssignment
  105. // =====================================================================
  106. // TC-0470: 超管分配 ADMIN → nil
  107. func TestCheckMemberTypeAssignment_SuperAdminAssignsAdmin(t *testing.T) {
  108. err := CheckMemberTypeAssignment(ctxhelper.SuperAdminCtx(), consts.MemberTypeAdmin)
  109. assert.NoError(t, err)
  110. }
  111. // TC-0471: ADMIN 分配 DEVELOPER → nil
  112. func TestCheckMemberTypeAssignment_AdminAssignsDeveloper(t *testing.T) {
  113. err := CheckMemberTypeAssignment(ctxhelper.AdminCtx("p1"), consts.MemberTypeDeveloper)
  114. assert.NoError(t, err)
  115. }
  116. // TC-0472: ADMIN 分配 ADMIN(同级)→ 403
  117. func TestCheckMemberTypeAssignment_AdminAssignsAdmin(t *testing.T) {
  118. err := CheckMemberTypeAssignment(ctxhelper.AdminCtx("p1"), consts.MemberTypeAdmin)
  119. require.Error(t, err)
  120. var ce *response.CodeError
  121. require.True(t, errors.As(err, &ce))
  122. assert.Equal(t, 403, ce.Code())
  123. }
  124. // TC-0473: DEVELOPER 分配 ADMIN(更高级)→ 403
  125. func TestCheckMemberTypeAssignment_DeveloperAssignsAdmin(t *testing.T) {
  126. err := CheckMemberTypeAssignment(ctxhelper.DeveloperCtx("p1"), consts.MemberTypeAdmin)
  127. require.Error(t, err)
  128. var ce *response.CodeError
  129. require.True(t, errors.As(err, &ce))
  130. assert.Equal(t, 403, ce.Code())
  131. }
  132. // TC-0474: MEMBER 分配 MEMBER(同级)→ 403
  133. func TestCheckMemberTypeAssignment_MemberAssignsMember(t *testing.T) {
  134. err := CheckMemberTypeAssignment(ctxhelper.MemberCtx("p1"), consts.MemberTypeMember)
  135. require.Error(t, err)
  136. var ce *response.CodeError
  137. require.True(t, errors.As(err, &ce))
  138. assert.Equal(t, 403, ce.Code())
  139. }
  140. // TC-0475: 无 UserDetails → 401
  141. func TestCheckMemberTypeAssignment_NoUserDetails(t *testing.T) {
  142. err := CheckMemberTypeAssignment(context.Background(), consts.MemberTypeMember)
  143. require.Error(t, err)
  144. var ce *response.CodeError
  145. require.True(t, errors.As(err, &ce))
  146. assert.Equal(t, 401, ce.Code())
  147. assert.Contains(t, ce.Error(), "未登录")
  148. }
  149. // =====================================================================
  150. // memberTypePriority (未导出,同包可测)
  151. // =====================================================================
  152. // TC-0484: 所有成员类型返回正确优先级
  153. func TestMemberTypePriority(t *testing.T) {
  154. tests := []struct {
  155. name string
  156. memberType string
  157. want int
  158. }{
  159. {"SUPER_ADMIN=0", consts.MemberTypeSuperAdmin, 0},
  160. {"ADMIN=1", consts.MemberTypeAdmin, 1},
  161. {"DEVELOPER=2", consts.MemberTypeDeveloper, 2},
  162. {"MEMBER=3", consts.MemberTypeMember, 3},
  163. {"unknown=MaxInt32", "UNKNOWN_TYPE", math.MaxInt32},
  164. {"empty=MaxInt32", "", math.MaxInt32},
  165. }
  166. for _, tt := range tests {
  167. t.Run(tt.name, func(t *testing.T) {
  168. got := memberTypePriority(tt.memberType)
  169. assert.Equal(t, tt.want, got)
  170. })
  171. }
  172. }
  173. // =====================================================================
  174. // CheckManageAccess (集成测试,需要真实 DB)
  175. // =====================================================================
  176. func newIntegrationSvcCtx() *svc.ServiceContext {
  177. return svc.NewServiceContext(testutil.GetTestConfig())
  178. }
  179. func createTestUser(t *testing.T, ctx context.Context, svcCtx *svc.ServiceContext, conn sqlx.SqlConn, opts struct {
  180. username string
  181. deptId int64
  182. isSuperAdmin int64
  183. }) int64 {
  184. t.Helper()
  185. now := time.Now().Unix()
  186. res, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  187. Username: opts.username,
  188. Password: testutil.HashPassword("test123"),
  189. Nickname: opts.username,
  190. Email: opts.username + "@test.com",
  191. Phone: "",
  192. Remark: "",
  193. DeptId: opts.deptId,
  194. IsSuperAdmin: opts.isSuperAdmin,
  195. MustChangePassword: consts.MustChangePasswordNo,
  196. Status: consts.StatusEnabled,
  197. CreateTime: now,
  198. UpdateTime: now,
  199. })
  200. require.NoError(t, err)
  201. id, _ := res.LastInsertId()
  202. return id
  203. }
  204. // TC-0476: SuperAdmin 跳过所有检查
  205. func TestCheckManageAccess_SuperAdminBypasses(t *testing.T) {
  206. ctx := context.Background()
  207. svcCtx := newIntegrationSvcCtx()
  208. conn := testutil.GetTestSqlConn()
  209. targetId := createTestUser(t, ctx, svcCtx, conn, struct {
  210. username string
  211. deptId int64
  212. isSuperAdmin int64
  213. }{
  214. username: fmt.Sprintf("target_sa_%d", rand.Intn(100000)),
  215. deptId: 0,
  216. isSuperAdmin: consts.IsSuperAdminNo,
  217. })
  218. t.Cleanup(func() {
  219. testutil.CleanTable(ctx, conn, "`sys_user`", targetId)
  220. })
  221. err := CheckManageAccess(ctxhelper.SuperAdminCtx(), svcCtx, targetId, "p1")
  222. assert.NoError(t, err)
  223. }
  224. // TC-0477: 操作自己豁免
  225. func TestCheckManageAccess_SelfManagement(t *testing.T) {
  226. selfCtx := ctxhelper.CustomCtx(&loaders.UserDetails{
  227. UserId: 100,
  228. Username: "self_user",
  229. IsSuperAdmin: false,
  230. MemberType: consts.MemberTypeMember,
  231. Status: consts.StatusEnabled,
  232. ProductCode: "p1",
  233. DeptId: 1,
  234. DeptPath: "/1/",
  235. })
  236. svcCtx := newIntegrationSvcCtx()
  237. err := CheckManageAccess(selfCtx, svcCtx, 100, "p1")
  238. assert.NoError(t, err)
  239. }
  240. // TC-0478: ADMIN 跳过部门层级检查
  241. func TestCheckManageAccess_AdminSkipsDeptCheck(t *testing.T) {
  242. ctx := context.Background()
  243. svcCtx := newIntegrationSvcCtx()
  244. conn := testutil.GetTestSqlConn()
  245. now := time.Now().Unix()
  246. pc := fmt.Sprintf("tp_access_%d", rand.Intn(100000))
  247. targetId := createTestUser(t, ctx, svcCtx, conn, struct {
  248. username string
  249. deptId int64
  250. isSuperAdmin int64
  251. }{
  252. username: fmt.Sprintf("target_admin_%d", rand.Intn(100000)),
  253. deptId: 0,
  254. isSuperAdmin: consts.IsSuperAdminNo,
  255. })
  256. pmRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &productmember.SysProductMember{
  257. ProductCode: pc, UserId: targetId, MemberType: consts.MemberTypeMember,
  258. Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
  259. })
  260. require.NoError(t, err)
  261. pmId, _ := pmRes.LastInsertId()
  262. t.Cleanup(func() {
  263. testutil.CleanTable(ctx, conn, "`sys_product_member`", pmId)
  264. testutil.CleanTable(ctx, conn, "`sys_user`", targetId)
  265. })
  266. adminCtx := ctxhelper.CustomCtx(&loaders.UserDetails{
  267. UserId: 2,
  268. Username: "admin_test",
  269. IsSuperAdmin: false,
  270. MemberType: consts.MemberTypeAdmin,
  271. Status: consts.StatusEnabled,
  272. ProductCode: pc,
  273. DeptId: 999,
  274. DeptPath: "/999/",
  275. MinPermsLevel: math.MaxInt64,
  276. })
  277. err = CheckManageAccess(adminCtx, svcCtx, targetId, pc)
  278. assert.NoError(t, err)
  279. }
  280. // TC-0479: 无 UserDetails → 401
  281. func TestCheckManageAccess_NoUserDetails(t *testing.T) {
  282. svcCtx := newIntegrationSvcCtx()
  283. err := CheckManageAccess(context.Background(), svcCtx, 1, "p1")
  284. require.Error(t, err)
  285. var ce *response.CodeError
  286. require.True(t, errors.As(err, &ce))
  287. assert.Equal(t, 401, ce.Code())
  288. }
  289. // TC-0480: DEVELOPER 无部门归属 → 403
  290. func TestCheckManageAccess_NoDept(t *testing.T) {
  291. svcCtx := newIntegrationSvcCtx()
  292. noDeptCtx := ctxhelper.CustomCtx(&loaders.UserDetails{
  293. UserId: 3,
  294. Username: "dev_nodept",
  295. IsSuperAdmin: false,
  296. MemberType: consts.MemberTypeDeveloper,
  297. Status: consts.StatusEnabled,
  298. ProductCode: "p1",
  299. DeptId: 0,
  300. DeptPath: "",
  301. MinPermsLevel: math.MaxInt64,
  302. })
  303. err := CheckManageAccess(noDeptCtx, svcCtx, 99999, "p1")
  304. require.Error(t, err)
  305. var ce *response.CodeError
  306. require.True(t, errors.As(err, &ce))
  307. assert.Equal(t, 403, ce.Code())
  308. assert.Contains(t, ce.Error(), "未归属任何部门")
  309. }
  310. // TC-0481: DEVELOPER 操作不同部门的用户 → 403
  311. func TestCheckManageAccess_CrossDeptForbidden(t *testing.T) {
  312. ctx := context.Background()
  313. svcCtx := newIntegrationSvcCtx()
  314. conn := testutil.GetTestSqlConn()
  315. now := time.Now().Unix()
  316. dept1Res, err := svcCtx.SysDeptModel.Insert(ctx, &deptModel.SysDept{
  317. ParentId: 0, Name: fmt.Sprintf("dept_a_%d", rand.Intn(100000)),
  318. Path: fmt.Sprintf("/%d/", rand.Intn(100000)),
  319. Sort: 1, DeptType: consts.DeptTypeNormal, Status: consts.StatusEnabled,
  320. CreateTime: now, UpdateTime: now,
  321. })
  322. require.NoError(t, err)
  323. dept1Id, _ := dept1Res.LastInsertId()
  324. dept2Res, err := svcCtx.SysDeptModel.Insert(ctx, &deptModel.SysDept{
  325. ParentId: 0, Name: fmt.Sprintf("dept_b_%d", rand.Intn(100000)),
  326. Path: fmt.Sprintf("/%d/", rand.Intn(100000)),
  327. Sort: 1, DeptType: consts.DeptTypeNormal, Status: consts.StatusEnabled,
  328. CreateTime: now, UpdateTime: now,
  329. })
  330. require.NoError(t, err)
  331. dept2Id, _ := dept2Res.LastInsertId()
  332. targetId := createTestUser(t, ctx, svcCtx, conn, struct {
  333. username string
  334. deptId int64
  335. isSuperAdmin int64
  336. }{
  337. username: fmt.Sprintf("target_crossdept_%d", rand.Intn(100000)),
  338. deptId: dept2Id,
  339. isSuperAdmin: consts.IsSuperAdminNo,
  340. })
  341. dept2, err := svcCtx.SysDeptModel.FindOne(ctx, dept2Id)
  342. require.NoError(t, err)
  343. dept1, err := svcCtx.SysDeptModel.FindOne(ctx, dept1Id)
  344. require.NoError(t, err)
  345. t.Cleanup(func() {
  346. testutil.CleanTable(ctx, conn, "`sys_user`", targetId)
  347. testutil.CleanTable(ctx, conn, "`sys_dept`", dept1Id, dept2Id)
  348. })
  349. callerCtx := ctxhelper.CustomCtx(&loaders.UserDetails{
  350. UserId: 99998,
  351. Username: "dev_crossdept",
  352. IsSuperAdmin: false,
  353. MemberType: consts.MemberTypeDeveloper,
  354. Status: consts.StatusEnabled,
  355. ProductCode: "p1",
  356. DeptId: dept1Id,
  357. DeptPath: dept1.Path,
  358. MinPermsLevel: math.MaxInt64,
  359. })
  360. _ = dept2
  361. err = CheckManageAccess(callerCtx, svcCtx, targetId, "p1")
  362. require.Error(t, err)
  363. var ce *response.CodeError
  364. require.True(t, errors.As(err, &ce))
  365. assert.Equal(t, 403, ce.Code())
  366. }
  367. // TC-0483: caller.DeptPath为空时拒绝
  368. func TestCheckManageAccess_EmptyDeptPath(t *testing.T) {
  369. svcCtx := newIntegrationSvcCtx()
  370. emptyPathCtx := ctxhelper.CustomCtx(&loaders.UserDetails{
  371. UserId: 88888,
  372. Username: "dev_emptypath",
  373. IsSuperAdmin: false,
  374. MemberType: consts.MemberTypeDeveloper,
  375. Status: consts.StatusEnabled,
  376. ProductCode: "p1",
  377. DeptId: 1,
  378. DeptPath: "",
  379. MinPermsLevel: math.MaxInt64,
  380. })
  381. err := CheckManageAccess(emptyPathCtx, svcCtx, 99999, "p1")
  382. require.Error(t, err)
  383. var ce *response.CodeError
  384. require.True(t, errors.As(err, &ce))
  385. assert.Equal(t, 403, ce.Code())
  386. assert.Contains(t, ce.Error(), "部门信息异常")
  387. }
  388. // TC-0482: DEVELOPER 操作同部门的 MEMBER 且权限级别更高 → nil
  389. func TestCheckManageAccess_SameDeptLowerLevel(t *testing.T) {
  390. ctx := context.Background()
  391. svcCtx := newIntegrationSvcCtx()
  392. conn := testutil.GetTestSqlConn()
  393. now := time.Now().Unix()
  394. pc := fmt.Sprintf("tp_samedept_%d", rand.Intn(100000))
  395. deptRes, err := svcCtx.SysDeptModel.Insert(ctx, &deptModel.SysDept{
  396. ParentId: 0, Name: fmt.Sprintf("dept_same_%d", rand.Intn(100000)),
  397. Path: "/", Sort: 1, DeptType: consts.DeptTypeNormal, Status: consts.StatusEnabled,
  398. CreateTime: now, UpdateTime: now,
  399. })
  400. require.NoError(t, err)
  401. deptId, _ := deptRes.LastInsertId()
  402. deptObj, err := svcCtx.SysDeptModel.FindOne(ctx, deptId)
  403. require.NoError(t, err)
  404. targetId := createTestUser(t, ctx, svcCtx, conn, struct {
  405. username string
  406. deptId int64
  407. isSuperAdmin int64
  408. }{
  409. username: fmt.Sprintf("target_samedept_%d", rand.Intn(100000)),
  410. deptId: deptId,
  411. isSuperAdmin: consts.IsSuperAdminNo,
  412. })
  413. pmRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &productmember.SysProductMember{
  414. ProductCode: pc, UserId: targetId, MemberType: consts.MemberTypeMember,
  415. Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
  416. })
  417. require.NoError(t, err)
  418. pmId, _ := pmRes.LastInsertId()
  419. t.Cleanup(func() {
  420. testutil.CleanTable(ctx, conn, "`sys_product_member`", pmId)
  421. testutil.CleanTable(ctx, conn, "`sys_user`", targetId)
  422. testutil.CleanTable(ctx, conn, "`sys_dept`", deptId)
  423. })
  424. callerCtx := ctxhelper.CustomCtx(&loaders.UserDetails{
  425. UserId: 99997,
  426. Username: "dev_samedept",
  427. IsSuperAdmin: false,
  428. MemberType: consts.MemberTypeDeveloper,
  429. Status: consts.StatusEnabled,
  430. ProductCode: pc,
  431. DeptId: deptId,
  432. DeptPath: deptObj.Path,
  433. MinPermsLevel: 1,
  434. })
  435. err = CheckManageAccess(callerCtx, svcCtx, targetId, pc)
  436. assert.NoError(t, err)
  437. }
  438. // suppress unused import
  439. var _ = sqlx.ErrNotFound