feat: add local_login_enabled setting with absent-means-on default

This commit is contained in:
2026-08-03 10:36:48 +01:00
parent 0858693d57
commit e2b01b62a5
4 changed files with 53 additions and 5 deletions
+7 -1
View File
@@ -1,6 +1,7 @@
package api
import (
"errors"
"fmt"
"net/http"
"os"
@@ -471,12 +472,17 @@ func saveSettings(c *gin.Context) {
Alerts models.AlertSettings `json:"alerts"`
Email models.EmailSettings `json:"email"`
WorkflowLogRetentionDays *int `json:"workflow_log_retention_days"`
LocalLoginEnabled *bool `json:"local_login_enabled"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := services.SaveSettings(auth.InstanceID(c), body.Alerts, body.Email, body.WorkflowLogRetentionDays); err != nil {
if err := services.SaveSettings(auth.InstanceID(c), body.Alerts, body.Email, body.WorkflowLogRetentionDays, body.LocalLoginEnabled); err != nil {
if errors.Is(err, services.ErrLockout) {
c.JSON(http.StatusConflict, gin.H{"error": err.Error(), "code": "local_login_required"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
+12
View File
@@ -11,6 +11,7 @@ import (
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/db"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
shared "gitea.hostxtra.co.uk/mrhid6/vantage/shared/models"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
@@ -230,6 +231,17 @@ func AckAuthProviderNotice(instanceID, providerID string) error {
return err
}
// IsLocalLoginEnabled fails open. A settings read error must not lock an
// instance out of its own login page, and the safe direction here is the one
// that still asks for a password.
func IsLocalLoginEnabled(instanceID string) bool {
s, err := GetSettings(instanceID)
if err != nil {
return true
}
return shared.LocalLoginEnabled(s)
}
// 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) {
+19 -4
View File
@@ -132,10 +132,7 @@ func ResolveSecretsReadToken(token string) (string, bool) {
return s.InstanceID, true
}
func SaveSettings(instanceID string, alerts models.AlertSettings, email models.EmailSettings, retentionDays *int) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
func SaveSettings(instanceID string, alerts models.AlertSettings, email models.EmailSettings, retentionDays *int, localLoginEnabled *bool) error {
if alerts.OfflineThresholdMinutes <= 0 {
alerts.OfflineThresholdMinutes = 5
}
@@ -143,10 +140,28 @@ func SaveSettings(instanceID string, alerts models.AlertSettings, email models.E
email.SMTPPort = 587
}
// The guard lives here rather than in the handler so the settings path and
// the provider path cannot disagree about what a lockout is.
if localLoginEnabled != nil && !*localLoginEnabled {
n, err := CountEnabledAuthProviders(instanceID)
if err != nil {
return err
}
if err := CheckLockout(false, n); err != nil {
return err
}
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
set := bson.M{"alerts": alerts, "email": email}
if retentionDays != nil {
set["workflow_log_retention_days"] = *retentionDays
}
if localLoginEnabled != nil {
set["local_login_enabled"] = *localLoginEnabled
}
_, err := db.Col("settings").UpdateOne(ctx,
bson.M{"instance_id": instanceID},
bson.M{"$set": set, "$setOnInsert": bson.M{"instance_id": instanceID}},
+15
View File
@@ -37,4 +37,19 @@ type Settings struct {
Secrets SecretsSettings `bson:"secrets" json:"secrets"`
WorkflowLogRetentionDays *int `bson:"workflow_log_retention_days,omitempty" json:"workflow_log_retention_days,omitempty"`
// LocalLoginEnabled is a pointer because it is absent on every settings
// document written before this feature existed, and a plain bool would read
// absent as disabled — turning off password login for the entire fleet at
// upgrade. Nil means enabled.
LocalLoginEnabled *bool `bson:"local_login_enabled,omitempty" json:"local_login_enabled,omitempty"`
}
// LocalLoginEnabled reads the setting with its absent-means-on default. Every
// caller must go through this rather than dereferencing the field.
func LocalLoginEnabled(s *Settings) bool {
if s == nil || s.LocalLoginEnabled == nil {
return true
}
return *s.LocalLoginEnabled
}