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, <% }) %> }), })