generated from coulomb/repo-seed
feat: implement T01-T04 — Go module, canonical model, LDAP validator, error taxonomy
- 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>
This commit is contained in:
475
workplans/KEY-WP-0001-keycape-implementation.md
Normal file
475
workplans/KEY-WP-0001-keycape-implementation.md
Normal file
@@ -0,0 +1,475 @@
|
||||
---
|
||||
id: KEY-WP-0001
|
||||
type: workplan
|
||||
title: "KeyCape Implementation — Lightweight IAM Profile"
|
||||
domain: netkingdom
|
||||
repo: key-cape
|
||||
status: active
|
||||
owner: Bernd
|
||||
topic_slug: netkingdom
|
||||
workstream_id: 2c9caad8-2ced-492d-9d63-376387b4b9b0
|
||||
topic_id: a6c6e745-bf54-4465-9340-1534a2be493e
|
||||
repo_id: 8a99bb74-1ec0-4478-ac70-35a7cddb0e3c
|
||||
created: 2026-03-13
|
||||
spec_refs:
|
||||
- wiki/KeyCapeSpecification_v0.1.md
|
||||
- wiki/KeyCapeSpecificationPack_v0.1.md
|
||||
decisions:
|
||||
- id: ADR-0001
|
||||
title: "Implementation language: Go"
|
||||
hub_decision_id: 620beb04-fa3f-4a9d-9806-02890a7a2b0d
|
||||
status: accepted
|
||||
ref: docs/adr/ADR-0001-choose-go-for-keycape.md
|
||||
---
|
||||
|
||||
# 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
|
||||
|
||||
```task
|
||||
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
|
||||
|
||||
```task
|
||||
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
|
||||
|
||||
```task
|
||||
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
|
||||
|
||||
```task
|
||||
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_profile`
|
||||
- `available_in_keycloak_mode_only`
|
||||
- `rejected_for_profile_safety`
|
||||
- `invalid_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)
|
||||
|
||||
```task
|
||||
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
|
||||
|
||||
```task
|
||||
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
|
||||
|
||||
```task
|
||||
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)
|
||||
|
||||
```task
|
||||
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)
|
||||
|
||||
```task
|
||||
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
|
||||
|
||||
```task
|
||||
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
|
||||
|
||||
```task
|
||||
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
|
||||
|
||||
```task
|
||||
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
|
||||
|
||||
```task
|
||||
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
|
||||
|
||||
```task
|
||||
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
|
||||
|
||||
```task
|
||||
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
|
||||
|
||||
```task
|
||||
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)
|
||||
|
||||
```task
|
||||
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)
|
||||
|
||||
```task
|
||||
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)
|
||||
|
||||
```task
|
||||
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)
|
||||
|
||||
```task
|
||||
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)
|
||||
|
||||
```task
|
||||
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
|
||||
|
||||
```task
|
||||
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
|
||||
|
||||
```task
|
||||
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)
|
||||
Reference in New Issue
Block a user