调整管理员与用户安全恢复为安全码或外部邮箱任选其一
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-10-02 18:00:51 +08:00
parent 1fd3812768
commit 4741b641d9
3 changed files with 55 additions and 8 deletions

View File

@ -127,8 +127,9 @@ func loginSecurityFields(user *models.User) map[string]any {
fields["totpConfigured"] = enabled
}
fields["recoveryCodesConfigured"] = configured
fields["showRecoverySetupPrompt"] = !configured
fields["showSecuritySetupPrompt"] = !configured || fields["externalEmailConfigured"] != true
needsRecoverySetup := !configured && fields["externalEmailConfigured"] != true
fields["showRecoverySetupPrompt"] = needsRecoverySetup
fields["showSecuritySetupPrompt"] = needsRecoverySetup
if user.EffectiveRole() == access.RoleSuperAdmin {
required := !configured && fields["externalEmailConfigured"] != true && fields["totpConfigured"] != true
fields["securitySetupRequired"] = required

View File

@ -189,11 +189,12 @@ func TestSecuritySetupRequiresUsableCredentials(t *testing.T) {
fields := loginSecurityFields(&user)
emailReady := emailStatus == models.ExternalEmailStatusVerified
codeReady := codeStatus == "valid"
wantPrompt := !emailReady || !codeReady
if role == access.RoleSuperAdmin {
wantPrompt = !emailReady && !codeReady
}
if fields["externalEmailConfigured"] != emailReady || fields["showSecuritySetupPrompt"] != wantPrompt {
wantPrompt := !emailReady && !codeReady
wantRequired := role == access.RoleSuperAdmin && wantPrompt
wantRecoveryPrompt := role != access.RoleSuperAdmin && wantPrompt
if fields["externalEmailConfigured"] != emailReady || fields["recoveryCodesConfigured"] != codeReady ||
fields["showSecuritySetupPrompt"] != wantPrompt || fields["showRecoverySetupPrompt"] != wantRecoveryPrompt ||
fields["securitySetupRequired"] != wantRequired {
t.Fatalf("unexpected security state: %#v", fields)
}
})
@ -202,6 +203,49 @@ func TestSecuritySetupRequiresUsableCredentials(t *testing.T) {
}
}
func TestNonSuperAdminRecoverySetupTracksUsableOptions(t *testing.T) {
for _, role := range []access.Role{access.RoleUser, access.RoleAdmin} {
t.Run(string(role), func(t *testing.T) {
engine := setupSecurityPromptTest(t)
user := models.User{Account: "alice", Role: string(role)}
if _, err := engine.Insert(&user); err != nil {
t.Fatal(err)
}
if _, err := engine.Insert(&models.TOTPAuthenticator{UserID: user.ID, Enabled: true}); err != nil {
t.Fatal(err)
}
assertPrompt := func(want bool) {
t.Helper()
fields := loginSecurityFields(&user)
if fields["showSecuritySetupPrompt"] != want || fields["showRecoverySetupPrompt"] != want ||
fields["securitySetupRequired"] != false {
t.Fatalf("unexpected security state: %#v", fields)
}
}
assertPrompt(true)
email := models.ExternalEmail{UserID: user.ID, Status: models.ExternalEmailStatusVerified, EmailFingerprint: "fixture"}
if _, err := engine.Insert(&email); err != nil {
t.Fatal(err)
}
assertPrompt(false)
if _, err := engine.ID(email.ID).Cols("status").Update(&models.ExternalEmail{Status: models.ExternalEmailStatusDisabled}); err != nil {
t.Fatal(err)
}
assertPrompt(true)
code := models.RecoveryCode{UserID: user.ID, CodeHash: "fixture"}
if _, err := engine.Insert(&code); err != nil {
t.Fatal(err)
}
assertPrompt(false)
now := time.Now()
if _, err := engine.ID(code.ID).Cols("used_at").Update(&models.RecoveryCode{UsedAt: &now}); err != nil {
t.Fatal(err)
}
assertPrompt(true)
})
}
}
func TestSecuritySetupRejectsAnonymous(t *testing.T) {
recorder := httptest.NewRecorder()
AcknowledgeSecuritySetup(nil, recorder, httptest.NewRequest(http.MethodPost, "/api/v1/security/setup/acknowledge", nil))

View File

@ -8,7 +8,9 @@
:show-close="false"
class="security-setup-dialog"
>
<p class="setup-description">{{ isSuperAdmin ? '请至少启用一种安全验证后继续。' : '请设置安全码并绑定外部邮箱。未完成前,每次登录都会提醒。' }}</p>
<p class="setup-description">
{{ isSuperAdmin ? '请至少启用一种安全验证后继续。' : '安全码或外部邮箱至少配置一项。未配置时,每次登录都会提醒。' }}
</p>
<div class="setup-options">
<section v-for="item in options" :key="item.key" class="setup-option">
<div>