- T01: Go module (keycape), full directory skeleton, Makefile, CI workflow - T02: spec/canonical-model.yaml with 6 entities + Go domain types - T03: spec/ldap-schema.yaml + validator binary with structural/semantic rules - T04: Error taxonomy — 4 stable error types, JSON format, HTTP helpers 28 tests pass, go vet clean, go build clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
16 KiB
id, type, title, domain, repo, status, owner, topic_slug, workstream_id, topic_id, repo_id, created, spec_refs, decisions
| id | type | title | domain | repo | status | owner | topic_slug | workstream_id | topic_id | repo_id | created | spec_refs | decisions | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| KEY-WP-0001 | workplan | KeyCape Implementation — Lightweight IAM Profile | netkingdom | key-cape | active | Bernd | netkingdom | 2c9caad8-2ced-492d-9d63-376387b4b9b0 | a6c6e745-bf54-4465-9340-1534a2be493e | 8a99bb74-1ec0-4478-ac70-35a7cddb0e3c | 2026-03-13 |
|
|
KEY-WP-0001 — KeyCape Implementation
Implements the NetKingdom IAM Profile via KeyCape: a stateless, profile-constrained OIDC server orchestrating Authelia, LLDAP, and privacyIDEA. Replaceable by Keycloak without application changes.
Language Decision
Go — decided 2026-03-13 by Bernd. See docs/adr/ADR-0001-choose-go-for-keycape.md.
KeyCape is an orchestrating boundary service (HTTP, adapters, JWT via library, CLI tooling) — Go's strongest domain. Rust revisitable if a subcomponent needs stronger guarantees later. Mandatory guardrails: typed domain models, narrow adapter interfaces, layered architecture, fuzz tests on validator/redirect/claim-mapping, no cleverness.
Repo Structure (from spec §12)
src/
server/
oidc/ # Profile endpoints
telemetry/ # Structured event emission
errors/ # Error taxonomy + enforcement middleware
adapters/
authelia/ # Auth flow delegation
lldap/ # Identity directory reads
privacyidea/ # MFA enforcement
validator/ # Canonical LDAP schema validator binary
migration/
lldap-export/ # LLDAP → canonical
keycape-to-keycloak/ # Canonical → Keycloak realm import
lldap-to-ldap/ # LLDAP → OpenLDAP/389DS/AD LDIF
spec/
canonical-model.yaml # Source of truth for all identity data
ldap-schema.yaml # Canonical LDAP schema rules
tests/
profile/ # Scenario A — lightweight baseline
negative/ # Scenario D — unsupported feature rejection
migration/ # Scenarios B & C — replacement
Dependency Order
T01 (project setup)
└─ T02 (canonical model) T04 (error taxonomy)
└─ T03 (LDAP validator) └─ T13 (telemetry)
└─ T10 (LLDAP adapter) └─ T14 (enforcement layer)
└─ T11 (Authelia) │
└─ T12 (privacyIDEA) │
│ │
T05 ─ T06 ─ T07 ─ T08 ─┴─ T09 (OIDC server)
│
T18 (profile tests / Scenario A)
T21 (negative tests / Scenario D)
│
T15 → T16 → T19 (Scenario B)
T15 → T17 → T20 (Scenario C)
│
T22 (dev stack)
T23 (production packaging)
Phase 1 — Foundations
T01 — Project setup: Go module, repo layout, CI skeleton
id: KEY-WP-0001-T01
hub_task_id: 25613e3f-2a65-409e-afaa-d23ded0bc256
priority: high
status: todo
Initialise language module in src/. Create directory skeleton per spec §12. Add Makefile
targets: build, test, lint. Set up CI (build + test on every push). Scaffolding only —
no application code. Agent must call record_decision() with chosen language (Go or Rust).
T02 — Canonical identity model: machine-readable schema
id: KEY-WP-0001-T02
hub_task_id: deee2929-9386-41db-bf91-fbd9ad646c28
priority: high
status: todo
depends_on: [T01]
Write spec/canonical-model.yaml. Six entities: User, Group, Role, Client, Membership,
MFAEnrollment (fields per spec §2). Include JSON Schema or CUE schema for programmatic
validation. This file is the source of truth — all other code derives from it.
T03 — Canonical LDAP schema + validator
id: KEY-WP-0001-T03
hub_task_id: 02592c65-db23-474b-b06b-019e95df8146
priority: high
status: todo
depends_on: [T01, T02]
Write spec/ldap-schema.yaml: tree layout (ou=users, ou=groups, ou=clients under
dc=netkingdom,dc=local), object classes (inetOrgPerson, groupOfNames), required/optional
attributes. Implement validator/ binary. Structural rules: valid DN, required attrs, no unknown
attrs, valid group memberships. Semantic rules: referenced users exist, no cycles, usernames
unique, email format valid. Validator runs in --mode=ci|provisioning|migration. Emits
machine-readable report.
T04 — Error taxonomy: types, JSON format, middleware
id: KEY-WP-0001-T04
hub_task_id: 46870fd6-0672-432b-8824-6bc2e24811b3
priority: high
status: todo
depends_on: [T01]
Implement four error types (spec §5):
feature_not_supported_by_profileavailable_in_keycloak_mode_onlyrejected_for_profile_safetyinvalid_profile_usage
JSON format: {"error": "...", "description": "...", "feature": "..."}. HTTP middleware wraps
all handler errors. Error type strings are stable and test-assertable.
Phase 2 — OIDC Server
T05 — OIDC discovery endpoint (/.well-known/openid-configuration)
id: KEY-WP-0001-T05
hub_task_id: 92eb8916-cb22-4786-9f16-a8a07272f818
priority: high
status: todo
depends_on: [T04]
GET /.well-known/openid-configuration. Advertise only profile-supported features:
authorization_code, S256 PKCE, RS256, static scopes. Must NOT advertise: dynamic registration,
implicit flow. Issuer configurable. Cacheable response.
T06 — Authorization endpoint with PKCE and redirect URI validation
id: KEY-WP-0001-T06
hub_task_id: c3df620b-9864-4ff1-ba6d-26057c6f4d59
priority: high
status: todo
depends_on: [T04, T11, T12, T13, T14]
GET/POST /authorize. Validate: client_id (static config), redirect_uri (exact match —
wildcard → rejected_for_profile_safety), response_type=code, scope contains openid,
code_challenge present (missing → invalid_profile_usage), code_challenge_method=S256.
Delegate to Authelia adapter. Store PKCE state server-side. No implicit or hybrid flow.
T07 — Token endpoint: JWT/RS256, canonical claim mapping
id: KEY-WP-0001-T07
hub_task_id: d3248f4d-e0e9-4144-9844-c9768dc896d6
priority: high
status: todo
depends_on: [T06, T08, T10]
POST /token. Validate PKCE code_verifier. Issue RS256 JWT via standard library (no custom
crypto). Mandatory claims: iss, sub (canonical user ID), aud, exp, iat. Optional
claims: preferred_username (LDAP uid), email (LDAP mail), groups (groupOfNames),
roles. Short, explicitly configured token lifetime. Any other grant type →
feature_not_supported_by_profile.
T08 — JWKS endpoint (/jwks)
id: KEY-WP-0001-T08
hub_task_id: 58a1d705-b788-4d66-8c0a-33edff63a885
priority: high
status: todo
depends_on: [T01]
GET /jwks. RS256 public key in JWK Set format. Key loaded from config. Key rotation: serve
multiple keys during rotation window, keyed by kid. Standard library key generation only.
T09 — Userinfo endpoint (/userinfo)
id: KEY-WP-0001-T09
hub_task_id: 742d3924-21e9-4304-86e9-0400af0e81ee
priority: medium
status: todo
depends_on: [T07, T10]
GET /userinfo. Optional per spec — implement if any registered client requires it. Validate
Bearer token (RS256 + expiry). Return claim subset scoped to granted scopes. Claims must be
identical to ID token for same scopes. If no client needs it: stub returning
available_in_keycloak_mode_only.
Phase 3 — Backend Adapters
T10 — LLDAP adapter: user and group reads
id: KEY-WP-0001-T10
hub_task_id: 2043b10a-6822-45f8-abcc-4e233d918fb0
priority: high
status: todo
depends_on: [T02, T03]
adapters/lldap. LDAP protocol connection to LLDAP. Interface: LookupUser(username) → canonical User, LookupGroups(userDN) → []Group, ValidatePassword(username, password) → bool. Attribute
map: uid→username, cn→displayName, mail→email, memberOf→groups. Run canonical LDAP schema
validator on every read. No LDAP internals exposed to server/.
T11 — Authelia adapter: session and auth flow delegation
id: KEY-WP-0001-T11
hub_task_id: ad129a14-1552-4717-b1dd-b529d18ce681
priority: high
status: todo
depends_on: [T04, T13]
adapters/authelia. Initiate auth redirect to Authelia, receive callback, extract authenticated
user identity, hand off to MFA check (T12). Must not leak Authelia session tokens/cookies into
profile layer. Unavailable Authelia → fail closed (auth_failure event).
T12 — privacyIDEA adapter: MFA enforcement
id: KEY-WP-0001-T12
hub_task_id: 1ef196e6-2304-4cf6-b205-47ac1da879ec
priority: high
status: todo
depends_on: [T02, T13]
adapters/privacyidea. KeyCape must NOT implement MFA logic. Interface:
CheckMFARequired(user) → bool, ValidateMFAToken(user, token) → bool. MFA failure → no token
issued + auth_failure telemetry. MFA enrollment state from canonical MFAEnrollment entity.
privacyIDEA remains stable across lightweight → expanded migration.
Phase 4 — Telemetry & Enforcement
T13 — Telemetry pipeline: structured event emission
id: KEY-WP-0001-T13
hub_task_id: 704146bf-cd60-4922-b18b-3d209cff3ac3
priority: high
status: todo
depends_on: [T01]
server/telemetry. Event types (spec §6.1): auth_start, auth_success, auth_failure,
token_issued, unsupported_feature, invalid_request, migration_event. Required fields
(spec §6.2): timestamp, client_id, endpoint, feature, result, error_type, scopes,
grant_type, environment, trace_id. Pluggable outputs: structured log (default), Prometheus
metrics endpoint. Every auth and error path emits an event — no silent paths.
T14 — Unsupported feature enforcement layer
id: KEY-WP-0001-T14
hub_task_id: 71f44886-ab61-4160-a435-72b35af472a0
priority: high
status: todo
depends_on: [T04, T13]
server/errors enforcement middleware. Intercept any parameter, grant type, scope, or client
config exceeding the profile. Return correct error type + emit unsupported_feature telemetry.
Maintain a registry of unsupported features (adding new ones requires no handler changes):
dynamic_client_registration, identity_broker, wildcard_redirect_uri, implicit_flow, etc.
Every registry entry must have a corresponding test in T21.
Phase 5 — Migration Tooling
T15 — Migration: LLDAP → canonical export
id: KEY-WP-0001-T15
hub_task_id: 1bd13f76-2d62-429d-b230-d785ef6a3f2f
priority: medium
status: todo
depends_on: [T02, T03, T10]
migration/lldap-export tool. Read all users, groups, memberships, attributes from LLDAP. Map
to canonical model. Validate against LDAP schema validator before writing. Output:
canonical-export.yaml. Emit migration_event telemetry. Idempotent. Include incompatibility
report for unmappable LLDAP data.
T16 — Migration: canonical → Keycloak import
id: KEY-WP-0001-T16
hub_task_id: f3d50e80-f6b5-4c0c-a5f3-08308ea1a95e
priority: medium
status: todo
depends_on: [T15]
migration/keycape-to-keycloak tool. Read canonical export (T15). Transform to Keycloak realm
import format: users, groups, clients, roles, scope mappings. Preserve: same issuer, same claims,
same scopes, same client behavior. Output: keycloak-realm-import.json. Emit migration_event.
Include round-trip validation report.
T17 — Migration: LLDAP → full LDAP (OpenLDAP / 389DS / AD)
id: KEY-WP-0001-T17
hub_task_id: 044d99c8-39cb-4f35-9308-912ae829bd22
priority: medium
status: todo
depends_on: [T15]
migration/lldap-to-ldap tool. Export via T15 canonical export. Generate LDIF for target
(--target=openldap|389ds|ad). Migrate: users, groups, memberships, attributes. Run validator
on output LDIF before import. Produce validation report. Orthogonal to T16 — the two
migration dimensions are independent (spec §14.1).
Phase 6 — Replacement Tests
T18 — Profile test suite: Scenario A (lightweight baseline)
id: KEY-WP-0001-T18
hub_task_id: 76abc3f6-9c4e-4aca-9995-72b728925812
priority: high
status: todo
depends_on: [T05, T06, T07, T08, T09, T22]
tests/profile. Provision canonical fixtures into LLDAP + Authelia + KeyCape. Test categories
(spec §15.3): discovery, login flow (PKCE), token claim assertions (all mandatory + optional),
redirect validation, client config, MFA policy, logout (if implemented). Tests are
backend-agnostic — same suite runs in T19 and T20. Must pass for Scenario A conformance.
T19 — Replacement test suite: Scenario B (IAM swap, same directory)
id: KEY-WP-0001-T19
hub_task_id: 56d03e89-934b-4992-bfe4-b32f275882e3
priority: medium
status: todo
depends_on: [T18, T16]
Run T18 suite against Keycloak + LLDAP (configured from T16 canonical export). No test code changes allowed. Migration successful only if all T18 tests pass. Proves IAM replaceability without directory migration.
T20 — Replacement test suite: Scenario C (full expansion)
id: KEY-WP-0001-T20
hub_task_id: ec3cae5c-9942-4be7-acc0-1eb9f02aba45
priority: medium
status: todo
depends_on: [T19, T17]
Apply T17 LLDAP→OpenLDAP migration, then T16 Keycloak import. Run T18 suite. Migration successful only if all tests pass. privacyIDEA must remain stable (no MFA re-enrollment required).
T21 — Negative profile tests: Scenario D (unsupported feature rejection)
id: KEY-WP-0001-T21
hub_task_id: a5112b63-121f-4d17-ac1a-fb46d160413e
priority: high
status: todo
depends_on: [T14]
tests/negative. For every entry in T14 unsupported-feature registry: attempt usage, assert
correct error.error string, assert unsupported_feature telemetry event emitted. Covered
cases: dynamic_client_registration, implicit_flow, wildcard redirect URIs, identity brokering,
missing PKCE, unknown scopes, unknown grant types. Must run in CI. Proves enforcement layer
is complete.
Phase 7 — Operations
T22 — Dev mode stack: docker-compose with LLDAP + Authelia
id: KEY-WP-0001-T22
hub_task_id: e840963f-cd19-4b38-857a-7c40df165d3d
priority: medium
status: todo
depends_on: [T01]
docker-compose.dev.yml: KeyCape, LLDAP, Authelia, privacyIDEA (or stub). Pre-seeded with
canonical fixtures from T02. Makefile targets: make dev (start + verify basic auth flow),
make seed (re-apply fixtures without full restart). Deterministic: same seed → same state.
Test environment for T18 and T21.
T23 — Slim production packaging: binary + config + health
id: KEY-WP-0001-T23
hub_task_id: d5723683-f739-4362-b62e-71213dc5a89e
priority: low
status: todo
depends_on: [T18, T21]
Single stateless binary. Declarative YAML config: profile version, client definitions, backend
connections, LDAP schema rules, privacyIDEA settings, telemetry destination, token lifetime,
issuer URL. Static config validation on startup. /healthz endpoint. Minimal container image
(distroless or Alpine). Config environment-promotable via env var overrides only.
Acceptance Criteria (from spec §15.4 and §20)
A release is conformant when:
- Scenario A tests pass (T18)
- Scenario D tests pass (T21)
- Scenario B tests pass (T19) — IAM migration verified
- Scenario C tests pass (T20) — full expansion verified
- All error responses use taxonomy types from spec §5
- All auth/error paths emit structured telemetry (T13)
- Canonical LDAP schema validator passes on all fixtures (T03)
- No handwritten cryptography anywhere in the codebase
- Config is statically validated at startup (T23)