From c2ea8348df75428acc70931ece83a7c6474e8a60 Mon Sep 17 00:00:00 2001 From: Ned Wolpert Date: Fri, 19 Jun 2026 11:02:32 -0700 Subject: [PATCH] mini-console: design + Slice 0 runnable skeleton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Graduate mini-console from roadmap placeholder to a runnable, admin-only skeleton, and add the design docs that scope the work. Design docs (docs/design/): - mini-console.md — full design: operator console + exercise harness, five new HTTP client libs + reuse of the existing mini-kms client, the security model, and a slice-by-slice roadmap. - mini-console-slice0.md — the Slice 0 implementation plan. DIRECTION.md records mini-kms's client as the deliberate socket-era exception (relocation considered and declined). Slice 0 (services/mini-console/): - Loopback HTTP server (virtual-thread-per-request), paste-the-console-token login minting a mini-token SessionService session under a distinct mini-console-session cookie, and an honest Dashboard ("n/a — client not wired yet" for each service; calls nothing downstream). - Reuses the family http/ router kit + JsonStore + AdminAuthenticator + Cookies; no-oracle login, double-submit CSRF, loopback bind, no secrets in logs, session store written 0600. - Fires the trip-wire: deletes MiniConsole.IMPLEMENTED (guard-only) and MiniConsoleTest, replaced by real ConsoleServer/ConsoleConfig tests. - build.gradle.kts -> application-conventions (deps: mini-token + jackson only). settings.gradle.kts stale "roadmap placeholder" comment fixed. mini-client-common is intentionally deferred to Slice 1 (driven by its first real consumer); /api + OpenAPI are Slice 8. No new authority. ./gradlew build green family-wide. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/DIRECTION.md | 14 + docs/design/mini-console-slice0.md | 258 ++++++++++++ docs/design/mini-console.md | 371 ++++++++++++++++++ services/mini-console/README.md | 150 +++---- services/mini-console/build.gradle.kts | 21 +- .../miniconsole/MiniConsole.java | 17 - .../miniconsole/ServerMain.java | 87 ++++ .../miniconsole/pages/DashboardPage.java | 67 ++++ .../miniconsole/pages/Layout.java | 65 +++ .../miniconsole/pages/LoginPage.java | 38 ++ .../server/AdminAuthenticator.java | 76 ++++ .../miniconsole/server/ConsoleConfig.java | 145 +++++++ .../miniconsole/server/ConsoleHandlers.java | 131 +++++++ .../miniconsole/server/ConsoleServer.java | 73 ++++ .../miniconsole/server/ConsoleSession.java | 58 +++ .../miniconsole/server/Cookies.java | 59 +++ .../miniconsole/server/Csrf.java | 48 +++ .../miniconsole/server/http/ApiException.java | 56 +++ .../miniconsole/server/http/HttpResponse.java | 67 ++++ .../miniconsole/server/http/Json.java | 51 +++ .../server/http/RequestContext.java | 118 ++++++ .../miniconsole/server/http/Router.java | 143 +++++++ .../miniconsole/store/JsonStore.java | 100 +++++ .../miniconsole/MiniConsoleTest.java | 14 - .../miniconsole/server/ConsoleConfigTest.java | 68 ++++ .../miniconsole/server/ConsoleServerTest.java | 180 +++++++++ settings.gradle.kts | 6 +- 27 files changed, 2337 insertions(+), 144 deletions(-) create mode 100644 docs/design/mini-console-slice0.md create mode 100644 docs/design/mini-console.md delete mode 100644 services/mini-console/src/main/java/com/codeheadsystems/miniconsole/MiniConsole.java create mode 100644 services/mini-console/src/main/java/com/codeheadsystems/miniconsole/ServerMain.java create mode 100644 services/mini-console/src/main/java/com/codeheadsystems/miniconsole/pages/DashboardPage.java create mode 100644 services/mini-console/src/main/java/com/codeheadsystems/miniconsole/pages/Layout.java create mode 100644 services/mini-console/src/main/java/com/codeheadsystems/miniconsole/pages/LoginPage.java create mode 100644 services/mini-console/src/main/java/com/codeheadsystems/miniconsole/server/AdminAuthenticator.java create mode 100644 services/mini-console/src/main/java/com/codeheadsystems/miniconsole/server/ConsoleConfig.java create mode 100644 services/mini-console/src/main/java/com/codeheadsystems/miniconsole/server/ConsoleHandlers.java create mode 100644 services/mini-console/src/main/java/com/codeheadsystems/miniconsole/server/ConsoleServer.java create mode 100644 services/mini-console/src/main/java/com/codeheadsystems/miniconsole/server/ConsoleSession.java create mode 100644 services/mini-console/src/main/java/com/codeheadsystems/miniconsole/server/Cookies.java create mode 100644 services/mini-console/src/main/java/com/codeheadsystems/miniconsole/server/Csrf.java create mode 100644 services/mini-console/src/main/java/com/codeheadsystems/miniconsole/server/http/ApiException.java create mode 100644 services/mini-console/src/main/java/com/codeheadsystems/miniconsole/server/http/HttpResponse.java create mode 100644 services/mini-console/src/main/java/com/codeheadsystems/miniconsole/server/http/Json.java create mode 100644 services/mini-console/src/main/java/com/codeheadsystems/miniconsole/server/http/RequestContext.java create mode 100644 services/mini-console/src/main/java/com/codeheadsystems/miniconsole/server/http/Router.java create mode 100644 services/mini-console/src/main/java/com/codeheadsystems/miniconsole/store/JsonStore.java delete mode 100644 services/mini-console/src/test/java/com/codeheadsystems/miniconsole/MiniConsoleTest.java create mode 100644 services/mini-console/src/test/java/com/codeheadsystems/miniconsole/server/ConsoleConfigTest.java create mode 100644 services/mini-console/src/test/java/com/codeheadsystems/miniconsole/server/ConsoleServerTest.java diff --git a/docs/DIRECTION.md b/docs/DIRECTION.md index 3322de7..66269e2 100644 --- a/docs/DIRECTION.md +++ b/docs/DIRECTION.md @@ -213,6 +213,20 @@ the reverse), so the dependency graph stays acyclic. mini-idp and mini-oidc enab `MINI{IDP,OIDC}_KMS_API_TOKEN` / `--kms-api-token-file` — and fall back to the plaintext store when it is absent. +> **mini-kms's client is the family's deliberate "client" exception — and it stays under +> `services/`.** It is the original **socket**-era client (`KmsClient` + its single `KmsClientException` +> + the `KmsSigningKeyStore` adapter, alongside the two CLIs `client`/`kms-admin`), written before the +> family settled on the later **HTTP** client-lib pattern. Its embeddable surface is exactly +> `{KmsClient, KmsClientException, KmsSigningKeyStore}`, consumed today by mini-idp, mini-oidc, and +> mini-ca. Relocating it into a `libs/mini-kms-client` was considered and is mechanically clean +> (behavior-preserving, graph stays acyclic), but it was **declined**: the benefit is cosmetic parity +> with a not-yet-built `libs/-client` convention, against real churn in three shipping services. +> Should a future service (e.g. **mini-console**) actually establish a `libs/-client` convention, +> mini-kms's client is the **first candidate** to follow it — at which point the `KmsClient` library +> would move to `libs/mini-kms-client` (with the CLIs staying behind in `:services:mini-kms:client`) +> and consumers would repoint. Until then, consuming `:services:mini-kms:client` directly is correct, +> not an oversight. + ### Bootstrap ordering (and why it is not actually circular) There is an apparent chicken-and-egg: **mini-kms** needs its passphrase; the **auth services** need diff --git a/docs/design/mini-console-slice0.md b/docs/design/mini-console-slice0.md new file mode 100644 index 0000000..fd0a135 --- /dev/null +++ b/docs/design/mini-console-slice0.md @@ -0,0 +1,258 @@ +# Slice 0 — mini-console skeleton: implementation plan + +Slice 0 is **pure server-rendered HTML** — no `/api`, no OpenAPI, no SwaggerUI, no client libs, no +mini-policy (those are Slice 1/8). It compiles, runs, serves a login + Dashboard behind a console +session, and ships the trip-wire. + +**Confirmed decisions (maintainer):** default port **8500**; login error re-renders `/login` at +**200** (no oracle); short "runnable skeleton" README note now + full rewrite at Slice 8; CSRF via +**double-submit cookie**. `libs/mini-client-common` is **deferred to Slice 1**. + +## Headline decision + +**Defer `libs/mini-client-common` to Slice 1. Do NOT create it in Slice 0.** Slice 0 has **zero** +real downstream calls (no client lib exists; the Dashboard renders "n/a — client not wired yet"). +Landing `mini-client-common` now means shipping an **unconsumed abstraction** whose API (token +resolver shape, HttpClient builder signature, error-collapse type) would be guessed rather than +driven by its first real consumer (the directory client in Slice 1) — the "scaffold that looks +finished" the family ethos forbids. Slice 0 needs only env/file **console-token** resolution, a +~12-line `resolveToken` copied from mini-oidc's `ServerMain`, local to the console module. When +Slice 1 writes `MiniDirectoryClient` and needs the same plumbing for the *downstream* token + +HttpClient, that is when `mini-client-common` is extracted, against a real caller. + +## Assumptions + +1. Console login uses a **paste-the-console-token-into-a-form** model (not a Bearer header) — the + operator is a human in a browser; a form+session is the right UX and lets the rest of the console + rely on a session cookie rather than re-presenting the token per request. +2. The console session reuses mini-token's `SessionService` over a `JsonStore` + `DocumentStore`, with a **distinct cookie name** `mini-console-session` (mini-token's + `DEFAULT_COOKIE_NAME` is `"mioidc_session"`, `SessionService.java:31` — the console must NOT share + it, or it collides with a co-hosted mini-oidc SSO session). +3. CSRF: one state-changing POST pair matters in Slice 0 (`/login`, `/logout`). A minimal `Csrf` + helper (double-submit cookie) is included now since `/login` genuinely needs it. +4. Console token env var `MINICONSOLE_ADMIN_TOKEN` + `--admin-token-file` (mirrors the family's + `MINI*_ADMIN_TOKEN` + `--admin-token-file`). +5. Default port **8500** (one above mini-ca's 8499). + +--- + +## 1. Module graduation — `services/mini-console/build.gradle.kts` + +```kotlin +/* + * mini-console - the optional unified admin console over the mini- family. + * + * Slice 0: the runnable skeleton — a loopback HTTP server, a console-login session, and a Dashboard + * that honestly reports "client not wired yet" for each downstream service (no client libs exist + * yet). It invents NO new authority; later slices add the per-service client libraries + pages. + * + * Graduates from library-conventions to application-conventions (it is now runnable). + */ + +plugins { + id("miniauth.application-conventions") +} + +dependencies { + // The shared browser-session mechanism (SessionService) + the DocumentStore SPI the copied + // JsonStore implements. This is the ONLY family dependency Slice 0 genuinely needs. + implementation(project(":libs:mini-token")) + // JSON for the session store document (Sessions) and any future page DTOs. + implementation(libs.jackson.databind) + // JUnit 5 (jupiter + launcher) is supplied by the convention plugin. +} + +application { + mainClass = "com.codeheadsystems.miniconsole.ServerMain" +} +``` + +**Not present in Slice 0:** no `libs.jackson.yaml` (no OpenAPI until Slice 8), no `:libs:mini-policy`, +no client libs, no bouncycastle. Only `mini-token` + `jackson.databind`. + +--- + +## 2. File-by-file creation list — `src/main/java/com/codeheadsystems/miniconsole/` + +Base package `com.codeheadsystems.miniconsole`. "Copy" = lift verbatim from the cited source, rename +package to `…miniconsole.*`, adjust imports. "Fresh" = new code. + +### `ServerMain.java` — fresh (model: mini-oidc `ServerMain.java:41-113`) +- `main(String[] args)` → `run(args, System.getenv())`, catch + exit-1 on config error. +- `run`: `ConsoleConfig.resolve(args, env)`; `resolveToken(env.get("MINICONSOLE_ADMIN_TOKEN"), + config.adminTokenFilePath(), …)`; `ConsoleServer.create(config, consoleToken, Clock.systemUTC())`; + `start()`; await shutdown latch. +- `resolveToken(String fromEnv, Path file, String missing)` — copy verbatim from + `ServerMain.java:101-113` (env → file → throw; trim/strip; never argv, never logged). + +### `server/ConsoleConfig.java` — fresh, trimmed (model: mini-oidc `ServerConfig.java:42-245`) +- Fields: `host`, `port`, `dataDir`, `adminTokenFilePath`, `secureCookies`, `sessionTtl`. Drop all + OIDC/KMS/argon knobs. +- `resolve(String[] args, Map env)`: flags `--host/--port/--data-dir/--admin-token-file/--secure-cookies/--session-ttl-seconds`; + envs `MINICONSOLE_HOST/PORT/DATA_DIR/ADMIN_TOKEN_FILE/SECURE_COOKIES/SESSION_TTL_SECONDS`. + `DEFAULT_HOST="127.0.0.1"`, `DEFAULT_PORT=8500`, session TTL 12h (`43_200`), `defaultDataDir` → + `$XDG_DATA_HOME/mini-console` or `~/.mini-console`. +- Copy helpers `requireValue`/`envInt`/`defaultDataDir`/port-range check from `ServerConfig.java`. + +### `server/http/` kit — copy verbatim (package → `…miniconsole.server.http`) +| File | Source | +| --- | --- | +| `Router.java` | mini-oidc `server/http/Router.java` | +| `RequestContext.java` | `server/http/RequestContext.java` | +| `HttpResponse.java` | `server/http/HttpResponse.java` | +| `ApiException.java` | `server/http/ApiException.java` | +| `Json.java` | `server/http/Json.java` (required: `HttpResponse.json` references `Json.toBytes`) | + +No `StaticResource.java` (Slice 0 serves no static assets). + +### `server/AdminAuthenticator.java` — copy verbatim (mini-oidc `AdminAuthenticator.java:19-67`) +- Constant-time `MessageDigest.isEqual`. Add a `boolean matches(String presented)` for the + form-login path alongside the existing `requireAdmin(authorizationHeader)`. Preserve the + touch-the-buffer-on-null trick. + +### `server/ConsoleSession.java` — fresh, thin wrapper over `SessionService` +- Construct as in `OidcServer.java:103-104`: `new SessionService(new JsonStore<>(dataDir.resolve("console-sessions.json"), Sessions.class), clock, sessionTtl)`. +- SessionService API: `String create(subject, authTime)`, `Optional lookup(id)`, + `void destroy(id)`. +- Methods: `establish()` → `create("console-admin", now)`; `isValid(id)` → `lookup(id).isPresent()`; + `end(id)` → `destroy(id)`. + +### `server/Cookies.java` — copy + adapt (mini-oidc `Cookies.java:17-47`) +- **Critical change:** replace `SESSION = SessionService.DEFAULT_COOKIE_NAME` with + `SESSION = "mini-console-session"`. Keep `session(value, maxAge)` + `clearSession()`. + +### `server/Csrf.java` — fresh, minimal (double-submit) +- `GET /login` mints a random base64url token, set in a short-lived HttpOnly `mini-console-csrf` + cookie and embedded in the form; `POST /login` requires `cookie == form field` (constant-time). +- Methods: `String mint()`, `boolean verify(String expected, String presented)`. + +### `store/JsonStore.java` — copy verbatim (mini-oidc `store/JsonStore.java:34-105`) +- Atomic temp-file → `ATOMIC_MOVE` → `0600` `DocumentStore`. Backs `console-sessions.json`. + +### `pages/Layout.java` — fresh +- `page(String title, String bodyHtml)` wrapping `…