generated from coulomb/repo-seed
Local Identity OICD bootstrap
This commit is contained in:
@@ -11,6 +11,7 @@ Commands:
|
||||
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.
|
||||
bootstrap-oidc Persist and print local OIDC client settings.
|
||||
serve [--port P] [--ttl T] Start the minimal OIDC server on 127.0.0.1.
|
||||
security-check Validate filesystem permissions.
|
||||
revoke-token <jti-or-jwt> Add a token JTI to the revocation list.
|
||||
@@ -21,7 +22,9 @@ Environment:
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shlex
|
||||
import sys
|
||||
import urllib.parse
|
||||
|
||||
from .gecos import current_username, get_gecos_fullname
|
||||
from .jwt_utils import JWTError, extract_unverified_payload
|
||||
@@ -35,6 +38,7 @@ from .security import enforce_permissions, print_security_check
|
||||
|
||||
# Commands that must not run the startup permission check
|
||||
_SKIP_ENFORCE = {"init", "security-check"}
|
||||
_LOCAL_OIDC_HOSTS = {"127.0.0.1", "localhost", "::1"}
|
||||
|
||||
|
||||
def _resolve_init_params(args: argparse.Namespace, config: dict) -> tuple[str, str, str]:
|
||||
@@ -152,6 +156,64 @@ def cmd_serve(args: argparse.Namespace) -> None:
|
||||
serve_mod.run_server(port=args.port, token_ttl=args.ttl)
|
||||
|
||||
|
||||
def _validate_loopback_redirect_uri(uri: str) -> None:
|
||||
parsed = urllib.parse.urlparse(uri)
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
||||
raise ValueError("redirect URI must be an absolute http(s) URL")
|
||||
if parsed.hostname not in _LOCAL_OIDC_HOSTS:
|
||||
raise ValueError("redirect URI must use localhost or a loopback address")
|
||||
|
||||
|
||||
def _oidc_bootstrap_payload(args: argparse.Namespace) -> dict:
|
||||
_validate_loopback_redirect_uri(args.redirect_uri)
|
||||
issuer = f"{args.scheme}://127.0.0.1:{args.port}"
|
||||
return {
|
||||
"issuer": issuer,
|
||||
"discovery_url": f"{issuer}/.well-known/openid-configuration",
|
||||
"client_id": args.client_id,
|
||||
"redirect_uri": args.redirect_uri,
|
||||
"scope": args.scope,
|
||||
"token_endpoint_auth_method": "none",
|
||||
}
|
||||
|
||||
|
||||
def cmd_bootstrap_oidc(args: argparse.Namespace) -> None:
|
||||
if not store.store_exists():
|
||||
print(
|
||||
"Error: store not initialised. Run 'local-identity init' first.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
payload = _oidc_bootstrap_payload(args)
|
||||
except ValueError as exc:
|
||||
print(f"Error: {exc}.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
config = store.read_config()
|
||||
clients = config.setdefault("oidc_clients", {})
|
||||
clients[args.client_id] = payload
|
||||
config["last_oidc_bootstrap"] = args.client_id
|
||||
store.write_config(config)
|
||||
|
||||
audit.log_event("bootstrap-oidc", args.client_id, "ok")
|
||||
|
||||
if args.output == "json":
|
||||
print(json.dumps(payload, indent=2))
|
||||
return
|
||||
|
||||
for key, value in {
|
||||
"OIDC_ISSUER": payload["issuer"],
|
||||
"OIDC_DISCOVERY_URL": payload["discovery_url"],
|
||||
"OIDC_CLIENT_ID": payload["client_id"],
|
||||
"OIDC_REDIRECT_URI": payload["redirect_uri"],
|
||||
"OIDC_SCOPE": payload["scope"],
|
||||
"OIDC_TOKEN_ENDPOINT_AUTH_METHOD": payload["token_endpoint_auth_method"],
|
||||
}.items():
|
||||
print(f"{key}={shlex.quote(value)}")
|
||||
|
||||
|
||||
def cmd_security_check(args: argparse.Namespace) -> None:
|
||||
rc = print_security_check()
|
||||
sys.exit(rc)
|
||||
@@ -256,6 +318,43 @@ def main() -> None:
|
||||
)
|
||||
p_serve.set_defaults(func=cmd_serve)
|
||||
|
||||
p_bootstrap_oidc = sub.add_parser(
|
||||
"bootstrap-oidc",
|
||||
help="Persist and print localhost OIDC client bootstrap settings",
|
||||
)
|
||||
p_bootstrap_oidc.add_argument(
|
||||
"--client-id",
|
||||
default="local-dev",
|
||||
help="OIDC client ID to advertise (default: local-dev)",
|
||||
)
|
||||
p_bootstrap_oidc.add_argument(
|
||||
"--redirect-uri",
|
||||
default="http://127.0.0.1:3000/callback",
|
||||
help="Loopback redirect URI for the local client",
|
||||
)
|
||||
p_bootstrap_oidc.add_argument(
|
||||
"--port", type=int, default=8443,
|
||||
help="Port used by local-identity serve (default: 8443)",
|
||||
)
|
||||
p_bootstrap_oidc.add_argument(
|
||||
"--scheme",
|
||||
choices=["https", "http"],
|
||||
default="https",
|
||||
help="Issuer URL scheme (default: https)",
|
||||
)
|
||||
p_bootstrap_oidc.add_argument(
|
||||
"--scope",
|
||||
default="openid profile email",
|
||||
help="OIDC scope string (default: openid profile email)",
|
||||
)
|
||||
p_bootstrap_oidc.add_argument(
|
||||
"--output",
|
||||
choices=["env", "json"],
|
||||
default="env",
|
||||
help="Output format (default: env)",
|
||||
)
|
||||
p_bootstrap_oidc.set_defaults(func=cmd_bootstrap_oidc)
|
||||
|
||||
sub.add_parser(
|
||||
"security-check",
|
||||
help="Validate filesystem permissions of the store",
|
||||
|
||||
Reference in New Issue
Block a user