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
- type Config
- type Principal
- type Reviewer
- type Service
- func (s *Service) GrantReviewer(ctx context.Context, actor *auth.Identity, email string) (*Reviewer, error)
- func (s *Service) MovePrincipal(ctx context.Context, email string) (*Transfer, error)
- func (s *Service) Principals(ctx context.Context) ([]Principal, error)
- func (s *Service) Reviewers(ctx context.Context, actor *auth.Identity) ([]Reviewer, error)
- func (s *Service) RevokeReviewer(ctx context.Context, actor *auth.Identity, userID uuid.UUID) error
- type Transfer
Constants ¶
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.
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 ¶
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 (*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 ¶
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 ¶
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 ¶
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 ¶
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.