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
5.5 KiB
| id | type | title | domain | repo | status | owner | topic_slug | created | updated | state_hub_workstream_id |
|---|---|---|---|---|---|---|---|---|---|---|
| KEY-WP-0023 | workplan | Prove the migration against live directories and fix what that surfaced | infotech | key-cape | finished | claude | live-migration-proof | 2026-09-08 | 2026-09-08 | 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
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
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
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
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
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.