feat: Phase 5a — REST and SSE API with tenant isolation - #22
Open
TheMeinerLP wants to merge 2 commits into
Open
Conversation
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.
TheMeinerLP
force-pushed
the
clean2/phase-4-sharding
branch
from
August 9, 2026 10:30
570d358 to
ddd0c69
Compare
TheMeinerLP
force-pushed
the
clean2/phase-5-api
branch
from
August 9, 2026 10:30
3be773d to
6df10e1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
BlueMapMapControllerHttpTestwith fakes, andTenantIsolationIntegrationTestagainst a real k3s cluster with a real JWT and a real resource belonging to tenant B.What this delivers
platform-admin,tenant-owner,tenant-operator,tenant-viewerper spec §10.3resourceVersionand 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_URLis configured, direct pod logs otherwise. The Loki path needs no pod RBAC at all, which is what spec §11.1 intended; the fallback needsget/liston 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
reactor-corewas deliberately not added; the hand-rolledSseSourceis kept and covered for abort, error and cancel.