فهرست منبع

feat: Add option to disable local auth form

Finn 1 سال پیش
والد
کامیت
770cc1dbb3

+ 12 - 0
internal/cli/cli.go

@@ -4,6 +4,7 @@
 package cli // import "miniflux.app/v2/internal/cli"
 
 import (
+	"errors"
 	"flag"
 	"fmt"
 	"io"
@@ -225,6 +226,17 @@ func Parse() {
 		return
 	}
 
+	if config.Opts.DisableLocalAuth() {
+		switch {
+		case config.Opts.OAuth2Provider() == "" && config.Opts.AuthProxyHeader() == "":
+			printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled but neither OAUTH2_PROVIDER nor AUTH_PROXY_HEADER is not set. Please enable at least one authentication source"))
+		case config.Opts.OAuth2Provider() != "" && !config.Opts.IsOAuth2UserCreationAllowed():
+			printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled and an OAUTH2_PROVIDER is configured, but OAUTH2_USER_CREATION is not enabled"))
+		case config.Opts.AuthProxyHeader() != "" && !config.Opts.IsAuthProxyUserCreationAllowed():
+			printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled and an AUTH_PROXY_HEADER is configured, but AUTH_PROXY_USER_CREATION is not enabled"))
+		}
+	}
+
 	startDaemon(store)
 }
 

+ 9 - 0
internal/config/options.go

@@ -70,6 +70,7 @@ const (
 	defaultOAuth2RedirectURL                  = ""
 	defaultOAuth2OidcDiscoveryEndpoint        = ""
 	defaultOAuth2Provider                     = ""
+	defaultDisableLocalAuth                   = false
 	defaultPocketConsumerKey                  = ""
 	defaultHTTPClientTimeout                  = 20
 	defaultHTTPClientMaxBodySize              = 15
@@ -154,6 +155,7 @@ type Options struct {
 	oauth2RedirectURL                  string
 	oidcDiscoveryEndpoint              string
 	oauth2Provider                     string
+	disableLocalAuth                   bool
 	pocketConsumerKey                  string
 	httpClientTimeout                  int
 	httpClientMaxBodySize              int64
@@ -231,6 +233,7 @@ func NewOptions() *Options {
 		oauth2RedirectURL:                  defaultOAuth2RedirectURL,
 		oidcDiscoveryEndpoint:              defaultOAuth2OidcDiscoveryEndpoint,
 		oauth2Provider:                     defaultOAuth2Provider,
+		disableLocalAuth:                   defaultDisableLocalAuth,
 		pocketConsumerKey:                  defaultPocketConsumerKey,
 		httpClientTimeout:                  defaultHTTPClientTimeout,
 		httpClientMaxBodySize:              defaultHTTPClientMaxBodySize * 1024 * 1024,
@@ -456,6 +459,11 @@ func (o *Options) OAuth2Provider() string {
 	return o.oauth2Provider
 }
 
+// DisableLocalAUth returns true if the local user database should not be used to authenticate users
+func (o *Options) DisableLocalAuth() bool {
+	return o.disableLocalAuth
+}
+
 // HasHSTS returns true if HTTP Strict Transport Security is enabled.
 func (o *Options) HasHSTS() bool {
 	return o.hsts
@@ -695,6 +703,7 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
 		"OAUTH2_PROVIDER":                        o.oauth2Provider,
 		"OAUTH2_REDIRECT_URL":                    o.oauth2RedirectURL,
 		"OAUTH2_USER_CREATION":                   o.oauth2UserCreationAllowed,
+		"DISABLE_LOCAL_AUTH":                     o.disableLocalAuth,
 		"POCKET_CONSUMER_KEY":                    redactSecretValue(o.pocketConsumerKey, redactSecret),
 		"POLLING_FREQUENCY":                      o.pollingFrequency,
 		"FORCE_REFRESH_INTERVAL":                 o.forceRefreshInterval,

+ 2 - 0
internal/config/parser.go

@@ -227,6 +227,8 @@ func (p *Parser) parseLines(lines []string) (err error) {
 			p.opts.oidcDiscoveryEndpoint = parseString(value, defaultOAuth2OidcDiscoveryEndpoint)
 		case "OAUTH2_PROVIDER":
 			p.opts.oauth2Provider = parseString(value, defaultOAuth2Provider)
+		case "DISABLE_LOCAL_AUTH":
+			p.opts.disableLocalAuth = parseBool(value, defaultDisableLocalAuth)
 		case "HTTP_CLIENT_TIMEOUT":
 			p.opts.httpClientTimeout = parseInt(value, defaultHTTPClientTimeout)
 		case "HTTP_CLIENT_MAX_BODY_SIZE":

+ 8 - 7
internal/template/functions.go

@@ -32,13 +32,14 @@ type funcMap struct {
 // Map returns a map of template functions that are compiled during template parsing.
 func (f *funcMap) Map() template.FuncMap {
 	return template.FuncMap{
-		"formatFileSize": formatFileSize,
-		"dict":           dict,
-		"hasKey":         hasKey,
-		"truncate":       truncate,
-		"isEmail":        isEmail,
-		"baseURL":        config.Opts.BaseURL,
-		"rootURL":        config.Opts.RootURL,
+		"formatFileSize":   formatFileSize,
+		"dict":             dict,
+		"hasKey":           hasKey,
+		"truncate":         truncate,
+		"isEmail":          isEmail,
+		"baseURL":          config.Opts.BaseURL,
+		"rootURL":          config.Opts.RootURL,
+		"disableLocalAuth": config.Opts.DisableLocalAuth,
 		"hasOAuth2Provider": func(provider string) bool {
 			return config.Opts.OAuth2Provider() == provider
 		},

+ 2 - 0
internal/template/templates/views/login.html

@@ -5,6 +5,7 @@
 
 {{ define "content"}}
 <section class="login-form">
+    {{ if not disableLocalAuth }}
     <form action="{{ route "checkLogin" }}" method="post">
         <input type="hidden" name="csrf" value="{{ .csrf }}">
 
@@ -22,6 +23,7 @@
             <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.loading" }}">{{ t "action.login" }}</button>
         </div>
     </form>
+    {{ end }}
     {{ if .webAuthnEnabled }}
     <div class="webauthn">
         <div role="alert" class="alert alert-error hidden" id="webauthn-error">

+ 2 - 0
internal/template/templates/views/settings.html

@@ -15,6 +15,7 @@
         <div role="alert" class="alert alert-error">{{ .errorMessage }}</div>
     {{ end }}
 
+    {{ if not disableLocalAuth }}
     <fieldset>
         <legend>{{ t "form.prefs.fieldset.authentication_settings" }}</legend>
 
@@ -49,6 +50,7 @@
             <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
         </div>
     </fieldset>
+    {{ end }}
 
     {{ if .webAuthnEnabled }}
     <fieldset>

+ 5 - 2
internal/ui/form/settings.go

@@ -7,6 +7,7 @@ import (
 	"net/http"
 	"strconv"
 
+	"miniflux.app/v2/internal/config"
 	"miniflux.app/v2/internal/locale"
 	"miniflux.app/v2/internal/model"
 )
@@ -86,7 +87,9 @@ func ExtractMarkAsReadBehavior(behavior MarkReadBehavior) (markReadOnView, markR
 
 // Merge updates the fields of the given user.
 func (s *SettingsForm) Merge(user *model.User) *model.User {
-	user.Username = s.Username
+	if !config.Opts.DisableLocalAuth() {
+		user.Username = s.Username
+	}
 	user.Theme = s.Theme
 	user.Language = s.Language
 	user.Timezone = s.Timezone
@@ -120,7 +123,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
 
 // Validate makes sure the form values are valid.
 func (s *SettingsForm) Validate() *locale.LocalizedError {
-	if s.Username == "" || s.Theme == "" || s.Language == "" || s.Timezone == "" || s.EntryDirection == "" || s.DisplayMode == "" || s.DefaultHomePage == "" {
+	if (s.Username == "" && !config.Opts.DisableLocalAuth()) || s.Theme == "" || s.Language == "" || s.Timezone == "" || s.EntryDirection == "" || s.DisplayMode == "" || s.DefaultHomePage == "" {
 		return locale.NewLocalizedError("error.settings_mandatory_fields")
 	}
 

+ 11 - 2
internal/ui/login_check.go

@@ -21,9 +21,18 @@ import (
 func (h *handler) checkLogin(w http.ResponseWriter, r *http.Request) {
 	clientIP := request.ClientIP(r)
 	sess := session.New(h.store, request.SessionID(r))
-	authForm := form.NewAuthForm(r)
-
 	view := view.New(h.tpl, r, sess)
+
+	if config.Opts.DisableLocalAuth() {
+		slog.Warn("blocking local auth login attempt, local auth is disabled",
+			slog.String("client_ip", clientIP),
+			slog.String("user_agent", r.UserAgent()),
+		)
+		html.OK(w, r, view.Render("login"))
+		return
+	}
+
+	authForm := form.NewAuthForm(r)
 	view.Set("errorMessage", locale.NewLocalizedError("error.bad_credentials").Translate(request.UserLanguage(r)))
 	view.Set("form", authForm)
 

+ 9 - 0
internal/ui/oauth2_unlink.go

@@ -7,6 +7,7 @@ import (
 	"log/slog"
 	"net/http"
 
+	"miniflux.app/v2/internal/config"
 	"miniflux.app/v2/internal/http/request"
 	"miniflux.app/v2/internal/http/response/html"
 	"miniflux.app/v2/internal/http/route"
@@ -15,6 +16,14 @@ import (
 )
 
 func (h *handler) oauth2Unlink(w http.ResponseWriter, r *http.Request) {
+	if config.Opts.DisableLocalAuth() {
+		slog.Warn("blocking oauth2 unlink attempt, local auth is disabled",
+			slog.String("user_agent", r.UserAgent()),
+		)
+		html.Redirect(w, r, route.Path(h.router, "login"))
+		return
+	}
+
 	printer := locale.NewPrinter(request.UserLanguage(r))
 	provider := request.RouteStringParam(r, "provider")
 	if provider == "" {

+ 8 - 0
miniflux.1

@@ -447,6 +447,14 @@ Possible values are "google" or "oidc"\&.
 .br
 Default is empty\&.
 .TP
+.B DISABLE_LOCAL_AUTH
+Only use oauth2 for auth\&.
+.br
+When set to true, the username/password form is hidden from the login screen, and the
+options to change username/password or unlink oauth2 account are hidden from the settings page.
+.br
+Default is false\&.
+.TP
 .B OAUTH2_REDIRECT_URL
 OAuth2 redirect URL\&.
 .br