loginServiceConstantTime_audit_test.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. package pub
  2. import (
  3. "context"
  4. "errors"
  5. "testing"
  6. "perms-system-server/internal/testutil"
  7. "github.com/stretchr/testify/assert"
  8. "github.com/stretchr/testify/require"
  9. )
  10. // ---------------------------------------------------------------------------
  11. // 覆盖目标:审计第 6 轮 H-2 修复回归。
  12. //
  13. // H-2 的本质:`ValidateProductLogin` 里"账号冻结"、"是超管"、"用户不存在"三条
  14. // 错误路径在修复前存在 **耗时 + 错误消息 + HTTP code** 三重差异,构成
  15. // 账号存在性 / 状态 oracle。修复后:
  16. // - bcrypt 无条件执行(dummy hash 对齐耗时)
  17. // - 只有密码正确之后才披露"冻结"/"超管"语义
  18. // - 用户名不存在 / 存在但密码错 / 存在但冻结且密码错 → 统一 401 "用户名或密码错误"
  19. //
  20. // 这些用例把上面契约全部钉死:任何一条被回退到"先检查 status 再 bcrypt"的旧路径,
  21. // 对应 TC 立刻 FAIL。
  22. // ---------------------------------------------------------------------------
  23. // TC-0838: 冻结用户 + 错误密码 —— 必须返回 401 "用户名或密码错误",禁止泄露冻结态。
  24. func TestValidateProductLogin_FrozenWrongPassword_Return401(t *testing.T) {
  25. ctx := context.Background()
  26. svcCtx := newTestSvcCtx()
  27. svcCtx.UsernameLoginLimit = nil // 隔离本测试变量
  28. username := "h2_frozen_wrong_" + testutil.UniqueId()
  29. // status=2(冻结),isSuperAdmin=2(非超管)
  30. _, clean := insertRefreshTestUser(t, ctx, username, "CorrectPass123", 2, 2)
  31. t.Cleanup(clean)
  32. _, err := ValidateProductLogin(ctx, svcCtx, username, "WrongPass", "test_product", "127.0.0.1")
  33. require.Error(t, err)
  34. var le *LoginError
  35. require.True(t, errors.As(err, &le))
  36. assert.Equal(t, 401, le.Code,
  37. "H-2:冻结用户 + 错误密码不得返回 403;必须 401 与'用户不存在/密码错'三合一")
  38. assert.Equal(t, "用户名或密码错误", le.Message,
  39. "H-2:文案不得泄露冻结态")
  40. }
  41. // TC-0839: 冻结用户 + 正确密码 —— 此时才允许披露"账号已被冻结"(攻击者已经猜中密码,继续隐藏已无意义)。
  42. func TestValidateProductLogin_FrozenCorrectPassword_Return403(t *testing.T) {
  43. ctx := context.Background()
  44. svcCtx := newTestSvcCtx()
  45. svcCtx.UsernameLoginLimit = nil
  46. username := "h2_frozen_right_" + testutil.UniqueId()
  47. _, clean := insertRefreshTestUser(t, ctx, username, "RightPass123", 2, 2)
  48. t.Cleanup(clean)
  49. _, err := ValidateProductLogin(ctx, svcCtx, username, "RightPass123", "test_product", "127.0.0.1")
  50. require.Error(t, err)
  51. var le *LoginError
  52. require.True(t, errors.As(err, &le))
  53. assert.Equal(t, 403, le.Code, "H-2:密码正确后的冻结分支仍走 403 披露")
  54. assert.Equal(t, "账号已被冻结", le.Message)
  55. }
  56. // TC-0840: 超管走产品端 + 错误密码 —— 不得提前暴露"该账号是超管"。
  57. func TestValidateProductLogin_SuperAdminWrongPassword_Return401(t *testing.T) {
  58. ctx := context.Background()
  59. svcCtx := newTestSvcCtx()
  60. svcCtx.UsernameLoginLimit = nil
  61. username := "h2_sa_wrong_" + testutil.UniqueId()
  62. // status=1(启用),isSuperAdmin=1(超管)
  63. _, clean := insertRefreshTestUser(t, ctx, username, "RightPass123", 1, 1)
  64. t.Cleanup(clean)
  65. _, err := ValidateProductLogin(ctx, svcCtx, username, "WrongPass", "test_product", "127.0.0.1")
  66. require.Error(t, err)
  67. var le *LoginError
  68. require.True(t, errors.As(err, &le))
  69. assert.Equal(t, 401, le.Code,
  70. "H-2:超管 + 错误密码必须归一到 401'用户名或密码错误',不得提前披露超管身份")
  71. assert.Equal(t, "用户名或密码错误", le.Message)
  72. }
  73. // TC-0841: 超管走产品端 + 正确密码 —— 密码正确后才披露"超管请走管理后台"。
  74. func TestValidateProductLogin_SuperAdminCorrectPassword_Return403(t *testing.T) {
  75. ctx := context.Background()
  76. svcCtx := newTestSvcCtx()
  77. svcCtx.UsernameLoginLimit = nil
  78. username := "h2_sa_right_" + testutil.UniqueId()
  79. _, clean := insertRefreshTestUser(t, ctx, username, "RightPass123", 1, 1)
  80. t.Cleanup(clean)
  81. _, err := ValidateProductLogin(ctx, svcCtx, username, "RightPass123", "test_product", "127.0.0.1")
  82. require.Error(t, err)
  83. var le *LoginError
  84. require.True(t, errors.As(err, &le))
  85. assert.Equal(t, 403, le.Code)
  86. assert.Equal(t, "超级管理员不允许通过产品端登录,请使用管理后台", le.Message)
  87. }
  88. // TC-0842: 用户名不存在 —— 沿用 dummy bcrypt 恒时对齐,文案必须与 TC-0838 完全一致。
  89. // 这条是三重 oracle 消除的"对照组",缺了它单看 0838/0840 还不能证明"三路归一"。
  90. func TestValidateProductLogin_UnknownUserSame401(t *testing.T) {
  91. ctx := context.Background()
  92. svcCtx := newTestSvcCtx()
  93. svcCtx.UsernameLoginLimit = nil
  94. _, err := ValidateProductLogin(ctx, svcCtx, "h2_noexist_"+testutil.UniqueId(), "anypwd", "test_product", "127.0.0.1")
  95. require.Error(t, err)
  96. var le *LoginError
  97. require.True(t, errors.As(err, &le))
  98. assert.Equal(t, 401, le.Code)
  99. assert.Equal(t, "用户名或密码错误", le.Message,
  100. "H-2:未知用户 / 冻结+错密 / 存在+错密 三路必须归一为同一 401 + 文案")
  101. }