generated from coulomb/repo-seed
feat(local-identity): Stage 2 — Keycloak export & bootstrap integration (NK-WP-0002-T02)
export.py:
- split_fullname(): last-token strategy (Bernd Worsch → firstName/lastName)
- _deterministic_id(): uuid5(DNS, "local-identity.{realm}.{username}") for stable,
re-import-idempotent Keycloak IDs
- user_to_keycloak(): full Keycloak Admin REST API user representation;
production_identity mapping applied to username + realm; isolation attributes
(local_identity_environment, local_identity_generated) always present;
validate_keycloak_user() called on every conversion to catch schema drift
- bulk_export_body(): partial import body (ifResourceExists/realm/users)
cli.py: add `export` subcommand
- export <username> single user, prints Keycloak JSON
- export (no args) bulk; primary users only; stderr note on skipped test users
- export --include-test bulk; all users including generated
- --realm / --if-resource-exists flags
docs/LocalIdentity.md: add two new sections
- Keycloak import procedure: export → partialImport API → password reset → retire
- Isolation guarantee: attribute schema, Keycloak Condition authenticator config,
production_identity mapping walkthrough
tests/test_export.py: 34 new tests (88 total, all passing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -141,6 +141,93 @@ it on a public interface.
|
||||
| File schema drifting from Keycloak model | `export` command validates against Keycloak representation; schema is versioned |
|
||||
| Bootstrap store becoming a long-lived crutch | Explicit scope limit: once Keycloak is operational, migrate and stop using Local Identity |
|
||||
|
||||
## Keycloak import procedure
|
||||
|
||||
Once the Keycloak realm is operational (NK-WP-0001 T06), migrate the primary
|
||||
user from Local Identity into Keycloak using the partial import endpoint.
|
||||
|
||||
**1. Export the primary user:**
|
||||
|
||||
```bash
|
||||
local-identity export --all --realm net-kingdom > /tmp/li-import.json
|
||||
# By default, only the primary user is exported (test users are excluded).
|
||||
# Check: the Note line on stderr confirms how many test users were skipped.
|
||||
```
|
||||
|
||||
**2. Import via the Keycloak Admin REST API:**
|
||||
|
||||
```bash
|
||||
# Requires a Keycloak admin token
|
||||
TOKEN=$(curl -s -X POST https://keycloak.yourdomain.com/realms/master/protocol/openid-connect/token \
|
||||
-d "client_id=admin-cli&grant_type=password&username=admin&password=<admin-pw>" \
|
||||
| jq -r .access_token)
|
||||
|
||||
curl -s -X POST \
|
||||
https://keycloak.yourdomain.com/admin/realms/net-kingdom/partialImport \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @/tmp/li-import.json
|
||||
```
|
||||
|
||||
**3. Set a password in Keycloak** (Local Identity does not export credentials):
|
||||
|
||||
```bash
|
||||
curl -s -X PUT \
|
||||
https://keycloak.yourdomain.com/admin/realms/net-kingdom/users/<user-id>/reset-password \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type":"password","value":"<new-password>","temporary":false}'
|
||||
```
|
||||
|
||||
**4. Retire Local Identity for this instance:**
|
||||
Once the user is operational in Keycloak, stop using `local-identity serve`
|
||||
for this environment and remove the store: `rm -rf ~/.local-identity`.
|
||||
|
||||
## Isolation guarantee
|
||||
|
||||
All users exported by Local Identity carry the attribute:
|
||||
|
||||
```json
|
||||
"attributes": {
|
||||
"local_identity_environment": ["local"]
|
||||
}
|
||||
```
|
||||
|
||||
Generated test users additionally carry `local_identity_generated: ["true"]`.
|
||||
|
||||
### Configuring Keycloak to reject local-identity users
|
||||
|
||||
Add a condition to the Keycloak browser authentication flow that denies login
|
||||
for any user with `local_identity_environment = local`. This is a
|
||||
defence-in-depth measure: even if test users are accidentally imported into a
|
||||
production realm, they cannot authenticate.
|
||||
|
||||
In Keycloak 23+, use a **Conditional Authenticator** with a User Attribute
|
||||
Condition:
|
||||
|
||||
1. In the realm's **Authentication → Flows → browser** flow, add a sub-flow.
|
||||
2. Add the **Condition - User Attribute** authenticator.
|
||||
3. Configure: attribute = `local_identity_environment`, value = `local`,
|
||||
negation = **false** (matches when attribute equals the value).
|
||||
4. Set the sub-flow to **DENY** when the condition is true.
|
||||
|
||||
Alternatively, use a Keycloak script authenticator or a custom policy enforcer.
|
||||
|
||||
### Production identity mapping
|
||||
|
||||
Before importing, you can assign a `production_identity` block to the user
|
||||
so the Keycloak username differs from the local username:
|
||||
|
||||
```yaml
|
||||
# ~/.local-identity/users/tegwick.yaml
|
||||
production_identity:
|
||||
username: bworsch # the username used in the production realm
|
||||
realm: net-kingdom
|
||||
```
|
||||
|
||||
Re-run `local-identity export --all` — the exported JSON will use `bworsch`
|
||||
as the Keycloak username and a deterministic UUID derived from `net-kingdom/bworsch`.
|
||||
|
||||
## Relationship to the SSO platform
|
||||
|
||||
Local Identity is a complementary workstream to the SSO & MFA Platform
|
||||
@@ -148,8 +235,4 @@ Local Identity is a complementary workstream to the SSO & MFA Platform
|
||||
Identity provides the bootstrap path that allows the SSO platform itself to
|
||||
be set up and tested.
|
||||
|
||||
When the Keycloak realm (NK-WP-0001 T06) is operational, primary and test
|
||||
users can be exported from Local Identity into Keycloak using
|
||||
`local-identity export` and the Keycloak admin API.
|
||||
|
||||
Implementation: see [NK-WP-0002](../workplans/NK-WP-0002-local-identity.md).
|
||||
|
||||
Reference in New Issue
Block a user