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:
@@ -8,16 +8,21 @@ Commands:
|
||||
resolved flag > config > system derivation.
|
||||
list List all users in the store.
|
||||
show <username> Display a user's YAML record.
|
||||
export [<username>] Export a single user as Keycloak JSON.
|
||||
export --all [--realm R] Bulk partial-import body (primary users only).
|
||||
Add --include-test to include generated users.
|
||||
|
||||
Environment:
|
||||
LOCAL_IDENTITY_HOME Override the store directory (default: ~/.local-identity).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
from .gecos import current_username, get_gecos_fullname
|
||||
from .user import UserRecord, make_test_user
|
||||
from . import export as export_mod
|
||||
from . import store
|
||||
|
||||
|
||||
@@ -87,6 +92,38 @@ def cmd_list(args: argparse.Namespace) -> None:
|
||||
print(f"{u.username:<20} {u.fullname:<30} {u.email:<40} {utype}")
|
||||
|
||||
|
||||
def cmd_export(args: argparse.Namespace) -> None:
|
||||
if args.username:
|
||||
# Single-user export
|
||||
try:
|
||||
user = store.read_user(args.username)
|
||||
except FileNotFoundError as exc:
|
||||
print(str(exc), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
kc = export_mod.user_to_keycloak(user, realm=args.realm)
|
||||
print(json.dumps(kc, indent=2))
|
||||
else:
|
||||
# Bulk export
|
||||
all_users = store.list_users()
|
||||
if args.include_test:
|
||||
users = all_users
|
||||
else:
|
||||
users = [u for u in all_users if not u.generated]
|
||||
skipped = len(all_users) - len(users)
|
||||
if skipped:
|
||||
print(
|
||||
f"Note: skipping {skipped} test user(s). "
|
||||
"Use --include-test to export them.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
body = export_mod.bulk_export_body(
|
||||
users,
|
||||
realm=args.realm,
|
||||
if_resource_exists=args.if_resource_exists,
|
||||
)
|
||||
print(json.dumps(body, indent=2))
|
||||
|
||||
|
||||
def cmd_show(args: argparse.Namespace) -> None:
|
||||
try:
|
||||
user = store.read_user(args.username)
|
||||
@@ -129,6 +166,29 @@ def main() -> None:
|
||||
)
|
||||
p_init.set_defaults(func=cmd_init)
|
||||
|
||||
p_export = sub.add_parser(
|
||||
"export",
|
||||
help="Export user(s) as Keycloak-compatible JSON",
|
||||
)
|
||||
p_export.add_argument(
|
||||
"username", nargs="?",
|
||||
help="Export a single user (omit for bulk export)",
|
||||
)
|
||||
p_export.add_argument(
|
||||
"--realm", default="net-kingdom",
|
||||
help="Target Keycloak realm name (default: net-kingdom)",
|
||||
)
|
||||
p_export.add_argument(
|
||||
"--include-test", action="store_true",
|
||||
help="Include generated test users in bulk export",
|
||||
)
|
||||
p_export.add_argument(
|
||||
"--if-resource-exists", default="SKIP",
|
||||
choices=["SKIP", "OVERWRITE", "FAIL"],
|
||||
help="Conflict strategy for bulk import (default: SKIP)",
|
||||
)
|
||||
p_export.set_defaults(func=cmd_export)
|
||||
|
||||
p_list = sub.add_parser("list", help="List all users in the store")
|
||||
p_list.set_defaults(func=cmd_list)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user