From c03360333bcee5f088d5e76539e7e7f76cc0f484 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Mon, 3 Aug 2026 14:11:22 +0100 Subject: [PATCH] fix: single source of truth for local-login lockout rescue HandleLocalLogin and HandleListPublicProviders each computed their own answer to whether password sign-in must stay available, and they could disagree: an instance with local login off and a licence that lapses loses its only provider and its password form in the same moment, with no endpoint left to recover. services.LocalLoginPermitted is now the one predicate both call. --- server/internal/auth/local.go | 10 ++------- server/internal/services/auth_provider.go | 25 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/server/internal/auth/local.go b/server/internal/auth/local.go index 84d68da..887bf24 100644 --- a/server/internal/auth/local.go +++ b/server/internal/auth/local.go @@ -67,7 +67,7 @@ func HandleLocalLogin(c *gin.Context) { } // The login page hides the form, but the page is a courtesy and the API is // the boundary. - if !services.IsLocalLoginEnabled(instanceID) { + if !services.LocalLoginPermitted(instanceID) { c.JSON(http.StatusForbidden, gin.H{"error": "password sign-in is disabled for this instance"}) return } @@ -118,13 +118,7 @@ func HandleListPublicProviders(c *gin.Context) { } } - localEnabled := services.IsLocalLoginEnabled(instanceID) - // Belt and braces against a hand-edited database: a login page with neither - // a form nor a button is unrecoverable without database access. - if !localEnabled && len(out) == 0 { - localEnabled = true - } - c.JSON(http.StatusOK, gin.H{"local_enabled": localEnabled, "providers": out}) + c.JSON(http.StatusOK, gin.H{"local_enabled": services.LocalLoginPermitted(instanceID), "providers": out}) } func HandleBootstrapStatus(c *gin.Context) { diff --git a/server/internal/services/auth_provider.go b/server/internal/services/auth_provider.go index 393f12d..2387a07 100644 --- a/server/internal/services/auth_provider.go +++ b/server/internal/services/auth_provider.go @@ -242,6 +242,31 @@ func IsLocalLoginEnabled(instanceID string) bool { return shared.LocalLoginEnabled(s) } +// LocalLoginPermitted answers whether password sign-in must be accepted for +// this instance, which is not the same question as whether an administrator +// turned it on. An instance whose only providers have become unusable — a +// lapsed licence, or every provider disabled — has to keep its password form, +// or nobody can sign in and there is no endpoint left to fix it with. +func LocalLoginPermitted(instanceID string) bool { + if IsLocalLoginEnabled(instanceID) { + return true + } + return CountUsableAuthProviders(instanceID) == 0 +} + +// CountUsableAuthProviders counts providers that could actually complete a +// sign-in right now: enabled, and permitted by the licence. +func CountUsableAuthProviders(instanceID string) int { + if !GetLicenseState(instanceID).Feature("oidc") { + return 0 + } + n, err := CountEnabledAuthProviders(instanceID) + if err != nil { + return 0 + } + return n +} + // randomProviderID is 8 bytes hex: short enough to read in a URL, wide enough // that guessing one is not a way to enumerate an instance's providers. func randomProviderID() (string, error) {