feat(client)!: move the client API under /projects/{projectID}#262
Conversation
|
Held back from the batch admin-merge. The rest of the stack (#243–#255, #261) is now in Why it can't be merged mechanically: #261 (now merged) replaced the What this PR actually wants is the trusted-issuer project-scoping (project_id column + project-scoped cache keys + child-row cleanup on delete) and the |
The client API derived the project from the authenticating credential, so a
trusted-issuer JWT was resolved by its self-asserted `iss` with no project
scope — a cross-tenant resolution gap. The project is now an explicit path
segment on every authenticated client endpoint:
/api/client/... -> /api/client/projects/{projectID}/...
- spec + regenerated chi-server bindings; every handler takes projectID.
- auth: every credential type fails closed unless its project matches the URL
(enforceURLProject); trusted-issuer resolution is now (project_id, issuer).
- store: GetTrustedIssuer is project-scoped; the issuer cache key is
project-namespaced; UNIQUE(project_id, issuer) among active methods
(denormalized project column + hard-delete child on soft-delete).
- docs updated.
BREAKING CHANGE: all client API paths require the project UUID; no legacy routes.
…sts on main Two PR merges collided in the v1/client test package, breaking `make lint` and `make test` on main (the build failure masked the second issue): - `capturingPublisher` was declared in both inbox_idor_test.go and ownscope_helpers_test.go. Drop the duplicate (simpler) copy from inbox_idor_test.go and adapt publishedIdentifiers to the canonical captured()/capturedMessage API in ownscope_helpers_test.go. - ownDataActor/allDataActor built actors with a random UUID and no auth-method row. After per-grant create-constraint enforcement landed (#261), CreateConstraints.Enforce looks the method up by the actor id and fails closed on a missing row (sql.ErrNoRows -> 500), breaking every PostUserEvents own/all-data test. Provision a real, unconstrained auth method and use its id as the actor identity, mirroring actorContext. go vet ./... and the full v1/client suite pass.
…RL rework These suites landed on main after this branch's base and exercise the pre-rework contracts; rebasing onto the grant_instances model surfaced them: - store invariants: the trusted-issuer invariant flipped from "globally unique" to "unique per project, reusable across projects". Rewrite the subtest to assert per-project uniqueness plus cross-tenant resolution isolation (each project resolves the shared iss to its own method). - auth middleware: WithSession / WithTrustedIssuer now bind the credential to the URL project (enforceURLProject / project-scoped GetTrustedIssuer). Drive the existing positive tests through clientRequestCtx so they carry the URL project, and update the issuerDB fake to match the new (project_id, issuer) query. - client controllers: every chi-server handler now takes a ProjectID path param. Thread it through the own-scope/guard/session/inbox/event test call sites (uuid.Nil where the handler derives the project from the actor; the real project where CreateSession enforces URL == method project). go vet ./... clean; full suite green.
453f75e to
cdcf9a9
Compare
Reworked onto
|
The client API derived the project from the authenticating credential, which left trusted-issuer JWTs resolved by their self-asserted
isswith no project scope — a cross-tenant resolution gap. This makes the project an explicit path segment on every authenticated client endpoint.{projectID}is the project UUID. Auth headers are unchanged — same API key / trusted-issuer JWT / session token; only the URL gains the project. This is a deliberate hard cut — no backwards-compatible routes.What changed
/api/client/*routes moved under/api/client/projects/{projectID}/…; regenerated chi-server bindings (every handler takesprojectID)./unsubscribeand/preferences/{projectID}/{userID}(public pages) are untouched.enforceURLProject). Trusted-issuer resolution is now(project_id, issuer)-scoped, so a self-assertedisscan never reach another project's method.GetTrustedIssuer(projectID, issuer); the Redis issuer-cache key is project-namespaced;UNIQUE(project_id, issuer)among active methods (denormalised project column + hard-delete of the trusted-issuer child on soft-delete, so an issuer is freed for re-registration).Contract delta for clients / SDKs
Every authenticated client endpoint gains
/projects/{projectID}after/api/client; the session-mint endpoint becomes/api/client/projects/{projectID}/auth-methods/{authMethodID}/sessions. A credential presented on a different project's URL is rejected (401). SDK PRs are open: lunogram/go-sdk#2, lunogram/js-sdk#7, lunogram/py-sdk#1.Verification
go build/vet ./...,gofmtclean; auth / store-management / client + management controller / rbac suites pass. New tests cover project mismatch rejection, same-issuer-different-project, and re-registration after delete.