feat(iam): provision the built-in login page

loginV2.required lives in the instance database, not in chart values, so
disabling the hosted Login UI in values is only half the change: the server
kept redirecting /oauth/v2/authorize to /ui/v2/login, which no longer had a
backend and returned 503.

Set loginV2.required=false in the provisioning run so the server serves its
own login page. Verified: authorize now redirects to /ui/login/login and the
page renders the "Welcome Back!" login form.
This commit is contained in:
2026-10-26 23:12:53 -07:00
parent eaf9804374
commit 3f93ddc002
+19
View File
@@ -299,6 +299,24 @@ def _ensure_project(token: str, name: str) -> str:
return project_id
def _ensure_builtin_login(token: str) -> None:
"""Force the built-in login page instead of the hosted Login UI.
The hosted UI authenticates as the machine user `login-client`, which the
chart creates only from its one-shot setup job. When that user is missing,
every session write fails with "Could not create session for user" and no
one can sign in, at ZITADEL or at any relying party.
Disabling the loginV2 feature makes the server serve its own login page,
which needs no machine user. This is instance state in the database, not a
values key, so it must be applied explicitly.
"""
code, d = _api("PUT", "/v2/features/instance", token, {"loginV2": {"required": False}})
if code not in (200, 201):
die(f"disable loginV2 failed: {code} {d}")
print("login page : built-in (loginV2.required=false)")
def _ensure_roles(token: str, project_id: str, roles: dict[str, str]) -> None:
code, d = _api("POST", f"/management/v1/projects/{project_id}/roles/_search", token, {})
have = {r.get("roleKey") for r in (d.get("result") or [])}
@@ -356,6 +374,7 @@ def phase_apply(_args: argparse.Namespace) -> int:
if not token:
die("no iam-admin PAT — run the keys phase first")
_ensure_builtin_login(token)
project_id = _ensure_project(token, PROJECT_NAME)
print(f"project : {PROJECT_NAME} ({project_id})")
_ensure_roles(token, project_id, ROLES)