identity

package
v0.48.1 Latest Latest
Warning

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

Go to latest
Published: Sep 28, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package identity owns the rules for account login names, and the single door that turns a typed identifier into a user.

Why this package exists

filex accounts have always been addressed by e-mail. The protocols filex is growing do not carry an e-mail comfortably: `sftp://ada@example.com@host` needs escaping, rclone and WinSCP config files split on `@`, and FTP's USER command with an `@` in it confuses proxying clients. So accounts get a second, short identifier — and every surface accepts EITHER (the owner's words, translated from Turkish: "wherever we can, let's look at both the e-mail and the username — think of it like a dual-side login").

"Every surface" is the load-bearing half. A login rule that lives in one handler is a login rule that is subtly different in the next one, and the difference is invisible until a user reports that the same credentials work in the browser and fail over SFTP. So resolution lives here, once, and the surfaces call Resolve.

The disambiguation rule

An identifier containing `@` is an e-mail; anything else is a username. The lookup NEVER tries both.

That is a security rule, not a tidiness one. If resolution fell back from one to the other, a username could be chosen to collide with the local part of someone else's e-mail (or vice versa), and which account answered would depend on row order and on which query happened to run first. Usernames are therefore forbidden from containing `@` (Validate rejects it), which makes the two namespaces provably disjoint and the rule total: every identifier has exactly one interpretation.

Index

Constants

View Source
const (
	MinLen = 3
	MaxLen = 32
)

Bounds for a username. 32 is also the width of the MySQL column, so the validator and the schema cannot disagree.

View Source
const (
	ProblemEmpty    = "empty"
	ProblemAt       = "at"
	ProblemShort    = "short"
	ProblemLong     = "long"
	ProblemUpper    = "upper"
	ProblemDigit    = "digit"
	ProblemChar     = "char"
	ProblemReserved = "reserved"
)

Problem kinds Check answers. They are the rule's names for the HTTP layer and the browser, which each say them in the reader's language; the English sentence Validate builds is for logs and the CLI.

View Source
const BootstrapUsername = "admin"

BootstrapUsername is the login name of the administrator filex creates at first run — the one account allowed to hold a reserved name.

Variables

View Source
var (
	ErrInvalidUsername  = errors.New("invalid username")
	ErrReservedUsername = errors.New("reserved username")
	ErrNotFound         = errors.New("no such account")
)

Sentinel errors. Callers match on these; the wrapped text is what a person reads, and the API boundary is where it gets translated.

Functions

func ClaimBootstrap

func ClaimBootstrap(ctx context.Context, c Claimer, u *model.User) error

ClaimBootstrap gives the first-run administrator the username "admin".

⚠ The owner's decision (2026-09-22): the first administrator of a new install is "admin", not "admin2". "admin" is reserved so that nobody else can take it — a later `admin@…` account still becomes "admin2" through EnsureUsername, and no person can rename themselves to it (Check refuses it) — but the bootstrap account is the one the reservation was made FOR. Every screen named it "admin2" because EnsureUsername's collision loop stepped past the reserved name.

Only server.FirstRun calls this, on an install with no accounts. Existing installs are not touched: an "admin2" stays "admin2" (no migration). A name already held by another account is left alone and reported.

func EnsureUsername

func EnsureUsername(ctx context.Context, c Claimer, u *model.User) (string, error)

EnsureUsername assigns a username to an account that has none, derived from its e-mail, and returns the name it claimed. It is idempotent: an account that already has one is left alone, and re-running the backfill after an interruption renames nobody.

Uniqueness is settled here rather than in Suggest because only this function can see the database. The suffix is a plain counter appended to the derived name — deterministic given the same order of arrival, and recognisable to its owner, which a random string would not be.

func LooksLikeEmail

func LooksLikeEmail(identifier string) bool

LooksLikeEmail reports whether an identifier should be resolved as an e-mail. The test is deliberately the crudest one that is total: usernames cannot contain `@` (Validate enforces it), so the presence of one is decisive. A stricter parse would only add ways for a valid address to be misread.

func Names

func Names(u *model.User, identifier string) bool

Names reports whether the identifier addresses this account — as its e-mail or as its username, case-insensitively.

It exists for the surfaces that have already resolved a user by some other route and now need to confirm the typed identifier belongs to them: the WebDAV credential cache (is the cached account still the one being named?) and the WebDAV API-token path (does the Basic username name the token's owner?). Comparing only the e-mail there would let a credential work in the browser and fail over the protocol, which is the per-surface drift this package exists to prevent.

func Normalize

func Normalize(identifier string) string

Normalize folds an identifier to its canonical form for lookup: trimmed and lowercased. Both namespaces are case-insensitive — a user who types `Ada` into FileZilla means `ada`, and an e-mail address is already stored lower.

func Resolve

func Resolve(ctx context.Context, l Lookup, identifier string) (*model.User, error)

Resolve turns what a person typed into an account: e-mail or username, one interpretation each (see the package comment for why it never tries both).

This is the ONLY function a login surface should call. The browser session, the desktop app, WebDAV Basic, FTPS, SFTP and the protocol credential pages all go through here, so "which identifiers work" has exactly one answer instead of one answer per surface.

Returns ErrNotFound when there is no such account — including when the identifier is not even a well-formed username, because "no account" is the only thing a login surface may reveal about a failed lookup.

func Suggest

func Suggest(email string) string

Suggest derives a candidate username from an e-mail address. It is deterministic: the same address always yields the same candidate, so a backfill that is interrupted and re-run does not rename anybody.

Uniqueness is NOT this function's job — Suggest can and will return a name that is already taken, including a reserved one. Claiming is EnsureUsername's job, because only it can see the database. That split is deliberate: Suggest never invents an unrelated name to dodge a collision.

func Validate

func Validate(username string) error

Validate reports whether a username is acceptable, as an error for logs, the CLI and callers that match the sentinels. The HTTP layer says it in the reader's language from Check instead.

Types

type Claimer

type Claimer interface {
	Lookup
	SetUserUsername(ctx context.Context, id int64, username string) error
}

Claimer additionally assigns a username. Used by account creation and by the one-time backfill.

type Lookup

type Lookup interface {
	GetUserByEmail(ctx context.Context, email string) (*model.User, error)
	GetUserByUsername(ctx context.Context, username string) (*model.User, error)
}

Lookup is the slice of db.Store this package needs to resolve an identifier. Declared here rather than imported so identity stays a leaf package that the db drivers, the auth drivers and every protocol server can all depend on.

type Problem

type Problem struct {
	Kind  string
	Char  rune // ProblemChar: the first character that is not allowed
	Limit int  // ProblemShort / ProblemLong: MinLen or MaxLen
}

Problem is why a username is refused, in parts: the kind, and the one detail the sentence needs (the character, or the limit).

func Check

func Check(username string) *Problem

Check reports why a username is not acceptable, nil when it is.

The character set is ASCII on purpose, and this is the one place in filex where that is the right answer: a username travels through SSH USER records, FTP command lines, S3 key labels and shell config files, all of which mangle non-ASCII differently. The DISPLAY name is where a person's name belongs, and it has always accepted the full alphabet.

⚠ The browser checks the same rules while a person types (packages/core/src/lib/accountRules.ts); this is the authority, that is the mirror. A rule added here must be added there, or the form lets through a name the save then refuses.

Jump to

Keyboard shortcuts

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