Phase 0: foundations (read-only client, Authentik 2026.8.3 contract tests) #1

Merged
KofTwentyTwo merged 2 commits from feature/phase-0-foundations into develop 2026-10-12 11:03:22 +00:00
KofTwentyTwo commented 2026-10-12 00:55:33 +00:00 (Migrated from github.com)

Phase 0, agent items 2-4 of DESIGN.md section 11.0 (spec QRun-IO/qqq#1065, now revision 3).

Contents

Client and metadata (read-only release line)

  • AuthentikQBitConfig:
    • validates its settings
    • resolves ${env.X} secrets itself (APIBackendMetaData.enrich() is off in QQQ)
    • keeps the token and webhook secret @JsonIgnore and redacted in toString()
    • refuses features that aren't built yet
  • AuthentikQBitProducer produces:
    • an API backend (CUSTOM auth, so no token in metadata)
    • read-only authentikUser / authentikGroup tables
    • the "Identity" app, which hosts can re-parent
  • AuthentikActionUtil (a BaseAPIActionUtil subclass) is the single path to Authentik:
    • page/page_size paging, with count from pagination.count
    • key lookups by GET, since users have no pk list filter
    • user scope: server-side params, re-checked per record
    • a shared token-bucket throttle (10 rps, burst 20)
    • 429 (with Retry-After) and GET-5xx retries
    • redacted error mapping
    • OutboundAPILog rows without PII
  • AuthentikFilterMapper: AND-only and exact. Anything Authentik cannot evaluate (OR, sub-filters, NOT_*, unmapped operators, a second search term) is rejected, never filtered client-side.
  • AuthentikGuard:
    • identity.* read checks; sensitive fields need identity.audit.read
    • every write refused in 0.1
    • protected-account detection: the service account, superusers, qqq.io/protected, configured names

Tests

  • 48 unit tests on recorded 2026.8.3 fixtures, captured and scrubbed by the contract tests.
  • Pinned schema src/test/resources/authentik/schema-2026.8.3.yml, fetched unauthenticated from auth.kof22.com/api/v3/schema/:
    • SchemaDriftTest checks every operation and parameter used. It already caught a spec error: the user detail GET takes no include_* params.
    • SchemaDriftIT diffs the running container's schema against the pin.
    • .github/workflows/schema-drift.yml runs the drift test weekly against the newest release.
  • 27 contract tests (Testcontainers: Authentik 2026.8.3 server and worker, Postgres, Mailpit) booting the kof22-shaped blueprints:
IT What it shows
BootstrapBlueprintIT (h): bootstrap on an empty instance via metaapplyblueprint; SA, role (global + object permissions), break-glass, seed groups; open point (i)
SuperuserFloorIT (b): every escalation route refused (ii). Pins that the lockout direction and self-minted tokens are not refused
BlueprintReapplyIT (c): API edits survive re-applying both blueprints; a removed role permission is re-asserted. (d): present entries wipe members, memberships and attributes
AccessCheckIT (iii): check_access?for_user ignored for non-superusers; policy test 500; app list filtered
SignInPolicyIT Attribute-driven sign-in policy; inert webhook plumbing
QbitReadContractIT The qbit's own read path end to end; records the fixtures
SchemaDriftIT Running schema = pinned schema for the surface used

Docs

  • docs/PHASE0-OWNER-CHECKLIST.md: items 6, 7, 8, 10, plus the decisions surfaced
  • docs/cluster-gitops-phase0/: the lint and a local rehearsal script
  • docs/STATUS.md

Results

CI=true ./mvnw verify locally: unit 48/48, contract 27/27, JaCoCo gate met. The contract suite takes about 10 minutes, mostly Authentik's first boot and asynchronous blueprint applies.

Related drafts (do not merge yet):

🤖 Generated with Claude Code

Phase 0, agent items 2-4 of [DESIGN.md](../blob/develop/docs/DESIGN.md) section 11.0 (spec QRun-IO/qqq#1065, now revision 3). ## Contents **Client and metadata** (read-only release line) - `AuthentikQBitConfig`: - validates its settings - resolves `${env.X}` secrets itself (`APIBackendMetaData.enrich()` is off in QQQ) - keeps the token and webhook secret `@JsonIgnore` and redacted in `toString()` - refuses features that aren't built yet - `AuthentikQBitProducer` produces: - an API backend (CUSTOM auth, so no token in metadata) - read-only `authentikUser` / `authentikGroup` tables - the "Identity" app, which hosts can re-parent - `AuthentikActionUtil` (a `BaseAPIActionUtil` subclass) is the single path to Authentik: - page/page_size paging, with count from `pagination.count` - key lookups by GET, since users have no pk list filter - user scope: server-side params, re-checked per record - a shared token-bucket throttle (10 rps, burst 20) - 429 (with Retry-After) and GET-5xx retries - redacted error mapping - `OutboundAPILog` rows without PII - `AuthentikFilterMapper`: AND-only and exact. Anything Authentik cannot evaluate (OR, sub-filters, NOT_*, unmapped operators, a second search term) is **rejected**, never filtered client-side. - `AuthentikGuard`: - identity.* read checks; sensitive fields need `identity.audit.read` - every write refused in 0.1 - protected-account detection: the service account, superusers, `qqq.io/protected`, configured names **Tests** - 48 unit tests on **recorded 2026.8.3 fixtures**, captured and scrubbed by the contract tests. - Pinned schema `src/test/resources/authentik/schema-2026.8.3.yml`, fetched unauthenticated from `auth.kof22.com/api/v3/schema/`: - `SchemaDriftTest` checks every operation and parameter used. It already caught a spec error: the user detail GET takes no `include_*` params. - `SchemaDriftIT` diffs the running container's schema against the pin. - `.github/workflows/schema-drift.yml` runs the drift test weekly against the newest release. - 27 **contract tests** (Testcontainers: Authentik 2026.8.3 server and worker, Postgres, Mailpit) booting the kof22-shaped blueprints: | IT | What it shows | |---|---| | `BootstrapBlueprintIT` | (h): bootstrap on an empty instance via `metaapplyblueprint`; SA, role (global + object permissions), break-glass, seed groups; open point (i) | | `SuperuserFloorIT` | (b): every escalation route refused (ii). Pins that the **lockout** direction and **self-minted tokens** are *not* refused | | `BlueprintReapplyIT` | (c): API edits survive re-applying both blueprints; a removed role permission is re-asserted. (d): `present` entries wipe members, memberships and attributes | | `AccessCheckIT` | (iii): `check_access?for_user` ignored for non-superusers; policy test 500; app list filtered | | `SignInPolicyIT` | Attribute-driven sign-in policy; inert webhook plumbing | | `QbitReadContractIT` | The qbit's own read path end to end; records the fixtures | | `SchemaDriftIT` | Running schema = pinned schema for the surface used | **Docs** - `docs/PHASE0-OWNER-CHECKLIST.md`: items 6, 7, 8, 10, plus the decisions surfaced - `docs/cluster-gitops-phase0/`: the lint and a local rehearsal script - `docs/STATUS.md` ## Results `CI=true ./mvnw verify` locally: unit 48/48, contract 27/27, JaCoCo gate met. The contract suite takes about 10 minutes, mostly Authentik's first boot and asynchronous blueprint applies. Related drafts (do not merge yet): - KofTwentyTwo/cluster-gitops#72 (blueprints) - KofTwentyTwo/cluster-gitops#73 (backend env) - the Website-Backend wiring 🤖 Generated with [Claude Code](https://claude.com/claude-code)
KofTwentyTwo commented 2026-10-12 10:28:14 +00:00 (Migrated from github.com)

Audit note (2026-10-12, AI-assisted): this PR commits a compiled Python cache file, docs/cluster-gitops-phase0/scripts/__pycache__/lint-authentik-blueprints.cpython-314.pyc. Suggest removing it and adding __pycache__/ to .gitignore. Also: develop's README links docs/PHASE0-OWNER-CHECKLIST.md and docs/STATUS.md, which only exist once this PR merges.

Audit note (2026-10-12, AI-assisted): this PR commits a compiled Python cache file, `docs/cluster-gitops-phase0/scripts/__pycache__/lint-authentik-blueprints.cpython-314.pyc`. Suggest removing it and adding `__pycache__/` to `.gitignore`. Also: develop's README links `docs/PHASE0-OWNER-CHECKLIST.md` and `docs/STATUS.md`, which only exist once this PR merges.
KofTwentyTwo commented 2026-10-12 11:03:19 +00:00 (Migrated from github.com)

Addressed: 447d4b1 removes the committed __pycache__/*.pyc and ignores Python bytecode; the README links to docs/PHASE0-OWNER-CHECKLIST.md and docs/STATUS.md resolve once this merges. gitleaks over full history: no leaks. Squash-merging so the bytecode never reaches develop history.

🤖 Generated with Claude Code

Addressed: 447d4b1 removes the committed `__pycache__/*.pyc` and ignores Python bytecode; the README links to `docs/PHASE0-OWNER-CHECKLIST.md` and `docs/STATUS.md` resolve once this merges. gitleaks over full history: no leaks. Squash-merging so the bytecode never reaches `develop` history. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.