Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions scripts/auth-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down Expand Up @@ -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);
}
Expand Down
5 changes: 4 additions & 1 deletion src/lib/templates/guards/scope.guard.eta
Original file line number Diff line number Diff line change
Expand Up @@ -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<% } %>
);
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib/templates/rest/controller-rest.eta
Original file line number Diff line number Diff line change
Expand Up @@ -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,
<% }) %>
}),
})
Expand Down
Loading