feat: add local_login_enabled setting with absent-means-on default
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user