Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Admin UI shell

Audience: operators who want to browse and edit the access-control model from a browser instead of the CLI or raw RPC.

Aperture ships a small embedded admin UI — a static, single-page frontend served from the root of a running aperture serve. It is not a separate service: the HTML, CSS, and JavaScript are compiled into the binary and mounted at /, so starting the server is all it takes to reach the UI. Every screen is a thin client over the same Twirp / HTTP surface; the shell holds no access-control logic of its own — it decodes what the server returns and posts mutations back through the facade.

What it is built from

The shell is deliberately dependency-light and builds with no Node pipeline. Its assets are pre-built, committed blobs embedded via //go:embed all:static (see internal/server/static.go), so the binary stays self-contained in dev and CI.

LayerRole in the shell
Alpine.jsSmall reactive framework driving each screen’s component and the hash-routed navigation.
BERA design tokensThe house design-token system. The shell’s authoritative styling is the hand-authored static/css/bera.css, written from the token spec; it is not Tailwind/DaisyUI.
Tailwind CSS / DaisyUIVendored utility/component CSS carried for the domain screens.
Rete.jsA node-graph editing library. The Rules screen mounts a Rete canvas to edit rule blueprints; the bundle is loaded at runtime from /vendor/rete/rete.min.js.

The full frontend lives under internal/server/static/ (index.html, css/, js/, and vendor/). Vendored blobs (Alpine, Tailwind, DaisyUI) are pinned and documented in internal/server/static/vendor/README.md. The Rete.js bundle is the one asset regenerated by a manual make vendor-rete target — it is never rebuilt in CI. Treat all of these as committed artifacts, not build outputs.

How to reach it

Start the server and open the listen address in a browser:

aperture serve            # listens on :8080 by default
  • Default address: :8080 (override with --addr). See the serve command for all flags.
  • The UI is mounted last on the mux at /. More specific routes — the Twirp path prefix, POST /check, GET /healthz — take precedence, so the frontend never shadows the API.

Auth expectation

Aperture’s authentication is external — the shell never issues credentials. Every request the UI makes is routed through a single apiFetch wrapper that attaches an Authorization: Bearer <token> header, and a 401 clears the token and re-opens the sign-in affordance.

With the default dev authenticator (aperture serve with no --auth override), the bearer token is the principal id: “signing in” through the UI just names which principal your session presents. Switching to the oidc or parsec adapters changes how that bearer is obtained, but the shell’s behaviour is unchanged — it still only carries whatever token it holds. A global account switcher in the top bar picks the active account shared by every screen; the server scopes the account list to what the signed-in principal may see (ListAccounts).

Screen tour

The left-hand nav routes by URL hash (#/crud, #/grants, …). Each screen is one Alpine component in static/js/, and each drives a small set of Twirp RPCs on aperture.ApertureService.

Model (CRUD) — #/crud

The model editor. Browse and mutate the core entities — principals, roles, groups, permissions, object types, and accounts — plus account memberships. It lists each entity type (ListPrincipals, ListRoles, ListGroups, ListPermissions, ListObjectTypes, ListAccounts), creates/updates via the matching Put*, and removes via Delete*; memberships use PutMembership / DeleteMembership. It probes the viewer’s admin tier with the open Check RPC and can Export the model.

Grants — #/grants

Bestowed grants, delegation, and grant templates. Lists and edits grants (ListGrants, PutGrant, DeleteGrant), issues and rescinds authority (Bestow, Revoke), and manages templates (ListTemplates, PutTemplate, DeleteTemplate, ApplyTemplate) with a bulk path (BulkPutGrants). It uses Check to resolve the viewer’s tier.

Graph — #/graph

A force-directed map of the whole model rendered on a <canvas>. It pulls the entire declarative model with a single Export RPC (seeding an account via ListAccounts first) and derives every node and edge — subjects, grants, objects, memberships, roles, and reachability — from that one document, so the picture always reflects the live store. The layout is a small self-contained simulation; no external graph library is used here.

Rules — #/rules

The rule-blueprint editor. This is where Rete.js is used: rules are edited as a node graph on a Rete canvas, then serialized to Aperture’s rule AST. It loads and saves rules (ListRules, GetRule, PutRule), lists object types (ListObjectTypes) and object identifiers (ObjectIdentifiers), and validates or dry-runs an expression against the live model (ValidateRule, EvaluateRule, Check).

What-if — #/whatif

A read-only decision simulator. Ask “would principal P be allowed action A on object O?” and see both the verdict and the full explain trace — the expanded subject set, every grant considered with its per-grant outcome, and the deciding grant(s). It drives the open Check and Explain RPCs (no mutation), listing accounts via ListAccounts. It runs a hypothetical against the live model without writing anything.

Audit — #/audit

A queryable table over the append-only audit trail via the QueryAudit RPC. Every mutation, impersonation, and delegation is always recorded; decisions are sampled. Read access is tier-gated — a system-admin reads everything, an account-admin reads only events scoped to their own account — and the screen probes both tiers with Check (using ListAccounts for scope). A viewer with neither tier is told why the table is empty.

Import / export (portability) — #/portability

Download the declarative model file or import one with a preview diff. It exports the whole model (Export), and imports a supplied document (Import) after resolving the viewer’s tier with Check and scoping with ListAccounts.