Documentation
¶
Overview ¶
Package store is the SQLite-backed home of the users table, the schema migration engine, and the user lifecycle every gogo app shares. The driver is modernc.org/sqlite (pure Go), so a binary built on gogo stays CGO-free.
An application creates its own domain tables through its own migrations, applied against the same handle, and cascades the user lifecycle into them through an OwnershipModel. The store enforces the identity invariants itself, so no API path can bypass them, and it names every refusal as a sentinel: see docs/design.md, FR-4.3 and FR-10.1.
Index ¶
- Constants
- Variables
- func EnumPref(allowed ...string) func(string) error
- func Exists(path string) bool
- func LibSchemaHead() string
- func NormalizeEmail(s string) (string, error)
- func NormalizePersonName(s string) (string, error)
- func ParseSignupReviewSubject(subject string) (signupID, adminID int64, ok bool)
- func ParseSignupSubject(subject string) (int64, bool)
- func ParseUserEmailSubject(subject string) (id int64, email string, ok bool)
- func SignupReviewSubject(signupID, adminID int64) string
- func SignupSubject(id int64) string
- func UserEmailSubject(id int64, email string) string
- func ValidateSetting(k SettingKey, value string) error
- type ActionToken
- type NopOwnership
- type OwnershipModel
- type PAT
- type PrefKey
- type SettingKey
- type SettingKind
- type Signup
- type Store
- func (s *Store) AppPref(key string) (string, error)
- func (s *Store) AppliedMigrations() []string
- func (s *Store) AuthGeneration() uint64
- func (s *Store) BeginTOTP(userID int64, secret string) (*TOTP, error)
- func (s *Store) Bootstrapped() (bool, error)
- func (s *Store) BumpSessionEpoch(id int64) (int64, error)
- func (s *Store) ClearPref(userID int64, key string) error
- func (s *Store) Close() error
- func (s *Store) CompleteSignup(id int64, passwordHash string) (*User, error)
- func (s *Store) ConfirmTOTP(userID, step int64) (*User, error)
- func (s *Store) ConsumeActionToken(purpose, tokenHash string) (*ActionToken, error)
- func (s *Store) CountRecoveryCodes(userID int64) (int, error)
- func (s *Store) CountUsers() (int, error)
- func (s *Store) CreateActionToken(purpose, subject, tokenHash string, ttl time.Duration) (*ActionToken, error)
- func (s *Store) CreatePAT(userID int64, label, tokenHash string, expiresAt *int64) (*PAT, error)
- func (s *Store) CreateSignup(name, first, last, email, ip string) (*Signup, error)
- func (s *Store) CreateUser(name, role, tokenHash, certCN string, info ...UserInfo) (*User, error)
- func (s *Store) DB() *sql.DB
- func (s *Store) DecideSignup(id, adminID int64, approve bool) (*Signup, error)
- func (s *Store) DeletePAT(userID, id int64) error
- func (s *Store) DeletePATs(userID int64) (int64, error)
- func (s *Store) DeleteSignup(id int64) error
- func (s *Store) DeleteUser(id int64) error
- func (s *Store) GetAnonymousUser() (*User, error)
- func (s *Store) GetPref(userID int64, key string) (string, error)
- func (s *Store) GetPrefs(userID int64) (map[string]string, error)
- func (s *Store) GetSignup(id int64) (*Signup, error)
- func (s *Store) GetTOTP(userID int64) (*TOTP, error)
- func (s *Store) GetUser(id int64) (*User, error)
- func (s *Store) GetUserByCertCN(cn string) (*User, error)
- func (s *Store) GetUserByEmail(email string) (*User, error)
- func (s *Store) GetUserByName(name string) (*User, error)
- func (s *Store) InvalidateUserActionTokens(purpose string, userID int64) (int64, error)
- func (s *Store) ListPATs(userID int64) ([]*PAT, error)
- func (s *Store) ListReviewAdmins() ([]*User, error)
- func (s *Store) ListSignups() ([]*Signup, error)
- func (s *Store) ListUsers() ([]*User, error)
- func (s *Store) MergeCertUser(sourceID, targetID int64) error
- func (s *Store) NonAdminRole() string
- func (s *Store) NoteLoginFailure(id int64) error
- func (s *Store) NoteLoginSuccess(id int64) error
- func (s *Store) PasswordHash(id int64) (string, error)
- func (s *Store) PeekActionToken(purpose, tokenHash string) (*ActionToken, error)
- func (s *Store) PrefKeyOf(key string) (PrefKey, bool)
- func (s *Store) RegisterPref(k PrefKey) error
- func (s *Store) RegisterSetting(k SettingKey) error
- func (s *Store) RegisteredPrefs() []PrefKey
- func (s *Store) RegisteredSettings() []SettingKey
- func (s *Store) SchemaVersion() string
- func (s *Store) SeedAnonymousUser(enable bool) error
- func (s *Store) SetAppPref(key, value string) error
- func (s *Store) SetAppPrefs(patch map[string]*string) error
- func (s *Store) SetNonAdminRole(role string)
- func (s *Store) SetPassword(id int64, hash string) (*User, error)
- func (s *Store) SetPref(userID int64, key, value string) error
- func (s *Store) SetPrefDefault(key, def string) error
- func (s *Store) SetPrefs(userID int64, patch map[string]*string) error
- func (s *Store) SetRecoveryCodes(userID int64, hashes []string) error
- func (s *Store) SetTOTPState(userID int64, state string) (*User, error)
- func (s *Store) SettingBool(key string) bool
- func (s *Store) SettingDuration(key string) time.Duration
- func (s *Store) SettingFloat(key string) float64
- func (s *Store) SettingInt(key string) int
- func (s *Store) SettingKeyOf(key string) (SettingKey, bool)
- func (s *Store) SettingString(key string) string
- func (s *Store) SignupOrigin(id int64) (string, error)
- func (s *Store) SignupVisits(id int64) (int, error)
- func (s *Store) SpendRecoveryCode(userID int64, hash string) error
- func (s *Store) SpendTOTPStep(userID, step int64) error
- func (s *Store) StoredAppPrefs() (map[string]string, error)
- func (s *Store) SweepActionTokens(grace time.Duration) (int64, error)
- func (s *Store) SweepPendingTOTP(grace time.Duration) (int, error)
- func (s *Store) SweepSignups(ttl, grace time.Duration) (int64, error)
- func (s *Store) TouchPAT(id int64)
- func (s *Store) TouchSignup(id int64, ip string)
- func (s *Store) TouchUser(id int64)
- func (s *Store) Unlock(id int64) error
- func (s *Store) UpdateUser(id int64, up UserUpdate) (*User, error)
- func (s *Store) UserByToken(tokenHash string) (*User, *PAT, error)
- func (s *Store) ValidateEmail(id int64, email string) (*User, error)
- func (s *Store) ValidateSignup(id int64) (*Signup, error)
- func (s *Store) ValidateStored() []error
- type TOTP
- type User
- type UserInfo
- type UserUpdate
Constants ¶
const ( // PurposeEmailVerify proves that the address on an account reaches the person // holding the link. PurposeEmailVerify = "email_verify" // PurposeSignupValidate proves a self-service applicant's address. PurposeSignupValidate = "signup_validate" // PurposeSignupReview carries an admin's approve/reject review of a signup. PurposeSignupReview = "signup_review" // PurposePasswordReset resets a forgotten password. PurposePasswordReset = "password_reset" // PurposeSignupFinish sets the password of an approved applicant, which creates // the account. It is deliberately not a password reset -- a reset acts on a user // that exists, this acts on a signup -- and the closed purpose set is what keeps // one from being spent as the other. PurposeSignupFinish = "signup_finish" )
The purposes an action token may carry. The set is closed: a purpose the library does not know is refused at mint and at use, so a token minted for one flow can never be spent in another (FR-7.3).
const ( LoginMaxFailures = 3 LoginLockout = 10 * time.Minute )
Lockout: three strikes, ten minutes. A password is the one credential worth guessing -- a 256-bit token is not -- so it is the one that needs a brake.
Two properties, both enforced above this file (FR-5.6):
- The lock is invisible. A locked account answers a sign-in exactly as a wrong password does, and an argon2 hash (real or dummy) runs either way, so the login form is no oracle for which accounts exist or which are under attack.
- The lock binds passwords only. A PAT and a client certificate authenticate a locked account normally, so nobody can lock an admin out of their own service by spraying passwords at a public username.
It is per account, and composes with the per-client rate limiter rather than replacing it.
const ( // SignupPending is an application whose address has not been proved. SignupPending = "pending" // SignupValidated is one whose address has been proved, and which is now // waiting on an admin. The sweeper never touches these: it is not stale, it is // queued (FR-9.8). SignupValidated = "validated" // SignupApproved is one an admin said yes to (or that was auto-approved with // review off). It is waiting for its applicant to choose a password. SignupApproved = "approved" // SignupRejected is one an admin said no to. It lingers for a grace period and // is then swept. SignupRejected = "rejected" )
The states a signup passes through. The set is closed, and every transition is guarded in this file: pending -> validated -> approved|rejected -> (a users row).
const ( RoleUser = "user" RoleAdmin = "admin" )
Roles. The non-admin role is "user"; a client certificate can only ever produce the non-admin role (a certificate never grants admin).
const ( TOTPOff = "off" // no second factor, none asked for TOTPEnabled = "enabled" // required; the next password login enrolls TOTPEnrolled = "enrolled" // a confirmed secret; every password login is challenged TOTPDisabled = "disabled" // a confirmed secret, prompt suspended by an admin )
The four second-factor states. The set is closed: a value the library does not know is refused on write.
const AnonymousName = "anonymous"
AnonymousName is the reserved name of the shared account that backs the GOGO_ANONYMOUS_SUBNET grant. It is seeded, never deletable, and cannot be claimed by any other user; it holds no credential, so it is only ever reached via the grant.
const DriverName = "sqlite-gogo"
DriverName is the database/sql driver gogo opens SQLite with. An application that opens a second database of its own -- a cache, a rollup, anything outside the shared schema -- can name it here to have those statements counted too:
db, err := sql.Open(store.DriverName, dsn)
const LegacyTokenLabel = "legacy"
LegacyTokenLabel names the PAT that carries an account's single pre-PAT bearer token (FR-5.2). CreateUser mints one, because the create API returns a token exactly once, and a regenerate replaces exactly this one. Its owner may revoke it once they hold another.
const MaxPrefValue = 4096
MaxPrefValue bounds one stored value.
Variables ¶
var ErrBadPrefValue = errors.New("store: that is not a legal value for this preference")
ErrBadPrefValue is returned when a key's validator refuses the value, or it is longer than MaxPrefValue.
var ErrBadSettingValue = errors.New("store: that is not a legal value for this setting")
ErrBadSettingValue is returned when a key's validator refuses the value, or it is longer than MaxPrefValue.
var ErrEmailChanged = errors.New("store: the address has changed since that link was sent")
ErrEmailChanged is returned when a validation is claimed for an address the account no longer holds -- a verification link followed after the address moved. Nobody proved the new address, so the claim is refused.
var ErrEmailTaken = errors.New("store: that e-mail address is already in use")
ErrEmailTaken is returned when an address is already held by another account. One address belongs to one account: verification and recovery are undefined if two accounts may claim one.
var ErrLastAdmin = errors.New("store: cannot remove the last admin")
ErrLastAdmin is returned when deleting or demoting the final admin user.
var ErrMergeIntoSelf = errors.New("store: cannot merge a user into itself")
ErrMergeIntoSelf is returned when a user is merged into itself.
var ErrNameTaken = errors.New("store: that name is taken")
ErrNameTaken means the username is already an account, or already spoken for by a live signup. Unlike a taken address, this one is reported to the applicant (FR-9.3).
var ErrNoEmail = errors.New("store: cannot validate an empty e-mail address")
ErrNoEmail is returned when an empty address is marked validated.
var ErrNoPassword = fmt.Errorf("store: that account has no password")
ErrNoPassword is returned when a password operation names an account that has none, or an empty hash.
var ErrNoSecret = errors.New("store: a second factor needs a secret")
ErrNoSecret is returned when an enrollment is begun with an empty secret.
var ErrNotCertOnly = errors.New("store: source is not a certificate-only user")
ErrNotCertOnly is returned when a merge source is not a cert-only user.
var ErrNotFound = errors.New("store: not found")
ErrNotFound is returned when a keyed row does not exist.
var ErrPATExpired = errors.New("store: that token has expired")
ErrPATExpired is returned when a presented token is genuine but its lifetime has run out. The resolver turns it into the same 401 an unknown token gets; the difference shows only in the metric and the log line.
var ErrPATLabel = errors.New("store: a token needs a short, printable label")
ErrPATLabel is returned for an empty or implausible label.
var ErrPrefRegistered = errors.New("store: that preference is already registered")
ErrPrefRegistered is returned when a key is registered twice.
var ErrReservedName = errors.New("store: that name is reserved")
ErrReservedName is returned when a create or rename would use a reserved name.
var ErrReservedUser = errors.New("store: that user is reserved and cannot be removed")
ErrReservedUser is returned when an operation targets a reserved account that must not be removed.
var ErrSettingRegistered = errors.New("store: that setting is already registered")
ErrSettingRegistered is returned when a key is registered twice.
var ErrSignupExists = errors.New("store: a signup for that address is already in flight")
ErrSignupExists means an application for this address is already in flight and past the point where a fresh one would replace it. The caller must not say so: see CreateSignup.
var ErrSignupState = errors.New("store: that signup has already been decided")
ErrSignupState means the signup is not in the state this transition needs -- an approval of something already rejected, a second decision on a decided one. It is how the second of two admins is told who won, rather than overruling them (FR-9.5).
var ErrTOTPEnrolled = errors.New("store: that account already has a second factor")
ErrTOTPEnrolled is returned when enrollment is begun on an account that already holds a confirmed secret. Clear it first, so two live secrets never exist.
var ErrTOTPReplay = errors.New("store: that code has already been used")
ErrTOTPReplay is returned when a code, or a step, has already been spent.
var ErrTOTPState = errors.New("store: that is not a legal second-factor state")
ErrTOTPState is returned for an unknown state, or a transition that is not legal from the account's current one.
var ErrTargetHasCert = errors.New("store: target already has a certificate")
ErrTargetHasCert is returned when a merge target already holds a certificate.
var ErrTokenExpired = errors.New("store: that link has expired")
ErrTokenExpired is returned when a token's lifetime has run out.
var ErrTokenInvalid = errors.New("store: that link is not valid")
ErrTokenInvalid is returned when a presented token is unknown, or was minted for a different purpose. The two are one error deliberately: a caller must not learn which of them it was.
var ErrTokenUsed = errors.New("store: that link has already been used")
ErrTokenUsed is returned when a token has already been spent. Single use is enforced in a transaction, so a link raced twice succeeds exactly once.
var ErrUnknownPref = errors.New("store: no such preference")
ErrUnknownPref is returned for a key that is not registered.
var ErrUnknownSetting = errors.New("store: no such setting")
ErrUnknownSetting is returned for a key that is not registered.
Functions ¶
func LibSchemaHead ¶
func LibSchemaHead() string
LibSchemaHead returns the latest migration version the library embeds, without opening a database. An app combines it with its own head for `-version`.
func NormalizeEmail ¶ added in v1.2.0
NormalizeEmail trims, lowercases, and validates an address. The empty string is valid and normalizes to empty ("no address"). Otherwise the address must be a bare RFC 5322 addr-spec -- no display name, no angle brackets, no comment, no whitespace -- with a dotted domain, at most 254 bytes in total and 64 in the local part. See docs/design.md, FR-4.3.
The whole address is case-folded, not just the domain, which is what lets "one address, one account" be a plain UNIQUE index rather than a functional one.
The display name is refused, not stripped: "Anyone <a@b.c>" parses, and accepting it would let attacker-chosen text ride in the stored value and into the From or To of a mail the library later sends.
func NormalizePersonName ¶ added in v1.2.0
NormalizePersonName trims a first or last name. Empty is valid. A name is at most 64 runes and carries no control character; it is otherwise unrestricted.
func ParseSignupReviewSubject ¶ added in v1.2.0
ParseSignupReviewSubject reads back what SignupReviewSubject wrote.
func ParseSignupSubject ¶ added in v1.2.0
ParseSignupSubject reads back what SignupSubject wrote.
func ParseUserEmailSubject ¶ added in v1.2.0
ParseUserEmailSubject reads back what UserEmailSubject wrote.
func SignupReviewSubject ¶ added in v1.2.0
SignupReviewSubject names one application and one admin: "signup:7/admin:3".
The admin is in the subject for two reasons. Minting invalidates the outstanding tokens for the same subject, so a subject shared by every admin would mean only the last one mailed could act. And it records who decided: the review page needs no session (FR-9.5).
func SignupSubject ¶ added in v1.2.0
SignupSubject is the subject of a token that acts on one application: "signup:7". It names no address: unlike a verification or a reset, the token claims nothing about an existing account's address. The address lives in the row.
func UserEmailSubject ¶ added in v1.2.0
UserEmailSubject is the subject of a token that acts on one account and one exact address: "user:42/pim@ipng.ch".
Binding the address, not just the account, is what makes a stale link safe: a token naming only the account would validate whatever address the account held when the link was finally followed -- an address nobody proved. A link outstanding across a change of address names an address the account no longer holds, and the consumer refuses it (FR-7.4).
func ValidateSetting ¶ added in v1.32.0
func ValidateSetting(k SettingKey, value string) error
ValidateSetting checks a value against a key's Kind and then against the key's own Validate. It is exported because an app registering its own settings validates the same way the library does, and because the settings screen's error text comes from here.
An empty value is admitted by every kind: it is how a setting says "not configured", and the loader's own rules -- mail needs a From address once a host is set -- catch the combinations a per-key check cannot see.
Types ¶
type ActionToken ¶ added in v1.2.0
type ActionToken struct {
ID int64
Purpose string
Subject string
Created int64
ExpiresAt int64
UsedAt *int64
}
ActionToken is one minted, single-use secret: what it is for, what it acts on, when it dies, and whether it has been spent. It never carries the plaintext -- that exists in exactly one place in the system, the body of one mail (FR-7.2).
func (*ActionToken) Spent ¶ added in v1.2.0
func (t *ActionToken) Spent() bool
Spent reports whether the token has been consumed.
type NopOwnership ¶
type NopOwnership struct{}
NopOwnership is the ownership model for an app that keys ownership by immutable user id and relies on an ON DELETE SET NULL foreign key: every hook is a no-op. It is the default when Open is passed a nil model.
type OwnershipModel ¶
type OwnershipModel interface {
// Reassign moves every resource owned by the source user to the target user,
// during a certificate-into-token merge.
Reassign(tx *sql.Tx, srcID, dstID int64, srcName, dstName string) error
// Rename updates ownership references after a user is renamed.
Rename(tx *sql.Tx, userID int64, oldName, newName string) error
// Release detaches a deleted user's resources.
Release(tx *sql.Tx, userID int64, name string) error
}
OwnershipModel connects the user lifecycle to an application's resource tables. The library manages users -- create, rename, disable, delete, and merge a certificate identity into a token identity -- but only the app knows how a resource records its owner. Each method runs inside the library's own transaction (tx), so an app must not commit or roll back; returning an error aborts the whole operation.
Two ownership models cover the family:
- By immutable user id (an integer foreign key with ON DELETE SET NULL): Rename and Release are no-ops, and Reassign updates the id column.
- By free-text owner name: Rename rewrites the name, Release is a no-op (rows go dormant, revived if the name returns), and Reassign rewrites the name from source to target.
type PAT ¶ added in v1.2.0
type PAT struct {
ID int64
UserID int64
Label string
Created int64
ExpiresAt *int64 // nil = never
LastUsed *int64
}
PAT is one personal access token: what it is for, when it dies, and when it was last used. It never carries the secret -- only its SHA-256 hash is stored, so a stolen database yields no working credential (NFR-5).
type PrefKey ¶ added in v1.31.0
type PrefKey struct {
Key string
// Default is what GetPref answers when the account has chosen nothing. It is not
// passed through Validate.
Default string
// Validate refuses a value; nil admits anything within MaxPrefValue.
Validate func(string) error
}
PrefKey is one preference this app admits.
type SettingKey ¶ added in v1.32.0
type SettingKey struct {
Key string
Kind SettingKind
// Default is the value a key reads as while it has no row. It is not passed through
// Validate.
Default string
// Label and Help are what the screen shows. Label is a short noun phrase; Help is one
// sentence, and says what a wrong value does rather than restating the label.
Label string
Help string
// Group collects related keys into one fieldset on the screen ("brand", "mail").
Group string
// Scope is the tab: empty is the library's, otherwise the app's slug.
Scope string
// Enum is the admissible set for KindEnum, and the options the screen offers.
Enum []string
// EnumLabels are what the screen shows for each Enum value, in the same order. Empty
// shows the raw values, which is right for a vocabulary that already reads as English
// ("light", "dark") and wrong for one that does not ("auto", "true", "false").
EnumLabels []string
// Restart says a change is not picked up until the process restarts, so the screen
// can say so on the row rather than implying everything is live.
Restart bool
// Validate refuses a value the kind alone cannot. It runs after the kind's own check.
Validate func(string) error
// contains filtered or unexported fields
}
SettingKey is one setting this deployment admits.
The registry is what makes the settings screen generic: a new key needs an entry here and no frontend work at all.
func (SettingKey) Secret ¶ added in v1.32.0
func (k SettingKey) Secret() bool
Secret reports whether this key holds a credential. A secret key's value leaves the process only to the service it authenticates against: the API answers "set" or "unset" for it and never the value itself.
type SettingKind ¶ added in v1.32.0
type SettingKind string
SettingKind is what a setting holds. It decides which widget the settings screen renders and, for everything but KindString, what the built-in validator checks -- so a key's Validate only ever carries a rule its type cannot express.
const ( KindString SettingKind = "string" KindBool SettingKind = "bool" KindInt SettingKind = "int" KindFloat SettingKind = "float" KindDuration SettingKind = "duration" KindEnum SettingKind = "enum" KindEmail SettingKind = "email" KindURL SettingKind = "url" KindColors SettingKind = "colors" KindSecret SettingKind = "secret" )
The kinds. KindSecret is a string that is never sent to a browser: see SettingKey.Secret, which it implies.
type Signup ¶ added in v1.2.0
type Signup struct {
ID int64
Name string
FirstName string
LastName string
Email string
State string
Created int64
ValidatedAt *int64
DecidedAt *int64
DecidedBy *int64
LastIP string
}
Signup is one application for an account. It is deliberately not a User, and nothing in the library that walks users will find it (FR-9.2).
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store wraps the SQLite database handle, the app's migrations, and the app's ownership model.
func Open ¶
Open opens the SQLite database at path, creating the parent directory, the file, and the schema if absent. It applies the library's migrations and the app's own (appMig may be nil), ordered together by version. own may be nil, in which case NopOwnership is used.
A freshly created file is locked to 0600: it holds hashed credentials. Foreign keys are enforced; WAL keeps readers and the writer from blocking; a single open connection serializes writes, which is what the store's read-and-write-in-one- transaction guarantees rest on (NFR-3).
func (*Store) AppPref ¶ added in v1.32.0
AppPref returns one setting: the stored value, or the registered default.
func (*Store) AppliedMigrations ¶
AppliedMigrations returns the migration versions applied during Open (empty if the database was already current).
func (*Store) AuthGeneration ¶ added in v1.25.0
AuthGeneration is the current value. auth reads it when it grants an approval and again on every hit; a difference means something was revoked and the approval is void.
func (*Store) BeginTOTP ¶ added in v1.22.0
BeginTOTP writes a pending enrollment, replacing any earlier pending one, and moves the account to enabled. A confirmed secret is ErrTOTPEnrolled: clear it first, which is a step only the account (with its password) or an admin can take.
func (*Store) Bootstrapped ¶ added in v1.30.0
Bootstrapped reports whether anybody has claimed this install: whether an account exists that a person set up, as opposed to one the deployment conjured. It is what closes both bootstrap paths -- the interactive /setup flow and the GOGO_ADMIN_TOKEN seeding -- and nothing else consults it.
Two kinds of row are not a claim, for the same reason. The seeded anonymous account is a credential the configuration grants (FR-1.9). A certificate-only account is one the reverse proxy conjured by letting a CN through: nobody chose to create it, it carries no password and no token, and its holder cannot act as an admin anyway -- a certificate is capped at the non-admin role, twice over (FR-1.4). Letting either close the door would leave an install with no way to ever get an admin, which is the lockout /setup exists to prevent.
The consequence is deliberate and worth knowing: with GOGO_ADMIN_TOFU on and certificate auth in front, /setup stays open however many certificate users have been through, until somebody claims it. That is what TOFU is -- an open door until the first caller through it -- and it is why GOGO_ADMIN_TOFU is off by default.
func (*Store) BumpSessionEpoch ¶ added in v1.2.0
BumpSessionEpoch invalidates every session of an account at once -- "sign out everywhere" -- without rotating GOGO_SESSION_SECRET, which would log out every account. It returns the new epoch, which the caller's own re-issued cookie must carry if they are to stay signed in (FR-5.5).
func (*Store) CompleteSignup ¶ added in v1.2.0
CompleteSignup turns an approved application into an account, in one transaction: the users row is written with the address already validated (the applicant proved it to reach the queue at all), the provenance is stamped on the account, and the signups row is deleted.
The name is checked again here, because an admin may have created an account with that name while the applicant was choosing a password, and the transaction that writes the row is the last guard against it.
func (*Store) ConfirmTOTP ¶ added in v1.22.0
ConfirmTOTP marks a pending enrollment proved and moves the account to enrolled. step is the one that proved it, and is spent in the same transaction, so the code somebody just typed cannot be replayed at the login form.
func (*Store) ConsumeActionToken ¶ added in v1.2.0
func (s *Store) ConsumeActionToken(purpose, tokenHash string) (*ActionToken, error)
ConsumeActionToken resolves a presented hash and spends it, in one transaction. The store keeps a single open connection, so two callers racing one link produce exactly one success and one ErrTokenUsed: the link proves its claim exactly once (FR-7.3).
func (*Store) CountRecoveryCodes ¶ added in v1.22.0
CountRecoveryCodes is how many unspent codes an account still holds.
func (*Store) CountUsers ¶
CountUsers returns the number of real user rows. The seeded anonymous account is excluded: it is a credential the configuration grants, not somebody's account.
func (*Store) CreateActionToken ¶ added in v1.2.0
func (s *Store) CreateActionToken(purpose, subject, tokenHash string, ttl time.Duration) (*ActionToken, error)
CreateActionToken records the hash of a freshly minted secret against a purpose and a subject, to expire after ttl. The caller mints the plaintext (auth.NewToken), hashes it (auth.Hash), and puts the plaintext in the mail: the store sees only the hash, exactly as with a bearer token.
Minting invalidates every outstanding token for the same purpose and subject, so at most one live credential exists per subject and a forwarded "resend" cannot be replayed. The last link wins (FR-7.3).
func (*Store) CreatePAT ¶ added in v1.2.0
CreatePAT issues a token for a user. The caller mints the plaintext (auth.NewToken) and hashes it (auth.Hash); the store sees only the hash, and the plaintext is shown to its owner exactly once.
expiresAt is a Unix time, or nil for a token that never expires.
func (*Store) CreateSignup ¶ added in v1.2.0
CreateSignup records an application. The two refusals are deliberately asymmetric (FR-9.3):
- A taken name is ErrNameTaken, and the caller says so plainly.
- A taken address is ErrEmailTaken, and the caller must say nothing: it answers the applicant as it answers everybody, and mails the existing owner instead. Saying "that address is already registered" would turn a public form into an oracle for who has an account here.
An application for an address with a live, unproved signup replaces it: the last one wins. One already validated or approved is ErrSignupExists, and the caller stays just as quiet about that as about a taken address.
func (*Store) CreateUser ¶
CreateUser inserts a user. tokenHash may be empty for a cert-only user; certCN may be empty for a token user. The name must be unique, and so must any address. info is optional: pass zero or one UserInfo.
A tokenHash becomes the account's legacy PAT (see pats.go): the create API returns a token exactly once, and that token has to authenticate.
func (*Store) DecideSignup ¶ added in v1.2.0
DecideSignup approves or rejects a validated application. adminID may be 0, which is the auto-approval that happens when user.signup_admin_review is off.
Only a validated application can be decided, and only once: two admins reading their mail at the same moment produce one decision and one ErrSignupState.
func (*Store) DeletePAT ¶ added in v1.2.0
DeletePAT revokes one token. The user id is part of the key, not a filter applied afterwards: a caller who may act on one account can never revoke another account's token by guessing an id.
func (*Store) DeletePATs ¶ added in v1.2.0
DeletePATs revokes every token a user holds, and returns how many. It is the "revoke all" button.
func (*Store) DeleteSignup ¶ added in v1.2.0
DeleteSignup removes an application outright, whatever state it is in. It is what "ignore" means: no account, no mail, no record, and the applicant is told nothing. It is deliberately not a rejection -- a rejection is a decision, and an applicant is told about a decision (FR-9.6).
func (*Store) DeleteUser ¶
DeleteUser removes a user. The last enabled admin cannot be deleted. The app's OwnershipModel.Release runs in the same transaction to detach the user's resources.
func (*Store) GetAnonymousUser ¶
GetAnonymousUser returns the seeded anonymous account, or ErrNotFound.
func (*Store) GetPref ¶ added in v1.31.0
GetPref returns one preference, or its default. An unregistered key is ErrUnknownPref.
func (*Store) GetPrefs ¶ added in v1.31.0
GetPrefs returns every registered key with this account's choice, or the default. A stored key that is no longer registered is not returned.
func (*Store) GetTOTP ¶ added in v1.22.0
GetTOTP returns an account's enrollment, pending or confirmed.
func (*Store) GetUserByCertCN ¶
GetUserByCertCN resolves a verified client-certificate CN to its user.
func (*Store) GetUserByEmail ¶ added in v1.2.0
GetUserByEmail returns the one account holding an address, normalizing it first so a caller may pass whatever a human typed. An empty or malformed address is ErrNotFound rather than an error: every stored address is normalized, so no account can hold one.
func (*Store) GetUserByName ¶
GetUserByName returns one user by name.
func (*Store) InvalidateUserActionTokens ¶ added in v1.2.0
InvalidateUserActionTokens spends every live token of one purpose that acts on one account, whatever address its subject names, and returns how many died. SetPassword calls it, so that setting a password kills the outstanding reset links in the same transaction that writes the hash (FR-8.5).
The subject match covers both shapes a user subject can take: "user:42" and "user:42/pim@ipng.ch". The address is deliberately not part of the match -- a link mailed to an address the account has since left must die with the rest.
func (*Store) ListPATs ¶ added in v1.2.0
ListPATs returns a user's tokens, oldest first. No secret material: a token's value exists only where its owner put it after it was issued once.
func (*Store) ListReviewAdmins ¶ added in v1.2.0
ListReviewAdmins returns the admins a review request may be mailed to: enabled, and holding an address they have proved. Mailing an approve-this-account link to an unproved address would hand the power of approval to whoever squatted it -- the same rule, and the same reason, as recovery (FR-8.5, FR-9.5).
func (*Store) ListSignups ¶ added in v1.2.0
ListSignups returns every application, newest first. It is what the admin queue is drawn from, and it is deliberately unfiltered: a rejected application and an approved one nobody finished are both things an administrator wants to see.
func (*Store) MergeCertUser ¶
MergeCertUser folds a certificate-only user (source) into an existing user (target), so one identity holds both a bearer token and a client certificate. In one transaction it cascades ownership through OwnershipModel.Reassign, moves the source's cert_cn onto the target, and deletes the source.
Guards, all enforced here so an API cannot bypass them: source and target must differ; source must be certificate-only (a cert_cn and no other credential, so a merge cannot destroy a token with the source row); target must not already hold a certificate; neither may be the anonymous account.
func (*Store) NonAdminRole ¶
NonAdminRole returns the configured non-admin role label.
func (*Store) NoteLoginFailure ¶ added in v1.2.0
NoteLoginFailure records one failed password attempt, and locks the account on the third consecutive one. The read and the write are one transaction, so a burst of parallel guesses cannot slip past the third by racing the counter.
func (*Store) NoteLoginSuccess ¶ added in v1.2.0
NoteLoginSuccess clears the failure counter and any lock: a correct password ends the episode.
func (*Store) PasswordHash ¶ added in v1.2.0
PasswordHash returns the stored argon2id hash of an account, or ErrNoPassword when it has none. Passwords are argon2id where tokens are SHA-256, deliberately: a password is low-entropy and worth guessing (FR-5.3). This is the one place the hash leaves the store, and it goes to one caller: the login handler, which compares it and drops it.
func (*Store) PeekActionToken ¶ added in v1.2.0
func (s *Store) PeekActionToken(purpose, tokenHash string) (*ActionToken, error)
PeekActionToken resolves a presented hash without consuming it: it is what the page behind a mailed link uses to say what is about to happen. A mailed link must not act on a GET -- mail scanners follow every URL in a message -- so the POST behind the button on that page is what consumes (FR-7.5).
func (*Store) PrefKeyOf ¶ added in v1.31.0
PrefKeyOf returns a registered key, and whether it is one.
func (*Store) RegisterPref ¶ added in v1.31.0
RegisterPref adds a key to this store's registry. Call it at startup. The registry is per-store, not package-level, so two stores in one process do not share it.
func (*Store) RegisterSetting ¶ added in v1.32.0
func (s *Store) RegisterSetting(k SettingKey) error
RegisterSetting adds a key to this store's settings registry. Call it at startup, before the configuration's second phase reads the table. The registry is per-store, not package-level, so two stores in one process do not share it.
func (*Store) RegisteredPrefs ¶ added in v1.31.0
RegisteredPrefs returns every registered key, ordered by name.
func (*Store) RegisteredSettings ¶ added in v1.32.0
func (s *Store) RegisteredSettings() []SettingKey
RegisteredSettings returns every registered key, ordered for display: the library's scope first, then each app scope, and within a scope the order they were registered in. The screen renders them in this order, so the order is part of the contract and is changed by reordering the slice that declares them.
func (*Store) SchemaVersion ¶
SchemaVersion returns the latest migration version the schema is at.
func (*Store) SeedAnonymousUser ¶
SeedAnonymousUser ensures the reserved anonymous account exists: a credential-less non-admin account, seeded disabled unless enable is true on first creation (GOGO_ANONYMOUS_POST=true). An existing account is left untouched, so an admin's later enable/disable choice survives restarts.
func (*Store) SetAppPref ¶ added in v1.32.0
SetAppPref sets one key.
func (*Store) SetAppPrefs ¶ added in v1.32.0
SetAppPrefs applies a patch in one transaction: a nil value deletes the row, which returns the key to the value it seeded from or to its default. Absent keys are left alone. The whole patch is validated before anything is written, so a screen that saves eight fields at once either saves all of them or changes nothing.
func (*Store) SetNonAdminRole ¶
SetNonAdminRole configures the non-admin role label. It must be called before any user is created or seeded. gogo.Run calls it from App.Role.
func (*Store) SetPassword ¶ added in v1.2.0
SetPassword stores a password hash and, in the same transaction:
- stamps password_set_at;
- bumps session_epoch, so every session opened with the old password dies;
- clears the lockout, because whoever just proved the current password (or the admin who reset it) is not the attacker the lock was for;
- invalidates every outstanding password-reset link for this account, so a link still sitting in a mailbox is dead. This lives here rather than in a handler, because a guard in one handler is a guard the next handler forgets -- and it must hold for /profile, for an admin's reset, and for recovery itself (FR-8.5).
The caller hashes (auth.HashPassword); the store never sees a plaintext password, exactly as it never sees a plaintext token.
func (*Store) SetPrefDefault ¶ added in v1.32.0
SetPrefDefault moves a registered key's default. It exists for exactly one case: the default of ui.theme is a deployment setting (ui.theme_default), and a setting can change while the process runs. Without this, an administrator who changes the default would change it only for accounts created by a future restart.
It does not touch a single stored row. An account that has chosen keeps its choice; this is what an account that has not chosen reads.
func (*Store) SetPrefs ¶ added in v1.31.0
SetPrefs applies a patch in one transaction: a nil value deletes the key, returning it to its default. Absent keys are left alone. The whole patch is validated before anything is written. The reserved anonymous account is shared and holds no preferences.
func (*Store) SetRecoveryCodes ¶ added in v1.22.0
SetRecoveryCodes replaces every code an account holds. The caller hashes; the store never sees a plaintext code, exactly as it never sees a plaintext token.
func (*Store) SetTOTPState ¶ added in v1.22.0
SetTOTPState moves an account between the four states, and is the whole state machine. The legal moves, and what each does:
-> off drop the secret and every recovery code
-> enabled drop them too, and bump session_epoch so the requirement takes
effect now rather than whenever a cookie lapses
-> disabled only from enrolled: keep the secret, suspend the prompt
-> enrolled only from disabled: restore the prompt on the same secret
An admin can require, suspend, restore and remove; an admin can never enroll, because enrollment means holding the phone. ConfirmTOTP is the only other writer of the state, and it is the one the account itself drives.
func (*Store) SettingBool ¶ added in v1.32.0
SettingBool returns a boolean setting.
func (*Store) SettingDuration ¶ added in v1.32.0
SettingDuration returns a duration setting, accepting either a Go duration or the gogo/duration grammar.
func (*Store) SettingFloat ¶ added in v1.32.0
SettingFloat returns a floating-point setting.
func (*Store) SettingInt ¶ added in v1.32.0
SettingInt returns an integer setting.
func (*Store) SettingKeyOf ¶ added in v1.32.0
func (s *Store) SettingKeyOf(key string) (SettingKey, bool)
SettingKeyOf returns a registered key, and whether it is one.
func (*Store) SettingString ¶ added in v1.32.0
SettingString returns a setting's value, or its default.
func (*Store) SignupOrigin ¶ added in v1.2.0
SignupOrigin is the address the application was first made from -- not last_ip. What an admin reviews is where the application came from, not where its owner last happened to be.
func (*Store) SignupVisits ¶ added in v1.2.0
SignupVisits returns how many times an application has been touched. Every touch is recorded: abuse of a public, mail-sending endpoint is otherwise invisible.
func (*Store) SpendRecoveryCode ¶ added in v1.22.0
SpendRecoveryCode marks one code used. An unknown or already-spent code is ErrNotFound: the caller must not learn which of the two it was.
func (*Store) SpendTOTPStep ¶ added in v1.22.0
SpendTOTPStep records that a step has been used, and refuses one already spent. The comparison is in the WHERE clause, so two requests racing one code cannot both win.
func (*Store) StoredAppPrefs ¶ added in v1.32.0
StoredAppPrefs returns only the rows that exist, and nothing about the registry. It is what the configuration layers over the environment, so an unregistered leftover row is included: dropping it here would make a key that was renamed silently revert to its environment value.
func (*Store) SweepActionTokens ¶ added in v1.2.0
SweepActionTokens deletes the tokens that can never be used again: those that expired, and those that were spent, more than grace ago. Nothing depends on it having run -- expiry and single use are both enforced on read -- so a missed sweep costs disk, never correctness (FR-7.3).
func (*Store) SweepPendingTOTP ¶ added in v1.22.0
SweepPendingTOTP drops enrollments nobody ever confirmed.
func (*Store) SweepSignups ¶ added in v1.2.0
SweepSignups deletes the applications that are over: pending ones older than ttl (nobody proved the address), rejected ones older than grace, and approved ones whose applicant never came back to choose a password, which would otherwise reserve a username forever. It never touches a validated signup: that one is not stale, it is queued behind an admin (FR-9.8).
func (*Store) TouchPAT ¶ added in v1.2.0
TouchPAT records that a token was just used. Failures are ignored: last_used is an audit convenience, never a correctness dependency. Because it is only that, the write is debounced by touchInterval, on a key of its own -- pat ids and user ids are separate spaces. See dueForTouch.
func (*Store) TouchSignup ¶ added in v1.2.0
TouchSignup records a visit and the address it came from. Failures are ignored: this is an audit convenience, never a correctness dependency.
func (*Store) TouchUser ¶
TouchUser records a successful authentication. Failures are ignored: last_used is an audit convenience, never a correctness dependency. Because it is only that, the write is debounced by touchInterval: every authenticated request passes through here, and a commit on a quiet database is not free.
func (*Store) Unlock ¶ added in v1.2.0
Unlock is the same clearing, done by an admin from /users for a user who does not want to wait out the lockout.
func (*Store) UpdateUser ¶
func (s *Store) UpdateUser(id int64, up UserUpdate) (*User, error)
UpdateUser applies a partial update. Disabling or demoting the last enabled admin is refused with ErrLastAdmin. Renaming cascades through OwnershipModel.Rename in the same transaction, so it can never half-apply.
The order is the contract: the guards, then the address (whose change clears the validation), then the validation flag (so an admin may set an address and vouch for it in one call, while a user changing their own address leaves it cleared), then the ownership cascade.
func (*Store) UserByToken ¶ added in v1.2.0
UserByToken resolves a presented bearer token, already hashed, to its account and the token it authenticated with. It is the only lookup the resolver's bearer branch makes: pats is the credential index. A disabled user is returned, not refused -- the caller decides and logs that refusal. An expired token is ErrPATExpired.
func (*Store) ValidateEmail ¶ added in v1.2.0
ValidateEmail stamps an address validated -- the one write that makes the system's claim -- but only if the account still holds exactly that address, checking and writing in one transaction. The address is an argument rather than an assumption: between the mail and the click the account may have moved, and stamping whatever it holds now would validate an address nobody proved (FR-6.2, FR-7.4). ErrEmailChanged is the refusal; the caller asks the user for a fresh link.
func (*Store) ValidateSignup ¶ added in v1.2.0
ValidateSignup marks the address proved. Only a pending application can be validated, and the transition is guarded here, so a replayed link cannot walk a decided signup backwards.
func (*Store) ValidateStored ¶ added in v1.32.0
ValidateStored reports every stored row that its key would refuse today: a value seeded from a variable that never passed validation, or one left behind by a key whose rules have since tightened. Seeding deliberately does not validate -- a deployment that booted yesterday must boot today -- so this is where that debt is named.
It is a warning, not an error. Each such key reads as its default until somebody fixes it on the settings screen.
type TOTP ¶ added in v1.22.0
type TOTP struct {
UserID int64
Secret string
Created int64
ConfirmedAt *int64
LastStep int64
LastUsed *int64
}
TOTP is one enrollment: the shared secret and what has been done with it.
type User ¶
type User struct {
ID int64
Name string
Role string // user | admin
// TokenHash mirrors the account's legacy PAT and is not a credential: nothing
// authenticates against it, and it exists only so an older binary rolled back
// onto this database still finds the token it knows about. Ask PATs instead.
TokenHash string
CertCN string // client-cert CN; empty otherwise
Created int64
LastUsed *int64
Disabled bool
FirstName string // may be empty
LastName string // may be empty
Email string // may be empty; lowercased and validated when set
// EmailValidated is when something proved the address belongs to this account,
// nil until then. It is a claim only the system may make: a user can change
// their address, never vouch for it, and any change of address clears this in
// the same transaction (FR-4.3).
EmailValidated *int64
// HasPassword is whether a password is set. The hash itself is never carried
// on the User: it leaves the store only through PasswordHash.
HasPassword bool
// PasswordSetAt is when it was last set, nil when there is none.
PasswordSetAt *int64
// SessionEpoch is carried in the signed session cookie, which the resolver
// admits only while the two match. Bumping it kills every live session of this
// account without rotating GOGO_SESSION_SECRET (FR-5.5).
SessionEpoch int64
// LockedUntil is when password authentication becomes possible again, nil when
// it is not barred. Tokens and certificates ignore it -- see passwords.go.
LockedUntil *int64
// PATs is how many personal access tokens the account holds. This -- not
// TokenHash -- is what "does this account hold a token" means (FR-5.1).
PATs int
// SignupAt is when this person applied for the account, nil for every account an
// admin created. SignupApproved says a human said yes, as opposed to the
// auto-approval with review off. The signups row is swept; this survives it.
SignupAt *int64
SignupApproved bool
// InvitedAt is when an admin created this account. Such an account starts with
// no credential at all: its owner proves the address and chooses a password,
// with no approval step (FR-6.1, FR-6.3).
InvitedAt *int64
// TOTPState is which of the four second-factor states this account is in. It is
// the policy; the totp table holds the secret, and the store keeps the two
// consistent in one transaction. See totp.go.
TOTPState string
}
User is an account. A user authenticates by a personal access token (see pats.go), by a password, or -- for auto-provisioned client-certificate identities -- by its CertCN. A certificate never grants admin.
Every field but the first three is optional: an account carrying none of them serializes exactly as it did before those fields existed (FR-4.2).
func (*User) CertOnly ¶ added in v1.2.0
CertOnly reports whether the account's only way in is its client certificate -- no token, no password. It is what a merge folds into a real identity, and it is deliberately strict: a certificate user who also holds a token is not cert-only, and merging them would delete that token with the source row.
func (*User) HasToken ¶ added in v1.2.0
HasToken reports whether the account holds any personal access token. It is a count of PATs, not a look at TokenHash (FR-5.1).
func (*User) Invited ¶ added in v1.3.0
Invited reports whether an admin created this account (as opposed to somebody signing themselves up, or the bootstrap).
func (*User) Locked ¶ added in v1.2.0
Locked reports whether password authentication is currently barred for this account. It says nothing about its tokens or its certificate, which keep working.
func (*User) TOTPMustEnroll ¶ added in v1.22.0
TOTPMustEnroll reports whether the next password login is an enrollment.
func (*User) TOTPRequired ¶ added in v1.22.0
TOTPRequired reports whether a password login must clear a second factor.
type UserInfo ¶ added in v1.2.0
type UserInfo struct {
FirstName string
LastName string
Email string
// EmailValidated stamps the address validated at creation. Only an admin
// reaches this through the API, and only with an address: validating an
// empty one is ErrNoEmail.
EmailValidated bool
// PasswordHash is an already-hashed password (auth.HashPassword). It is how
// GOGO_ADMIN_PASSWORD seeds the first admin, and how /setup accepts one.
PasswordHash string
// Invited stamps invited_at: an admin made this account, and its owner has not
// claimed it yet. Such an account may be created with no credential at all -- the
// credential is the one thing an admin cannot choose for somebody else (FR-6.1).
Invited bool
}
UserInfo is the optional information a user may be created with. CreateUser takes it variadically, so a four-argument call site keeps compiling.
type UserUpdate ¶
type UserUpdate struct {
Name *string
Disabled *bool
Role *string
TokenHash *string // set by a token regeneration
FirstName *string // "" clears
LastName *string // "" clears
Email *string // "" clears (and so clears EmailValidated)
// EmailValidated is the system's claim about the address, not the user's: true
// stamps the moment, false clears it, and the web layer populates it only for an
// admin. A nil pointer leaves it alone -- except that a change of address clears
// it regardless, which UpdateUser enforces and no caller can bypass.
EmailValidated *bool
}
UserUpdate carries the mutable fields of a user. A nil field is unchanged; an empty string clears the field it names.