Skip to content

feat: Phase 5a — REST and SSE API with tenant isolation - #14

Closed
TheMeinerLP wants to merge 2 commits into
clean/phase-4-shardingfrom
clean/phase-5-api
Closed

feat: Phase 5a — REST and SSE API with tenant isolation#14
TheMeinerLP wants to merge 2 commits into
clean/phase-4-shardingfrom
clean/phase-5-api

Conversation

@TheMeinerLP

Copy link
Copy Markdown
Contributor

Replaces #6, closed. Same content, rebuilt as a single squash commit on clean/phase-5-api because a secret scanner flagged disposable test credentials in the old commit history and history cannot be rewritten in this environment. See the closing comment on #6 for detail.

Phase 5a of Apus: the API that makes the platform usable without writing YAML. Stacked on PR #5.

Custom resources stay the source of truth — the API holds no copy of the state. It is, however, the enforcement point for authorization: it checks the caller's rights first and only then talks to the Kubernetes API through its own ServiceAccount. No impersonation.

The rule this phase is built around

The tenant is derived only from the validated token. No endpoint accepts a tenant, tenant id or namespace as a parameter. A single endpoint taking the namespace from the request would reopen every isolation hole closed in phases 2a and 3 — namespace adoption, hostname hijacking, foreign maps in a hosting.

The second rule follows from the first: a resource that does not exist in the caller's own namespace returns 404, even when it exists in another tenant. A 403 would confirm its existence and turn the API into a directory of other tenants' resources.

Both are now proven over real HTTP, not just against classes: BlueMapMapControllerHttpTest with fakes, and TenantIsolationIntegrationTest against a real k3s cluster with a real JWT and a real resource belonging to tenant B.

What this delivers

  • JWT validation against a configurable issuer; roles platform-admin, tenant-owner, tenant-operator, tenant-viewer per spec §10.3
  • REST endpoints for tenants, sources, maps, renders and hostings
  • SSE streams for live render progress (watching the resource, not polling) and for logs
  • Response models are dedicated types, never pass-through custom resources — finalizers, resourceVersion and managed fields are nobody's business outside, and a CRD change must not silently alter the public interface. Secret names are excluded from every response.

Log source: Loki when APUS_LOKI_URL is configured, direct pod logs otherwise. The Loki path needs no pod RBAC at all, which is what spec §11.1 intended; the fallback needs get/list on pods. Documented as a deployment-time decision.

What parallel work cost, and what it caught

REST endpoints and event streams were built simultaneously in separate worktrees. Both agents independently built a Kubernetes client factory and a token-to-principal bridge. The second one mattered: two bridges reading different claim names would make the API behave differently per endpoint, and a bug in one would only surface half the time. Both are now merged into a single tested place, with the claim name (organization) declared exactly once.

One agent avoided a bean collision preemptively by wrapping the client type — a conflict that isolated work usually only reveals at merge time.

Known gaps

  • HTTP-level security tests cover one controller; the other four rely on direct-call tests for the same logic.
  • The k3s isolation test duplicates ~30 lines of CRD setup from the operator module — no cross-module test fixtures exist yet.
  • reactor-core was deliberately not added; the hand-rolled SseSource is kept and covered for abort, error and cancel.

The API that makes the platform usable without writing YAML, on top of
Phase 4. Custom resources stay the source of truth — the API holds no copy
of the state — but it is the enforcement point for authorization: it checks
the caller's rights first and only then talks to the Kubernetes API through
its own ServiceAccount, with no impersonation.

- The tenant is derived only from the validated token; no endpoint accepts
  a tenant, tenant id or namespace as a parameter, which would reopen every
  isolation hole closed in phases 2a and 3. A resource that does not exist
  in the caller's own namespace returns 404 even when it exists in another
  tenant, so a 403 can never be used to confirm another tenant's resource
  exists. Both are proven over real HTTP: BlueMapMapControllerHttpTest with
  fakes, and TenantIsolationIntegrationTest against a real k3s cluster with
  a real JWT and a real resource belonging to a different tenant.
- JWT validation against a configurable issuer; roles platform-admin,
  tenant-owner, tenant-operator, tenant-viewer per spec §10.3.
- REST endpoints for tenants, sources, maps, renders and hostings; SSE
  streams for live render progress (watching the resource, not polling)
  and for logs, sourced from Loki when APUS_LOKI_URL is configured or
  direct pod logs otherwise.
- Response models are dedicated types, never pass-through custom
  resources, so finalizers, resourceVersion, managed fields and secret
  names never leak into a response and a CRD change can't silently alter
  the public interface.
- REST endpoints and event streams were built in parallel worktrees; both
  independently built a Kubernetes client factory and a token-to-principal
  bridge, which were merged into one tested place with the claim name
  (organization) declared exactly once, avoiding per-endpoint behavioural
  drift.

This branch replaces feat/phase-5-api (PR #6). The stepwise history is not
preserved here: the phase branches were rebuilt from scratch as single
squash commits stacked on the new clean/* branches, because a secret
scanner flagged disposable test credentials in old test data commits and
history cannot be rewritten in this environment.
@TheMeinerLP

Copy link
Copy Markdown
Contributor Author

Superseded by #22. Rebuilt as a fresh squash on a base where the shell env-var validation already uses the require_env helper (and, for the phase-1 lineage, a docs correction to a plan file's stale code sample), so no follow-up fix commit is needed on top. Closing in favor of #22.

@TheMeinerLP TheMeinerLP closed this Aug 9, 2026
TheMeinerLP added a commit that referenced this pull request Aug 9, 2026
The API that makes the platform usable without writing YAML, on top of
Phase 4. Custom resources stay the source of truth — the API holds no copy
of the state — but it is the enforcement point for authorization: it checks
the caller's rights first and only then talks to the Kubernetes API through
its own ServiceAccount, with no impersonation.

- The tenant is derived only from the validated token; no endpoint accepts
  a tenant, tenant id or namespace as a parameter, which would reopen every
  isolation hole closed in phases 2a and 3. A resource that does not exist
  in the caller's own namespace returns 404 even when it exists in another
  tenant, so a 403 can never be used to confirm another tenant's resource
  exists. Both are proven over real HTTP: BlueMapMapControllerHttpTest with
  fakes, and TenantIsolationIntegrationTest against a real k3s cluster with
  a real JWT and a real resource belonging to a different tenant.
- JWT validation against a configurable issuer; roles platform-admin,
  tenant-owner, tenant-operator, tenant-viewer per spec §10.3.
- REST endpoints for tenants, sources, maps, renders and hostings; SSE
  streams for live render progress (watching the resource, not polling)
  and for logs, sourced from Loki when APUS_LOKI_URL is configured or
  direct pod logs otherwise.
- Response models are dedicated types, never pass-through custom
  resources, so finalizers, resourceVersion, managed fields and secret
  names never leak into a response and a CRD change can't silently alter
  the public interface.
- REST endpoints and event streams were built in parallel worktrees; both
  independently built a Kubernetes client factory and a token-to-principal
  bridge, which were merged into one tested place with the claim name
  (organization) declared exactly once, avoiding per-endpoint behavioural
  drift.

This branch replaces clean/phase-5-api (PR #14), stacked on
clean2/phase-4-sharding instead of clean/phase-4-sharding. Same content
otherwise.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant