Help us shape the future of Go! Take the 2026 Go Developer Survey

auth

package
v0.43.0 Latest Latest
Warning

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

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

Documentation

Overview

Package auth — audit_middleware.go

Chi middleware that watches mutating routes and records a row in audit_log when the response is a 2xx. Wraps the response writer to capture the final status code and reads the *model.User from the request context (so it must be installed AFTER auth.Middleware).

Package auth defines the AuthDriver interface, registry, and middleware.

Drivers live under auth/drivers/ and self-register via init() blocks. The middleware reads request headers/cookies, attempts each enabled driver in turn, and stores the resulting *model.User on the request context for downstream handlers.

Index

Constants

View Source
const (
	// ProbeOK: this step was checked and holds.
	ProbeOK = "ok"
	// ProbeFail: this step was checked and does not hold.
	ProbeFail = "fail"
	// ProbeUnchecked: this step cannot be checked without a person signing
	// in (an OIDC redirect address, a public client). Said, never assumed.
	ProbeUnchecked = "unchecked"
)

Probe statuses.

Variables

View Source
var ErrNoTenantForLogin = errors.New("auth: cannot determine which tenant this login belongs to")

ErrNoTenantForLogin is returned when a multi-tenant install cannot tell which tenant a just-in-time account belongs to.

⚠ It is deliberately a REFUSAL and not a fallback. The only fallback available is `default`, and homing a directory user there is the bug: it hands a confine-exempt, every-storage account to whoever the directory says exists. Refusing costs an operator one config line (`provider:`) or one DNS host; the fallback costs them the tenant boundary.

View Source
var ErrUnauthorized = errors.New("auth: unauthorized")

ErrUnauthorized is returned when no driver could authenticate a request.

Functions

func AIAdminAction

func AIAdminAction(method, path, id, name string) (string, string, string)

AIAdminAction derives the audit (action, targetType, targetID) for a request on the AI admin surface (/api/ai/admin/*). It normalizes the path down to the native /api/admin/* form, reuses ActionForPath's mapping, and prefixes the resulting action with "ai." so AI-token-driven admin writes are clearly distinguishable from native-panel ones in the Audit page. id/name are the corresponding chi URL params ("" when absent). Returns an empty action when the call isn't an auditable mutating admin op.

Used both by the HTTP AuditMiddleware (for /api/ai/admin REST calls) and by the MCP admin tools (which bypass HTTP middleware and audit in-process).

func APITokenMiddleware

func APITokenMiddleware(store db.Store) func(http.Handler) http.Handler

APITokenMiddleware authenticates a request strictly via an API token (X-Filex-Token or Authorization: Bearer), attaches both the bound user and the matched token to the context, and rejects anything else with 401.

Unlike the generic Middleware chain it does NOT accept cookie sessions — the AI/MCP namespace is token-only by design. Use RequireScope after it to gate individual verbs.

func ActionForPath

func ActionForPath(method, p, id, name string) (string, string, string)

ActionForPath maps a (method, path) pair to (action, target_type, target_id).

The path matching uses strings.HasPrefix on /api/admin/storages/ etc. so trailing slashes / IDs are handled uniformly. We intentionally don't try to read JSON bodies — only URL-derivable identifiers. id/name are the already-resolved chi URL params (passed in so this function is reusable outside an *http.Request context, e.g. from the in-process MCP invoker).

func AddAuditDetail

func AddAuditDetail(ctx context.Context, key string, v interface{})

AddAuditDetail records one fact for the request's audit row; a no-op when the request is not being audited.

func AnnotateToken

func AnnotateToken(store db.Store) func(http.Handler) http.Handler

AnnotateToken attaches a valid API token to the context and NOTHING else. It never authenticates a user, never consults the session drivers, and — the whole point — never rejects: an unknown, revoked, expired or malformed token, a disabled owner, a bogus X-Filex-Token-User all simply leave the context untouched and the request proceeds as anonymous.

It exists for PUBLIC routes that must merely describe the caller — /api/files/capabilities reports `caller_kind` so the explorer knows whether to draw a person's surfaces. Reaching for MiddlewareWithToken there was wrong in a way that only shows up later: that chain answers 403 to a disabled account, so a disabled user's browser (which still holds a cookie) would have had the LOGIN page's capabilities probe fail closed. A public probe that can fail is a worse trade than a capability field.

⚠ Do not add a rejection path here. If a route needs one it needs a different middleware; this one's contract is that a public page can always get an answer.

func AnnotateUser

func AnnotateUser() func(http.Handler) http.Handler

AnnotateUser attaches the caller's user to the context when a credential resolves to one, and NOTHING else. It never rejects.

Middleware(false) is nearly this, but it still answers 403 for a disabled account — which is why /api/capabilities cannot use it: that route is fetched by the login screen and the public share pages, and a disabled user's browser still holds a cookie. Here a credential that does not work is simply an anonymous caller.

The one thing the annotation buys is the ability to answer a PUBLIC endpoint differently for a stranger: /api/capabilities publishes the operator's external service hosts, and a caller with no credential has no business learning them (see handlers.Capabilities).

⚠ Cheap on the anonymous path: every driver returns ErrUnauthorized immediately when the request carries no credential of its kind — no database round trip, no LDAP bind.

func AuditMiddleware

func AuditMiddleware(store db.Store) func(http.Handler) http.Handler

AuditMiddleware returns a chi middleware that records audit_log entries for successful (2xx) mutating requests on a curated set of paths.

func CallerMayAdminister

func CallerMayAdminister(ctx context.Context) bool

CallerMayAdminister reports whether the request's caller may use an account-wide operator power: the account is an administrator AND, when the request was authenticated by a token, that token may administer.

A session (no token on the context) is judged by the account alone, exactly as before.

func CfgBool

func CfgBool(cfg map[string]any, key string) bool

CfgBool reads a configuration value as a bool. The settings table stores every value as a string, so "true"/"1"/"yes"/"on" count as well as a JSON true.

func CfgString

func CfgString(cfg map[string]any, key string) string

CfgString reads a configuration value as a trimmed string.

func LoginAllowed

func LoginAllowed(ctx context.Context, store db.Store, multiTenant bool, u *model.User) bool

LoginAllowed reports whether an authenticated user may start a session.

Two tenant-level gates (docs/MULTI-TENANCY.md):

  • SUSPEND: a disabled provider's users cannot log in (data intact) — in both modes. The supertenant cannot be suspended out of its own platform.
  • MAINTENANCE MODE: multi-tenant OFF on an install that has grown tenants ⇒ only the supertenant provider's users may log in; tenants are locked out until the flag returns (reversible, nothing touched). A plain single-tenant install is unaffected — its only provider IS the supertenant.

One user-level gate:

  • DISABLED ACCOUNT: users.enabled = false (migration 00022) refuses the session outright, in both modes, with files/quota/grants untouched. Checked first so it holds in a plain single-tenant install too.

Otherwise it fails toward availability: a NULL provider (bootstrap/legacy admin) or an unresolvable provider is allowed, so an operator is never locked out by a glitch. Only a resolvable, non-supertenant tenant is ever refused.

func LoginHostFrom

func LoginHostFrom(ctx context.Context) string

LoginHostFrom returns the stamped host, or "".

func Middleware

func Middleware(required bool) func(http.Handler) http.Handler

Middleware tries each enabled auth Driver in order. The first one that returns a non-nil user wins; subsequent drivers are not consulted.

On failure with required=true, a 401 JSON error is written and the downstream handler is NOT called. With required=false, the request proceeds with no user attached (useful for public viewer endpoints).

func MiddlewareWithToken

func MiddlewareWithToken(store db.Store, required bool) func(http.Handler) http.Handler

MiddlewareWithToken authenticates EITHER via an API token (X-Filex-Token / Bearer) OR, failing that, the regular user-session driver chain. It powers the /api/files surface so a host app can proxy with a root-confined API token (see package confine) while the native admin panel keeps using its cookie/JWT session. An API-token bearer and a session JWT never collide: the token's sha256 simply won't match a JWT, so we fall through cleanly.

func Names

func Names() []string

Names returns the registered driver names sorted alphabetically.

func NetReason

func NetReason(err error) string

NetReason classifies a connection error into the reasons the panel has words for: refused, timeout, dns, tls, or other.

func ProbeOKAll

func ProbeOKAll(checks []ProbeCheck) bool

ProbeOKAll reports whether a probe passed: nothing failed and something was actually checked.

func ProvisionUser

func ProvisionUser(ctx context.Context, store ProvisionStore, h TenantHoming, driver, email, role string) (*model.User, error)

ProvisionUser creates `email` and homes it in the tenant this login arrived for. `driver` names the caller, for the log line an operator will need when a directory user cannot sign in.

The three beats are the ones the invite path established: create; home; and on a failure to home, DELETE the row rather than leave a half-created supertenant account behind.

func Register

func Register(name string, f Factory)

Register adds a driver factory keyed by name. Called from drivers' init().

func RequireAdmin

func RequireAdmin(next http.Handler) http.Handler

RequireAdmin denies with 403 a caller that may not administer: a non-admin account, or — when the request was authenticated by an API token — a token that does not grant the admin scope or is confined to a folder (see CallerMayAdminister). A signed-in administrator's session is judged by the account alone, as it always was.

⚠ It can only judge a token it can see, so the route group must attach the token to the context: MiddlewareWithToken does, and so does Middleware since the api-token driver reports its token to the chain (TokenAuthenticator).

func RequireAdminToken

func RequireAdminToken(next http.Handler) http.Handler

RequireAdminToken is the gate for the token-only admin surface (/api/ai/admin), where the token's scope — not the account's role — is what authorizes (the handlers there elevate the bound principal). It replaces RequireScope("admin") so a confined token is refused too. Must run after APITokenMiddleware.

func RequireScope

func RequireScope(scope string) func(http.Handler) http.Handler

RequireScope rejects requests whose token does not grant `scope`. A token grants only what its list names; an empty list grants nothing. Must run after APITokenMiddleware.

func ScopeForUser

func ScopeForUser(ctx context.Context, store db.Store, u *model.User) *tenant.Scope

ScopeForUser resolves a user's tenant scope. Callers that authenticate outside the middleware chain — /dav does its own Basic auth — use this to attach the same scope the HTTP middleware would have.

ALWAYS returns a non-nil scope so the scoped store filters. A user whose provider is missing or unknown gets tenant.DenyAll (sees nothing) — fail closed, never a silent fall-through to "see everything".

func SetAuditTarget

func SetAuditTarget(ctx context.Context, id, name string)

SetAuditTarget names the thing the request acted on, for the audit row the middleware writes: id fills target_id when the route did not carry one, name is stored as metadata["target_name"].

⚠⚠ Why: the Panel and the Audit page printed the KIND of thing and nothing else — "Kullanıcı: oluşturuldu — Kullanıcı", "Depo: oluşturuldu — Depo" (release-candidate sweep, 2026-09-21) — because a create's URL has no id and the middleware never reads bodies. The handler knows what it made; this is how it says so. The name is kept because it is what a reader wants AFTER the thing is gone ("which user was deleted?"). A name here is a label — an e-mail, a storage name, a path — never a secret.

func SetEnabled

func SetEnabled(drivers []Driver)

SetEnabled stores the active driver chain (ordered) for the middleware.

func TenantResolver

func TenantResolver(store db.Store, multiTenant bool) func(http.Handler) http.Handler

TenantResolver derives the request's tenant (provider) scope from the authenticated user's provider_id and stashes it in the context for the scoped store and storage confinement to read. It is a **no-op when multi-tenant mode is off** — no scope is set, and callers treat the absence as "unscoped", so single-tenant behaviour is byte-unchanged. Mount it AFTER the auth middleware so the user is already resolved.

The user's own provider_id — not the request Host — is the source of truth for scoping: a user belongs to exactly one tenant regardless of which host they reach. (Host binding drives which OIDC realm authenticates a *login*; that is the login layer's concern, not request scoping.)

A supertenant scope carries IsSupertenant + no StorageIDs; it is confine-exempt and cross-tenant (see docs/MULTI-TENANCY.md).

func TokenFrom

func TokenFrom(ctx context.Context) *model.APIToken

TokenFrom returns the API token from ctx, nil if the request was not authenticated by an API token (e.g. a cookie session).

func TokenIsConfined

func TokenIsConfined(tok *model.APIToken) bool

TokenIsConfined reports whether tok carries a `root:` confinement scope.

⚠ Syntactic on purpose: ANY `root:` entry counts, well-formed or not. A scope the confinement parser cannot read (`root:projects`, no storage) would otherwise make the token look unconfined here — the one direction a mistake in an authorization check must not fall.

func TokenMayAdminister

func TokenMayAdminister(tok *model.APIToken) bool

TokenMayAdminister reports whether a token is an operator credential: it grants the admin scope and is not confined. It says nothing about the account the token is bound to — see CallerMayAdminister for that.

func TokenUserFrom

func TokenUserFrom(ctx context.Context) string

TokenUserFrom returns the identity a token-authenticated request acts under ("" for cookie sessions). Set by the token middlewares alongside WithToken.

func UserFrom

func UserFrom(ctx context.Context) *model.User

UserFrom returns the user from ctx, nil if absent.

func WithLoginHost

func WithLoginHost(ctx context.Context, host string) context.Context

WithLoginHost stamps the Host of the HTTP request that started a login onto the context.

⚠ It exists because auth.LoginDriver.Login takes only a ctx — no *http.Request — so a driver in the login chain cannot see the host by itself. Widening the interface would touch every driver and every protocol caller for the benefit of one; the context carries it instead, and a login that arrives without one (a protocol login) simply has no host, which is the truth.

func WithRequestLoginHost

func WithRequestLoginHost(ctx context.Context, r *http.Request) context.Context

WithRequestLoginHost is WithLoginHost for an *http.Request.

func WithToken

func WithToken(ctx context.Context, t *model.APIToken) context.Context

WithToken stores the matched API token on ctx, and notes its id (never the secret) on the request's access-log holder, like WithUser does the account.

func WithTokenUser

func WithTokenUser(ctx context.Context, username string) context.Context

WithTokenUser stores the resolved token username on ctx.

func WithUser

func WithUser(ctx context.Context, u *model.User) context.Context

WithUser stores u on ctx.

It also notes the account on the request's access-log holder, when there is one (httpx.RequestLog): every door that authenticates a request passes through here, so the access log learns who asked without each of them having to remember to say so.

Types

type AuditDetail

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

AuditDetail is what a handler adds to the audit row the middleware writes for its request: the facts only the handler knows (which fields changed, before and after) — so a change is ONE row that says what it did, not a bare "auth_provider.update".

⚠ Names and flags only. Nothing a handler puts here may be a secret: the audit log is read by every administrator and exported.

func WithAuditDetail

func WithAuditDetail(ctx context.Context) (context.Context, *AuditDetail)

WithAuditDetail returns a context carrying an empty detail holder.

func (*AuditDetail) ApplyTarget

func (d *AuditDetail) ApplyTarget(targetID string, meta map[string]interface{}) (string, map[string]interface{})

ApplyTarget folds SetAuditTarget's answer into a row: the id only where the route gave none, the name into meta. Nil-safe.

func (*AuditDetail) Into

func (d *AuditDetail) Into(meta map[string]interface{}) map[string]interface{}

Into merges the recorded facts into meta (allocating it when needed).

type Capabilities

type Capabilities struct {
	SignIn         bool `json:"sign_in"`
	Logout         bool `json:"logout"`
	ChangePassword bool `json:"change_password"`
	Register       bool `json:"register"`
}

Capabilities advertises what an auth driver supports — used by the UI to decide whether to render Login / Register / ChangePassword buttons.

type Driver

type Driver interface {
	Name() string
	Init(ctx context.Context, cfg map[string]any) error
	Authenticate(r *http.Request) (*model.User, error)
	Capabilities() Capabilities
}

Driver is the abstract authentication backend.

Authenticate is called by the auth middleware for every protected request and must be cheap. Drivers should NOT block on network IO during Authenticate — heavy work belongs in Init or in callbacks.

func Enabled

func Enabled() []Driver

Enabled returns the configured driver chain.

func Get

func Get(name string) (Driver, error)

Get returns a freshly-constructed driver instance, error if unknown.

type Factory

type Factory func() Driver

Factory builds a fresh Driver instance — invoked once per server start.

type LoginChain

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

LoginChain tries a password against several login drivers in turn.

Why this type exists

Before it, the login handler held ONE LoginDriver and the server bootstrap only ever assigned the `local` driver to it. A directory driver could be configured, initialised, and listed in the boot banner while its Login method was unreachable from any code path — `FILEX_AUTH_DRIVERS=local,ldap` produced a server that said "Auth: ldap, local" and answered every directory account with 401 in ~350us, which is less than one LDAPS round trip. The docs meanwhile described exactly the behaviour this type implements ("filex tries each enabled driver in order"), so the gap read as a directory misconfiguration to everyone who hit it.

Order is the contract

Drivers are tried in the order the operator listed them in `auth.drivers` / FILEX_AUTH_DRIVERS. Keeping `local` first matters: it is a bcrypt compare against a row that is already local, so the built-in admin@local account and every break-glass password stay answerable when the directory is unreachable. A directory driver placed first would put a network round trip in front of every local login and, on an unreachable directory, in front of the one login that is supposed to still work.

func NewLoginChain

func NewLoginChain(drivers ...LoginDriver) *LoginChain

NewLoginChain returns a chain over drivers, in order. Nil drivers are dropped so a caller can pass an optional driver without a nil check.

func (*LoginChain) Len

func (c *LoginChain) Len() int

Len reports how many drivers the chain will try. Used by the bootstrap to avoid wrapping a single driver in a chain for nothing.

func (*LoginChain) Login

func (c *LoginChain) Login(ctx context.Context, identifier, password string) (*model.User, string, error)

Login tries each driver in order and returns the first success.

A driver that answers ErrUnauthorized has judged the credentials and said no; the chain moves on, because "not my user" and "wrong password" are the same answer by design (no account enumeration). Any OTHER error means the driver could not judge at all — an unreachable directory, a TLS failure, a bad service-account bind — and that is NOT swallowed: it is logged with the driver name and returned when no later driver succeeds, so an operator reading the log can tell "wrong password" from "the directory is down".

func (*LoginChain) Logout

func (c *LoginChain) Logout(ctx context.Context, token string) error

Logout revokes the session on every member.

Every login driver mints its session in the same sessions table (that is what makes one cookie work regardless of which driver authenticated), so revoking on all of them is idempotent rather than wasteful — and it stays correct if a future driver keeps state of its own. The first error is reported; the remaining drivers still run, because a half-revoked session is the one outcome worth avoiding.

func (*LoginChain) Name

func (c *LoginChain) Name() string

Name reports the chain and its members, so a boot line or a log entry names the drivers a password will actually be tried against — the thing that was impossible to see from the outside before.

type LoginDriver

type LoginDriver interface {
	Login(ctx context.Context, email, password string) (*model.User, string, error) // user, sessionToken, err
	Logout(ctx context.Context, token string) error
}

LoginDriver is an optional capability — drivers that accept username + password directly (i.e. local) implement it.

type OIDCDriver

type OIDCDriver interface {
	StartFlow(w http.ResponseWriter, r *http.Request) error
	HandleCallback(w http.ResponseWriter, r *http.Request) (*model.User, string, error)
}

OIDCDriver is an optional capability — drivers that perform browser redirects to an external IdP implement it.

type OIDCLogoutDriver

type OIDCLogoutDriver interface {
	EndSessionURL(r *http.Request, idToken, postLogoutRedirect string) string
}

OIDCLogoutDriver is an optional capability of an OIDCDriver: RP-initiated logout (OpenID Connect RP-Initiated Logout 1.0). EndSessionURL returns where to send the browser so the IdP ends its own session too — idToken is the one kept for the session being signed out — or "" to keep sign-out local.

type ProbeCheck

type ProbeCheck struct {
	ID     string            `json:"id"`
	Status string            `json:"status"`
	Params map[string]string `json:"params,omitempty"`
}

ProbeCheck is one step of a provider test. ID names the step (the panel words it in the reader's language); Params carries what the sentence needs — a host, a field list, a reason, the technical detail.

func Check

func Check(id, status string, kv ...string) ProbeCheck

Check builds a ProbeCheck from key/value pairs.

type Prober

type Prober interface {
	Probe(ctx context.Context, cfg map[string]any, r *http.Request) []ProbeCheck
}

Prober is a driver that can test a configuration without anyone signing in. cfg is the configuration to test (the form's values over the stored ones), not the running driver's; r is the administrator's own request, which the proxy-header driver examines.

type ProvisionStore

type ProvisionStore interface {
	CreateUser(ctx context.Context, email, passwordHash, role, locale, tz string) (*model.User, error)
	DeleteUser(ctx context.Context, id int64) error
	GetUser(ctx context.Context, id int64) (*model.User, error)
	SetUserProvider(ctx context.Context, userID, providerID int64, oidcSubject string) error
	GetProviderByHost(ctx context.Context, host string) (*model.Provider, error)
	GetProviderBySlug(ctx context.Context, slug string) (*model.Provider, error)
}

ProvisionStore is the slice of db.Store just-in-time provisioning needs.

type TenantHoming

type TenantHoming struct {
	// MultiTenant mirrors cfg.MultiTenant. False = do nothing at all.
	MultiTenant bool
	// Pin is an operator-declared provider SLUG, used when the login carries no
	// host that maps to a tenant.
	//
	// ⚠ It exists for the logins that structurally have no Host: SFTP, FTPS and
	// NFS present a password on a socket, not an HTTP request. Without it, a
	// multi-tenant install simply cannot provision a directory user over those
	// protocols — which is a defensible default (the account still works the
	// moment it has logged in through the web once) but a bad surprise, so the
	// operator gets a way to say it out loud.
	Pin string
}

TenantHoming is a driver's policy for where its JIT accounts land.

type TokenAuthenticator

type TokenAuthenticator interface {
	AuthenticateToken(r *http.Request) (*model.User, *model.APIToken, error)
}

TokenAuthenticator is implemented by a Driver whose credential IS an API token (drivers/apitoken). The chain calls it instead of Authenticate so the matched token reaches the request context together with the user.

⚠ Why this exists: the api-token driver is always enabled, so a token authenticates on any route group built on Middleware — and the driver used to hand back the user and drop the token. Every scope check downstream then saw a request with no token at all and judged it by the account's role, which made a read-only token minted on an administrator's account a full administrator credential on the admin routes.

Directories

Path Synopsis
drivers
apitoken
Package apitoken implements bearer-token authentication for non-interactive callers: AI agents, the work.example.com FilexClient, and the embedded MCP server.
Package apitoken implements bearer-token authentication for non-interactive callers: AI agents, the work.example.com FilexClient, and the embedded MCP server.
ldap
Package ldap implements simple-bind LDAP/Active Directory authentication.
Package ldap implements simple-bind LDAP/Active Directory authentication.
local
Package local implements username + password authentication backed by the users table.
Package local implements username + password authentication backed by the users table.
multioidc
Package multioidc dispatches OIDC flows to per-tenant IdP realms (docs/MULTI-TENANCY.md §5,§7).
Package multioidc dispatches OIDC flows to per-tenant IdP realms (docs/MULTI-TENANCY.md §5,§7).
oidc
Package oidc implements OpenID Connect authentication.
Package oidc implements OpenID Connect authentication.
proxyheader
Package proxyheader implements authentication via headers injected by a trusted upstream reverse proxy (nginx, Caddy, oauth2-proxy, Cloudflare Access, Authelia, …).
Package proxyheader implements authentication via headers injected by a trusted upstream reverse proxy (nginx, Caddy, oauth2-proxy, Cloudflare Access, Authelia, …).

Jump to

Keyboard shortcuts

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