Files
key-cape/workplans/KEY-WP-0001-keycape-implementation.md
tegwick ece58bc363
Some checks failed
CI / Build and Test (push) Has been cancelled
feat(close): mark KEY-WP-0001 done — all 23 tasks complete, tests passing
All implementation phases complete: OIDC server (Authorization Code + PKCE),
canonical identity model + LDAP validator, backend adapters (Authelia/LLDAP/
privacyIDEA), telemetry, enforcement middleware, migration tooling, and all
four replacement test scenarios (A–D). Tests pass with Go 1.23.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 02:49:13 +01:00

17 KiB

id, type, title, domain, repo, status, owner, topic_slug, topic_id, repo_id, created, updated, spec_refs, decisions, state_hub_workstream_id
id type title domain repo status owner topic_slug topic_id repo_id created updated spec_refs decisions state_hub_workstream_id
KEY-WP-0001 workplan KeyCape Implementation — Lightweight IAM Profile netkingdom key-cape done Bernd netkingdom a6c6e745-bf54-4465-9340-1534a2be493e 8a99bb74-1ec0-4478-ac70-35a7cddb0e3c 2026-03-13 2026-03-13
wiki/KeyCapeSpecification_v0.1.md
wiki/KeyCapeSpecificationPack_v0.1.md
id title hub_decision_id status ref
ADR-0001 Implementation language: Go 620beb04-fa3f-4a9d-9806-02890a7a2b0d accepted docs/adr/ADR-0001-choose-go-for-keycape.md
0d34dfc1-7ccb-4bd5-b872-5c7379b9adce

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: done
state_hub_task_id: "38822bc0-4189-4909-874e-ea40e5771250"

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: done
depends_on: [T01]
state_hub_task_id: "940c118b-c1e6-4dda-bd4c-4fac105822be"

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: done
depends_on: [T01, T02]
state_hub_task_id: "c1715d70-f10f-45e9-b73a-b54a3d360342"

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: done
depends_on: [T01]
state_hub_task_id: "6e3b6b97-ac77-44c5-959e-be12751f1b63"

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)

id: KEY-WP-0001-T05
hub_task_id: 92eb8916-cb22-4786-9f16-a8a07272f818
priority: high
status: done
depends_on: [T04]
state_hub_task_id: "0dbc08e3-c465-4c37-a219-832a580bedfd"

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: done
depends_on: [T04, T11, T12, T13, T14]
state_hub_task_id: "cdb4b06d-3d54-49dd-ac05-ca9ed6d7322f"

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: done
depends_on: [T06, T08, T10]
state_hub_task_id: "534d8616-90de-4d32-961c-c2ef719642e4"

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: done
depends_on: [T01]
state_hub_task_id: "7e2167be-bcc7-49c2-8681-e518abd5bc0c"

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: done
depends_on: [T07, T10]
state_hub_task_id: "78094ca5-a831-4443-9ccf-fc476ff87b91"

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: done
depends_on: [T02, T03]
state_hub_task_id: "97d19662-f482-4ea5-84fd-9fccb84ff317"

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: done
depends_on: [T04, T13]
state_hub_task_id: "6461865b-f57c-4591-9cf3-68c79af22723"

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: done
depends_on: [T02, T13]
state_hub_task_id: "e403a783-c856-4d6d-b859-a9cad7545fe1"

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: done
depends_on: [T01]
state_hub_task_id: "4df7bda1-5b84-4b4c-9b16-bcb1d3cca096"

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: done
depends_on: [T04, T13]
state_hub_task_id: "ae16fba9-5bb4-4780-ac77-558e3ed7e1dd"

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: done
depends_on: [T02, T03, T10]
state_hub_task_id: "f7549cd7-33f0-4407-a656-ab8f5a184e64"

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: done
depends_on: [T15]
state_hub_task_id: "96486c41-9f33-42a5-b7b6-ad0a9eb2bdee"

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: done
depends_on: [T15]
state_hub_task_id: "1ec335a2-80ca-4c34-b08e-211f537e4214"

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: done
depends_on: [T05, T06, T07, T08, T09, T22]
state_hub_task_id: "1b0e9f26-d441-42b8-b532-1eb713fb355d"

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: done
depends_on: [T18, T16]
state_hub_task_id: "a02d24e7-32de-4be6-935c-896c10dde020"

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: done
depends_on: [T19, T17]
state_hub_task_id: "545f319f-053d-48bd-8d94-c8c05cd56736"

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: done
depends_on: [T14]
state_hub_task_id: "5856afe0-2a9e-4489-b057-35e59f86c359"

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: done
depends_on: [T01]
state_hub_task_id: "b98f2671-a20a-4438-99c9-fbe0e5324534"

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: done
depends_on: [T18, T21]
state_hub_task_id: "8c1752c2-7fb3-4da5-aab3-6b7acf12ea64"

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) — src/tests/profile/profile_test.go (8 tests)
  • Scenario D tests pass (T21) — src/tests/negative/negative_test.go (8 tests)
  • Scenario B tests pass (T19) — src/tests/migration/scenario_b_test.go (7 tests)
  • Scenario C tests pass (T20) — src/tests/migration/scenario_c_test.go (6 tests)
  • All error responses use taxonomy types from spec §5 — internal/errors/taxonomy.go
  • All auth/error paths emit structured telemetry (T13) — internal/server/telemetry/
  • Canonical LDAP schema validator passes on all fixtures (T03) — internal/validator/
  • No handwritten cryptography anywhere in the codebase — stdlib crypto/rsa only
  • Config is statically validated at startup (T23) — internal/config/validate.go