170 lines
5.7 KiB
Python
170 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Provision the isolated public Keycloak client used by the 15173 UI."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import getpass
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
|
if str(REPOSITORY_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(REPOSITORY_ROOT))
|
|
|
|
from scripts.provision_operational_worker_identity import ( # noqa: E402
|
|
KeycloakProvisioner,
|
|
ProvisionError,
|
|
_request,
|
|
)
|
|
|
|
|
|
REDIRECT_URIS = [
|
|
"http://127.0.0.1:15173/*",
|
|
"http://localhost:15173/*",
|
|
]
|
|
WEB_ORIGINS = [
|
|
"http://127.0.0.1:15173",
|
|
"http://localhost:15173",
|
|
]
|
|
|
|
|
|
def ensure_public_client(
|
|
keycloak: KeycloakProvisioner, client_id: str
|
|
) -> tuple[bool, dict]:
|
|
matches = _request(
|
|
keycloak.client,
|
|
"GET",
|
|
f"{keycloak.admin_base}/clients",
|
|
params={"clientId": client_id},
|
|
).json()
|
|
exact = [item for item in matches if item.get("clientId") == client_id]
|
|
if len(exact) > 1:
|
|
raise ProvisionError("multiple Keycloak clients use the integration client id")
|
|
created = not exact
|
|
if created:
|
|
_request(
|
|
keycloak.client,
|
|
"POST",
|
|
f"{keycloak.admin_base}/clients",
|
|
json={
|
|
"clientId": client_id,
|
|
"name": "TJWater isolated integration UI",
|
|
"description": "Local 15173 UI; authorization code flow with PKCE only",
|
|
"enabled": True,
|
|
"protocol": "openid-connect",
|
|
"publicClient": True,
|
|
"bearerOnly": False,
|
|
"serviceAccountsEnabled": False,
|
|
"standardFlowEnabled": True,
|
|
"implicitFlowEnabled": False,
|
|
"directAccessGrantsEnabled": False,
|
|
"fullScopeAllowed": False,
|
|
"redirectUris": REDIRECT_URIS,
|
|
"webOrigins": WEB_ORIGINS,
|
|
"attributes": {"pkce.code.challenge.method": "S256"},
|
|
},
|
|
)
|
|
matches = _request(
|
|
keycloak.client,
|
|
"GET",
|
|
f"{keycloak.admin_base}/clients",
|
|
params={"clientId": client_id},
|
|
).json()
|
|
exact = [item for item in matches if item.get("clientId") == client_id]
|
|
if len(exact) != 1 or not isinstance(exact[0].get("id"), str):
|
|
raise ProvisionError("could not resolve the integration UI client")
|
|
internal_id = exact[0]["id"]
|
|
representation = _request(
|
|
keycloak.client,
|
|
"GET",
|
|
f"{keycloak.admin_base}/clients/{internal_id}",
|
|
).json()
|
|
attributes = dict(representation.get("attributes") or {})
|
|
attributes["pkce.code.challenge.method"] = "S256"
|
|
representation.update(
|
|
{
|
|
"enabled": True,
|
|
"publicClient": True,
|
|
"bearerOnly": False,
|
|
"serviceAccountsEnabled": False,
|
|
"standardFlowEnabled": True,
|
|
"implicitFlowEnabled": False,
|
|
"directAccessGrantsEnabled": False,
|
|
"fullScopeAllowed": False,
|
|
"redirectUris": REDIRECT_URIS,
|
|
"webOrigins": WEB_ORIGINS,
|
|
"attributes": attributes,
|
|
}
|
|
)
|
|
_request(
|
|
keycloak.client,
|
|
"PUT",
|
|
f"{keycloak.admin_base}/clients/{internal_id}",
|
|
json=representation,
|
|
)
|
|
verified = _request(
|
|
keycloak.client,
|
|
"GET",
|
|
f"{keycloak.admin_base}/clients/{internal_id}",
|
|
).json()
|
|
expected = {
|
|
"enabled": True,
|
|
"publicClient": True,
|
|
"bearerOnly": False,
|
|
"serviceAccountsEnabled": False,
|
|
"standardFlowEnabled": True,
|
|
"implicitFlowEnabled": False,
|
|
"directAccessGrantsEnabled": False,
|
|
"fullScopeAllowed": False,
|
|
}
|
|
if any(verified.get(key) != value for key, value in expected.items()):
|
|
raise ProvisionError("integration UI client flow settings did not persist")
|
|
if set(verified.get("redirectUris") or []) != set(REDIRECT_URIS):
|
|
raise ProvisionError("integration UI redirect URI allowlist did not persist")
|
|
if set(verified.get("webOrigins") or []) != set(WEB_ORIGINS):
|
|
raise ProvisionError("integration UI web origin allowlist did not persist")
|
|
if (verified.get("attributes") or {}).get("pkce.code.challenge.method") != "S256":
|
|
raise ProvisionError("integration UI PKCE policy did not persist")
|
|
return created, {
|
|
"client_id": client_id,
|
|
"public_client": True,
|
|
"standard_flow": True,
|
|
"pkce": "S256",
|
|
"implicit_flow": False,
|
|
"direct_grants": False,
|
|
"service_accounts": False,
|
|
"redirect_uris": REDIRECT_URIS,
|
|
"web_origins": WEB_ORIGINS,
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--keycloak-url", default="http://192.168.1.114:8088")
|
|
parser.add_argument("--realm", default="tjwater")
|
|
parser.add_argument("--admin-username", default="admin")
|
|
parser.add_argument("--client-id", default="tjwater-integration-ui")
|
|
args = parser.parse_args()
|
|
password = os.environ.get("KEYCLOAK_ADMIN_PASSWORD") or getpass.getpass(
|
|
"Keycloak admin password: "
|
|
)
|
|
if not password:
|
|
raise SystemExit("administrator password is required")
|
|
try:
|
|
keycloak = KeycloakProvisioner(
|
|
args.keycloak_url, args.realm, args.admin_username, password
|
|
)
|
|
try:
|
|
created, result = ensure_public_client(keycloak, args.client_id)
|
|
finally:
|
|
keycloak.close()
|
|
except ProvisionError as exc:
|
|
raise SystemExit(f"provisioning failed: {exc}") from None
|
|
print(json.dumps({"created": created, **result}, sort_keys=True))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|