From 58e089b9f785fba6297f75959386e77105970255 Mon Sep 17 00:00:00 2001 From: cultron Date: Wed, 28 Oct 2026 22:49:22 -0500 Subject: [PATCH] fix(auth): scopeBy fields with other names take the caller's tenant ScopeGuard and @CrudAuth looked the tenant up under the scopeBy field's own name, which the auth guards only set for workspaceId and organizationId. An imported schema scoped on orgId refused every request. With auth configured, both now fall back to the matching auth context field, then the auth tenant. The auth smoke covers a Note entity scoped on orgId. --- scripts/auth-smoke.mjs | 11 +++++++++++ src/lib/templates/guards/scope.guard.eta | 5 ++++- src/lib/templates/rest/controller-rest.eta | 4 ++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/scripts/auth-smoke.mjs b/scripts/auth-smoke.mjs index 8c50f6f..d8f7b7d 100644 --- a/scripts/auth-smoke.mjs +++ b/scripts/auth-smoke.mjs @@ -225,6 +225,8 @@ const ENTITIES = [ ], }, { name: "Project", created_at: true, updated_at: true, scopeBy: "organizationId", fields: [{ name: "title", type: "text" }] }, + // A tenant field that is not named workspaceId/organizationId (imported schemas). + { name: "Note", created_at: true, updated_at: true, scopeBy: "orgId", fields: [{ name: "body", type: "text" }, { name: "orgId", type: "integer", nullable: true }] }, ]; const RELATIONSHIPS = [ { from: "Organization", to: "ApiKey", type: "OneToMany" }, @@ -357,6 +359,15 @@ async function crudAndScope(base, t, auth, auth2, noTenant) { const mine = await call("GET", base + PROJECTS, { headers: as1 }); t.assert("own list includes the project", rows(mine.json).some((p) => p.id === pid)); if (noTenant) t.expect("caller with no tenant cannot list scoped rows", await call("GET", base + PROJECTS, { headers: noTenant }), 403); + // scopeBy "orgId" takes the caller's tenant too. + const NOTES = route("Notes"); + const note = await call("POST", base + NOTES, { headers: as1, body: { body: "hi", orgId: id2 } }); + if (t.expect("create note (scopeBy orgId)", note, 200, 201)) { + t.assert("note stores the caller's tenant", String(note.json?.orgId) === String(id1), `orgId=${note.json?.orgId}`); + t.expect("other tenant cannot read the note", await call("GET", `${base}${NOTES}/${note.json.id}`, { headers: as2 }), 403, 404); + const theirNotes = await call("GET", base + NOTES, { headers: as2 }); + t.assert("other tenant's notes exclude it", theirNotes.status === 200 && !rows(theirNotes.json).some((n) => n.id === note.json.id), `${theirNotes.status}`); + } } t.expect("delete project", await call("DELETE", `${base}${PROJECTS}/${pid}`, { headers: as1 }), 200, 204); } diff --git a/src/lib/templates/guards/scope.guard.eta b/src/lib/templates/guards/scope.guard.eta index 9335644..ec53cb1 100644 --- a/src/lib/templates/guards/scope.guard.eta +++ b/src/lib/templates/guards/scope.guard.eta @@ -324,7 +324,10 @@ export class ScopeGuard implements CanActivate { request[contextKey] || request.user?.[contextKey] || request.scope?.[contextKey] || - request.context?.[contextKey] + request.context?.[contextKey]<% if (it.hasAuth) { %> || + // A scope field with another name (orgId, tenantId) is the caller's tenant. + request.auth?.[contextKey] || + request.auth?.workspaceId<% } %> ); } diff --git a/src/lib/templates/rest/controller-rest.eta b/src/lib/templates/rest/controller-rest.eta index 9a85458..941eee0 100644 --- a/src/lib/templates/rest/controller-rest.eta +++ b/src/lib/templates/rest/controller-rest.eta @@ -56,12 +56,12 @@ import {<%= it.svcName %>} from './<%= it.entityName %>.service'; @CrudAuth({ filter: (req: any) => ({ <% it.crudAuth.forEach((s) => { %> - <%= s.field %>: req.<%= s.contextKey %> ?? req.user?.<%= s.contextKey %>, + <%= s.field %>: req.<%= s.contextKey %> ?? req.user?.<%= s.contextKey %> ?? req.auth?.<%= s.contextKey %> ?? req.auth?.workspaceId, <% }) %> }), persist: (req: any) => ({ <% it.crudAuth.forEach((s) => { %> - <%= s.field %>: req.<%= s.contextKey %> ?? req.user?.<%= s.contextKey %>, + <%= s.field %>: req.<%= s.contextKey %> ?? req.user?.<%= s.contextKey %> ?? req.auth?.<%= s.contextKey %> ?? req.auth?.workspaceId, <% }) %> }), })