access_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  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-0435: 超管通过
  27. func TestRequireSuperAdmin_SuperAdmin(t *testing.T) {
  28. err := RequireSuperAdmin(ctxhelper.SuperAdminCtx())
  29. assert.NoError(t, err)
  30. }
  31. // TC-0436: 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-0437: 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-0438: 无 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-0439: SuperAdmin → nil
  61. func TestRequireProductAdmin_SuperAdmin(t *testing.T) {
  62. err := RequireProductAdmin(ctxhelper.SuperAdminCtx())
  63. assert.NoError(t, err)
  64. }
  65. // TC-0440: ADMIN → nil
  66. func TestRequireProductAdmin_Admin(t *testing.T) {
  67. err := RequireProductAdmin(ctxhelper.AdminCtx("p1"))
  68. assert.NoError(t, err)
  69. }
  70. // TC-0441: DEVELOPER → 403
  71. func TestRequireProductAdmin_Developer(t *testing.T) {
  72. err := RequireProductAdmin(ctxhelper.DeveloperCtx("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-0442: MEMBER → 403
  79. func TestRequireProductAdmin_Member(t *testing.T) {
  80. err := RequireProductAdmin(ctxhelper.MemberCtx("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-0443: 无 UserDetails → 401
  87. func TestRequireProductAdmin_NoUserDetails(t *testing.T) {
  88. err := RequireProductAdmin(context.Background())
  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. // =====================================================================
  96. // CheckMemberTypeAssignment
  97. // =====================================================================
  98. // TC-0444: 超管分配 ADMIN → nil
  99. func TestCheckMemberTypeAssignment_SuperAdminAssignsAdmin(t *testing.T) {
  100. err := CheckMemberTypeAssignment(ctxhelper.SuperAdminCtx(), consts.MemberTypeAdmin)
  101. assert.NoError(t, err)
  102. }
  103. // TC-0445: ADMIN 分配 DEVELOPER → nil
  104. func TestCheckMemberTypeAssignment_AdminAssignsDeveloper(t *testing.T) {
  105. err := CheckMemberTypeAssignment(ctxhelper.AdminCtx("p1"), consts.MemberTypeDeveloper)
  106. assert.NoError(t, err)
  107. }
  108. // TC-0446: ADMIN 分配 ADMIN(同级)→ 403
  109. func TestCheckMemberTypeAssignment_AdminAssignsAdmin(t *testing.T) {
  110. err := CheckMemberTypeAssignment(ctxhelper.AdminCtx("p1"), consts.MemberTypeAdmin)
  111. require.Error(t, err)
  112. var ce *response.CodeError
  113. require.True(t, errors.As(err, &ce))
  114. assert.Equal(t, 403, ce.Code())
  115. }
  116. // TC-0447: DEVELOPER 分配 ADMIN(更高级)→ 403
  117. func TestCheckMemberTypeAssignment_DeveloperAssignsAdmin(t *testing.T) {
  118. err := CheckMemberTypeAssignment(ctxhelper.DeveloperCtx("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-0448: MEMBER 分配 MEMBER(同级)→ 403
  125. func TestCheckMemberTypeAssignment_MemberAssignsMember(t *testing.T) {
  126. err := CheckMemberTypeAssignment(ctxhelper.MemberCtx("p1"), consts.MemberTypeMember)
  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-0449: 无 UserDetails → 401
  133. func TestCheckMemberTypeAssignment_NoUserDetails(t *testing.T) {
  134. err := CheckMemberTypeAssignment(context.Background(), consts.MemberTypeMember)
  135. require.Error(t, err)
  136. var ce *response.CodeError
  137. require.True(t, errors.As(err, &ce))
  138. assert.Equal(t, 401, ce.Code())
  139. assert.Contains(t, ce.Error(), "未登录")
  140. }
  141. // =====================================================================
  142. // memberTypePriority (未导出,同包可测)
  143. // =====================================================================
  144. // TC-0457: 所有成员类型返回正确优先级
  145. func TestMemberTypePriority(t *testing.T) {
  146. tests := []struct {
  147. name string
  148. memberType string
  149. want int
  150. }{
  151. {"SUPER_ADMIN=0", consts.MemberTypeSuperAdmin, 0},
  152. {"ADMIN=1", consts.MemberTypeAdmin, 1},
  153. {"DEVELOPER=2", consts.MemberTypeDeveloper, 2},
  154. {"MEMBER=3", consts.MemberTypeMember, 3},
  155. {"unknown=MaxInt32", "UNKNOWN_TYPE", math.MaxInt32},
  156. {"empty=MaxInt32", "", math.MaxInt32},
  157. }
  158. for _, tt := range tests {
  159. t.Run(tt.name, func(t *testing.T) {
  160. got := memberTypePriority(tt.memberType)
  161. assert.Equal(t, tt.want, got)
  162. })
  163. }
  164. }
  165. // =====================================================================
  166. // CheckManageAccess (集成测试,需要真实 DB)
  167. // =====================================================================
  168. func newIntegrationSvcCtx() *svc.ServiceContext {
  169. return svc.NewServiceContext(testutil.GetTestConfig())
  170. }
  171. func createTestUser(t *testing.T, ctx context.Context, svcCtx *svc.ServiceContext, conn sqlx.SqlConn, opts struct {
  172. username string
  173. deptId int64
  174. isSuperAdmin int64
  175. }) int64 {
  176. t.Helper()
  177. now := time.Now().Unix()
  178. res, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  179. Username: opts.username,
  180. Password: testutil.HashPassword("test123"),
  181. Nickname: opts.username,
  182. Email: opts.username + "@test.com",
  183. Phone: "",
  184. Remark: "",
  185. DeptId: opts.deptId,
  186. IsSuperAdmin: opts.isSuperAdmin,
  187. MustChangePassword: consts.MustChangePasswordNo,
  188. Status: consts.StatusEnabled,
  189. CreateTime: now,
  190. UpdateTime: now,
  191. })
  192. require.NoError(t, err)
  193. id, _ := res.LastInsertId()
  194. return id
  195. }
  196. // TC-0450: SuperAdmin 跳过所有检查
  197. func TestCheckManageAccess_SuperAdminBypasses(t *testing.T) {
  198. ctx := context.Background()
  199. svcCtx := newIntegrationSvcCtx()
  200. conn := testutil.GetTestSqlConn()
  201. targetId := createTestUser(t, ctx, svcCtx, conn, struct {
  202. username string
  203. deptId int64
  204. isSuperAdmin int64
  205. }{
  206. username: fmt.Sprintf("target_sa_%d", rand.Intn(100000)),
  207. deptId: 0,
  208. isSuperAdmin: consts.IsSuperAdminNo,
  209. })
  210. t.Cleanup(func() {
  211. testutil.CleanTable(ctx, conn, "`sys_user`", targetId)
  212. })
  213. err := CheckManageAccess(ctxhelper.SuperAdminCtx(), svcCtx, targetId, "p1")
  214. assert.NoError(t, err)
  215. }
  216. // TC-0451: 操作自己豁免
  217. func TestCheckManageAccess_SelfManagement(t *testing.T) {
  218. selfCtx := ctxhelper.CustomCtx(&loaders.UserDetails{
  219. UserId: 100,
  220. Username: "self_user",
  221. IsSuperAdmin: false,
  222. MemberType: consts.MemberTypeMember,
  223. Status: consts.StatusEnabled,
  224. ProductCode: "p1",
  225. DeptId: 1,
  226. DeptPath: "/1/",
  227. })
  228. svcCtx := newIntegrationSvcCtx()
  229. err := CheckManageAccess(selfCtx, svcCtx, 100, "p1")
  230. assert.NoError(t, err)
  231. }
  232. // TC-0452: ADMIN 跳过部门层级检查
  233. func TestCheckManageAccess_AdminSkipsDeptCheck(t *testing.T) {
  234. ctx := context.Background()
  235. svcCtx := newIntegrationSvcCtx()
  236. conn := testutil.GetTestSqlConn()
  237. now := time.Now().Unix()
  238. pc := fmt.Sprintf("tp_access_%d", rand.Intn(100000))
  239. targetId := createTestUser(t, ctx, svcCtx, conn, struct {
  240. username string
  241. deptId int64
  242. isSuperAdmin int64
  243. }{
  244. username: fmt.Sprintf("target_admin_%d", rand.Intn(100000)),
  245. deptId: 0,
  246. isSuperAdmin: consts.IsSuperAdminNo,
  247. })
  248. pmRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &productmember.SysProductMember{
  249. ProductCode: pc, UserId: targetId, MemberType: consts.MemberTypeMember,
  250. Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
  251. })
  252. require.NoError(t, err)
  253. pmId, _ := pmRes.LastInsertId()
  254. t.Cleanup(func() {
  255. testutil.CleanTable(ctx, conn, "`sys_product_member`", pmId)
  256. testutil.CleanTable(ctx, conn, "`sys_user`", targetId)
  257. })
  258. adminCtx := ctxhelper.CustomCtx(&loaders.UserDetails{
  259. UserId: 2,
  260. Username: "admin_test",
  261. IsSuperAdmin: false,
  262. MemberType: consts.MemberTypeAdmin,
  263. Status: consts.StatusEnabled,
  264. ProductCode: pc,
  265. DeptId: 999,
  266. DeptPath: "/999/",
  267. MinPermsLevel: math.MaxInt64,
  268. })
  269. err = CheckManageAccess(adminCtx, svcCtx, targetId, pc)
  270. assert.NoError(t, err)
  271. }
  272. // TC-0453: 无 UserDetails → 401
  273. func TestCheckManageAccess_NoUserDetails(t *testing.T) {
  274. svcCtx := newIntegrationSvcCtx()
  275. err := CheckManageAccess(context.Background(), svcCtx, 1, "p1")
  276. require.Error(t, err)
  277. var ce *response.CodeError
  278. require.True(t, errors.As(err, &ce))
  279. assert.Equal(t, 401, ce.Code())
  280. }
  281. // TC-0454: DEVELOPER 无部门归属 → 403
  282. func TestCheckManageAccess_NoDept(t *testing.T) {
  283. svcCtx := newIntegrationSvcCtx()
  284. noDeptCtx := ctxhelper.CustomCtx(&loaders.UserDetails{
  285. UserId: 3,
  286. Username: "dev_nodept",
  287. IsSuperAdmin: false,
  288. MemberType: consts.MemberTypeDeveloper,
  289. Status: consts.StatusEnabled,
  290. ProductCode: "p1",
  291. DeptId: 0,
  292. DeptPath: "",
  293. MinPermsLevel: math.MaxInt64,
  294. })
  295. err := CheckManageAccess(noDeptCtx, svcCtx, 99999, "p1")
  296. require.Error(t, err)
  297. var ce *response.CodeError
  298. require.True(t, errors.As(err, &ce))
  299. assert.Equal(t, 403, ce.Code())
  300. assert.Contains(t, ce.Error(), "未归属任何部门")
  301. }
  302. // TC-0455: DEVELOPER 操作不同部门的用户 → 403
  303. func TestCheckManageAccess_CrossDeptForbidden(t *testing.T) {
  304. ctx := context.Background()
  305. svcCtx := newIntegrationSvcCtx()
  306. conn := testutil.GetTestSqlConn()
  307. now := time.Now().Unix()
  308. dept1Res, err := svcCtx.SysDeptModel.Insert(ctx, &deptModel.SysDept{
  309. ParentId: 0, Name: fmt.Sprintf("dept_a_%d", rand.Intn(100000)),
  310. Path: fmt.Sprintf("/%d/", rand.Intn(100000)),
  311. Sort: 1, DeptType: consts.DeptTypeNormal, Status: consts.StatusEnabled,
  312. CreateTime: now, UpdateTime: now,
  313. })
  314. require.NoError(t, err)
  315. dept1Id, _ := dept1Res.LastInsertId()
  316. dept2Res, err := svcCtx.SysDeptModel.Insert(ctx, &deptModel.SysDept{
  317. ParentId: 0, Name: fmt.Sprintf("dept_b_%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. dept2Id, _ := dept2Res.LastInsertId()
  324. targetId := createTestUser(t, ctx, svcCtx, conn, struct {
  325. username string
  326. deptId int64
  327. isSuperAdmin int64
  328. }{
  329. username: fmt.Sprintf("target_crossdept_%d", rand.Intn(100000)),
  330. deptId: dept2Id,
  331. isSuperAdmin: consts.IsSuperAdminNo,
  332. })
  333. dept2, err := svcCtx.SysDeptModel.FindOne(ctx, dept2Id)
  334. require.NoError(t, err)
  335. dept1, err := svcCtx.SysDeptModel.FindOne(ctx, dept1Id)
  336. require.NoError(t, err)
  337. t.Cleanup(func() {
  338. testutil.CleanTable(ctx, conn, "`sys_user`", targetId)
  339. testutil.CleanTable(ctx, conn, "`sys_dept`", dept1Id, dept2Id)
  340. })
  341. callerCtx := ctxhelper.CustomCtx(&loaders.UserDetails{
  342. UserId: 99998,
  343. Username: "dev_crossdept",
  344. IsSuperAdmin: false,
  345. MemberType: consts.MemberTypeDeveloper,
  346. Status: consts.StatusEnabled,
  347. ProductCode: "p1",
  348. DeptId: dept1Id,
  349. DeptPath: dept1.Path,
  350. MinPermsLevel: math.MaxInt64,
  351. })
  352. _ = dept2
  353. err = CheckManageAccess(callerCtx, svcCtx, targetId, "p1")
  354. require.Error(t, err)
  355. var ce *response.CodeError
  356. require.True(t, errors.As(err, &ce))
  357. assert.Equal(t, 403, ce.Code())
  358. }
  359. // TC-0523: caller.DeptPath为空时拒绝
  360. func TestCheckManageAccess_EmptyDeptPath(t *testing.T) {
  361. svcCtx := newIntegrationSvcCtx()
  362. emptyPathCtx := ctxhelper.CustomCtx(&loaders.UserDetails{
  363. UserId: 88888,
  364. Username: "dev_emptypath",
  365. IsSuperAdmin: false,
  366. MemberType: consts.MemberTypeDeveloper,
  367. Status: consts.StatusEnabled,
  368. ProductCode: "p1",
  369. DeptId: 1,
  370. DeptPath: "",
  371. MinPermsLevel: math.MaxInt64,
  372. })
  373. err := CheckManageAccess(emptyPathCtx, svcCtx, 99999, "p1")
  374. require.Error(t, err)
  375. var ce *response.CodeError
  376. require.True(t, errors.As(err, &ce))
  377. assert.Equal(t, 403, ce.Code())
  378. assert.Contains(t, ce.Error(), "部门信息异常")
  379. }
  380. // TC-0456: DEVELOPER 操作同部门的 MEMBER 且权限级别更高 → nil
  381. func TestCheckManageAccess_SameDeptLowerLevel(t *testing.T) {
  382. ctx := context.Background()
  383. svcCtx := newIntegrationSvcCtx()
  384. conn := testutil.GetTestSqlConn()
  385. now := time.Now().Unix()
  386. pc := fmt.Sprintf("tp_samedept_%d", rand.Intn(100000))
  387. deptRes, err := svcCtx.SysDeptModel.Insert(ctx, &deptModel.SysDept{
  388. ParentId: 0, Name: fmt.Sprintf("dept_same_%d", rand.Intn(100000)),
  389. Path: "/", Sort: 1, DeptType: consts.DeptTypeNormal, Status: consts.StatusEnabled,
  390. CreateTime: now, UpdateTime: now,
  391. })
  392. require.NoError(t, err)
  393. deptId, _ := deptRes.LastInsertId()
  394. deptObj, err := svcCtx.SysDeptModel.FindOne(ctx, deptId)
  395. require.NoError(t, err)
  396. targetId := createTestUser(t, ctx, svcCtx, conn, struct {
  397. username string
  398. deptId int64
  399. isSuperAdmin int64
  400. }{
  401. username: fmt.Sprintf("target_samedept_%d", rand.Intn(100000)),
  402. deptId: deptId,
  403. isSuperAdmin: consts.IsSuperAdminNo,
  404. })
  405. pmRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &productmember.SysProductMember{
  406. ProductCode: pc, UserId: targetId, MemberType: consts.MemberTypeMember,
  407. Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now,
  408. })
  409. require.NoError(t, err)
  410. pmId, _ := pmRes.LastInsertId()
  411. t.Cleanup(func() {
  412. testutil.CleanTable(ctx, conn, "`sys_product_member`", pmId)
  413. testutil.CleanTable(ctx, conn, "`sys_user`", targetId)
  414. testutil.CleanTable(ctx, conn, "`sys_dept`", deptId)
  415. })
  416. callerCtx := ctxhelper.CustomCtx(&loaders.UserDetails{
  417. UserId: 99997,
  418. Username: "dev_samedept",
  419. IsSuperAdmin: false,
  420. MemberType: consts.MemberTypeDeveloper,
  421. Status: consts.StatusEnabled,
  422. ProductCode: pc,
  423. DeptId: deptId,
  424. DeptPath: deptObj.Path,
  425. MinPermsLevel: 1,
  426. })
  427. err = CheckManageAccess(callerCtx, svcCtx, targetId, pc)
  428. assert.NoError(t, err)
  429. }
  430. // suppress unused import
  431. var _ = sqlx.ErrNotFound