authn

package
v0.2.33 Latest Latest
Warning

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

Go to latest
Published: Oct 1, 2026 License: MPL-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package authn is the stack-plane identity store: the users, principal bindings and credentials a TENANT's stacks manage, and that the protocol heads authenticate against.

Two planes share auth.db and nothing else. The ACCOUNT plane (chassis/auth: actors, keys, memberships, browser sessions) says who may administer a tenant. The STACK plane — this package — says who a tenant's own product knows about: its people, its ponies, its services. The tenant is the boundary between them. No row here references an actor and this package never imports the account plane's registry types (only its SQL dialect seam), so the two cannot be joined by accident.

Three tables (db/schema/*/auth/0005_identity.sql):

users               durable state about a human
principal_bindings  identifier (an email, an OIDC subject) → principal
credentials         a revocable secret that authenticates AS a principal

Every row records the stack that wrote it, and only that stack may change a principal's rows — see owner.go.

Index

Constants

View Source
const (
	VerbInvoke  = "invoke"
	VerbRelease = "release"
)

The verbs built.

View Source
const (

	// Word counts accepted for StyleWords; the default is apppass's.
	MinPasswordWords = 4
	MaxPasswordWords = 12
)
View Source
const (
	// MaxRunAllow bounds one run grant's allowlist.
	MaxRunAllow = 64
	// MaxRunSandboxes bounds the sandboxes one run grant names.
	MaxRunSandboxes = 16
	// MaxRunDepth bounds how many times a grant may be narrowed into
	// another: work that dispatches work that dispatches work.
	MaxRunDepth = 4
	// MaxRunTTL and MaxRunBudget are the store's own ceilings, behind the
	// operator's (--run-grant-ttl-max, --run-grant-budget-max).
	MaxRunTTL    = 24 * time.Hour
	MaxRunBudget = 1_000_000
)
View Source
const (
	StatusActive   = "active"
	StatusDisabled = "disabled"
)

User statuses.

View Source
const MaxCredentials = 64

MaxCredentials bounds the LIVE credentials one principal may hold — far past "one per device", and enough to stop a loop from minting argon2 hashes without end.

View Source
const MaxGrants = 1024

MaxGrants bounds the LIVE grants one principal may hold, so a loop cannot write rows without end.

View Source
const MaxScopes = 32

MaxScopes bounds one credential's scope list.

Variables

View Source
var (
	// ErrExceedsStanding: the allowlist names something the principal holds
	// no standing grant for. The error names it.
	ErrExceedsStanding = errors.New("authn: the run grant exceeds the principal's standing grants")
	// ErrExceedsParent: the allowlist names something the parent grant does
	// not, or the budget is more than the parent has left.
	ErrExceedsParent = errors.New("authn: the run grant exceeds its parent")
	// ErrParentNotLive: the parent grant has expired, ended or been revoked.
	ErrParentNotLive = errors.New("authn: the parent run grant is not live")
	// ErrDepth: the grant would be narrowed more than MaxRunDepth times.
	ErrDepth = errors.New("authn: run grants are nested too deep")
	// ErrStaleGeneration: the run already has this generation or a later one.
	ErrStaleGeneration = errors.New("authn: the run already has this generation or a later one")
	// ErrRunNotLive: the run grant has expired, ended or been revoked.
	ErrRunNotLive = errors.New("authn: the run grant is not live")
	// ErrRunBudget: the run grant's budget is spent.
	ErrRunBudget = errors.New("authn: the run grant's budget is spent")
)
View Source
var (
	// ErrNotFound: no such user, binding or credential in this tenant.
	ErrNotFound = errors.New("authn: not found")
	// ErrInvalid wraps every argument error; the message says what to fix.
	ErrInvalid = errors.New("authn: invalid argument")
	// ErrNotOwner: the principal's rows were written by another stack. The
	// returned error is an *OwnerError naming it.
	ErrNotOwner = errors.New("authn: principal is managed by another stack")
	// ErrNoStack: a write arrived with no dispatching stack to attribute it to.
	ErrNoStack = errors.New("authn: no stack in request scope")
	// ErrBound: the identifier is already bound to a different principal.
	ErrBound = errors.New("authn: identifier is bound to another principal")
	// ErrDisabled: the user is disabled, so nothing new may be issued to it.
	ErrDisabled = errors.New("authn: user is disabled")
	// ErrTooMany: the principal already holds MaxCredentials live credentials.
	ErrTooMany = errors.New("authn: too many credentials")
)
View Source
var ErrTooManyGrants = errors.New("authn: too many grants")

ErrTooManyGrants: the principal already holds MaxGrants live grants.

Functions

func BaseStack

func BaseStack(stack string) string

BaseStack reduces a dispatching stack name to the name that owns rows: its first slash segment. `web`, its canary slot `web/canary` and its mail channel `web/_mail` are all the stack `web` — a slot is a sparse override of its base (processor.OpsForStage), so if it did not count as the base a canary could never manage the users its stable slot created.

func NormalizeEmail

func NormalizeEmail(addr string) (string, error)

NormalizeEmail returns the form an address is bound and looked up by: trimmed and lowercased. It accepts `local@domain` and nothing fancier — no display name, no comment, no quoted local part.

func ShortIDOf

func ShortIDOf(password string) (shortID string, ok bool)

ShortIDOf extracts the credential short id from a presented password. ok is false for anything the chassis could not have issued (a legacy password, a typo in the first group) — the caller then burns a dummy verify and fails the login like any wrong password.

It accepts any id length from shortIDLen up, so the id can grow later without stranding the passwords already issued.

func WithAuthenticated

func WithAuthenticated(ctx context.Context, a Authenticated) context.Context

WithAuthenticated pins a on ctx. A zero a pins "no one", which is not the same as no pin at all only to code that cares; AuthenticatedFrom reports both as ok=false.

Types

type Attempt

type Attempt struct {
	// Tenant is the tenant SLUG the account belongs to — what the protocol
	// stores key on. The resolver turns it into the tenant id.
	Tenant string
	// Username is the account's login name, which an email binding resolves
	// to the principal. It is ignored when Principal is set.
	Username string
	// Principal, when set, IS the identity: the credential is looked up
	// against it directly and no binding is read. It is for a door whose
	// address already says whose it is — a printer's row names the one
	// principal that may print there, so the username a print client sends
	// adds nothing, and asking a person to type an address into a phone
	// costs more than it proves. The password alone authenticates, exactly
	// as a password-only AirPrint printer does.
	Principal string
	Password  string
	IP        string
	// Want is the door: imap:<username>:login, drive:<collection-id>:login…
	Want Scope
}

Attempt is one login a head asks about.

type Authenticated

type Authenticated struct {
	Principal Principal
	// Credential is the id (crd_…) of the credential that signed in: which
	// device, which printer. Not a secret.
	Credential string
}

Authenticated is who a request acts as: the principal a head signed in, and the credential that proved it.

It travels in Go state, never in the envelope. The head that verified the password pins it on the context it dispatches with (WithAuthenticated); the processor makes `_txc.principal` a read-only copy of that pin, and removes any `_txc.principal` a request arrived with when there is none. Access decisions read the context (processor.PrincipalScope), never the envelope.

func AuthenticatedFrom

func AuthenticatedFrom(ctx context.Context) (Authenticated, bool)

AuthenticatedFrom returns the principal pinned on ctx.

func (Authenticated) IsZero

func (a Authenticated) IsZero() bool

IsZero reports whether no one is authenticated.

type Binding

type Binding struct {
	ID        string
	TenantID  string
	Principal Principal
	Kind      BindingKind
	Issuer    string
	Subject   string
	Verified  bool
	Metadata  string // a JSON object, or ""
	CreatedBy string
	CreatedAt time.Time
	RevokedAt *time.Time
}

Binding mirrors a principal_bindings row.

type BindingKind

type BindingKind string

BindingKind is the kind of identifier a binding maps to a principal.

const (
	// BindEmail binds an address. issuer is empty; subject is the lowercased
	// address. A login's username resolves through this kind.
	BindEmail BindingKind = "email"
	// BindOIDC binds an (issuer, subject) pair from an identity provider.
	// The store keeps it; nothing consumes it yet (OIDC is its own project).
	BindOIDC BindingKind = "oidc"
)

type Credential

type Credential struct {
	ID         string // crd_<hxid>
	TenantID   string
	Principal  Principal
	ShortID    string
	Kind       CredentialKind
	Scopes     Scopes
	Label      string
	CreatedBy  string
	CreatedAt  time.Time
	LastUsedAt *time.Time
	RevokedAt  *time.Time
}

Credential mirrors a credentials row, minus the hash: nothing outside this package ever holds one.

It records only the principal it authenticates AS. Who holds it, or on whose behalf, is deliberately not modeled: when a pony's password is typed by its owner, the pony is the principal.

func (Credential) Revoked

func (c Credential) Revoked() bool

Revoked reports whether the credential can no longer authenticate.

type CredentialKind

type CredentialKind string

CredentialKind is the kind of secret a credential holds.

const KindAppPassword CredentialKind = "app_password"

KindAppPassword is a chassis-issued password (see password.go), stored as an argon2id PHC hash. The only kind built.

type Grant

type Grant struct {
	ID        string // grt_<hxid>
	TenantID  string
	Principal Principal
	Resource  Resource
	Verbs     []string // sorted
	CreatedBy string
	CreatedAt time.Time
	RevokedAt *time.Time
}

Grant mirrors a resource_grants row.

func (Grant) Allows

func (g Grant) Allows(verb string) bool

Allows reports whether the grant names verb.

func (Grant) Revoked

func (g Grant) Revoked() bool

Revoked reports whether the grant no longer holds.

type NewBinding

type NewBinding struct {
	Kind    BindingKind
	Issuer  string
	Subject string
	// Verified records that the PRODUCT proved the identifier (a magic link
	// was clicked). The chassis cannot check it and never sets it itself.
	Verified bool
	Metadata string
}

NewBinding is the identifier to bind.

type NewCredential

type NewCredential struct {
	Scopes []string
	Label  string
	Style  PasswordStyle // "" ⇒ StyleToken
	Words  int           // StyleWords only; 0 ⇒ the default
}

NewCredential is what txco://credential/create supplies.

type NewGrant

type NewGrant struct {
	Kind ResourceKind
	Name string
	// Verbs the principal may use. Empty means every verb the kind admits.
	Verbs []string
}

NewGrant is what txco://grant/put supplies.

type NewRunGrant

type NewRunGrant struct {
	// Run names the work: a task id, a job id. Minting the same run again
	// replaces the grant it had.
	Run string
	// Generation orders the grants of one run. Zero means the next one; a
	// caller with a counter of its own (a lease) passes it, and it must be
	// later than any the run has had.
	Generation int64
	// Stack is the minting rule's app stack.
	Stack string
	// Workspace and WorkspaceID say where the work runs; both empty for
	// work that runs nowhere.
	Workspace, WorkspaceID string
	NodeClass              NodeClass // "" ⇒ NodeUnreviewed
	// Allow names what the work may be handed, each entry in Resource's
	// String form. Every secret a sandbox names is added to it.
	Allow []string
	// Sandboxes is what the work may open: sandbox → variable → reference,
	// as the minting stack's declarations say (chassis/sandbox.Decl.Env).
	// The store checks the shape; the caller resolved the names. A sandbox
	// that only names capabilities is an empty map here: its names are in
	// Allow.
	Sandboxes   map[string]map[string]string
	BudgetCalls int64
	TTL         time.Duration
	// Parent is the run grant this one is narrowed from, or "".
	Parent  string
	TraceID string
}

NewRunGrant is what txco://delegate/mint supplies.

type NewUser

type NewUser struct {
	// Email is required: it is the identifier the user is found by, bound as
	// a BindEmail binding in the same transaction.
	Email string
	// EmailVerified: the product proved the address (see NewBinding.Verified).
	EmailVerified bool
	DisplayName   string
}

NewUser is what txco://user/create supplies.

type NodeClass

type NodeClass string

NodeClass says what is allowed to run where the work is sent. It is a promise by whoever dispatches the work; the chassis cannot prove it.

const (
	// NodeUnreviewed runs anything, including code written a moment ago.
	// The default.
	NodeUnreviewed NodeClass = "unreviewed"
	// NodeReviewed runs only what was reviewed before it was installed.
	NodeReviewed NodeClass = "reviewed"
)

type Outcome

type Outcome string

Outcome is how a login ended, for the head's reply and its log line.

const (
	OutcomeOK Outcome = "ok"
	// OutcomeFailed: an unknown username, a wrong password, a password the
	// chassis never issued (it has no credential id), or a revoked one. The
	// client learns only that it failed.
	OutcomeFailed Outcome = "failed"
	// OutcomeThrottled: over --login-rate for this client IP or principal.
	OutcomeThrottled Outcome = "throttled"
	// OutcomeScope: the right password for the wrong door — a credential
	// whose scopes do not cover what the head opens. Answered like a wrong
	// password; logged apart so an operator can see it.
	OutcomeScope Outcome = "scope"
	// OutcomeDisabled: the principal is a user who has been disabled.
	OutcomeDisabled Outcome = "disabled"
	// OutcomeError: the store failed. Answer "try again later", not "wrong
	// password" — a client that believes its password is wrong asks for it.
	OutcomeError Outcome = "error"
)

type OwnerError

type OwnerError struct {
	Principal Principal
	Owner     string
}

OwnerError is the ErrNotOwner a refused write returns: it names the stack that does own the principal, so the author knows where to `_txc.goto`.

func (*OwnerError) Error

func (e *OwnerError) Error() string

func (*OwnerError) Is

func (e *OwnerError) Is(target error) bool

type PasswordStyle

type PasswordStyle string

PasswordStyle picks the issued form.

const (
	// StyleToken is the machine form, `txc_<short-id>_<secret>`. The default.
	StyleToken PasswordStyle = "token"
	// StyleWords is the form a person types, `<short-id>-word-word-…`.
	StyleWords PasswordStyle = "words"
)

type Principal

type Principal struct {
	// ID is the full `<kind>:<name>` form — what rows store and what
	// `_txc.principal` will carry: `user:usr_7Hq…`, `pony:paris`.
	ID   string
	Kind PrincipalKind
}

Principal is the authenticated actor in TxCo: the thing a credential authenticates AS, and the thing authorization will be asked about.

It is not the DAV notion of a principal (the `principal` types in the calendar, contacts and webdav heads name a DAV resource URL).

func ParsePrincipal

func ParsePrincipal(s string) (Principal, error)

ParsePrincipal validates a `<kind>:<name>` principal id. Ids are case-sensitive and compared as written: `usr_` ids are base58, and a product's own ids may be too.

func UserPrincipal

func UserPrincipal(userID string) Principal

UserPrincipal is the principal of a users row.

func (Principal) IsZero

func (p Principal) IsZero() bool

IsZero reports whether p is the zero Principal (no one).

func (Principal) Name

func (p Principal) Name() string

Name is the part of the id after the kind: `usr_7Hq…`, `paris`.

func (Principal) String

func (p Principal) String() string

func (Principal) UserID

func (p Principal) UserID() (string, bool)

UserID returns the users-table id of a `user` principal.

type PrincipalKind

type PrincipalKind string

PrincipalKind is the part of a principal id before the colon. `user` is minted by the chassis; every other kind is named by the product.

const KindUser PrincipalKind = "user"

KindUser is the one kind the chassis mints: `user:usr_<hxid>`, backed by a users row. A product cannot name a user principal into existence.

type Resolver

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

Resolver verifies logins against the identity store.

func NewResolver

func NewResolver(store *Store, cfg ResolverConfig) *Resolver

NewResolver builds the resolver the heads share.

func (*Resolver) Live

func (r *Resolver) Live(ctx context.Context, who Authenticated) (bool, error)

Live reports whether who may still be signed in: its credential is not revoked and, for a user principal, the user is active. It is for a head that holds a connection open after the login (IMAP): Login re-reads the credential on every request, but a session that logged in once never asks again, so without this a revoked password keeps an open mailbox open.

One indexed read, no password and no throttle. It does NOT see a revoked binding or a deleted account — those end a session at its next login. An error means "could not tell"; the caller keeps the session, so a database blip does not drop every mail client at once.

func (*Resolver) Login

func (r *Resolver) Login(ctx context.Context, a Attempt) Result

Login verifies one attempt.

func (*Resolver) Miss

func (r *Resolver) Miss(ip, username, password string) Outcome

Miss answers a login for a username the head found no account for. It costs what a real check costs — the throttles, then one argon2 verify — so a reply's timing does not say whether an account exists.

type ResolverConfig

type ResolverConfig struct {
	// Rate is the most password checks a minute, per client IP and per
	// principal, across every head (--login-rate). A cache hit is not a
	// check. 0 disables throttling.
	Rate int
	// TenantID resolves a tenant slug to the id the identity tables key on.
	TenantID func(ctx context.Context, slug string) (string, error)
}

ResolverConfig configures a Resolver.

type Resource

type Resource struct {
	Kind ResourceKind
	Name string
}

Resource names one thing a grant covers.

func NewResource

func NewResource(kind ResourceKind, name string) (Resource, error)

NewResource validates a kind and a name.

func ParseResource

func ParseResource(s string) (Resource, error)

ParseResource reads the String form: `crm.lookup`, `capability:crm.lookup` or `secret:CRM_KEY`.

func (Resource) String

func (r Resource) String() string

String is the form a run grant's allowlist holds: a capability by its bare name, anything else as `<kind>:<name>`. A capability name holds no colon, so the two never collide.

func (Resource) Verb

func (r Resource) Verb() string

Verb is the one verb the resource's kind admits today.

type ResourceKind

type ResourceKind string

ResourceKind is the kind of thing a grant names.

const (
	// ResourceCapability is a named capability; its verb is VerbInvoke.
	ResourceCapability ResourceKind = "capability"
	// ResourceSecret is a tenant secret, by name; its verb is VerbRelease:
	// the secret itself may be handed to the principal's work.
	ResourceSecret ResourceKind = "secret"
)

type Result

type Result struct {
	Outcome Outcome
	// Who is set when Outcome is OutcomeOK.
	Who Authenticated
	// Cached: the password was verified recently on this node, so no argon2
	// ran. A head uses it to log one line per check, not one per request.
	Cached bool
	// Err is set when Outcome is OutcomeError.
	Err error
}

Result is the answer to an Attempt.

type RunGrant

type RunGrant struct {
	ID          string // rgr_<hxid>
	TenantID    string
	Principal   Principal
	MintedBy    string // base name of the minting stack
	Stack       string // the minting rule's app stack
	Run         string
	Generation  int64
	WorkspaceID string
	Workspace   string
	NodeClass   NodeClass
	// FileKey is a random key of the row's own, minted with it. Nothing
	// reads a grant by it today; it is for the chassis alone and never put
	// in an envelope.
	FileKey string
	// Allow is what the work may be handed: every secret its sandboxes
	// name, and anything the minter allowed by name.
	Allow []Resource
	// Sandboxes is what the work may OPEN, by name: sandbox → variable →
	// reference (`secret:NAME`), snapshotted at mint from the minting
	// stack's declarations (chassis/sandbox).
	Sandboxes   map[string]map[string]string
	BudgetCalls int64
	SpentCalls  int64
	ParentGrant string
	Depth       int
	TraceID     string
	IssuedAt    time.Time
	ExpiresAt   time.Time
	ClosedAt    *time.Time
	CloseReason string
	RevokedAt   *time.Time
}

RunGrant mirrors a run_grants row.

func (RunGrant) AllowStrings

func (g RunGrant) AllowStrings() []string

AllowStrings is the allowlist in its stored / displayed form.

func (RunGrant) Allows

func (g RunGrant) Allows(res Resource) bool

Allows reports whether the allowlist names res.

func (RunGrant) Live

func (g RunGrant) Live(now time.Time) bool

Live reports whether the grant still holds at now.

func (RunGrant) Remaining

func (g RunGrant) Remaining() int64

Remaining is the budget left.

func (RunGrant) Sandbox

func (g RunGrant) Sandbox(name string) (map[string]string, bool)

Sandbox returns what opening the named sandbox hands the program: variable → reference. The second result is false when the grant does not name the sandbox.

func (RunGrant) SandboxNames

func (g RunGrant) SandboxNames() []string

SandboxNames is the grant's sandboxes, sorted.

type Scope

type Scope struct {
	Domain, Instance, Action string
}

Scope is one parsed `domain:instance:action` capability.

func ParseScope

func ParseScope(s string) (Scope, error)

ParseScope validates one scope string.

func (Scope) Covers

func (s Scope) Covers(want Scope) bool

Covers reports whether this granted scope covers want: segment by segment, a granted `*` matches anything, anything else must be equal. A `*` in want is matched only by a granted `*` — asking for "any" needs a grant of "any".

func (Scope) String

func (s Scope) String() string

type Scopes

type Scopes []Scope

Scopes is a credential's scope list: validated, de-duplicated, sorted.

func ParseScopes

func ParseScopes(in []string) (Scopes, error)

ParseScopes validates a scope list. It must hold at least one scope (a credential that opens nothing is a mistake, not a policy) and at most MaxScopes.

func (Scopes) Allows

func (ss Scopes) Allows(domain, instance, action string) bool

Allows is Cover for a caller holding the three segments.

func (Scopes) Cover

func (ss Scopes) Cover(want Scope) bool

Cover reports whether any scope in the list covers want.

func (Scopes) Strings

func (ss Scopes) Strings() []string

Strings is the list in its stored / displayed form.

type Store

type Store struct {
	DB      *sql.DB
	Dialect registry.Dialect
	// contains filtered or unexported fields
}

Store reads and writes the three identity tables. It owns their layout: the ops (chassis/server) and, later, the protocol account ops and the login resolver go through these methods and never see a column.

The tables live in auth.db (SQLite by default, the shared Postgres on a fleet), created by numbered migration 0005 — the Store ships no DDL.

func NewStore

func NewStore(db *sql.DB, d registry.Dialect) *Store

NewStore builds a Store over an open, migrated auth DB. A nil dialect is SQLite.

func (*Store) BindPrincipal

func (s *Store) BindPrincipal(ctx context.Context, tenantID, stack string, p Principal, nb NewBinding) (Binding, bool, error)

BindPrincipal binds an identifier to p on behalf of stack. It is the one way a binding is written: txco://user/create uses it for the user's email, and the protocol account ops will use it for their usernames, so neither sees the table.

Binding the same identifier to the same principal again is a no-op that returns the existing row (created=false), upgrading verified false→true if asked — never the reverse. An identifier live on a different principal is ErrBound; a principal owned by another stack is ErrNotOwner.

func (*Store) ChargeRunGrant

func (s *Store) ChargeRunGrant(ctx context.Context, tenantID, grantID string, n int64, enforceBudget bool) error

ChargeRunGrant spends n calls of a run grant's budget, in one statement: the grant is charged only if it is live at that moment and, when enforceBudget is set, only if the budget covers it. Two requests racing for the last call cannot both be charged.

It answers ErrRunNotLive or ErrRunBudget when nothing was charged. Charge BEFORE handing over what was asked for: a charge that fails after the fact recalls nothing.

func (*Store) CloseRunGrant

func (s *Store) CloseRunGrant(ctx context.Context, tenantID, stack, grantID, reason string) (RunGrant, bool, error)

CloseRunGrant ends a run grant because its work ended, with every grant narrowed from it. reason is a short note for whoever reads the row ("done", "failed", "timeout"); empty means "done". It returns the row and whether THIS call closed it (false: it had already ended).

func (*Store) CreateUser

func (s *Store) CreateUser(ctx context.Context, tenantID, stack string, in NewUser) (User, Binding, bool, error)

CreateUser creates a user and binds its email, atomically, on behalf of stack. It is idempotent on the email: creating a user whose address is already bound to a user THIS stack manages returns that user with created=false (and upgrades the binding to verified if asked), so a retried pipeline does not fail or fork the person in two. The display name is not touched on that path.

An address bound to another stack's user is ErrNotOwner; one bound to a non-user principal (a pony's mailbox) is ErrBound.

func (*Store) CredentialLive

func (s *Store) CredentialLive(ctx context.Context, credentialID string) (bool, error)

CredentialLive is Resolver.Live's read.

func (*Store) GetRunGrant

func (s *Store) GetRunGrant(ctx context.Context, tenantID, stack, grantID string) (RunGrant, error)

GetRunGrant reads a run grant on behalf of stack — the stack that minted it only: the row says what a piece of its work may ask for.

func (*Store) GetUser

func (s *Store) GetUser(ctx context.Context, tenantID, userID string) (User, error)

GetUser reads a user by id. Reads are tenant-wide: any stack may look a user up (and learn from CreatedBy which stack manages it).

func (*Store) Granted

func (s *Store) Granted(ctx context.Context, tenantID string, p Principal, res Resource, verb string) (bool, error)

Granted reports whether p holds a live grant of verb on the resource. It is the question the chassis asks on every request, so it takes no stack: the asker is the chassis, not a rule.

A disabled user holds nothing while disabled. Nothing is revoked, so enabling the user restores every grant.

func (*Store) GrantsOn

func (s *Store) GrantsOn(ctx context.Context, tenantID string, kind ResourceKind, name string) ([]Grant, error)

GrantsOn returns the live grants on one resource: who can reach this. Reads by resource are tenant-wide, as GetUser is; each row names the stack that wrote it.

func (*Store) IssueCredential

func (s *Store) IssueCredential(ctx context.Context, tenantID, stack string, p Principal, in NewCredential) (Credential, string, error)

IssueCredential mints a credential that authenticates as p, on behalf of stack, and returns its password — the only time the cleartext exists outside the caller. Only the argon2id hash is stored.

A principal with no rows yet becomes this stack's: issuing its first credential is what claims it. A `user` principal must exist and be active.

func (*Store) ListCredentials

func (s *Store) ListCredentials(ctx context.Context, tenantID, stack string, p Principal, includeRevoked bool) ([]Credential, error)

ListCredentials returns p's credentials, newest first, on behalf of stack — the owner only: labels, scopes and last-used times are for whoever manages the principal. Revoked rows are included when asked.

func (*Store) ListGrants

func (s *Store) ListGrants(ctx context.Context, tenantID, stack string, p Principal, kind ResourceKind, includeRevoked bool) ([]Grant, error)

ListGrants returns p's grants, newest first, on behalf of stack — the owner only. kind narrows the list when it is not empty. Revoked rows are included when asked.

func (*Store) LookupBinding

func (s *Store) LookupBinding(ctx context.Context, tenantID string, kind BindingKind, issuer, subject string) (Binding, error)

LookupBinding resolves a live identifier to its binding — the first step of a login. issuer is "" for email. ErrNotFound when nothing live matches.

func (*Store) MintRunGrant

func (s *Store) MintRunGrant(ctx context.Context, tenantID, stack string, p Principal, in NewRunGrant) (RunGrant, error)

MintRunGrant writes the grant for one piece of work that acts for p, on behalf of stack, which must manage p. Minting a run that already has a grant closes the older one: the newest generation is the only live one.

It refuses an allowlist that names anything p holds no standing grant for (ErrExceedsStanding), and — narrowing from a parent — anything the parent does not name or a budget the parent does not have left (ErrExceedsParent). A parent's expiry caps the child's; it is shortened, not refused.

The child's whole budget is taken from the parent at mint, spent or not.

func (*Store) PutGrant

func (s *Store) PutGrant(ctx context.Context, tenantID, stack string, p Principal, in NewGrant) (Grant, bool, error)

PutGrant grants p the resource on behalf of stack, which must manage p. It is idempotent: putting a grant that already holds returns it with created=false. Putting it with different verbs revokes the old row and writes a new one.

The principal must already exist (RequireOwner), so a grant can never name a principal another stack could later claim. The resource need not: a grant may be written before the secret it names is stored.

func (*Store) ReadRunGrant

func (s *Store) ReadRunGrant(ctx context.Context, tenantID, grantID string) (RunGrant, error)

ReadRunGrant reads a run grant for the chassis itself, which asks on behalf of no stack. An op reads through GetRunGrant.

func (*Store) RequireOwner

func (s *Store) RequireOwner(ctx context.Context, tenantID, stack string, p Principal) (base string, err error)

RequireOwner reports whether stack manages p, for an op that grants p something the identity tables do not hold (a printer, say). Unlike the writes above it does not make stack the owner of a principal nobody has written yet: p must already exist — ErrNotFound otherwise — so a grant cannot name a principal another stack could later claim. Another stack's principal is an *OwnerError. It returns the base stack.

func (*Store) RevokeCredential

func (s *Store) RevokeCredential(ctx context.Context, tenantID, stack, credentialID string) (Credential, bool, error)

RevokeCredential revokes one credential by id, on behalf of stack. It returns the row and whether THIS call revoked it (false: it already was). Revocation is permanent; the row stays, so its short id is never reused.

func (*Store) RevokeCredentialOf

func (s *Store) RevokeCredentialOf(ctx context.Context, tenantID, stack string, p Principal, credentialID string) (Credential, bool, error)

RevokeCredentialOf is RevokeCredential for a caller that knows WHOSE the credential should be: it revokes credentialID only if it belongs to p, and answers ErrNotFound otherwise — the same answer as an id that does not exist, so a caller learns nothing about another principal's credentials.

It exists because the creator-stack rule is not a wall between a stack's own principals: one stack may manage thousands (every pony of a product), and a bare id taken from a request would let one of that stack's users revoke a credential of another's. Naming the principal pins the id to it.

func (*Store) RevokeCredentials

func (s *Store) RevokeCredentials(ctx context.Context, tenantID, stack string, p Principal, exceptID string) (int, error)

RevokeCredentials revokes every live credential of p except the one named, and returns how many it revoked. It is the second half of a rotation: issue the new credential, then revoke the rest.

exceptID must name a LIVE credential of p (ErrNotFound otherwise, and nothing is revoked). That is the point of it: in a pipeline the id comes from the issue step, and if that step failed the id is missing — revoking "all but nothing" would then lock the principal out with no password left. Pass exceptID == "" only to mean revoke ALL, deliberately.

func (*Store) RevokeGrant

func (s *Store) RevokeGrant(ctx context.Context, tenantID, stack, grantID string) (Grant, bool, error)

RevokeGrant revokes one grant by id, on behalf of stack. It returns the row and whether THIS call revoked it (false: it already was). The row stays.

Revoking a standing grant ends every run grant that leans on it at that run's next request: each request reads the standing grant again.

func (*Store) RevokeGrantOf

func (s *Store) RevokeGrantOf(ctx context.Context, tenantID, stack string, p Principal, grantID string) (Grant, bool, error)

RevokeGrantOf is RevokeGrant for a caller that knows WHOSE the grant should be: it revokes grantID only if it is p's, and answers ErrNotFound otherwise. For an id that came from a request (see RevokeCredentialOf).

func (*Store) RevokeGrantOn

func (s *Store) RevokeGrantOn(ctx context.Context, tenantID, stack string, p Principal, kind ResourceKind, name string) (Grant, bool, error)

RevokeGrantOn revokes p's grant on a resource, named the way it was put. It returns ErrNotFound when p never held one, and revoked=false when the last one it held is already revoked.

func (*Store) RevokeRunGrant

func (s *Store) RevokeRunGrant(ctx context.Context, tenantID, stack, grantID string) (RunGrant, bool, error)

RevokeRunGrant revokes a run grant and every grant narrowed from it, on behalf of the stack that minted it. It returns the row and whether THIS call revoked it. The work's next request is refused.

Revoking stops what the work may still ask for. It recalls nothing the work was already given.

func (*Store) SetClock

func (s *Store) SetClock(now func() time.Time)

SetClock replaces the store's clock (tests).

func (*Store) SetUserDisabled

func (s *Store) SetUserDisabled(ctx context.Context, tenantID, stack, userID string, disabled bool) (User, error)

SetUserDisabled disables (or re-enables) a user on behalf of stack. A disabled user keeps its bindings and credentials — nothing is revoked, so re-enabling restores access — but may not be issued new credentials, and the login resolver refuses it.

func (*Store) UserByEmail

func (s *Store) UserByEmail(ctx context.Context, tenantID, email string) (User, Binding, error)

UserByEmail resolves a live email binding to its user. ErrNotFound when the address is unbound, or bound to a principal that is not a user.

func (*Store) VerifyPassword

func (s *Store) VerifyPassword(ctx context.Context, tenantID string, p Principal, password string) (Credential, bool, error)

VerifyPassword checks a presented password against p's credentials: the short id in the password picks the row, then ONE argon2 verify decides. It returns the credential so the caller can check its scopes cover the head being opened.

Every miss — no id in the password, no such credential, a revoked one — burns the same dummy verify, so a wrong id costs what a wrong secret does.

This is the store's half of a login. The resolver on top of it (caching, throttling, the user's status, last_used_at) is the authentication runtime.

type User

type User struct {
	ID          string // usr_<hxid>
	TenantID    string
	DisplayName string
	Status      string
	CreatedBy   string
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

User mirrors a users row: durable state about a human. A pony or a service is a principal without one.

func (User) Disabled

func (u User) Disabled() bool

Disabled reports whether the user may no longer sign in or be issued credentials.

func (User) Principal

func (u User) Principal() Principal

Principal is the principal this user authenticates and is authorized as.

Directories

Path Synopsis
Package authntest holds the identity store's behaviour suite and the helpers to stand a store up in a test.
Package authntest holds the identity store's behaviour suite and the helpers to stand a store up in a test.

Jump to

Keyboard shortcuts

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