adminLoginTiming_audit_test.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. package pub
  2. import (
  3. "context"
  4. "errors"
  5. "testing"
  6. "time"
  7. "perms-system-server/internal/response"
  8. "perms-system-server/internal/testutil"
  9. "perms-system-server/internal/types"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/stretchr/testify/require"
  12. )
  13. // ---------------------------------------------------------------------------
  14. // 覆盖目标:审计 L-N3 修复 —— AdminLogin 在 IsSuperAdmin 分支也必须强制走一次 dummy bcrypt,
  15. // 保证三条错误分支的耗时近似齐平,关闭"靠耗时差筛超管账号"的时序 oracle。
  16. // 分支:
  17. // (A) 用户不存在 → dummy bcrypt → 401
  18. // (B) 用户存在但非超管 → dummy bcrypt → 401 (L-N3 关键修复)
  19. // (C) 用户存在且是超管但密码错 → 真 bcrypt → 401
  20. //
  21. // 三条路径的错误响应(code + body)必须 **完全一致**;耗时必须处于同一数量级
  22. // (均经过一次 bcrypt)。
  23. // ---------------------------------------------------------------------------
  24. // TC-1008: L-N3 —— 非超管账号 + 错误密码 必须和"用户不存在"返回完全相同的 401 body。
  25. func TestAdminLogin_LN3_NonSuperAdminWrongPassword_IndistinguishableFromAbsent(t *testing.T) {
  26. ctx := context.Background()
  27. svcCtx := newTestSvcCtx()
  28. svcCtx.UsernameLoginLimit = nil
  29. username := "ln3_nonsa_" + testutil.UniqueId()
  30. // status=1(启用),isSuperAdmin=2(普通用户)
  31. _, clean := insertTestUser(t, ctx, svcCtx, username, "RightPass123", 1, 2)
  32. t.Cleanup(clean)
  33. logic := NewAdminLoginLogic(ctx, svcCtx)
  34. // (B) 用户存在但非超管 —— 走 L-N3 新增的 dummy bcrypt 分支
  35. _, errExisting := logic.AdminLogin(&types.AdminLoginReq{
  36. Username: username,
  37. Password: "WrongPass",
  38. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  39. })
  40. require.Error(t, errExisting)
  41. var ceB *response.CodeError
  42. require.True(t, errors.As(errExisting, &ceB))
  43. // (A) 用户不存在 —— 原有 dummy bcrypt 分支
  44. _, errAbsent := logic.AdminLogin(&types.AdminLoginReq{
  45. Username: "ln3_absent_" + testutil.UniqueId(),
  46. Password: "WhateverPass",
  47. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  48. })
  49. require.Error(t, errAbsent)
  50. var ceA *response.CodeError
  51. require.True(t, errors.As(errAbsent, &ceA))
  52. assert.Equal(t, ceA.Code(), ceB.Code(),
  53. "L-N3:'非超管 + 错误密码' 与 '用户不存在' 必须返回相同 code")
  54. assert.Equal(t, ceA.Error(), ceB.Error(),
  55. "L-N3:'非超管 + 错误密码' 与 '用户不存在' 必须返回相同 body")
  56. assert.Equal(t, "用户名或密码错误", ceB.Error())
  57. }
  58. // TC-1009: L-N3 —— 非超管账号 + 任意密码(包括正确密码)都必须 401,且仍触发一次 bcrypt,
  59. // 保证即使攻击者命中密码,也不得通过 response 推断该账号是"存在的普通用户"。
  60. func TestAdminLogin_LN3_NonSuperAdminCorrectPassword_Still401(t *testing.T) {
  61. ctx := context.Background()
  62. svcCtx := newTestSvcCtx()
  63. svcCtx.UsernameLoginLimit = nil
  64. username := "ln3_cp_" + testutil.UniqueId()
  65. password := "KnownPass123"
  66. _, clean := insertTestUser(t, ctx, svcCtx, username, password, 1, 2)
  67. t.Cleanup(clean)
  68. _, err := NewAdminLoginLogic(ctx, svcCtx).AdminLogin(&types.AdminLoginReq{
  69. Username: username,
  70. Password: password,
  71. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  72. })
  73. require.Error(t, err)
  74. var ce *response.CodeError
  75. require.True(t, errors.As(err, &ce))
  76. assert.Equal(t, 401, ce.Code(),
  77. "L-N3:非超管走 AdminLogin 一律 401,即使密码正确也不得披露账号存在性")
  78. assert.Equal(t, "用户名或密码错误", ce.Error())
  79. }
  80. // TC-1010: L-N3 时序等齐 —— "非超管 + 错密码" 必须与 "用户不存在" 同阶(两者都走一次 dummyBcryptHash)。
  81. //
  82. // 注意:测试环境里通过 testutil.HashPassword 生成真实用户的 bcrypt 哈希时使用了 MinCost(cost=4)
  83. // 以提速;而生产代码里的 dummyBcryptHash 固定用 DefaultCost(cost=10)。因此"超管 + 错密码"走
  84. // 真 bcrypt(cost=4) 会显著快于两条 dummy 分支,这里无法把 SA+wrong 的耗时纳入对比。
  85. // 本 TC 只对比两条 dummy 分支——它们共用同一份 dummyBcryptHash,理应严格齐平(2× 以内)。
  86. // 若非超管分支被回退到"不走 dummy bcrypt",dNonSa 会突然下降一个数量级,ratio 会突破 5× 触发 FAIL。
  87. func TestAdminLogin_LN3_DummyBcryptBranches_TimingEqualized(t *testing.T) {
  88. if testing.Short() {
  89. t.Skip("timing-sensitive test skipped under -short")
  90. }
  91. ctx := context.Background()
  92. svcCtx := newTestSvcCtx()
  93. svcCtx.UsernameLoginLimit = nil
  94. normalUser := "ln3_t_nm_" + testutil.UniqueId()
  95. _, cleanNm := insertTestUser(t, ctx, svcCtx, normalUser, "RealNormalPass123", 1, 2)
  96. t.Cleanup(cleanNm)
  97. logic := NewAdminLoginLogic(ctx, svcCtx)
  98. mk := svcCtx.Config.Auth.ManagementKey
  99. measure := func(username, password string) time.Duration {
  100. _, _ = logic.AdminLogin(&types.AdminLoginReq{Username: username, Password: password, ManagementKey: mk})
  101. const N = 3
  102. var total time.Duration
  103. for i := 0; i < N; i++ {
  104. start := time.Now()
  105. _, _ = logic.AdminLogin(&types.AdminLoginReq{Username: username, Password: password, ManagementKey: mk})
  106. total += time.Since(start)
  107. }
  108. return total / N
  109. }
  110. dAbsent := measure("ln3_absent_"+testutil.UniqueId(), "xx")
  111. dNonSa := measure(normalUser, "WrongPass")
  112. t.Logf("L-N3 dummy bcrypt timing: absent=%v nonSa=%v", dAbsent, dNonSa)
  113. ratio := func(a, b time.Duration) float64 {
  114. if b <= 0 {
  115. return 0
  116. }
  117. if a > b {
  118. return float64(a) / float64(b)
  119. }
  120. return float64(b) / float64(a)
  121. }
  122. const tol = 3.0 // CI 抖动容忍
  123. assert.Less(t, ratio(dNonSa, dAbsent), tol,
  124. "L-N3:'非超管 + 错密码' 必须与 '用户不存在' 耗时同阶;若 >3× 说明 L-N3 被回退(非超管分支没走 dummy bcrypt)")
  125. }