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
- Variables
- func BaseStack(stack string) string
- func NormalizeEmail(addr string) (string, error)
- func ShortIDOf(password string) (shortID string, ok bool)
- func WithAuthenticated(ctx context.Context, a Authenticated) context.Context
- type Attempt
- type Authenticated
- type Binding
- type BindingKind
- type Credential
- type CredentialKind
- type Grant
- type NewBinding
- type NewCredential
- type NewGrant
- type NewRunGrant
- type NewUser
- type NodeClass
- type Outcome
- type OwnerError
- type PasswordStyle
- type Principal
- type PrincipalKind
- type Resolver
- type ResolverConfig
- type Resource
- type ResourceKind
- type Result
- type RunGrant
- type Scope
- type Scopes
- type Store
- func (s *Store) BindPrincipal(ctx context.Context, tenantID, stack string, p Principal, nb NewBinding) (Binding, bool, error)
- func (s *Store) ChargeRunGrant(ctx context.Context, tenantID, grantID string, n int64, enforceBudget bool) error
- func (s *Store) CloseRunGrant(ctx context.Context, tenantID, stack, grantID, reason string) (RunGrant, bool, error)
- func (s *Store) CreateUser(ctx context.Context, tenantID, stack string, in NewUser) (User, Binding, bool, error)
- func (s *Store) CredentialLive(ctx context.Context, credentialID string) (bool, error)
- func (s *Store) GetRunGrant(ctx context.Context, tenantID, stack, grantID string) (RunGrant, error)
- func (s *Store) GetUser(ctx context.Context, tenantID, userID string) (User, error)
- func (s *Store) Granted(ctx context.Context, tenantID string, p Principal, res Resource, verb string) (bool, error)
- func (s *Store) GrantsOn(ctx context.Context, tenantID string, kind ResourceKind, name string) ([]Grant, error)
- func (s *Store) IssueCredential(ctx context.Context, tenantID, stack string, p Principal, in NewCredential) (Credential, string, error)
- func (s *Store) ListCredentials(ctx context.Context, tenantID, stack string, p Principal, includeRevoked bool) ([]Credential, error)
- func (s *Store) ListGrants(ctx context.Context, tenantID, stack string, p Principal, kind ResourceKind, ...) ([]Grant, error)
- func (s *Store) LookupBinding(ctx context.Context, tenantID string, kind BindingKind, issuer, subject string) (Binding, error)
- func (s *Store) MintRunGrant(ctx context.Context, tenantID, stack string, p Principal, in NewRunGrant) (RunGrant, error)
- func (s *Store) PutGrant(ctx context.Context, tenantID, stack string, p Principal, in NewGrant) (Grant, bool, error)
- func (s *Store) ReadRunGrant(ctx context.Context, tenantID, grantID string) (RunGrant, error)
- func (s *Store) RequireOwner(ctx context.Context, tenantID, stack string, p Principal) (base string, err error)
- func (s *Store) RevokeCredential(ctx context.Context, tenantID, stack, credentialID string) (Credential, bool, error)
- func (s *Store) RevokeCredentialOf(ctx context.Context, tenantID, stack string, p Principal, credentialID string) (Credential, bool, error)
- func (s *Store) RevokeCredentials(ctx context.Context, tenantID, stack string, p Principal, exceptID string) (int, error)
- func (s *Store) RevokeGrant(ctx context.Context, tenantID, stack, grantID string) (Grant, bool, error)
- func (s *Store) RevokeGrantOf(ctx context.Context, tenantID, stack string, p Principal, grantID string) (Grant, bool, error)
- func (s *Store) RevokeGrantOn(ctx context.Context, tenantID, stack string, p Principal, kind ResourceKind, ...) (Grant, bool, error)
- func (s *Store) RevokeRunGrant(ctx context.Context, tenantID, stack, grantID string) (RunGrant, bool, error)
- func (s *Store) SetClock(now func() time.Time)
- func (s *Store) SetUserDisabled(ctx context.Context, tenantID, stack, userID string, disabled bool) (User, error)
- func (s *Store) UserByEmail(ctx context.Context, tenantID, email string) (User, Binding, error)
- func (s *Store) VerifyPassword(ctx context.Context, tenantID string, p Principal, password string) (Credential, bool, error)
- type User
Constants ¶
const ( VerbInvoke = "invoke" VerbRelease = "release" )
The verbs built.
const ( // Word counts accepted for StyleWords; the default is apppass's. MinPasswordWords = 4 MaxPasswordWords = 12 )
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 )
const ( StatusActive = "active" StatusDisabled = "disabled" )
User statuses.
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.
const MaxGrants = 1024
MaxGrants bounds the LIVE grants one principal may hold, so a loop cannot write rows without end.
const MaxScopes = 32
MaxScopes bounds one credential's scope list.
Variables ¶
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") )
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") )
var ErrTooManyGrants = errors.New("authn: too many grants")
ErrTooManyGrants: the principal already holds MaxGrants live grants.
Functions ¶
func BaseStack ¶
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 ¶
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 ¶
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.
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.
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 ¶
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 ¶
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 ¶
UserPrincipal is the principal of a users row.
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 ¶
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.
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 ¶
ParseResource reads the String form: `crm.lookup`, `capability:crm.lookup` or `secret:CRM_KEY`.
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 ¶
AllowStrings is the allowlist in its stored / displayed form.
func (RunGrant) Sandbox ¶
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 ¶
SandboxNames is the grant's sandboxes, sorted.
type Scope ¶
type Scope struct {
Domain, Instance, Action string
}
Scope is one parsed `domain:instance:action` capability.
type Scopes ¶
type Scopes []Scope
Scopes is a credential's scope list: validated, de-duplicated, sorted.
func ParseScopes ¶
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.
type Store ¶
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 (*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 ¶
CredentialLive is Resolver.Live's read.
func (*Store) GetRunGrant ¶
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 ¶
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 ¶
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) 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 ¶
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.