Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P Assistant: claude-code Assistant-Model: opus Assistant-Process: 713576@bnt-lap001 Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
136 lines
5.5 KiB
Markdown
136 lines
5.5 KiB
Markdown
---
|
|
id: KEY-WP-0023
|
|
type: workplan
|
|
title: "Prove the migration against live directories and fix what that surfaced"
|
|
domain: infotech
|
|
repo: key-cape
|
|
status: finished
|
|
owner: claude
|
|
topic_slug: live-migration-proof
|
|
created: "2026-09-08"
|
|
updated: "2026-09-08"
|
|
state_hub_workstream_id: "9f19bfa0-78d1-5b5c-9b64-e251fa4838d6"
|
|
---
|
|
|
|
Finishes the unproven half of gap G04. KEY-WP-0022 made the harness runnable and
|
|
targeted conformance at a live issuer; what remained was directory migration
|
|
against a real directory, both sides of the swap measured, and the explicit
|
|
limits recorded.
|
|
|
|
Running it found four defects that no amount of reading would have. That is the
|
|
result worth keeping: every one of them passed the full unit suite.
|
|
|
|
## Run the export against a live LLDAP
|
|
|
|
```task
|
|
id: KEY-WP-0023-T01
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "b7b2ea88-8457-5fae-a500-a2d0f75754de"
|
|
```
|
|
|
|
**Defect: every LLDAP user search pointed at a branch that does not exist.**
|
|
`Config.userOU()` defaulted to `ou=users`; LLDAP stores users under `ou=people`,
|
|
and nothing in this repository set `UserOU` to correct it. `LookupUser`,
|
|
`ListUsers` and `ValidatePassword` all derive their search base from it, so all
|
|
three silently found nothing against a stock LLDAP — human login included, not
|
|
just the export.
|
|
|
|
Symptom that exposed it: exporting from a live LLDAP returned **zero users while
|
|
still emitting a membership referencing `uid=admin`**, a snapshot the repository's
|
|
own validator rejects with `referenced_users_exist`. Default corrected to
|
|
`ou=people` and pinned by a test; the re-run exports one user and validates
|
|
clean.
|
|
|
|
## Correct the rule I broke in KEY-WP-0021
|
|
|
|
```task
|
|
id: KEY-WP-0023-T02
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "2f988626-ed08-52ce-919a-cd45d48e712d"
|
|
```
|
|
|
|
**Defect of my own making.** The LLDAP adapter records `_validation_warning`
|
|
inside `LDAPAttributes`. The `raw_attributes_well_formed` rule added in
|
|
KEY-WP-0021 rejects underscores as invalid attribute descriptors, so the
|
|
exporter's own annotation made its own output fail validation — a false positive
|
|
shipped one workplan earlier and caught only by validating a real export.
|
|
|
|
Keys prefixed with `_` are now recognised as tooling annotations and skipped,
|
|
documented as such. A test proves the exemption does not weaken the rule: a
|
|
malformed real attribute alongside an annotation still fails.
|
|
|
|
## Prove the LDIF loads into a real OpenLDAP
|
|
|
|
```task
|
|
id: KEY-WP-0023-T03
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "0f503342-0ca8-55ad-b7e4-e2316d4d7bad"
|
|
```
|
|
|
|
**Defect: migrated group memberships were dangling.** `resolveMemberDN` returned
|
|
a member that already looked like a DN unchanged, so members kept their *source*
|
|
DNs (`uid=admin,ou=people,…`) while `writeUser` wrote entries to the *target*
|
|
branch (`ou=users`). Groups named entries the migrated directory does not
|
|
contain. Only the naming value is carried across now; branch and naming
|
|
attribute come from the target.
|
|
|
|
**Defect: empty groups could not be loaded at all.** `groupOfNames` makes
|
|
`member` a MUST, so OpenLDAP rejected every memberless group with an object class
|
|
violation and aborted the load. Since KEY-WP-0018 the export deliberately
|
|
preserves empty groups, so this was reachable by any real migration. Empty groups
|
|
now reference a placeholder entry the same LDIF creates — an `organizationalRole`,
|
|
not a person, so it cannot be mistaken for a migrated user — emitted only when
|
|
some group needs it.
|
|
|
|
**Defect: the compose file could not start.** `bitnami/openldap:2.6` does not
|
|
exist in the registry. It passed `docker compose config` in KEY-WP-0022, which is
|
|
exactly the gap between a file that parses and a stack that runs. Pinned to
|
|
`osixia/openldap:1.5.0`, the image the scenario was actually proved against.
|
|
|
|
Proof: LLDAP → export → validate → LDIF → `ldapadd` into OpenLDAP 1.5.0, all
|
|
seven entries added with no errors, and every `member` value in the loaded
|
|
directory resolves to an entry that exists.
|
|
|
|
## Measure both sides of the swap
|
|
|
|
```task
|
|
id: KEY-WP-0023-T04
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "8a7fe95e-d463-5e74-81fa-5fa60a879774"
|
|
```
|
|
|
|
KeyCape was run locally against a generated key and targeted with the
|
|
conformance suite: all five checks pass, including the excluded-grant check
|
|
Keycloak fails. The Keycloak side was exercised twice — the stock realm in
|
|
KEY-WP-0022, and here the *migrated* realm produced from the live export, which
|
|
imported cleanly and served discovery. The migrated user arrived carrying
|
|
`keycape.canonicalId`.
|
|
|
|
The divergence is now measured rather than asserted: KeyCape passes 5/5, a
|
|
migrated Keycloak passes 4/5 and fails on advertising the excluded `implicit` and
|
|
`password` grants.
|
|
|
|
## Record what is proved and what is not
|
|
|
|
```task
|
|
id: KEY-WP-0023-T05
|
|
status: done
|
|
priority: medium
|
|
state_hub_task_id: "acef6ceb-6dea-56e1-94bc-e5d589ef3b6f"
|
|
```
|
|
|
|
G04 is substantially closed: stacks are reproducible and were run, conformance is
|
|
externally targeted and was run against three issuers, and directory migration is
|
|
proved end to end into a real directory.
|
|
|
|
Not closed, and stated as such: no browser login was performed against the
|
|
migrated realm, so unchanged relying-party behaviour is not established, and MFA
|
|
was not exercised. Credential and MFA migration is a **scope exclusion** rather
|
|
than a test gap — SCOPE says it is not supplied, so no harness can demonstrate
|
|
it. `keycape-to-keycloak` already reports that subject continuity is not
|
|
preserved: the canonical ID survives as an attribute while Keycloak mints its own
|
|
`sub`, so relying parties keyed on `sub` will not recognise migrated users.
|