From 7761acf86a091480945c0fd430dae54b1a82342f Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 22 Jul 2026 00:30:17 +0200 Subject: [PATCH] Link Temporal UI from ops console; propose SSO access WP-0025 Add nav/deep link to Temporal Web UI (configurable URL, default 127.0.0.1:8080 for port-forward). Document dual port-forward and draft ACTIVITY-WP-0025 for Keycloak SSO ingress without port-forward. --- docs/runbook.md | 22 +- src/activity_core/ops_api.py | 24 ++ tests/test_ops_console_api.py | 4 + .../ACTIVITY-WP-0025-ops-ui-sso-access.md | 236 ++++++++++++++++++ 4 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md diff --git a/docs/runbook.md b/docs/runbook.md index 124a75a..775f285 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -105,15 +105,31 @@ the UI** — change definition files and sync. ### Production access (railiance01) -API remains **ClusterIP** (no public Ingress in WP-0024). +API remains **ClusterIP** (no public Ingress in WP-0024; SSO planned in +ACTIVITY-WP-0025). Use the **hosteurope / railiance01** kubeconfig, not a local +empty cluster. ```bash -# From a machine with kubectl to railiance01: +export KUBECONFIG=~/.kube/config-hosteurope + +# Ops console (activity-core API) kubectl -n activity-core port-forward svc/actcore-api 8010:8010 # Browser: http://127.0.0.1:8010/ops/ui -# Ensure ACTIVITY_CORE_OPERATOR_TOKEN is set on actcore-api (runtime secret key). + +# Temporal Web UI (second terminal — linked from ops nav as "Temporal UI") +kubectl -n activity-core port-forward svc/actcore-temporal-ui 8080:8080 +# Browser: http://127.0.0.1:8080 ``` +Override the Temporal link target if needed: + +```bash +# on actcore-api +ACTIVITY_CORE_TEMPORAL_UI_URL=http://127.0.0.1:8080 +``` + +Ensure `ACTIVITY_CORE_OPERATOR_TOKEN` is set on actcore-api (runtime secret key). + Bootstrap token (operator workstation; never commit the value): ```bash diff --git a/src/activity_core/ops_api.py b/src/activity_core/ops_api.py index 8e7c765..eeaf203 100644 --- a/src/activity_core/ops_api.py +++ b/src/activity_core/ops_api.py @@ -247,6 +247,7 @@ async def auth_status() -> dict[str, Any]: "operator_token_configured": operator_token_configured(), "mutation_header": HEADER_NAME, "mutations_require_token": operator_token_configured() or not allow, + "temporal_ui_url": temporal_ui_url(), } @@ -256,6 +257,7 @@ _CSS = """ :root { font-family: system-ui, sans-serif; color: #1a1a1a; } body { margin: 1.5rem; max-width: 1100px; } nav a { margin-right: 1rem; } +nav a.external::after { content: " ↗"; font-size: 0.75em; opacity: 0.7; } table { border-collapse: collapse; width: 100%; margin: 1rem 0; } th, td { border: 1px solid #ccc; padding: 0.4rem 0.6rem; text-align: left; font-size: 0.9rem; } th { background: #f4f4f4; } @@ -268,7 +270,22 @@ pre { background: #f8f8f8; padding: 0.75rem; overflow: auto; font-size: 0.8rem; """ +def temporal_ui_url() -> str: + """Browser URL for Temporal Web UI (port-forward or future SSO ingress). + + Override with ACTIVITY_CORE_TEMPORAL_UI_URL. Default assumes a local + port-forward of actcore-temporal-ui to 127.0.0.1:8080 (see runbook). + """ + raw = ( + os.environ.get("ACTIVITY_CORE_TEMPORAL_UI_URL") + or os.environ.get("TEMPORAL_UI_URL") + or "http://127.0.0.1:8080" + ).strip() + return raw.rstrip("/") or "http://127.0.0.1:8080" + + def _page(title: str, body: str) -> HTMLResponse: + temporal_href = html.escape(temporal_ui_url(), quote=True) doc = f""" @@ -281,6 +298,8 @@ def _page(title: str, body: str) -> HTMLResponse: activity-core ops Inventory Status + Temporal UI Auth JSON
@@ -373,6 +392,11 @@ async def ui_index() -> HTMLResponse: {''.join(rows) or 'No automations found'}

Schedule cron is read-only in MVP — edit definition files + sync.

+

Temporal Web UI (workflow debugger): + {html.escape(temporal_ui_url())} + — requires port-forward of svc/actcore-temporal-ui 8080:8080 + until SSO ingress (ACTIVITY-WP-0025).

""" return _page("Inventory", body) diff --git a/tests/test_ops_console_api.py b/tests/test_ops_console_api.py index d78b78b..30e7e48 100644 --- a/tests/test_ops_console_api.py +++ b/tests/test_ops_console_api.py @@ -48,6 +48,8 @@ async def test_auth_status(ops_app: FastAPI) -> None: body = res.json() assert body["operator_token_configured"] is True assert body["mutation_header"] == "X-Operator-Token" + assert "temporal_ui_url" in body + assert body["temporal_ui_url"].startswith("http") @pytest.mark.asyncio @@ -215,6 +217,8 @@ async def test_ui_index_renders(ops_app: FastAPI, monkeypatch: pytest.MonkeyPatc assert res.status_code == 200 assert "Daily Triage" in res.text assert "Operator token" in res.text + assert "Temporal UI" in res.text + assert "127.0.0.1:8080" in res.text @pytest.mark.asyncio diff --git a/workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md b/workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md new file mode 100644 index 0000000..03ee24e --- /dev/null +++ b/workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md @@ -0,0 +1,236 @@ +--- +id: ACTIVITY-WP-0025 +type: workplan +title: "SSO access control for ops console and Temporal UI" +domain: infotech +repo: activity-core +status: proposed +owner: codex +topic_slug: activity-core +created: "2026-07-22" +updated: "2026-07-22" +--- + +# SSO access control for ops console and Temporal UI + +## Origin + +ACTIVITY-WP-0024 shipped the operator automation console (`/ops/ui`) with +ClusterIP + port-forward access and a shared operator token for mutations. +Operators also use Temporal Web UI (`actcore-temporal-ui:8080`) for workflow +debugging; the ops UI now deep-links to it. + +**Pain:** port-forward + kubeconfig switching is fragile; shared token is not +identity-aware. **Goal:** proper browser access via SSO without port-forward. + +Closes SCOPE **G10** (API external access) for the **ops and Temporal UIs** +(not necessarily for unauthenticated programmatic API from the public internet). + +## Goal + +Expose **activity-core ops UI** and **Temporal Web UI** behind authenticated +ingress with **Keycloak / key-cape OIDC SSO**, so an authorized operator can: + +1. Open HTTPS URLs in a browser (no `kubectl port-forward`) +2. Authenticate with org SSO (MFA per IAM profile where required) +3. Use ops console mutations under **their identity** (not only a shared token) +4. Open Temporal UI under the same access policy + +## Non-goals + +- Public anonymous access to `/ops` or Temporal UI +- Replacing Temporal’s own RBAC model with fine-grained multi-tenant Temporal + namespaces for all of Coulomb (single activity-core Temporal is fine for MVP) +- Full API productization for third-party clients (focus is **human UI access**) +- Moving Temporal server hosting ownership out of the activity-core namespace + (unless railiance-platform already owns that path) + +## Architecture sketch (proposed) + +``` +Browser + │ HTTPS + ▼ +Ingress (TLS) ──► oauth2-proxy / key-cape edge ── OIDC ──► Keycloak (key-cape) + │ │ + │ allowlisted groups │ identity headers / session + ▼ ▼ +actcore-api (/ops, /ops/ui) actcore-temporal-ui + mutations: require SSO subject + (retire shared token as primary; keep break-glass optional) +``` + +Credential routing: **key-cape** owns OIDC/login (`warden route show key-cape-oidc-login`); +**flex-auth** for authorization decisions if used; **do not** invent secrets in +git or State Hub. + +## Design decisions to lock in T01 + +| Decision | Options | Lean | +| --- | --- | --- | +| Hostnames | e.g. `activity.ops.…` / `temporal.ops.…` vs path-based single host | Prefer **two hostnames** or one host with path prefixes | +| Auth edge | oauth2-proxy vs nginx auth_request vs key-cape native | Prefer **oauth2-proxy** or fleet-standard edge if one already exists | +| Mutation identity | SSO email/sub as principal; drop shared token for normal use | Shared token = break-glass only | +| Temporal UI auth | Same edge in front of `actcore-temporal-ui` | Yes for MVP | +| Who may access | Explicit Keycloak group (e.g. `activity-core-operators`) | Least privilege | + +## Tasks + +## Task: Access policy brief and hostnames + +```task +id: ACTIVITY-WP-0025-T01 +status: todo +priority: high +``` + +1. Document intended audience (founders / operators), MFA expectation, and + allowed IdP groups. +2. Propose hostnames + TLS cert path (cluster issuer / railiance DNS). +3. Align with railiance-platform / key-cape conventions; open coordination + messages if host or client registration is external. +4. Update SCOPE G10 with the chosen posture (UI SSO vs full public API). + +**Done when:** short design note in `docs/` or this workplan is approved; +hostnames and IdP group named. + +## Task: Inventory fleet SSO ingress patterns + +```task +id: ACTIVITY-WP-0025-T02 +status: todo +priority: high +``` + +1. Find an existing oauth2-proxy / OIDC ingress pattern on railiance01 or + coulombcore (railiance-platform, state-hub dashboard, etc.). +2. Prefer **reuse** over inventing a new edge stack. +3. Record the pattern (manifest paths, secrets ownership, callback URLs). + +**Done when:** “pattern to copy” is written with repo/file pointers. + +## Task: Ingress + TLS for ops API (read path) + +```task +id: ACTIVITY-WP-0025-T03 +status: todo +priority: high +``` + +1. Add Ingress (or Gateway) for `actcore-api` serving at least `/ops` and + `/ops/ui` (and health for probes as needed). +2. Terminate TLS with the platform cert strategy. +3. Gate with SSO edge; unauthenticated browser hits redirect to login. +4. Keep ClusterIP service; no LoadBalancer unless required. + +**Done when:** authorized browser reaches `/ops/ui` over HTTPS without +port-forward; unauthorized gets login or 403. + +## Task: Ingress + SSO for Temporal Web UI + +```task +id: ACTIVITY-WP-0025-T04 +status: todo +priority: high +``` + +1. Expose `actcore-temporal-ui` behind the same auth edge (hostname or path). +2. Ensure Temporal UI env (`TEMPORAL_ADDRESS`, public URL / CORS if required) + works behind the external hostname. +3. Ops console `ACTIVITY_CORE_TEMPORAL_UI_URL` points at the **public SSO URL**. + +**Done when:** Temporal UI opens from the ops nav link without a second +port-forward, under SSO. + +## Task: Map SSO identity into ops mutations + +```task +id: ACTIVITY-WP-0025-T05 +status: todo +priority: high +``` + +1. Accept trusted identity from the auth edge (e.g. `X-Forwarded-User` / + `X-Auth-Request-Email` — exact headers depend on T02 pattern). +2. Prefer SSO principal over shared `ACTIVITY_CORE_OPERATOR_TOKEN` for + mutations when present. +3. Audit log records SSO subject (never tokens). +4. Optional break-glass: keep operator token for emergency; document custody. + +**Done when:** Run now / pause audit shows human identity from SSO; shared +token not required for normal operator use. + +## Task: Keycloak client and group membership + +```task +id: ACTIVITY-WP-0025-T06 +status: todo +priority: medium +``` + +1. Register OIDC client(s) via key-cape / netkingdom process (not ad-hoc). +2. Create/bind operator group; grant only named operators. +3. Document how to add/remove operators. + +**Done when:** login works for a test operator and fails for an unprivileged +account. + +## Task: Docs and runbook cutover + +```task +id: ACTIVITY-WP-0025-T07 +status: todo +priority: medium +``` + +1. Runbook: primary access = SSO URLs; port-forward = break-glass only. +2. k8s README: Ingress manifests and secret ownership. +3. Credential routing notes: key-cape for login; no secrets in git. + +**Done when:** an operator can follow runbook without kubectl for daily use. + +## Task: Verify and harden + +```task +id: ACTIVITY-WP-0025-T08 +status: todo +priority: medium +``` + +1. Confirm ClusterIP APIs without Ingress path remain not publicly reachable. +2. Confirm mutations without SSO (and without break-glass token) fail closed. +3. Smoke: inventory, status, Run now dry path, Temporal UI namespaces list. +4. Optional: network policies locking ingress → services. + +**Done when:** checklist signed off in workplan closeout notes. + +## Success criteria + +- [ ] HTTPS SSO URLs for ops console and Temporal UI (no port-forward required) +- [ ] Only authorized IdP group can access +- [ ] Ops mutations attribute to SSO identity; audits show principal +- [ ] Ops nav Temporal link points at SSO Temporal URL +- [ ] Runbook documents SSO primary + break-glass port-forward +- [ ] SCOPE G10 updated to reflect UI SSO posture + +## Dependencies / coordination + +- **key-cape** / Keycloak: OIDC clients, groups (`warden route show key-cape-oidc-login`) +- **railiance-platform**: DNS, cert-manager, ingress controller norms +- **ACTIVITY-WP-0024**: ops console exists; this WP unlocks production access + +## Implementation order + +1. T01–T02 design + pattern reuse +2. T06 IdP client (can parallel with T03) +3. T03 ops Ingress + SSO +4. T04 Temporal UI Ingress + env URL +5. T05 mutation identity +6. T07–T08 docs and verify + +## References + +- `docs/runbook.md` — operator console + dual port-forward (pre-SSO) +- `src/activity_core/ops_api.py` — `ACTIVITY_CORE_TEMPORAL_UI_URL` +- `k8s/railiance/` — `actcore-api`, `actcore-temporal-ui` ClusterIP services +- ACTIVITY-WP-0024 operator automation console