修复管理员仅配置安全码时无法独立找回密码
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-10-03 20:12:54 +08:00
parent 9040a4c98d
commit c47155d600
3 changed files with 54 additions and 8 deletions

View File

@ -73,7 +73,7 @@ func (s *Service) IdentifyPublicFlow(identifier string) (*PublicFlow, error) {
result.Methods = append(result.Methods, "email")
}
}
if result.Role == access.RoleUser {
if result.Role == access.RoleUser || result.Role == access.RoleAdmin {
configured, err := s.credentials.HasValidRecoveryCode(user.ID)
if err != nil {
return nil, ErrUnavailable
@ -194,7 +194,7 @@ func (s *Service) VerifyPublicFlow(token, method, code, remoteIP string) (string
err = s.verification.VerifyBoundWithContext(item.ChallengeID, item.Account, email, strings.TrimSpace(code), verification.PurposePasswordRecover, item.CredentialID, remoteIP, token)
}
case "security_code":
if item.Role != access.RoleUser {
if item.Role != access.RoleUser && item.Role != access.RoleAdmin {
err = ErrRecoveryDenied
} else {
err = s.credentials.ConsumeRecoveryCode(item.UserID, code)

View File

@ -130,13 +130,20 @@ func TestPublicFlowEmailResetAndRoleBoundary(t *testing.T) {
}
func TestPublicFlowSecurityCodeRoleAndFailureLimits(t *testing.T) {
for _, role := range []access.Role{access.RoleAdmin, access.RoleUser} {
for _, role := range []access.Role{access.RoleSuperAdmin, access.RoleAdmin, access.RoleUser} {
t.Run(string(role), func(t *testing.T) {
s, _, engine, user := newRecoveryFixture(t)
user.Role = string(role)
if _, err := engine.ID(user.ID).Cols("role").Update(user); err != nil {
t.Fatal(err)
}
binding, err := s.external.FindVerifiedPrimaryByUser(user.ID)
if err != nil {
t.Fatal(err)
}
if err := s.external.Disable(user.ID, binding.ID); err != nil {
t.Fatal(err)
}
codes, err := s.credentials.GenerateRecoveryCodes(user.ID, 5)
if err != nil {
t.Fatal(err)
@ -145,19 +152,55 @@ func TestPublicFlowSecurityCodeRoleAndFailureLimits(t *testing.T) {
if err != nil {
t.Fatal(err)
}
ticket, err := s.VerifyPublicFlow(flow.Token, "security_code", codes[0], "192.0.2.2")
if role == access.RoleAdmin {
if err == nil || strings.Contains(strings.Join(flow.Methods, ","), "security_code") {
t.Fatal("admin used security code instead of email")
if role == access.RoleSuperAdmin {
if flow.Token != "" || len(flow.Methods) != 0 || flow.Email != "" {
t.Fatal("超级管理员不应获得公共安全码恢复入口")
}
if _, err := s.VerifyPublicFlow(flow.Token, "security_code", codes[0], "192.0.2.2"); err == nil {
t.Fatal("超级管理员安全码被公共恢复流程接受")
}
return
}
if flow.Token == "" || flow.Email != "" || len(flow.Methods) != 1 || flow.Methods[0] != "security_code" {
t.Fatal("仅配置安全码的账号未获得独立恢复方式")
}
if err := s.CompletePublicFlow(flow.Token, "NewPass123"); err == nil {
t.Fatal("未经安全码验证就完成了密码重置")
}
ticket, err := s.VerifyPublicFlow(flow.Token, "security_code", codes[0], "192.0.2.2")
if err != nil {
t.Fatal(err)
}
replay, err := s.IdentifyPublicFlow(user.Account)
if err != nil {
t.Fatal(err)
}
if _, err := s.VerifyPublicFlow(replay.Token, "security_code", codes[0], "192.0.2.2"); err == nil {
t.Fatal("已核销的安全码被重复使用")
}
if err := s.CompletePublicFlow(ticket, "NewPass123"); err != nil {
t.Fatal(err)
}
var updated models.User
if found, err := engine.ID(user.ID).Get(&updated); err != nil || !found {
t.Fatal("无法读取重置后的账号", err)
}
if ok, err := password.Check(updated.Password, "NewPass123"); err != nil || !ok {
t.Fatal("安全码恢复未更新密码", err)
}
if updated.SessionVersion != user.SessionVersion+1 || updated.SecurityVersion != user.SecurityVersion+1 {
t.Fatal("安全码恢复未更新密码或撤销旧安全状态")
}
if configured, err := s.credentials.HasValidRecoveryCode(user.ID); err != nil || configured {
t.Fatal("密码重置后仍保留旧安全码", err)
}
if err := s.CompletePublicFlow(ticket, "OtherPass123"); err == nil {
t.Fatal("密码重置令牌被重复使用")
}
codes, err = s.credentials.GenerateRecoveryCodes(user.ID, 5)
if err != nil {
t.Fatal(err)
}
flow, err = s.IdentifyPublicFlow(user.Account)
if err != nil {
t.Fatal(err)
@ -170,6 +213,9 @@ func TestPublicFlowSecurityCodeRoleAndFailureLimits(t *testing.T) {
if err := s.SendPublicFlowCode(flow.Token, "192.0.2.2"); err == nil {
t.Fatal("exhausted flow usable")
}
if _, err := s.VerifyPublicFlow(flow.Token, "security_code", codes[0], "192.0.2.2"); err == nil {
t.Fatal("达到失败次数上限后仍可验证有效安全码")
}
})
}
}

View File

@ -151,7 +151,7 @@ func (s *Service) completePublicLink(token, newPassword, recoveryCode string, fl
s.mu.Unlock()
return ErrRecoveryDenied
}
if item.Flow != flow || (flow && (!item.Verified || (item.Role == access.RoleAdmin && item.Method != "email"))) {
if item.Flow != flow || (flow && (!item.Verified || (item.Method != "email" && item.Method != "security_code"))) {
s.mu.Unlock()
return ErrRecoveryDenied
}