package pub import ( "context" "errors" "testing" "time" "perms-system-server/internal/response" "perms-system-server/internal/testutil" "perms-system-server/internal/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // --------------------------------------------------------------------------- // 覆盖目标:审计 L-N3 修复 —— AdminLogin 在 IsSuperAdmin 分支也必须强制走一次 dummy bcrypt, // 保证三条错误分支的耗时近似齐平,关闭"靠耗时差筛超管账号"的时序 oracle。 // 分支: // (A) 用户不存在 → dummy bcrypt → 401 // (B) 用户存在但非超管 → dummy bcrypt → 401 (L-N3 关键修复) // (C) 用户存在且是超管但密码错 → 真 bcrypt → 401 // // 三条路径的错误响应(code + body)必须 **完全一致**;耗时必须处于同一数量级 // (均经过一次 bcrypt)。 // --------------------------------------------------------------------------- // TC-1008: L-N3 —— 非超管账号 + 错误密码 必须和"用户不存在"返回完全相同的 401 body。 func TestAdminLogin_LN3_NonSuperAdminWrongPassword_IndistinguishableFromAbsent(t *testing.T) { ctx := context.Background() svcCtx := newTestSvcCtx() svcCtx.UsernameLoginLimit = nil username := "ln3_nonsa_" + testutil.UniqueId() // status=1(启用),isSuperAdmin=2(普通用户) _, clean := insertTestUser(t, ctx, svcCtx, username, "RightPass123", 1, 2) t.Cleanup(clean) logic := NewAdminLoginLogic(ctx, svcCtx) // (B) 用户存在但非超管 —— 走 L-N3 新增的 dummy bcrypt 分支 _, errExisting := logic.AdminLogin(&types.AdminLoginReq{ Username: username, Password: "WrongPass", ManagementKey: svcCtx.Config.Auth.ManagementKey, }) require.Error(t, errExisting) var ceB *response.CodeError require.True(t, errors.As(errExisting, &ceB)) // (A) 用户不存在 —— 原有 dummy bcrypt 分支 _, errAbsent := logic.AdminLogin(&types.AdminLoginReq{ Username: "ln3_absent_" + testutil.UniqueId(), Password: "WhateverPass", ManagementKey: svcCtx.Config.Auth.ManagementKey, }) require.Error(t, errAbsent) var ceA *response.CodeError require.True(t, errors.As(errAbsent, &ceA)) assert.Equal(t, ceA.Code(), ceB.Code(), "L-N3:'非超管 + 错误密码' 与 '用户不存在' 必须返回相同 code") assert.Equal(t, ceA.Error(), ceB.Error(), "L-N3:'非超管 + 错误密码' 与 '用户不存在' 必须返回相同 body") assert.Equal(t, "用户名或密码错误", ceB.Error()) } // TC-1009: L-N3 —— 非超管账号 + 任意密码(包括正确密码)都必须 401,且仍触发一次 bcrypt, // 保证即使攻击者命中密码,也不得通过 response 推断该账号是"存在的普通用户"。 func TestAdminLogin_LN3_NonSuperAdminCorrectPassword_Still401(t *testing.T) { ctx := context.Background() svcCtx := newTestSvcCtx() svcCtx.UsernameLoginLimit = nil username := "ln3_cp_" + testutil.UniqueId() password := "KnownPass123" _, clean := insertTestUser(t, ctx, svcCtx, username, password, 1, 2) t.Cleanup(clean) _, err := NewAdminLoginLogic(ctx, svcCtx).AdminLogin(&types.AdminLoginReq{ Username: username, Password: password, ManagementKey: svcCtx.Config.Auth.ManagementKey, }) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 401, ce.Code(), "L-N3:非超管走 AdminLogin 一律 401,即使密码正确也不得披露账号存在性") assert.Equal(t, "用户名或密码错误", ce.Error()) } // TC-1010: L-N3 时序等齐 —— "非超管 + 错密码" 必须与 "用户不存在" 同阶(两者都走一次 dummyBcryptHash)。 // // 注意:测试环境里通过 testutil.HashPassword 生成真实用户的 bcrypt 哈希时使用了 MinCost(cost=4) // 以提速;而生产代码里的 dummyBcryptHash 固定用 DefaultCost(cost=10)。因此"超管 + 错密码"走 // 真 bcrypt(cost=4) 会显著快于两条 dummy 分支,这里无法把 SA+wrong 的耗时纳入对比。 // 本 TC 只对比两条 dummy 分支——它们共用同一份 dummyBcryptHash,理应严格齐平(2× 以内)。 // 若非超管分支被回退到"不走 dummy bcrypt",dNonSa 会突然下降一个数量级,ratio 会突破 5× 触发 FAIL。 func TestAdminLogin_LN3_DummyBcryptBranches_TimingEqualized(t *testing.T) { if testing.Short() { t.Skip("timing-sensitive test skipped under -short") } ctx := context.Background() svcCtx := newTestSvcCtx() svcCtx.UsernameLoginLimit = nil normalUser := "ln3_t_nm_" + testutil.UniqueId() _, cleanNm := insertTestUser(t, ctx, svcCtx, normalUser, "RealNormalPass123", 1, 2) t.Cleanup(cleanNm) logic := NewAdminLoginLogic(ctx, svcCtx) mk := svcCtx.Config.Auth.ManagementKey measure := func(username, password string) time.Duration { _, _ = logic.AdminLogin(&types.AdminLoginReq{Username: username, Password: password, ManagementKey: mk}) const N = 3 var total time.Duration for i := 0; i < N; i++ { start := time.Now() _, _ = logic.AdminLogin(&types.AdminLoginReq{Username: username, Password: password, ManagementKey: mk}) total += time.Since(start) } return total / N } dAbsent := measure("ln3_absent_"+testutil.UniqueId(), "xx") dNonSa := measure(normalUser, "WrongPass") t.Logf("L-N3 dummy bcrypt timing: absent=%v nonSa=%v", dAbsent, dNonSa) ratio := func(a, b time.Duration) float64 { if b <= 0 { return 0 } if a > b { return float64(a) / float64(b) } return float64(b) / float64(a) } const tol = 3.0 // CI 抖动容忍 assert.Less(t, ratio(dNonSa, dAbsent), tol, "L-N3:'非超管 + 错密码' 必须与 '用户不存在' 耗时同阶;若 >3× 说明 L-N3 被回退(非超管分支没走 dummy bcrypt)") }