authsetup

package
v0.44.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 25, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package authsetup builds the instance's sign-in providers and keeps the live set of them — the ONE place a provider is constructed, whether the environment/config file or the Admin → Identity providers page asked for it.

Why this package exists

Until v0.43.0 the admin page wrote `auth.<name>.*` settings rows that nothing read: the server built its drivers from FILEX_AUTH_DRIVERS and the config file only, once, at boot (release-candidate sweep, 2026-09-21). The page looked like it changed who could sign in and changed nothing — the worst kind of screen. The owner's decision: the page really manages sign-in, applied without a restart, and it can never lock the instance out.

The rules, in one place

  • One construction path: Build. server.go calls it for every provider the environment lists; Live calls it for every provider the page enabled.
  • The environment wins, visibly. A provider the environment lists is built from the environment only; rows the page may hold under the same name are kept but not used, and the page shows the provider read-only with where it comes from (Live.EnvDefines, FromWords).
  • Page providers are ADDED after the environment's, never in front of them: `local` stays the first password judge wherever the operator put it, so a directory that hangs never sits in front of the administrator's password.
  • Password sign-in (`local`) and the recovery sign-in (the bootstrap administrator's break-glass, FILEX_AUTH_RECOVERY_LOGIN) are the environment's to decide and nobody else's. The page cannot add or remove either — see docs/SSO.md, "Why nothing on this page can lock you out".
  • A page provider that cannot start is left out and says why; it never takes anything else down with it (assemble).

Index

Constants

View Source
const (
	OriginEnvironment = "environment"
	OriginPage        = "page"
)

Where a provider's configuration comes from.

View Source
const (

	// SchemaSetting records that the one-time upgrade below has run.
	SchemaSetting = "auth.providers.schema"
)

Settings keys. `auth.<name>.<field>` is the shape the page always wrote; two control keys sit beside the fields.

Variables

View Source
var ErrOIDCOff = errors.New("OIDC is not configured")

ErrOIDCOff answers an OIDC flow when no OIDC provider is running.

View Source
var ErrPasswordSignInOff = errors.New("password sign-in is off")

ErrPasswordSignInOff answers a password sign-in when no driver takes one.

View Source
var ErrSecretKeyRequired = errors.New("a client secret or bind password is stored sealed, and that needs FILEX_SECRET_KEY")

ErrSecretKeyRequired: a secret was given and there is no FILEX_SECRET_KEY to seal it with. A secret is never stored in the clear.

View Source
var Managed = []string{"oidc", "ldap", "proxy-header"}

Managed lists the providers the admin page can configure, in the order they are added to the chain. `local` and `api-token` are not among them: password sign-in is the environment's to decide (see the package comment) and API tokens are always on.

View Source
var Schema = map[string][]Field{
	"oidc": {
		{Key: "issuer", Kind: FieldText, Required: true},
		{Key: "client_id", Kind: FieldText, Required: true},
		{Key: "client_secret", Kind: FieldSecret},
		{Key: "redirect_url", Kind: FieldText},
		{Key: "scopes", Kind: FieldText},
		{Key: "role_claim", Kind: FieldText},
		{Key: "admin_group", Kind: FieldText},
	},
	"ldap": {
		{Key: "url", Kind: FieldText, Required: true},
		{Key: "base_dn", Kind: FieldText, Required: true},
		{Key: "bind_dn", Kind: FieldText},
		{Key: "bind_password", Kind: FieldSecret},
		{Key: "user_filter", Kind: FieldText},
		{Key: "email_attr", Kind: FieldText},
		{Key: "start_tls", Kind: FieldBool},
		{Key: "ca_file", Kind: FieldText},
		{Key: "protocol_login", Kind: FieldBool, Default: "true"},
	},
	"proxy-header": {
		{Key: "trusted_proxies", Kind: FieldText, Required: true},
		{Key: "header_user", Kind: FieldText},
		{Key: "header_email", Kind: FieldText},
		{Key: "header_name", Kind: FieldText},
		{Key: "header_roles", Kind: FieldText},
		{Key: "admin_role", Kind: FieldText},
		{Key: "auto_provision", Kind: FieldBool, Default: "true"},
	},
}

Schema is every field the page may store per provider. A key outside it is dropped on save: the settings table is not a scratchpad, and an unknown key is either a typo or a client that means something this server does not do.

Functions

func Build

func Build(ctx context.Context, store db.Store, name string, cfg map[string]any) (auth.Driver, error)

Build constructs and initialises ONE sign-in provider from a configuration map (the keys the driver's Init reads).

⚠⚠ The single construction path. The environment/config file (server.go) and the admin page (Live.Reload) both come here, so a provider configured on the page is exactly the driver the same values in the environment would have produced — no second, subtly different path. A caller that needs a retry (the environment's OIDC, whose IdP often boots alongside filex) wraps this call; it does not grow its own construction.

func Canonical

func Canonical(name string) string

Canonical returns the name a provider is known by. The environment has always accepted a few spellings of the header driver; the page and the wire use one.

func DraftConfig

func DraftConfig(s *Stored, draft map[string]any, box *secretbox.Box, opts Options) (map[string]any, error)

DraftConfig is the configuration "Test now" checks: what is stored with the form's unsaved values laid over it. A secret the form left blank, or sent back as "***", is the stored one — opened for the test, never returned.

func DriverConfig

func DriverConfig(s *Stored, box *secretbox.Box, opts Options) (map[string]any, bool, error)

DriverConfig turns stored rows into the map the driver's Init reads — secrets opened, booleans as booleans, defaults filled — plus whether an LDAP provider also judges passwords on the file protocols.

⚠ The opened map lives in memory only, for the length of a build or a test. It is never logged and never sent anywhere but the driver.

func FromWords

func FromWords(from, lang string) string

FromWords says where an environment provider is defined, in the reader's language: config.Auth.DriversFrom is "FILEX_AUTH_DRIVERS", "file:<path>" or "default"; anything else is shown as it is.

The words are the server catalogue's (`server.auth_provider.from_*`), so a language pack's language reads them too — not the English half of a hard-coded en/tr pair.

func IsManaged

func IsManaged(name string) bool

IsManaged reports whether the page may configure a provider.

func LoadStored

func LoadStored(ctx context.Context, store db.Store) (map[string]*Stored, error)

LoadStored reads every managed provider's rows.

func Save

func Save(ctx context.Context, store db.Store, next *Stored) error

Save writes a provider's next state. A save is a review: it clears the "saved before v0.43.0, never applied" mark.

func UpgradeLegacy

func UpgradeLegacy(ctx context.Context, store db.Store, box *secretbox.Box, log *slog.Logger) error

UpgradeLegacy runs once per database: rows the page saved before v0.43.0 were never applied by any server, so turning them on now would switch on, on upgrade, a sign-in configuration an operator may have typed long ago and forgotten. Each such provider is imported SWITCHED OFF and marked "saved before this version, never applied" for the page to show; the operator reviews and enables it.

A secret among those rows is sealed with FILEX_SECRET_KEY. With no key it cannot be sealed, and it is not kept in the clear either: it is cleared (it was never used), the page shows it as not set, and the log says which provider and which field — never the value.

func WithRecoveryLogin

func WithRecoveryLogin(loginDrvs []auth.LoginDriver, enabled bool, store db.Store) ([]auth.LoginDriver, bool)

WithRecoveryLogin appends the recovery sign-in driver when no enabled login driver is `local` and recovery is on, and reports whether it did.

The owner's ruling, 2026-09-14: on an installation that signs in through an identity provider alone, the administrator created at installation must still be able to sign in with a password — for recovery only. Without it an unreachable IdP, an expired client secret or a broken realm locks out the one person who can repair filex's side of it. local.RecoveryLogin accepts that account and no other, so password sign-in stays off for everyone else.

⚠ It is switched off by the environment only (FILEX_AUTH_RECOVERY_LOGIN): the admin page, which can break the identity provider it would recover from, can never switch off the way back.

It goes LAST: a directory driver in the list judges its own accounts first.

func WithSessionAuthenticator

func WithSessionAuthenticator(enabled []auth.Driver, store db.Store) []auth.Driver

WithSessionAuthenticator makes sure something in the chain can turn a filex session back into a user, whichever login drivers are enabled.

⚠⚠ Issue #24. Every sign-in — a password, an LDAP bind, an OIDC callback — ends in the same sessions row and the same `filex_session` cookie, but only the `local` driver's Authenticate reads that row: OIDC's and LDAP's answer "unauthorized" by design. With `FILEX_AUTH_DRIVERS=oidc` the callback minted a session the very next request refused, so nobody could sign in and every open session died on the restart that applied the setting.

The validator is not a LoginDriver, so it enables no password sign-in; and when `local` is already in the list it adds nothing, because that driver does the same lookup.

Types

type Change

type Change struct {
	Enabled *bool
	Values  map[string]any
}

Change is what a save asks for. Values holds the fields sent; a secret sent as "" or "***" keeps the stored one (the page never has it to send back).

type Directory

type Directory interface {
	VerifyPassword(ctx context.Context, identifier, password string) (*model.User, error)
}

Directory is an external password authority (protocolauth.Directory).

type Entry

type Entry struct {
	Name   string
	Origin string
	// From says where an environment provider is defined
	// (FILEX_AUTH_DRIVERS, the config file, the built-in default).
	From string
	// Driver is nil when the provider could not start; Err says why.
	Driver auth.Driver
	Err    string
	// Directory: an LDAP provider that also judges passwords on the file
	// protocols (WebDAV, SFTP, FTPS, S3, NFS).
	Directory bool
	// Display is the configuration shown on the page for an environment
	// provider — secrets left out (Secrets says which are set).
	Display map[string]any
	// Secrets names the secret fields an environment provider has set.
	Secrets []string
	// contains filtered or unexported fields
}

Entry is one provider the instance knows of.

func NewEnvEntry

func NewEnvEntry(name, from string, cfg map[string]any, d auth.Driver, err error, directory bool) Entry

NewEnvEntry records a provider the environment/config file defines. cfg is what Build was given; the page is shown a copy with every secret removed.

func (Entry) Config

func (e Entry) Config() map[string]any

Config is the configuration the environment gave a provider (secrets included; in memory only).

func (Entry) Live

func (e Entry) Live() bool

Live reports whether the provider is running.

type Field

type Field struct {
	Key      string    `json:"key"`
	Kind     FieldKind `json:"kind"`
	Required bool      `json:"required,omitempty"`
	// Default is what an absent value means, said so the page can show it.
	Default string `json:"default,omitempty"`
}

Field is one setting a managed provider takes on the page.

func FieldOf

func FieldOf(name, key string) (Field, bool)

FieldOf returns a provider's field by key.

type FieldKind

type FieldKind string

FieldKind is how a page field is stored and handed to the driver.

const (
	FieldText   FieldKind = "text"
	FieldSecret FieldKind = "secret"
	FieldBool   FieldKind = "bool"
)

Field kinds.

type Live

type Live struct {
	// contains filtered or unexported fields
}

Live owns the running providers and swaps them when the page saves.

func New

func New(ctx context.Context, opts Options, env []Entry) (*Live, error)

New prepares a Live over the environment's providers (already built by the caller through Build) and installs a first set holding only those, so the password sign-in the environment configured works before anything the page saved has been looked at.

func (*Live) AdminPaths

func (l *Live) AdminPaths(ctx context.Context, without string) []string

AdminPaths lists the ways an administrator can still sign in with the running set, leaving out one provider (the one about to be switched off).

  • "password": password sign-in is on (`local`) and at least one enabled administrator has a password;
  • "recovery": the bootstrap administrator's recovery sign-in is on, and that account exists, is enabled and has a password;
  • the name of every other running identity provider or directory.

⚠ An identity provider counts as a way in although filex cannot prove an administrator's account is on the other side of it: the rule this serves is "never switch off the LAST way in", and a provider that is running is a way in until shown otherwise. The two password paths are checked for real, because an instance whose administrators all came in through SSO has a password form nobody can use.

func (*Live) Current

func (l *Live) Current() *Set

Current is the running set.

func (*Live) Dir

func (l *Live) Dir() Directory

Dir is the directory the file protocols consult.

func (*Live) EnvDefines

func (l *Live) EnvDefines(name string) (Entry, bool)

EnvDefines reports whether the environment lists a provider — in which case it is the effective one, and the page may not change it.

func (*Live) Environment

func (l *Live) Environment() []Entry

Environment is the environment's providers, in the order it listed them.

func (*Live) Login

func (l *Live) Login() auth.LoginDriver

Login is the password sign-in the handlers hold. It always asks the running set, so a reload changes what it does without re-wiring anything.

func (*Live) LoginName

func (l *Live) LoginName() string

LoginName names the password chain, for the boot line.

func (*Live) OIDC

func (l *Live) OIDC() auth.OIDCDriver

OIDC is the OIDC flow the handlers hold.

func (*Live) OnSwap

func (l *Live) OnSwap(f func(*Set))

OnSwap registers a callback run with every new set (and once, now).

func (*Live) Options

func (l *Live) Options() Options

Options returns the options Live runs with.

func (*Live) Reload

func (l *Live) Reload(ctx context.Context) error

Reload reads the page's providers and swaps in the set they make.

⚠ A provider that did not change is not rebuilt (an OIDC discovery per save of an unrelated LDAP field would be a network round trip for nothing), and a provider that fails to start is left out with its reason — the environment's providers and every other page provider stay exactly as they were. There is no path through here that drops `local`.

func (*Live) Start

func (l *Live) Start(ctx context.Context)

Start imports what an earlier version saved (switched off), builds the page's enabled providers and, when one could not start, keeps re-trying it in the background.

type Options

type Options struct {
	Store         db.Store
	Box           *secretbox.Box
	MultiTenant   bool
	RecoveryLogin bool
	PublicURL     string
	// BuildTimeout bounds one page provider's construction (an OIDC
	// discovery that hangs must not hold a save, or the boot, hostage).
	BuildTimeout time.Duration
	// RetryBackoff re-tries, after boot, a page provider that could not
	// start — its IdP may be booting alongside filex.
	RetryBackoff []time.Duration
	Log          *slog.Logger
}

Options is what Live needs from the server.

type Set

type Set struct {
	Entries []Entry
	// contains filtered or unexported fields
}

Set is one immutable arrangement of the providers. Live swaps whole sets, so a request never sees half of a reload.

func (*Set) Entry

func (s *Set) Entry(name string) (Entry, bool)

Entry returns a provider by name.

func (*Set) HasDirectory

func (s *Set) HasDirectory() bool

HasDirectory reports whether a directory judges passwords on the file protocols.

func (*Set) Names

func (s *Set) Names() []string

Names are the sign-in methods the login page offers: every provider the environment lists (as before) and every page provider that is running.

func (*Set) PasswordLogin

func (s *Set) PasswordLogin() bool

PasswordLogin reports whether the password form is answered at all.

func (*Set) Recovery

func (s *Set) Recovery() bool

Recovery reports whether the bootstrap administrator's recovery sign-in is on.

type Stored

type Stored struct {
	Name    string
	Enabled bool
	// Legacy: saved before v0.43.0 and never applied — review and enable.
	Legacy bool
	// Values are the stored field values; a secret stays SEALED here.
	Values map[string]string
	// Exists: at least one row is stored for this provider.
	Exists bool
}

Stored is what the settings table holds for one managed provider.

func Merge

func Merge(cur *Stored, ch Change, box *secretbox.Box) (*Stored, []string, error)

Merge applies a change to what is stored, without writing anything, and returns the next stored state plus the names of the fields that changed (never their values — this list goes into the audit row).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL