Commit a1b9984c authored by Dimitrios Gogos's avatar Dimitrios Gogos
Browse files

docs: update ADR-0066 to clarify Keycloak API client registration details; add...

docs: update ADR-0066 to clarify Keycloak API client registration details; add implementation and delivery documentation for the portal
parent 031a5b89
Loading
Loading
Loading
Loading
+1 −1
Changes for decisions/0066-api-clients-through-keycloak-client-registration.md: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -55,7 +55,7 @@ What becomes easier:
  read their secrets, except `oeg`'s.
- API client tokens are ordinary Keycloak tokens that OEG already checks.
- The Keycloak calls a later sync needs (`read_scopes`, `set_scopes`, `set_secret`, `revoke`)
  are already built and tested against a real Keycloak.
  are already built, unit-tested, and checked by hand against a real Keycloak.

What becomes harder:

+199 −0
Changes for portal/implementation-and-delivery.md: 199 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
type: Implementation & Delivery
title: "Portal — Implementation & Delivery"
description: "How the portal code is laid out, its settings, how it is built and deployed, what it logs, how it is tested, the known gaps of Release 2, and what comes in the next release and how."
tags: [portal, implementation, delivery]
timestamp: 2026-10-08T00:00:00Z
component: Portal
version: "2.0"
status: "ETSI SDG OOP"
---

# Portal — Implementation & Delivery

**Companion documents:**
[portal/overview-and-architecture.md](/portal/overview-and-architecture.md),
[portal/requirements-and-design.md](/portal/requirements-and-design.md)

Code: the `portal` repository. The backend is in `src/portal_service/`, the React app in
`portal-gui/`, the Helm chart in `deployment/`.

---

## O. Code Layout

### O.1 Backend

| Folder | Holds |
|---|---|
| `domain/` | Models, errors, the API product catalog, and the ports (interfaces) the rest implements. No web, database or Keycloak code |
| `application/` | The rules: logging in, sessions, API clients, housekeeping, encryption |
| `adapters/database/` | Tables and repositories (SQLAlchemy, PostgreSQL) |
| `adapters/keycloak/` | Login, token checks, client registration, the Admin API calls |
| `api/` | Routes, wiring (`dependencies.py`), RFC 7807 errors, request-id and logging middleware, serving the app, health |
| `main.py`, `config.py`, `background.py` | Building the app, settings, the housekeeping loop |

import-linter enforces the direction: `domain` imports nothing of ours, and `application` never
imports `api` or `adapters`. Business rules can then be tested with in-memory stand-ins for every
port.

### O.2 React app

| Folder | Holds |
|---|---|
| `app/` | Router, routes, API addresses, and the guards (logged in, logged out, role) |
| `pages/` | One entry per route |
| `features/` | Each page's content |
| `components/ui/` | Base components (shadcn on Radix) |
| `components/common/`, `data-table/`, `forms/` | Pieces every page shares: page states, confirm dialog, tables, form fields |
| `components/<feature>/` | A feature's own components, such as dialogs and table columns |
| `layouts/` | The page frames: sidebar, dashboard and login layouts |
| `hooks/`, `services/`, `types/`, `lib/` | Data hooks, calls to the backend, types, helpers |

Behaviour that more than one page needs lives in the shared components, so every page behaves the
same. A fix to a table, dialog or form goes there, not into one feature.

---

## P. Configuration

Environment variables, nested with `__`. `.env.example` lists the local set; the Helm chart sets
them in the cluster.

| Group | Settings | Notes |
|---|---|---|
| Login | `AUTH__SECURITY_MODE` | `jwt` (default) or `none` for local development only |
| Keycloak | `OIDC__ISSUER`, `OIDC__CLIENT_ID`, `OIDC__CLIENT_SECRET`, `OIDC__REDIRECT_URI` | Required with `jwt`. The other endpoints are derived from the issuer. Helm points the backend's own calls (signing keys, token, Admin API) at Keycloak's in-cluster address; the login and logout pages stay public |
| Session | `SESSION__HTTPS_ONLY`, `SESSION__MAX_AGE` | `HTTPS_ONLY` sets the cookies' `Secure` flag |
| Database | `POSTGRESQL_SETTINGS__URL`, `POSTGRESQL_SETTINGS__CREATE_SCHEMA_ON_STARTUP` | Creating tables is off by default; Helm turns it on |
| OEG | `OEG__BASE_URL`, `OEG__PUBLIC_BASE_URL` | Where `/api/oeg/*` goes, and the address shown to developers |
| AI² | `AI2__CHAT_URL` | Empty: `/api/ai2/chat` answers `503` |
| API products | `API_PRODUCTS__FILE` | JSON list of products and scopes. The portal does not start without it |
| API clients | `PROVISIONING__CLIENT_ID`, `__CLIENT_SECRET`, `__OEG_CLIENT_UUID`, `__ENCRYPTION_KEY`, `__MAX_CLIENTS_PER_CREATOR` | All absent turns API clients off. The key is 64 hex characters |
| Other | `GUI_DIR`, `CLEANUP__INTERVAL_SECONDS`, `CORS__ALLOWED_ORIGINS`, `APP_VERSION` | `GUI_DIR` is set in the image; empty in development, where Vite serves the app |

---

## Q. Build and Deployment

**Image.** The `Dockerfile` has three stages: Node builds the React app, uv installs the backend,
and the final `python:3.12-slim` image holds only the virtualenv and the built app. It runs as a
non-root user on port 8000.

**Helm.** The chart is in the portal repository (`deployment/`) and, identical, in the helm
repository (`oop-platform-chart/charts/portal`). Change both.

| The chart | How |
|---|---|
| Database | Database and role `portal` from the platform's PostgreSQL chart; an init container waits until the role can log in |
| Keycloak secrets | `oop-portal` and `oop-portal-provisioner` secrets from `keycloak-credentials` |
| Encryption key | Generated into the Secret `portal-credentials` on first install, kept on `helm uninstall` |
| API products | From `global.apiProducts`, the same list Keycloak's product roles are built from |
| Login off | `global.auth.enabled: false` sets `AUTH__SECURITY_MODE=none` |
| Pod | `/healthz` and `/readyz` probes, non-root, read-only root filesystem |

**Local.** `make dev` runs the backend on `:8000` and Vite on `:3000`, which proxies `/api` to the
backend. The repository README has the steps.

---

## R. Observability

- **Logs.** One line when a request arrives and one when it completes, with request id, method,
  path, client, status and duration. Security events are logged on their own: why a login
  failed, refused cross-site writes, OEG or AI² refusing a token, an orphaned Keycloak client.
  Tokens and secrets are never logged. The output is structlog's default, not yet the
  platform's JSON format.
- **Request id.** `X-Request-ID` is taken from the request when it is a UUID4, otherwise made,
  and returned on the response.
- **Health.** `/healthz` answers while the process runs; `/readyz` checks the database.
- **Metrics.** None yet.

---

## S. Testing

| Kind | Covers | Runs with |
|---|---|---|
| Unit | Rules and routes with in-memory stand-ins for the ports; the Keycloak adapters against fake Keycloak HTTP; encryption, settings | `make test` (runs the integration tests too) |
| Integration | Repositories, routes and concurrent refresh on a real PostgreSQL; the login flow against a Keycloak container with a committed test realm. Optionally, a deployed realm's settings when `KEYCLOAK_ISSUER` is set | `make integration` (testcontainers, needs Docker) |
| Static | ruff (format and lint), mypy strict, import-linter; for the app, Prettier, ESLint, TypeScript | `make check` |

`make check` runs what the GitLab pipeline runs. The React app has no unit tests yet.

---

## T. Known Gaps in Release 2

These are deliberate. Each has a planned home in §U.

| Gap | Why it is acceptable for now |
|---|---|
| Nothing keeps the portal's records and Keycloak in sync (requirements §L.5) | A few trusted operators follow the how-to in the Keycloak chart README |
| API clients belong to their creator, not to an organization | OEG also scopes by creator in Release 2; the table already stores `application_owner_id` |
| A leaked secret is fixed by deleting and recreating the client; products cannot change | Keeps Release 2 to create, list and delete |
| A client an operator deleted in Keycloak cannot be deleted from the portal | Rare; the client is already gone from Keycloak, so only the portal's list is wrong |
| Revoking a user takes up to 5 minutes | OEG checks tokens locally, without asking Keycloak |
| Apps acting for an end user (browser or mobile, with consent) are not supported | Needs browser-login clients and consent, which OEG does not handle yet |

---

## U. Next Release

In the agreed order. Each item says what is missing, how we plan to build it, and what is
already in place.

### U.1 Ownership by organization

- **Missing.** API clients, their lists and their clean-up follow the creator. Product grants are
  per user.

  `organization`.

### U.2 Keeping the portal and Keycloak in sync

- **Missing.** A disabled developer's clients keep working until an operator deletes them
  (ADR-0066).

### U.3 API client lifecycle

- **Missing.** Rotating a secret, disabling and enabling a client, and changing its products.

### U.4 Operator approvals in the portal

- **Missing.** Operators grant products in the Keycloak console.

### U.5 User administration

- **Missing.** `platform-admin` has no page (ADR-0048 decision 4).

### U.6 No-access state and "Your API products"

- **Missing.** A page for a product the user does not hold shows OEG's error.

### U.7 Quality on Demand and Traffic Influence pages

- **Missing.** Only Edge Application Management has pages to work with; the other APIs have the
  catalog and "try it".

### U.8 Deployments

- **Missing.** The app treats OEG's `501` on deployments as "not available yet". OEG `develop` now
  accepts deployments to several zones; updating a deployment still answers `501`.

### U.9 AI² conversation history

- **Missing.** The list of conversations lives only in the browser tab and is lost on reload.

### U.10 Later

A TMF931-shaped API in front of the same services, then the product catalogue (TMF936) and usage
(TMF937). OEG logging which client called (`azp`) already makes usage per client possible.

---

## Revision History

| Version | Date | Summary |
|---------|------|---------|
| 2.0 | 2026-10-08 | First version, describing the Release 2 implementation and the next release. |