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
- Variables
- func Build(ctx context.Context, store db.Store, name string, cfg map[string]any) (auth.Driver, error)
- func Canonical(name string) string
- func DraftConfig(s *Stored, draft map[string]any, box *secretbox.Box, opts Options) (map[string]any, error)
- func DriverConfig(s *Stored, box *secretbox.Box, opts Options) (map[string]any, bool, error)
- func FromWords(from, lang string) string
- func IsManaged(name string) bool
- func LoadStored(ctx context.Context, store db.Store) (map[string]*Stored, error)
- func Save(ctx context.Context, store db.Store, next *Stored) error
- func UpgradeLegacy(ctx context.Context, store db.Store, box *secretbox.Box, log *slog.Logger) error
- func WithRecoveryLogin(loginDrvs []auth.LoginDriver, enabled bool, store db.Store) ([]auth.LoginDriver, bool)
- func WithSessionAuthenticator(enabled []auth.Driver, store db.Store) []auth.Driver
- type Change
- type Directory
- type Entry
- type Field
- type FieldKind
- type Live
- func (l *Live) AdminPaths(ctx context.Context, without string) []string
- func (l *Live) Current() *Set
- func (l *Live) Dir() Directory
- func (l *Live) EnvDefines(name string) (Entry, bool)
- func (l *Live) Environment() []Entry
- func (l *Live) Login() auth.LoginDriver
- func (l *Live) LoginName() string
- func (l *Live) OIDC() auth.OIDCDriver
- func (l *Live) OnSwap(f func(*Set))
- func (l *Live) Options() Options
- func (l *Live) Reload(ctx context.Context) error
- func (l *Live) Start(ctx context.Context)
- type Options
- type Set
- type Stored
Constants ¶
const ( OriginEnvironment = "environment" OriginPage = "page" )
Where a provider's configuration comes from.
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 ¶
var ErrOIDCOff = errors.New("OIDC is not configured")
ErrOIDCOff answers an OIDC flow when no OIDC provider is running.
var ErrPasswordSignInOff = errors.New("password sign-in is off")
ErrPasswordSignInOff answers a password sign-in when no driver takes one.
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.
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.
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 ¶
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 ¶
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 ¶
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 LoadStored ¶
LoadStored reads every managed provider's rows.
func Save ¶
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 ¶
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 ¶
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 ¶
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.
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.
type FieldKind ¶
type FieldKind string
FieldKind is how a page field is stored and handed to the driver.
type Live ¶
type Live struct {
// contains filtered or unexported fields
}
Live owns the running providers and swaps them when the page saves.
func New ¶
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 ¶
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) EnvDefines ¶
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 ¶
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) Reload ¶
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`.
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) HasDirectory ¶
HasDirectory reports whether a directory judges passwords on the file protocols.
func (*Set) Names ¶
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 ¶
PasswordLogin reports whether the password form is answered at all.
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.