setup

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 7, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Overview

Package setup implements the interactive first-run admin provisioning slice.

Two Public HTTP endpoints:

GET  /api/v1/access/setup/status   — returns {"hasAdmin": bool}
POST /api/v1/access/setup/admin    — creates the first admin; 410 Gone after initialized

Race-safe admin creation is delegated to cells/accesscore/internal/adminprovision so the semantics match the headless initialadmin Lifecycle exactly.

Index

Constants

View Source
const (
	MaxUsernameLen   = 128
	MaxEmailLen      = 256
	MinPasswordBytes = 8
	MaxPasswordBytes = 72
)

Password bounds for the setup endpoint:

  • Password bytes must be printable ASCII so JSON Schema maxLength and bcrypt's byte-counted input limit have the same semantics.
  • MaxPasswordBytes matches golang.org/x/crypto/bcrypt's hard input limit.

Variables

This section is empty.

Functions

func SliceMetadata

func SliceMetadata() *metadata.SliceMeta

SliceMetadata returns the package-scope *metadata.SliceMeta projected from slice.yaml. Composition roots consume this via cell.MustNewBaseSliceFromMeta(<slicePkg>.SliceMetadata()) — the typed funnel that replaces the legacy `cell.NewBaseSlice(id, cellID, level)` literal pattern.

Types

type AdminAdapter

type AdminAdapter struct{ S *Service }

AdminAdapter implements adminGen.Service for http.auth.setup.admin.v1.

func (AdminAdapter) Admin

Admin implements adminGen.Service. The generated handler validates and decodes username+email+password from the request body and populates req.XTenantID from the X-Tenant-ID header. A missing/malformed tenant fails closed inside Service.CreateAdmin (tenant.ParseTenantID on CreateAdminInput.TenantID) to a 400 ERR_AUTH_IDENTITY_INVALID_INPUT (ADR 1160).

type CreateAdminInput

type CreateAdminInput struct {
	TenantID string
	Username string
	Email    string
	Password string
}

CreateAdminInput holds the operator-supplied first-admin fields. TenantID is the tenant for which the admin is being provisioned; it must be a canonical UUID and is required (bootstrap must designate a specific tenant).

type CreateAdminOutput

type CreateAdminOutput struct {
	ID        string `json:"id"`
	Username  string `json:"username"`
	Email     string `json:"email"`
	CreatedAt string `json:"createdAt"`
}

CreateAdminOutput is the response shape for POST /api/v1/access/setup/admin.

type Handler

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

Handler exposes the setup endpoints over HTTP.

The status endpoint is always Public (no admin exists yet during first-run). The admin endpoint is Bootstrap (HTTP Basic Auth via env credentials); the bootstrapAuth middleware is mandatory and is threaded straight to the generated handler — see ADR §D1 (auth.bootstrap is a closed contract: no "declared bootstrap but no auth wired" intermediate state).

func NewHandler

func NewHandler(svc *Service, bootstrapAuth func(http.Handler) http.Handler) *Handler

NewHandler creates a setup Handler.

bootstrapAuth is REQUIRED — it is the per-route replacement authentication (typically runtime/auth.NewBootstrapMiddleware wired by the composition root). The generated admin handler panics at construction if bootstrapAuth is nil; this constructor enforces the same invariant up front so the failure mode is "Cell.Init returns a clear error" rather than "process panic deep in the generated layer".

func (*Handler) RegisterRoutes

func (h *Handler) RegisterRoutes(mux kcell.RouteHandler) error

RegisterRoutes mounts the setup contract handlers on mux. The X-Tenant-ID header is consumed by the generated handlers (Request.XTenantID), so no tenant-injection middleware is needed.

type Option

type Option func(*Service)

Option configures a Service.

func WithEmitter

func WithEmitter(e outbox.CellEmitter) Option

WithEmitter sets the event emitter. Accepts a sealed outbox.CellEmitter; typed-nil inputs are silently ignored (builder-option semantics).

func WithPasswordHasher

func WithPasswordHasher(h credential.Hasher) Option

WithPasswordHasher overrides the password hasher (default credential.NewProductionHasher(), cost 12). Unit tests wire credential.NewTestHasher(bcrypt.MinCost) for speed. BCRYPT-COST-FUNNEL-01 guards that production never reaches the low-cost door. Bare/typed-nil inputs are silently ignored (builder-option semantics) so the production default survives.

func WithSetupLock

func WithSetupLock(lock ports.SetupLockAcquirer) Option

WithSetupLock injects the REQUIRED serialization primitive for the admin-provisioning path. CreateAdmin acquires the lock at the start of the RunInTx body before calling adminprovision.Ensure — the lock, user write, and outbox emit share a single transaction scope.

NewService rejects a missing or nil setupLock with ErrCellInvalidConfig so that mis-wired assemblies fail at startup rather than at the first CreateAdmin call. The cell-level WithSetupLock injects this option from cells/accesscore composition; see accesscore.WithSetupLock godoc for the PG vs memstore wiring choice.

Bare-nil inputs are silently ignored (builder-option semantics); final nil validation (including typed-nil) is handled by validateRequired().

func WithTxManager

func WithTxManager(tx persistence.CellTxManager) Option

WithTxManager sets the CellTxManager for L2 atomicity (user write + event emit). Callers obtain the sealed marker via persistence.WrapForCell from a composition root.

type Service

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

Service implements the setup slice's business logic.

func NewService

func NewService(clk clock.Clock, provisioner *adminprovision.Provisioner, logger *slog.Logger, opts ...Option) (*Service, error)

NewService constructs a Service. provisioner is required; passing nil returns an error so mis-wired assemblies fail at startup.

func (*Service) CreateAdmin

func (s *Service) CreateAdmin(ctx context.Context, in CreateAdminInput) (*CreateAdminOutput, error)

CreateAdmin provisions the first admin user with an operator-chosen password.

Returns errcode.ErrSetupAlreadyInitialized when an admin already exists (either at the fast-path Status check or after a race detected inside adminprovision.Ensure).

Consistency: L2 (OutboxFact) in durable mode. The user write + event emit share a single TxRunner scope so event publication is atomic with row persistence — if the emit fails, the tx rolls back and the user row is removed.

Demo-mode caveat: When wired with persistence.NoopTxRunner (in-memory repositories), RunInTx has no rollback, so a publishUserCreated failure after a successful adminprovision.Ensure leaves the user + role persisted without the event emitted. The next POST hits the fast-path 410 via CountByRole. Production must wire a real TxRunner; demo mode accepts this gap as it matches the identitymanage.Create pattern (service.go:128-139).

Security: bcrypt runs AFTER the Status fast-path so a flood of POSTs after admin exists returns 410 in ~milliseconds without CPU burn. The hash cost (credential.ProductionCost in production) is only paid on the single winning request (plus same-process concurrent race-losers serialized by memTxRunner.RunInTx holding store.mu in memstore mode, or by pg_advisory_xact_lock in PG mode).

func (*Service) RecordBootstrapAuthFail

func (s *Service) RecordBootstrapAuthFail(ctx context.Context, reason string, clientIPHash redaction.IPHash) error

RecordBootstrapAuthFail emits event.auth.bootstrap-failed.v1 inside a transaction so the outbox row is persisted atomically with the emitter database write. Called from the accesscore bootstrap observer closure after a 401/429 response is written; errors are logged by the caller and not surfaced to the HTTP response (best-effort audit for non-durable emitters, persistent outbox row for durable mode).

reason must be one of "missing_header", "wrong_credentials", "rate_limited". clientIPHash is the sealed, keyed hash of the client IP (redaction.HashIP), computed by the composition root — the plaintext IP never reaches the cell or the replayable payload (#1488). It is the zero IPHash when no IP was available.

func (*Service) Status

func (s *Service) Status(ctx context.Context, t tenant.TenantID) (StatusOutput, error)

Status returns whether the given tenant already has at least one admin.

The admin-existence check reads role_assignments / users, which are under FORCE ROW LEVEL SECURITY (migration 053). setup is a PRE-AUTH endpoint (no JWT → no ctxkeys.TenantID fallback), so the read is scoped explicitly through scopedtx.Do (#1617 PR-3b review F1): under the restricted app-serving pool (#1676) a bare-pool read would be fail-closed to 0 rows by the unset app.tenant_id GUC, falsely reporting hasAdmin:false.

type StatusAdapter

type StatusAdapter struct{ S *Service }

StatusAdapter implements statusGen.Service for http.auth.setup.status.v1. The status endpoint is always Public (no JWT required): no admin exists yet during first-run bootstrap.

func (StatusAdapter) Status

Status implements statusGen.Service. The generated handler populates req.XTenantID from the X-Tenant-ID header declared in contract.yaml endpoints.http.headers; this adapter parses it to scope the admin existence check to the tenant.

type StatusOutput

type StatusOutput struct {
	HasAdmin bool `json:"hasAdmin"`
}

StatusOutput is the response shape for GET /api/v1/access/setup/status.

Jump to

Keyboard shortcuts

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