fix(auth): resolve the tenant membership inside the tenant scope (coreci-chat#13)
ci / test-pglite (PGlite, default) (push) Failing after 1m50s
ci / test-postgres (Postgres 16, G-022 RLS) (push) Failing after 1m24s
ci / test-pglite (PGlite, default) (pull_request) Failing after 1m49s
ci / test-postgres (Postgres 16, G-022 RLS) (pull_request) Failing after 1m24s
ci / test-pglite (PGlite, default) (push) Failing after 1m50s
ci / test-postgres (Postgres 16, G-022 RLS) (push) Failing after 1m24s
ci / test-pglite (PGlite, default) (pull_request) Failing after 1m49s
ci / test-postgres (Postgres 16, G-022 RLS) (pull_request) Failing after 1m24s
This commit is contained in:
@@ -80,41 +80,51 @@ export async function provisionTenant(
|
||||
createdUser = true;
|
||||
}
|
||||
|
||||
// 3. Resolve-or-create the membership (tenant-scoped).
|
||||
const membership = await resolveMembership(db, tenant.id, user.id);
|
||||
if (!membership) {
|
||||
const role: "admin" | "operator" | "viewer" = createdTenant ? "admin" : "viewer";
|
||||
await withTenant(tenant.id, async (c: ScopedClient) => {
|
||||
await c.query(
|
||||
`INSERT INTO tenant_memberships (tenant_id, user_id, role)
|
||||
VALUES ($1, $2, $3)`,
|
||||
[tenant.id, user.id, role],
|
||||
);
|
||||
// Audit: provision event (only when we actually created something).
|
||||
await appendAudit(c, {
|
||||
tenantId: tenant.id,
|
||||
eventType: "provision",
|
||||
payload: {
|
||||
workosOrgId,
|
||||
workosUserId,
|
||||
email,
|
||||
role,
|
||||
createdTenant,
|
||||
createdUser,
|
||||
},
|
||||
userId: user.id,
|
||||
});
|
||||
// 3. Resolve-or-create the membership (tenant-scoped). The lookup runs
|
||||
// INSIDE the tenant scope on purpose: under RLS a statement outside a
|
||||
// scope sees zero rows, so resolving outside always missed an existing
|
||||
// membership and re-inserted it (the unique constraint on
|
||||
// (tenant_id, user_id) then rejected the second call). It also keeps the
|
||||
// statement off the empty-scope crash path: casting an unset scope to
|
||||
// uuid raised `invalid input syntax for type uuid: ""` under real
|
||||
// Postgres (coreci-chat#13).
|
||||
const role: "admin" | "operator" | "viewer" = createdTenant ? "admin" : "viewer";
|
||||
const membership = await withTenant(tenant.id, async (c: ScopedClient) => {
|
||||
const found = await resolveMembership(c, tenant.id, user.id);
|
||||
if (found) return { role: found.role, created: false };
|
||||
await c.query(
|
||||
`INSERT INTO tenant_memberships (tenant_id, user_id, role)
|
||||
VALUES ($1, $2, $3)`,
|
||||
[tenant.id, user.id, role],
|
||||
);
|
||||
// Audit: provision event (only when we actually created something).
|
||||
await appendAudit(c, {
|
||||
tenantId: tenant.id,
|
||||
eventType: "provision",
|
||||
payload: {
|
||||
workosOrgId,
|
||||
workosUserId,
|
||||
email,
|
||||
role,
|
||||
createdTenant,
|
||||
createdUser,
|
||||
},
|
||||
userId: user.id,
|
||||
});
|
||||
return { role, created: true };
|
||||
});
|
||||
|
||||
if (membership.created) {
|
||||
return {
|
||||
tenantId: tenant.id,
|
||||
userId: user.id,
|
||||
role,
|
||||
role: membership.role,
|
||||
createdTenant,
|
||||
createdUser,
|
||||
};
|
||||
}
|
||||
|
||||
// Both exist + membership exists → idempotent. No audit (nothing changed).
|
||||
// Both exist + membership exists -> idempotent. No audit (nothing changed).
|
||||
return {
|
||||
tenantId: tenant.id,
|
||||
userId: user.id,
|
||||
@@ -123,7 +133,6 @@ export async function provisionTenant(
|
||||
createdUser: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveTenantByOrg(db: ProvisionDb, workosOrgId: string): Promise<TenantRow | null> {
|
||||
const res = await db.query<TenantRow>(
|
||||
"SELECT id, name FROM tenants WHERE workos_org_id = $1",
|
||||
|
||||
Reference in New Issue
Block a user