Documentation
¶
Overview ¶
Package registry reads (and minimally mutates) the actor / actor_keys / tenants / actor_memberships tables. The auth middleware uses it for every signed request; admin endpoints use it for enrolment, listing, revocation, and membership management.
Index ¶
- Constants
- Variables
- func HashToken(token string) string
- func RetrySchema(ctx context.Context, fn func(context.Context) error) error
- func RunInSavepoint(ctx context.Context, tx *sql.Tx, name string, op func() error) error
- type Actor
- type Bootstrap
- type ConsumeResult
- type Dialect
- type Invitation
- type Key
- type Membership
- type Registry
- func (r *Registry) ActorReauthRequired(ctx context.Context, actorID string) (bool, error)
- func (r *Registry) ClearActorReauthRequired(ctx context.Context, actorID string) error
- func (r *Registry) ConsumeBootstrap(ctx context.Context, plaintext, consumerIP string) (*Bootstrap, error)
- func (r *Registry) ConsumeInvitation(ctx context.Context, tokenHash, newActorID, newKeyID string, ...) (*ConsumeResult, error)
- func (r *Registry) CreateActor(ctx context.Context, a Actor) error
- func (r *Registry) CreateBootstrap(ctx context.Context, actorID, tenantID string, caps []string, superAdmin bool, ...) (plaintext string, expiresAt time.Time, err error)
- func (r *Registry) CreateInvitation(ctx context.Context, inv Invitation) error
- func (r *Registry) CreateKey(ctx context.Context, k Key) error
- func (r *Registry) CreateMembership(ctx context.Context, m Membership) (*Membership, error)
- func (r *Registry) CreateOIDCSubject(ctx context.Context, issuer, subject, tenantID string) error
- func (r *Registry) CreateSession(ctx context.Context, b *Bootstrap, ua, ip string, ttl time.Duration) (*Session, error)
- func (r *Registry) GetSession(ctx context.Context, sessionID string) (*Session, error)
- func (r *Registry) HasAnyActiveActor(ctx context.Context) (bool, error)
- func (r *Registry) ListActors(ctx context.Context) ([]Actor, error)
- func (r *Registry) ListActorsInTenant(ctx context.Context, tenantID string) ([]Actor, error)
- func (r *Registry) ListInvitations(ctx context.Context, tenantID string) ([]Invitation, error)
- func (r *Registry) ListMembershipsForActor(ctx context.Context, actorID string) ([]Membership, error)
- func (r *Registry) ListMembershipsForTenant(ctx context.Context, tenantID string) ([]Membership, error)
- func (r *Registry) ListSessions(ctx context.Context, tenantID string) ([]Session, error)
- func (r *Registry) LoadMembership(ctx context.Context, actorID, tenantID string) (*Membership, error)
- func (r *Registry) LookupActor(ctx context.Context, actorID string) (*Actor, error)
- func (r *Registry) LookupInvitationByTokenHash(ctx context.Context, tokenHash string) (*Invitation, error)
- func (r *Registry) LookupKey(ctx context.Context, keyID string) (*Key, error)
- func (r *Registry) LookupKeyByPublicKey(ctx context.Context, pub ed25519.PublicKey) (*Key, error)
- func (r *Registry) LookupOIDCSubject(ctx context.Context, issuer, subject string) (string, error)
- func (r *Registry) MarkActorReauthRequired(ctx context.Context, actorID string) error
- func (r *Registry) RevokeActor(ctx context.Context, actorID string) error
- func (r *Registry) RevokeActorSessions(ctx context.Context, actorID, by string) error
- func (r *Registry) RevokeInvitation(ctx context.Context, invitationID, tenantID string) error
- func (r *Registry) RevokeKey(ctx context.Context, keyID string) error
- func (r *Registry) RevokeMembership(ctx context.Context, actorID, tenantID string) error
- func (r *Registry) RevokeSession(ctx context.Context, sessionID, by string) error
- func (r *Registry) SetActorSuperAdmin(ctx context.Context, actorID string, super bool) error
- func (r *Registry) TouchSession(ctx context.Context, sessionID string, now time.Time) error
- type Session
- type TenantLookup
Constants ¶
const DefaultTenantID = "tnt_default"
DefaultTenantID mirrors the seeded tenant_id from db/schema/sqlite/runtime/0002_tenants.sql. Kept as a local constant (rather than importing chassis/tenants) so the auth registry stays independent of the runtime DB's package layout — invitations and pre-tenant backfills default to this id, and the value must match what the runtime seed inserts.
Variables ¶
var ErrBootstrapInvalid = errors.New("bootstrap token invalid or expired")
ErrBootstrapInvalid covers all the "this token can't be redeemed" cases — never minted, expired, already consumed. We collapse them into one error to avoid leaking which subtype the caller hit, which would let an attacker probe token existence.
var ErrKeyAlreadyEnrolled = errors.New("key already enrolled")
ErrKeyAlreadyEnrolled is returned by CreateKey when the public_key is already enrolled by a non-revoked actor_keys row (enforced via the partial UNIQUE index in migration 0009). Handlers surface this as a 409 with the existing actor_id so the caller knows to use invite/accept or revoke the old key first.
var ErrNotFound = errors.New("not found")
ErrNotFound is returned by lookups when no matching row exists. The caller (middleware) maps this to unknown_key / actor_revoked / etc.
var ErrSubjectAlreadyMapped = errors.New("oidc subject already mapped")
ErrSubjectAlreadyMapped is returned by CreateOIDCSubject when an (issuer, subject) row already exists. The cloud-enroll handler treats this as a benign first-enroll race: it re-reads the mapping and proceeds as a returning user rather than minting a second tenant.
Functions ¶
func HashToken ¶
HashToken returns the canonical token_hash form used in the schema: lowercase hex of SHA-256(token). Exported so handlers and tests share the same digest function.
func RetrySchema ¶ added in v0.2.31
RetrySchema runs a store's EnsureSchema body, and runs it again if it fails.
On a fleet every node creates its protocol stores' tables in the shared Postgres at boot, with no lock. Two nodes booting together can race a release that adds a table or a column: both probe, both find it missing, and the loser's CREATE or ALTER fails (a duplicate — IF NOT EXISTS does not make concurrent DDL safe on Postgres). By then the winner has committed, so the retry's probe finds the column and its CREATE ... IF NOT EXISTS is a no-op. It is the migration runner's answer to the same race (app.applyMigrationsRetrying), and like it holds no lock and no connection between attempts.
fn must be safe to re-run: every statement idempotent, every additive column probed first. A schema that is genuinely broken fails all three attempts, and the error comes back a few seconds later than it would have.
func RunInSavepoint ¶ added in v0.2.21
RunInSavepoint runs op inside a named SAVEPOINT so a recoverable failure — in particular a Postgres unique_violation (SQLSTATE 23505), which otherwise poisons the whole transaction until a rollback — leaves the enclosing tx usable. It returns op's error UNCHANGED for the caller to classify (e.g. IsUniqueViolationGeneric → re-lookup/adopt); on that error it first ROLLBACKs TO the savepoint. It always RELEASEs the savepoint afterward.
Unlike an ad-hoc inline SAVEPOINT/RELEASE, this surfaces SAVEPOINT / ROLLBACK TO / RELEASE control errors rather than dropping them: a release/rollback failure means the transaction is not in the state the caller assumes, and should abort the write, not be silently ignored. Such a control error is wrapped (never a 23505), so IsUniqueViolationGeneric won't misclassify it as the recoverable case.
Portable: SAVEPOINT syntax is identical on SQLite and Postgres. On SQLite the rollback is merely unnecessary for a constraint error (SQLite localizes it rather than poisoning the tx) — the savepoint ops are still real transaction control there, not no-ops. name must be a fixed identifier (these do not nest recursively per call).
Types ¶
type Actor ¶
type Actor struct {
ActorID string
Label string
Kind string
Subject string
Tenant string
Stack string
SuperAdmin bool
CreatedAt time.Time
RevokedAt *time.Time
Meta string
}
Actor mirrors the actors table row.
Tenant and Stack are vestiges of v1's chassis-wide model and are no longer the source of truth for authz scoping — auth_memberships is. They linger as nullable columns until phase 4's table rebuild and stay readable here for back-compat with existing meta and tooling. SuperAdmin is the chassis-wide override: actors with super_admin=1 pass every RequireCapability regardless of membership.
type Bootstrap ¶
type Bootstrap struct {
TokenHash string
ActorID string
TenantID string
Capabilities []string
SuperAdmin bool
Label string
CreatedAt time.Time
ExpiresAt time.Time
ConsumedAt *time.Time
ConsumedIP string
}
Bootstrap is one row of browser_bootstrap.
type ConsumeResult ¶
type ConsumeResult struct {
Invitation Invitation
ActorID string
KeyID string
TenantID string
Reused bool
}
ConsumeResult is the outcome of a successful ConsumeInvitation. Reused is true when the consumer's public key was already enrolled — in that case ActorID and KeyID point at the EXISTING principal, so a single ssh-agent key can collect memberships across many tenants without spawning duplicate actor rows.
type Dialect ¶
type Dialect interface {
// Rebind rewrites `?` placeholders for the target engine. SQLite
// returns the query unchanged; Postgres returns `$1, $2, …`.
Rebind(query string) string
// IsUniqueViolation reports whether err is the actor_keys public-key
// uniqueness conflict (the only UNIQUE the registry distinguishes —
// it surfaces as ErrKeyAlreadyEnrolled). Deliberately column-specific
// on SQLite so an unrelated UNIQUE can't masquerade as that error.
IsUniqueViolation(err error) bool
// IsUniqueViolationGeneric reports whether err is *any* unique-constraint
// violation, regardless of table/column. This is the general check the
// runtime stores need on Postgres — a duplicate hostname, secret, or DNS
// zone must map to its typed error on either engine. On Postgres the
// SQLSTATE (23505) already carries no column identity, so the narrow and
// generic checks coincide there; the distinction only matters for SQLite's
// string-based detection.
IsUniqueViolationGeneric(err error) bool
// BeginImmediate starts the transaction used by the atomic consume
// paths (invitation / bootstrap redemption) with an upfront write
// lock appropriate to the engine.
BeginImmediate(ctx context.Context, db *sql.DB) (*sql.Tx, error)
// BeginWrite opens the transaction used by the runtime control-plane write
// path. On SQLite it is a plain BeginTx; whether that takes the whole-DB
// RESERVED write lock UP FRONT is NOT provided by BeginWrite itself — it is a
// JOINT guarantee with how the runtime DB was opened: only a connection
// opened with `_txlock=immediate` issues BEGIN IMMEDIATE and takes the lock
// eagerly (see openSQLiteOrDie for kind=="runtime"). On any other SQLite DSN
// this is a deferred tx that acquires the write lock lazily on first write.
// Postgres returns an explicit READ COMMITTED tx so that a LockClause()
// `FOR UPDATE` BLOCKS on contention rather than aborting — under an accidental
// SERIALIZABLE session a lock wait would surface as a 40001 serialization
// failure instead. Distinct from BeginImmediate (SERIALIZABLE on Postgres,
// for the auth registry).
BeginWrite(ctx context.Context, db *sql.DB) (*sql.Tx, error)
// LockClause returns the row-lock suffix appended to a SELECT to take a
// pessimistic lock on the matched rows. Postgres returns " FOR UPDATE";
// SQLite returns "" (empty — SQLite has no per-row lock; serialization comes
// from the whole-DB write lock, held eagerly only under _txlock=immediate).
// Empty on
// SQLite means the query text is unchanged.
LockClause() string
// SkipLockedClause returns the competing-consumer claim suffix for the
// subselect feeding a claim UPDATE. Postgres returns
// " FOR UPDATE SKIP LOCKED" — rows another claimer holds are skipped,
// not waited on, so concurrent pollers partition the due set. SQLite
// returns "" (single writer; a status guard on the UPDATE is the race
// protection, and the whole statement is atomic anyway).
SkipLockedClause() string
}
Dialect isolates the handful of SQL behaviours that differ between the in-tree default (SQLite) and a shared Postgres auth store used by an HA control plane. The registry SQL is otherwise identical: timestamps are RFC3339 TEXT, JSON is TEXT, booleans are INTEGER 0/1, and upserts use the `INSERT … ON CONFLICT(...) DO UPDATE SET … excluded.…` form that both engines accept — so nothing here touches Go scan/marshal.
Only three things vary:
- Placeholders: SQLite binds `?`, Postgres binds `$1, $2, …`.
- Unique-violation detection: SQLite carries it in the error string, Postgres in SQLSTATE 23505. We never import a driver here — the Postgres case duck-types `interface{ SQLState() string }` so core keeps SQLite as its only compiled driver.
- The upfront write lock: SQLite uses `BEGIN IMMEDIATE` to fail fast instead of deadlocking mid-transaction; Postgres uses a SERIALIZABLE transaction. Correctness of the single-winner consume paths comes from the conditional `UPDATE … WHERE consumed_at IS NULL` (rows-affected) guard, which is dialect-neutral — this is only the lock strategy.
var Postgres Dialect = postgresDialect{}
Postgres is selected when the auth DSN is a postgres:// URL. The database/sql driver for chassis STORAGE is registered out of tree (overlay blank-import); the bundled chassis only compiles pgx as the outlet:// protocol client (chassis/outlet/postgres), which never touches this seam.
var SQLite Dialect = sqliteDialect{}
SQLite is the default dialect — identity placeholders, error-string unique detection, and the historical `BEGIN IMMEDIATE` behaviour. Behaviour is byte-for-byte what the registry did before the seam.
func DialectForDSN ¶
DialectForDSN picks the dialect from an auth DSN. Anything that isn't a recognised Postgres URL stays on SQLite (the safe default).
type Invitation ¶
type Invitation struct {
InvitationID string
TokenHash string // hex sha-256 of the raw token
Label string
Kind string
TenantID string
Capabilities []string
CreatedBy string
CreatedAt time.Time
ExpiresAt time.Time
ConsumedAt *time.Time
ConsumedBy string
RevokedAt *time.Time
}
Invitation mirrors the actor_invitations table. Capabilities is the decoded JSON array; v1 always materialises ["admin:all"] (no NULL). TenantID is the slug the invitation scopes its membership grant to — added in migration 0008, backfilled to DefaultTenantID for older rows. Empty in only one situation: a pre-0008 invitation that somehow escaped the backfill, which the handler treats as default.
type Key ¶
type Key struct {
KeyID string
ActorID string
PublicKey ed25519.PublicKey
Algorithm string
CreatedAt time.Time
RevokedAt *time.Time
Meta string
}
Key mirrors actor_keys. PublicKey is the raw 32-byte Ed25519 public key.
type Membership ¶
type Membership struct {
ActorID string
TenantID string
TenantSlug string
Capabilities []string
CreatedAt time.Time
RevokedAt *time.Time
}
Membership mirrors actor_memberships. Capabilities is the decoded JSON column — never NULL, always at least one entry. TenantSlug is populated by the optional TenantLookup passed to New; when no resolver is configured (or it returns ErrNotFound) the slug is left empty.
type Registry ¶
type Registry struct {
DB *sql.DB
Tenants TenantLookup
// Dialect adapts the few SQL behaviours that differ between the
// in-tree SQLite default and a shared Postgres auth store (HA
// control plane). nil ⇒ SQLite (see dia()).
Dialect Dialect
}
Registry is the thin façade over the auth tables. It takes a *sql.DB directly so it can run inside transactions started by the admin handlers (e.g. when enrollment writes an actor + a key + capabilities atomically).
Tenants is the cross-DB slug resolver — tenants live in runtime.db (chassis/tenants package), and membership queries need slugs for the common authz / display paths. May be nil for tests or contexts that don't care about slug enrichment; membership methods then leave TenantSlug empty.
func New ¶
func New(db *sql.DB, tenants TenantLookup) *Registry
New builds a Registry on the SQLite dialect (the in-tree default). tenants may be nil — see TenantLookup doc.
func NewWithDialect ¶
func NewWithDialect(db *sql.DB, tenants TenantLookup, d Dialect) *Registry
NewWithDialect builds a Registry on an explicit dialect — used when the auth DSN selects Postgres for an HA control plane. A nil dialect falls back to SQLite.
func (*Registry) ActorReauthRequired ¶ added in v0.2.23
ActorReauthRequired reports whether the actor must re-authenticate through the identity provider before minting a browser session.
An unknown actor returns false, not ErrNotFound: this gates a mint that is itself already authenticated, so a missing row means "nothing is blocking you" and the caller's own auth checks stay the gate.
func (*Registry) ClearActorReauthRequired ¶ added in v0.2.23
ClearActorReauthRequired lifts the marker after the actor has proved a fresh identity-provider login. Idempotent — clearing an unmarked actor is a no-op.
func (*Registry) ConsumeBootstrap ¶
func (r *Registry) ConsumeBootstrap(ctx context.Context, plaintext, consumerIP string) (*Bootstrap, error)
ConsumeBootstrap is the one-shot redeem: matches the plaintext against the stored hash, marks the row consumed inside a single transaction, and returns the row to the caller so they can mint a session. Returns ErrBootstrapInvalid for any miss — not-found, expired, already-consumed — so the wire response can't be used to probe which tokens exist.
The conditional UPDATE mirrors the SQLite pattern documented in `feedback_sqlite_transactions.md`: a SELECT alone races with another consumer; a `UPDATE … WHERE consumed_at IS NULL` + checking RowsAffected == 1 is atomic on the writer thread.
func (*Registry) ConsumeInvitation ¶
func (r *Registry) ConsumeInvitation(ctx context.Context, tokenHash, newActorID, newKeyID string, pub ed25519.PublicKey, label, kind string) (*ConsumeResult, error)
ConsumeInvitation atomically redeems a token. The whole sequence lives in one BEGIN IMMEDIATE transaction so two concurrent consumers can't both succeed.
Sequence:
- BEGIN IMMEDIATE — write lock up front
- Look up actor_keys by public_key (active rows only). If found, the consumer is an EXISTING principal — we'll bind a new membership to them instead of minting a duplicate.
- UPDATE actor_invitations SET consumed_at,consumed_by — guarded by the WHERE clause. consumed_by records the EXISTING actor_id when reusing, otherwise the freshly-minted newActorID.
- Re-read the invitation row to recover capabilities + tenant_id.
- If not reusing: INSERT actors + INSERT actor_keys + INSERT actor_capabilities*N (the chassis-wide back-compat row). If reusing: skip — the principal already has all that.
- Upsert membership(actor_id, invitation.tenant_id, invitation.capabilities) — the tenant grant is the point of this whole flow.
- COMMIT
If anything past step 3 fails, the tx rolls back and the burn is reverted — the invitation goes back to "live" and a retry can succeed. ErrKeyAlreadyEnrolled bubbles up from a race where another consumer enrolled the key between our lookup and our insert; the caller's invitation row is rolled back too.
func (*Registry) CreateActor ¶
CreateActor inserts a new actor row. The caller is responsible for generating actor_id (use chassis/hxid). Capability rows are written separately via GrantCapability so this stays a thin façade.
func (*Registry) CreateBootstrap ¶
func (r *Registry) CreateBootstrap(ctx context.Context, actorID, tenantID string, caps []string, superAdmin bool, label string, ttl time.Duration) (plaintext string, expiresAt time.Time, err error)
CreateBootstrap mints a new exchange token for the given actor + tenant + capability snapshot. Returns the plaintext token (which the caller surfaces to the user exactly once) and its expiry.
Multiple outstanding bootstraps for the same actor are allowed — the short TTL bounds the blast radius and forbidding it would break retry flows.
func (*Registry) CreateInvitation ¶
func (r *Registry) CreateInvitation(ctx context.Context, inv Invitation) error
CreateInvitation inserts an invitation row. The caller is responsible for hashing the token and assigning a hxid invitation_id. Capabilities must already be set (no defaulting here — that's policy, not storage). TenantID defaults to DefaultTenantID when empty so callers that haven't yet migrated to tenant-aware invites keep working against the seeded default tenant.
func (*Registry) CreateKey ¶
CreateKey inserts an actor_keys row. PublicKey must be a valid ed25519.PublicKey (32 bytes).
Returns ErrKeyAlreadyEnrolled if the public_key is already bound to a non-revoked actor_keys row (enforced by the partial UNIQUE index from migration 0009). Callers should pre-check via LookupKeyByPublicKey when they want to branch on the existing row; CreateKey detects the race and surfaces the same typed error.
func (*Registry) CreateMembership ¶
func (r *Registry) CreateMembership(ctx context.Context, m Membership) (*Membership, error)
CreateMembership upserts an (actor, tenant) membership. v1 stores the capability set as a JSON array; passing the same (actor, tenant) pair twice REPLACES the capabilities — useful for "add me to this tenant with these capabilities, even if I'm already a member."
Returns the freshly-written membership so callers can echo it without a follow-up read. TenantSlug is filled via the registry's TenantLookup if one is configured.
func (*Registry) CreateOIDCSubject ¶ added in v0.2.4
CreateOIDCSubject records the (issuer, subject) → tenant_id mapping. An existing mapping is surfaced as the typed ErrSubjectAlreadyMapped so callers can recover from a concurrent first-enroll without minting a duplicate tenant. A pre-check covers the common case cleanly across dialects (the dialect's IsUniqueViolation is purpose-built for actor_keys, not this table); a generic post-insert check catches the pre-check→insert race.
func (*Registry) CreateSession ¶
func (r *Registry) CreateSession(ctx context.Context, b *Bootstrap, ua, ip string, ttl time.Duration) (*Session, error)
CreateSession turns a successfully-consumed Bootstrap into a session. Capabilities are snapshotted from the bootstrap row, not re-resolved from memberships, so a capability change after this point requires a session revoke + fresh login to take effect.
func (*Registry) GetSession ¶
GetSession reads a session by id. Returns ErrNotFound when the session doesn't exist; returns a Session whose IsValid() is false when the row exists but is revoked or expired (caller decides how to surface that — middleware treats it as 401).
func (*Registry) HasAnyActiveActor ¶
HasAnyActiveActor reports whether at least one actors row has revoked_at IS NULL. The admin server uses this during startup to decide whether to auto-generate a first-boot dev-enroll secret; it also serves as the burn-after-use gate on `/auth/dev/enroll`.
Cheap by design: LIMIT 1, primary-key scan on a small table.
func (*Registry) ListActors ¶
ListActors returns every row in actors, newest first. v1 returns them all; pagination ships in v2 when there's a real need.
Chassis-wide: it sees every tenant's actors, so it must never back a tenant-scoped route — use ListActorsInTenant there.
func (*Registry) ListActorsInTenant ¶ added in v0.2.30
ListActorsInTenant returns the actors holding an active membership in tenantID, newest first. A revoked actor whose membership row is still active is included (with RevokedAt set) so tenant admins can see who was cut off.
func (*Registry) ListInvitations ¶
ListInvitations returns tenantID's invitation rows, newest first. Status derivation happens at the HTTP layer — this method is pure storage.
func (*Registry) ListMembershipsForActor ¶
func (r *Registry) ListMembershipsForActor(ctx context.Context, actorID string) ([]Membership, error)
ListMembershipsForActor returns every active membership for an actor. Slugs are resolved via the registry's TenantLookup; rows are sorted by resolved slug (or by tenant_id when slug resolution is unavailable) so the caller sees a stable order.
Two-pass shape: drain the membership rows fully *before* calling fillSlug. The tenants table lives in runtime.db (a different *sql.DB in production, but possibly the same handle under test); resolving slugs while the outer Rows is still open would block on a single- connection pool, since the cursor is holding the only connection.
func (*Registry) ListMembershipsForTenant ¶
func (r *Registry) ListMembershipsForTenant(ctx context.Context, tenantID string) ([]Membership, error)
ListMembershipsForTenant returns every active member of a tenant. Used by `txco auth tenant members <slug>` and for admin auditing. Slug is resolved once before the loop (all rows share the same tenant) and then stamped onto each row.
func (*Registry) ListSessions ¶
ListSessions returns every session for a tenant — both active and revoked/expired — newest first. The admin UI uses this to render a "manage sessions" view; the CLI uses it for `txco auth sessions list`.
func (*Registry) LoadMembership ¶
func (r *Registry) LoadMembership(ctx context.Context, actorID, tenantID string) (*Membership, error)
LoadMembership returns a single (actor, tenant) row. The slug is resolved via the registry's TenantLookup; if no resolver is set or the lookup fails, TenantSlug is left empty. Returns ErrNotFound when no active membership exists.
func (*Registry) LookupActor ¶
LookupActor reads a single actors row by primary key.
func (*Registry) LookupInvitationByTokenHash ¶
func (r *Registry) LookupInvitationByTokenHash(ctx context.Context, tokenHash string) (*Invitation, error)
LookupInvitationByTokenHash reads a single invitation row by its (already-hashed) token. Used by the unsigned consume endpoint's auditing log line; the actual atomic consume goes through ConsumeInvitation. Returns ErrNotFound if the row is missing.
func (*Registry) LookupKeyByPublicKey ¶
LookupKeyByPublicKey returns the active actor_keys row whose public_key matches the given bytes, or ErrNotFound when no row (active or otherwise) carries that key. Used by /auth/dev/enroll to refuse re-enrolment and by /auth/invitations/consume to bind new memberships to an existing principal instead of minting a duplicate actor.
func (*Registry) LookupOIDCSubject ¶ added in v0.2.4
LookupOIDCSubject resolves an OIDC (issuer, subject) to the tenant_id minted on that identity's first cloud enrollment. Returns ErrNotFound when the identity has never enrolled (the caller then runs the first-enroll / slug-selection path).
func (*Registry) MarkActorReauthRequired ¶ added in v0.2.23
MarkActorReauthRequired stamps the actor as needing a fresh identity-provider login before it can mint another browser session. Idempotent: re-stamping an already-marked actor just refreshes the timestamp, which is harmless (the column is a flag, not an audit trail — revoked_by-style attribution lives on browser_sessions).
func (*Registry) RevokeActor ¶
RevokeActor sets revoked_at on an actor and cascades to all its keys. Capability rows are left as-is — the actor's revocation is what matters for auth decisions.
func (*Registry) RevokeActorSessions ¶
RevokeActorSessions revokes every session belonging to an actor. Called from the existing actor-revoke handler so revoking a user kicks them out of every browser they're signed into.
func (*Registry) RevokeInvitation ¶
RevokeInvitation marks one of tenantID's invitations as revoked. Idempotent: revoking an already-revoked / already-consumed invitation is a no-op (the WHERE clause filters those out). An invitation that belongs to another tenant returns ErrNotFound, same as an unknown id.
func (*Registry) RevokeKey ¶
RevokeKey sets revoked_at on a single key. Idempotent: revoking an already-revoked key is a no-op (the column is set to the same now).
func (*Registry) RevokeMembership ¶
RevokeMembership soft-deletes a membership by setting revoked_at. Idempotent: already-revoked rows are left alone.
func (*Registry) RevokeSession ¶
RevokeSession marks a session revoked. Idempotent — calling it on an already-revoked or non-existent session is a no-op (returns nil), which simplifies UI logout flows that may double-fire.
func (*Registry) SetActorSuperAdmin ¶
SetActorSuperAdmin flips the actors.super_admin flag. v1 callers only set this during bootstrap (the first enrolled actor) or via a future admin command. Idempotent.
type Session ¶
type Session struct {
SessionID string
ActorID string
TenantID string
Capabilities []string
// SuperAdmin snapshots actors.super_admin at bootstrap time so a
// cookie-authed request can be recognised as a real operator
// (verifyCookie → auth.Context.SuperAdmin → RequireSuperAdmin)
// rather than every browser session being treated as one.
SuperAdmin bool
UA string
IP string
CreatedAt time.Time
ExpiresAt time.Time
RevokedAt *time.Time
RevokedBy string
LastSeenAt time.Time
}
Session is one row of browser_sessions.
type TenantLookup ¶
TenantLookup resolves a tenant_id to its slug. Satisfied by chassis/tenants.Store via the adapter constructed in chassis/server/admin/server.go. Function type rather than interface because there's exactly one operation and zero state.