Loading oop-platform-chart/charts/keycloak/files/realm-setup.py +71 −1 Changes for oop-platform-chart/charts/keycloak/files/realm-setup.py: 71 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -2,7 +2,7 @@ chart. Every step only does what is missing, so running the job again changes nothing. Reads /config/config.json (rendered by the chart) and these environment variables: KEYCLOAK_URL, KEYCLOAK_ADMIN_USERNAME, KEYCLOAK_ADMIN_PASSWORD. KEYCLOAK_URL, KEYCLOAK_ADMIN_USERNAME, KEYCLOAK_ADMIN_PASSWORD, PROVISIONER_CLIENT_SECRET. """ import json Loading Loading @@ -156,6 +156,73 @@ def ensure_scopes_gated(kc: Keycloak, oeg: str, cfg: dict) -> None: say("added", f"{scope} to {cfg['portalClientId']}") PROVISIONER_ROLES = {"create-client", "view-users", "query-users"} # Would expose every client's secret, the oop-portal-admin one included. FORBIDDEN_PROVISIONER_ROLES = {"view-clients"} def ensure_provisioner(kc: Keycloak, cfg: dict, secret: str) -> str: """The client the portal creates API clients with, and exactly the roles it may hold. Returns the client's id.""" client_id = cfg["provisionerClientId"] found = kc.get(f"/clients?clientId={client_id}") if found: say("ok", f"client {client_id}") provisioner = found[0]["id"] else: kc.call( "POST", "/clients", { "clientId": client_id, "name": "OOP Portal API client provisioning", "description": "Service account for creating API clients (ADR-0048); no login flows.", "publicClient": False, "clientAuthenticatorType": "client-secret", "secret": secret, "standardFlowEnabled": False, "implicitFlowEnabled": False, "directAccessGrantsEnabled": False, "serviceAccountsEnabled": True, }, ) provisioner = kc.get(f"/clients?clientId={client_id}")[0]["id"] say("created", f"client {client_id}") account = kc.get(f"/clients/{provisioner}/service-account-user")["id"] management = kc.get("/clients?clientId=realm-management")[0]["id"] path = f"/users/{account}/role-mappings/clients/{management}" held = {role["name"]: role for role in kc.get(path)} available = {role["name"]: role for role in kc.get(f"/clients/{management}/roles")} for name in sorted(PROVISIONER_ROLES - held.keys()): kc.call("POST", path, [available[name]]) say("granted", f"{name} to {client_id}") for name in sorted(FORBIDDEN_PROVISIONER_ROLES & held.keys()): kc.call("DELETE", path, [held[name]]) say("removed", f"{name} from {client_id}") return provisioner def check_provisioner_secret(kc: Keycloak, cfg: dict, secret: str) -> None: body = urllib.parse.urlencode( { "grant_type": "client_credentials", "client_id": cfg["provisionerClientId"], "client_secret": secret, } ).encode() try: urllib.request.urlopen( f"{kc.url}/realms/{kc.realm}/protocol/openid-connect/token", body, timeout=10 ).close() except urllib.error.HTTPError as exc: raise SystemExit( f"{cfg['provisionerClientId']} refuses the secret in the credentials Secret " f"({exc.code}): change the client's secret in Keycloak to match it" ) from exc say("ok", "provisioner secret") def main() -> None: cfg = json.load(open(CONFIG_FILE)) kc = Keycloak( Loading @@ -168,6 +235,9 @@ def main() -> None: oeg = ensure_oeg(kc, cfg) ensure_product_roles(kc, oeg, cfg) ensure_scopes_gated(kc, oeg, cfg) secret = os.environ["PROVISIONER_CLIENT_SECRET"] ensure_provisioner(kc, cfg, secret) check_provisioner_secret(kc, cfg, secret) say("done", "") Loading Loading
oop-platform-chart/charts/keycloak/files/realm-setup.py +71 −1 Changes for oop-platform-chart/charts/keycloak/files/realm-setup.py: 71 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -2,7 +2,7 @@ chart. Every step only does what is missing, so running the job again changes nothing. Reads /config/config.json (rendered by the chart) and these environment variables: KEYCLOAK_URL, KEYCLOAK_ADMIN_USERNAME, KEYCLOAK_ADMIN_PASSWORD. KEYCLOAK_URL, KEYCLOAK_ADMIN_USERNAME, KEYCLOAK_ADMIN_PASSWORD, PROVISIONER_CLIENT_SECRET. """ import json Loading Loading @@ -156,6 +156,73 @@ def ensure_scopes_gated(kc: Keycloak, oeg: str, cfg: dict) -> None: say("added", f"{scope} to {cfg['portalClientId']}") PROVISIONER_ROLES = {"create-client", "view-users", "query-users"} # Would expose every client's secret, the oop-portal-admin one included. FORBIDDEN_PROVISIONER_ROLES = {"view-clients"} def ensure_provisioner(kc: Keycloak, cfg: dict, secret: str) -> str: """The client the portal creates API clients with, and exactly the roles it may hold. Returns the client's id.""" client_id = cfg["provisionerClientId"] found = kc.get(f"/clients?clientId={client_id}") if found: say("ok", f"client {client_id}") provisioner = found[0]["id"] else: kc.call( "POST", "/clients", { "clientId": client_id, "name": "OOP Portal API client provisioning", "description": "Service account for creating API clients (ADR-0048); no login flows.", "publicClient": False, "clientAuthenticatorType": "client-secret", "secret": secret, "standardFlowEnabled": False, "implicitFlowEnabled": False, "directAccessGrantsEnabled": False, "serviceAccountsEnabled": True, }, ) provisioner = kc.get(f"/clients?clientId={client_id}")[0]["id"] say("created", f"client {client_id}") account = kc.get(f"/clients/{provisioner}/service-account-user")["id"] management = kc.get("/clients?clientId=realm-management")[0]["id"] path = f"/users/{account}/role-mappings/clients/{management}" held = {role["name"]: role for role in kc.get(path)} available = {role["name"]: role for role in kc.get(f"/clients/{management}/roles")} for name in sorted(PROVISIONER_ROLES - held.keys()): kc.call("POST", path, [available[name]]) say("granted", f"{name} to {client_id}") for name in sorted(FORBIDDEN_PROVISIONER_ROLES & held.keys()): kc.call("DELETE", path, [held[name]]) say("removed", f"{name} from {client_id}") return provisioner def check_provisioner_secret(kc: Keycloak, cfg: dict, secret: str) -> None: body = urllib.parse.urlencode( { "grant_type": "client_credentials", "client_id": cfg["provisionerClientId"], "client_secret": secret, } ).encode() try: urllib.request.urlopen( f"{kc.url}/realms/{kc.realm}/protocol/openid-connect/token", body, timeout=10 ).close() except urllib.error.HTTPError as exc: raise SystemExit( f"{cfg['provisionerClientId']} refuses the secret in the credentials Secret " f"({exc.code}): change the client's secret in Keycloak to match it" ) from exc say("ok", "provisioner secret") def main() -> None: cfg = json.load(open(CONFIG_FILE)) kc = Keycloak( Loading @@ -168,6 +235,9 @@ def main() -> None: oeg = ensure_oeg(kc, cfg) ensure_product_roles(kc, oeg, cfg) ensure_scopes_gated(kc, oeg, cfg) secret = os.environ["PROVISIONER_CLIENT_SECRET"] ensure_provisioner(kc, cfg, secret) check_provisioner_secret(kc, cfg, secret) say("done", "") Loading