fix: keep passkey switch coupled to WebAuthn config and fix CI issues

- parseSettings now reports passkey_enabled=false whenever the WebAuthn
  deployment config is absent: a stale "true" row left behind after the
  config is removed previously made the admin update gate reject every
  settings save while the UI toggle was disabled, leaving no recovery
  path from the admin panel. Added a regression test.
- update the admin settings API contract goldens with the new
  passkey_enabled/passkey_configured/passkey_rp_id/passkey_rp_origins
  fields.
- errcheck: check rows.Close in passkey repository (repo convention).
- staticcheck QF1001: apply De Morgan's law in WebAuthn origin scheme
  validation.
This commit is contained in:
shaw
2026-07-28 11:45:18 +08:00
parent 357c5b917b
commit 97f44b21bb
5 changed files with 32 additions and 4 deletions
+1 -1
View File
@@ -2634,7 +2634,7 @@ func (c *Config) Validate() error {
u.Host = strings.ToLower(u.Host)
host := strings.ToLower(u.Hostname())
localDevelopment := host == "localhost" || host == "127.0.0.1" || host == "::1"
if u.Scheme != "https" && !(u.Scheme == "http" && localDevelopment) {
if u.Scheme != "https" && (u.Scheme != "http" || !localDevelopment) {
return fmt.Errorf("webauthn.rp_origins entry %q must use HTTPS (HTTP is allowed only for localhost)", origin)
}
if host != c.WebAuthn.RPID && !strings.HasSuffix(host, "."+c.WebAuthn.RPID) {
+1 -1
View File
@@ -92,7 +92,7 @@ func (r *passkeyRepository) ListByUserID(
if err != nil {
return nil, fmt.Errorf("list passkey credentials: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()
records := make([]service.PasskeyCredentialRecord, 0)
for rows.Next() {
@@ -711,6 +711,10 @@ func TestAPIContracts(t *testing.T) {
"frontend_url": "",
"totp_enabled": false,
"totp_encryption_key_configured": false,
"passkey_enabled": false,
"passkey_configured": false,
"passkey_rp_id": "",
"passkey_rp_origins": [],
"session_binding_enabled": false,
"step_up_enabled": false,
"audit_log_retention_days": 180,
@@ -1028,6 +1032,10 @@ func TestAPIContracts(t *testing.T) {
"invitation_code_enabled": false,
"totp_enabled": false,
"totp_encryption_key_configured": false,
"passkey_enabled": false,
"passkey_configured": false,
"passkey_rp_id": "",
"passkey_rp_origins": [],
"session_binding_enabled": false,
"step_up_enabled": false,
"audit_log_retention_days": 180,
+8 -2
View File
@@ -182,7 +182,7 @@ func (s *SettingService) PasskeyEnabled(ctx context.Context) (bool, error) {
}
value, err := s.settingRepo.GetValue(ctx, SettingKeyPasskeyEnabled)
if errors.Is(err, ErrSettingNotFound) {
return true, nil // preserve enabled deployments when upgrading from config-only releases
return true, nil // configured deployments default to enabled until the admin persists the switch
}
if err != nil {
return false, fmt.Errorf("read passkey setting: %w", err)
@@ -206,10 +206,16 @@ func (s *SettingService) passkeyConfigured() bool {
return s != nil && s.cfg != nil && s.cfg.WebAuthn.Enabled
}
// passkeySettingEnabled must stay ANDed with passkeyConfigured: a stale
// "true" row after the WebAuthn config is removed would otherwise make the
// admin update gate reject every settings save while the UI toggle is locked.
func (s *SettingService) passkeySettingEnabled(settings map[string]string) bool {
if !s.passkeyConfigured() {
return false
}
value, ok := settings[SettingKeyPasskeyEnabled]
if !ok {
return s.passkeyConfigured()
return true
}
return value == "true"
}
@@ -879,3 +879,17 @@ func TestSettingService_PasskeySwitchPersistsAndDefaultsToConfigured(t *testing.
require.NoError(t, err)
require.False(t, publicSettings.PasskeyEnabled)
}
// 移除 WebAuthn 配置后,残留的 passkey_enabled="true" 不得再让 GetAllSettings
// 报告开关开启:admin 更新门控以此为准,一旦误报为 true 会拒绝所有设置保存,
// 而此时前端开关处于禁用态,管理员无法在 UI 里自救。
func TestSettingService_StalePasskeyTrueWithoutConfigReportsDisabled(t *testing.T) {
repo := &settingGetAllRepoStub{values: map[string]string{
SettingKeyPasskeyEnabled: "true",
}}
service := NewSettingService(repo, &config.Config{})
settings, err := service.GetAllSettings(context.Background())
require.NoError(t, err)
require.False(t, settings.PasskeyEnabled)
}