instance

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package instance is the instance-level principal: who administers the box rather than a tenant in it, and what they may hand to somebody else.

D38 recorded that this product had no such principal, and three findings bottomed out there. D98 introduces one, with two constraints that shape everything here:

**Only the instance-owner level may delegate.** The principal confers instance-level review; a holder of instance-level review may not confer it onwards. That is structural rather than checked — `auth.InstanceGrantable` does not contain `instance.admin`, so there is no path by which a grant produces another grantor, and the set of people who may delegate cannot grow. It is the same argument D43 makes about key-issued invitations: bound what a grant may produce, not only who may make one.

**A change requires a person.** Not implemented here, and deliberately: it is `destinations.decide` sitting in `auth.NonDelegableScopes`, so a key is refused by the map rather than by a check on what kind of credential is calling. This package contains no reference to `IsAPIKey`, and that absence is the design.

The scopes are enumerated in internal/auth and nothing inherits from holding one. A principal that accumulates scopes because it exists is how the thing D38 avoided gets built by accident.

Index

Constants

View Source
const (
	ActionReviewerGranted = "instance.reviewer_granted"
	ActionReviewerRevoked = "instance.reviewer_revoked"
	// ActionPrincipalMoved is the operator's recovery (F140). Its actor is
	// recorded as `system`, because nobody in the product performed it; see
	// MovePrincipal.
	ActionPrincipalMoved = "instance.principal_moved"
)

Audit actions for the writes here. All three are instance-wide events: they change who may act on every organization's disputes, so they belong to no tenant.

View Source
const PermAdmin = auth.PermInstanceAdmin

PermAdmin guards conferring and withdrawing instance-level review.

Re-exported from internal/auth rather than declared again, because the identity resolver has to know the slug and this package must not be able to drift from it.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// Audit records the three writes. Nil records nothing.
	Audit audit.Recorder
	Log   *slog.Logger
}

Config is what a Service needs.

type Principal

type Principal struct {
	UserID uuid.UUID
	Email  string
	Name   string
}

Principal is an account that administers the instance, as an operator at a shell sees it.

type Reviewer

type Reviewer struct {
	UserID uuid.UUID `json:"user_id"`
	Email  string    `json:"email"`
	Name   string    `json:"name"`
	// GrantedAt is when they were appointed. GrantedBy is who appointed them,
	// absent for the two grants nobody performed interactively — the bootstrap in
	// migration 03400 and the setup flow that claimed a fresh instance.
	GrantedAt time.Time  `json:"granted_at"`
	GrantedBy *uuid.UUID `json:"granted_by,omitempty"`
	// CanDecide separates the two halves of the dispute permission. A reviewer
	// normally holds both; the field exists because the halves are separately
	// revocable and a list that folded them together would show a state the
	// database can be in as a state it cannot.
	CanDecide bool `json:"can_decide"`
}

Reviewer is somebody holding instance-level review, as the principal sees them.

type Service

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

Service reads and writes instance-level grants.

func NewService

func NewService(pool *pgxpool.Pool, cfg Config) *Service

func (*Service) GrantReviewer

func (s *Service) GrantReviewer(
	ctx context.Context, actor *auth.Identity, email string,
) (*Reviewer, error)

GrantReviewer confers instance-level review on the account with an address.

By address rather than by id, because the principal appointing somebody knows who they are and not what their uuid is, and because the address is what both surfaces already collect.

Idempotent, so appointing somebody who is already a reviewer succeeds. Two administrators doing the same obvious thing must not produce something that reads like a refusal.

func (*Service) MovePrincipal

func (s *Service) MovePrincipal(ctx context.Context, email string) (*Transfer, error)

MovePrincipal moves the instance principal onto the account with an address, and off every account that held it.

**F140: the gap D98 named and left open.** The principal is conferred at `POST /api/v1/auth/setup` and nowhere else, `InstanceGrantable` deliberately excludes `instance.admin` so that no holder can mint another, and nothing in the product deletes a `users` row — so a conferred principal cannot be *lost* through the product, but the account it sits on can be. A forgotten password with no mailer configured, a departed colleague, an instance whose earliest surviving account was never the operator's: each of those ended at `psql`, and [F141] establishes that this product has no account recovery at all, so the principal's password and the principal are the same thing to lose.

**It takes no actor, and that is the design rather than an omission.** The authority here is the shell. An operator running `lctl` has filesystem and deploy access, which is the same claim to the box that `internal/auth`'s `Register` already rests the whole setup flow on — *the first user is trusted by construction: they had filesystem or deploy access to reach the setup page*. Asking for an `*auth.Identity` and checking `Can(PermAdmin)` would be theatre, because whoever runs this picks the identity; worse, it would read as an in-product authorization and invite somebody to hang an HTTP route off it. Nothing routes here, and the absence of a permission check is what says so.

**D98's delegation bound is not widened, and this asserts it rather than claiming it.** A move is not a grant: the transaction ends with exactly one account holding `instance.admin`, verified before the commit, so the set of people who may delegate cannot grow — which is the property the bound exists to protect, and the same direction migration 03400 took when it moved the role grant onto the earliest surviving account. An operation that *added* a principal is the one D98 forbids, and this is not one.

Idempotent, like GrantReviewer and for the same reason: moving the principal onto the account that already holds it succeeds and changes nothing.

func (*Service) Principals

func (s *Service) Principals(ctx context.Context) ([]Principal, error)

Principals is who holds `instance.admin`, for an operator at a shell.

It takes no actor for the reason MovePrincipal takes none, and it exists because a move that could not be checked before or after would be a repair performed blind: the state this whole gap is about — *which account administers this instance* — had no answer outside `psql`, and an operator who cannot read it cannot tell a successful move from a move onto the wrong address.

Deliberately not the same thing as Reviewers, which is gated on PermAdmin and lists who was *appointed*. This lists who does the appointing, and there is normally exactly one.

func (*Service) Reviewers

func (s *Service) Reviewers(ctx context.Context, actor *auth.Identity) ([]Reviewer, error)

Reviewers lists who holds instance-level review.

Gated on PermAdmin rather than on holding review itself. Who else administers the instance is the principal's business; a reviewer needs the queue, not the roster, and handing them one would disclose the full set of administrators to everybody appointed to work through disputes.

func (*Service) RevokeReviewer

func (s *Service) RevokeReviewer(
	ctx context.Context, actor *auth.Identity, userID uuid.UUID,
) error

RevokeReviewer withdraws instance-level review from an account.

It cannot withdraw `instance.admin`, because that scope is not in InstanceGrantable and this loop is over that set: the principal is not removable through the surface it uses to appoint people, which is the same reason it is not conferrable through it. An instance that could revoke its own last principal would be an instance with nobody able to appoint one, and the dispute queue would be stranded exactly as it is stranded today by the finding this whole change closes.

type Transfer

type Transfer struct {
	To   Principal
	From []Principal
}

Transfer is what one move did: who holds the principal now, and who held it before and does not any more.

From is a slice because the table can hold more than one row and this package is not the only thing that has ever written to it — migration 03400 and the setup flow both do, and an operator with `psql` is the reason this method exists at all. Reporting the set rather than "the previous principal" means a box that had two says so instead of naming one of them.

Jump to

Keyboard shortcuts

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