diff --git a/apps/api/migrations/042_media_assets_rls.sql b/apps/api/migrations/042_media_assets_rls.sql new file mode 100644 index 0000000..1325a28 --- /dev/null +++ b/apps/api/migrations/042_media_assets_rls.sql @@ -0,0 +1,11 @@ +-- 042: media_assets row-level security. +-- +-- Enable and force the tenant_isolation policy declared in 038, with an +-- explicit WITH CHECK. All media_assets queries run inside withTenant. + +ALTER TABLE media_assets ENABLE ROW LEVEL SECURITY; +ALTER TABLE media_assets FORCE ROW LEVEL SECURITY; +DROP POLICY IF EXISTS tenant_isolation ON media_assets; +CREATE POLICY tenant_isolation ON media_assets + USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid) + WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid); diff --git a/apps/api/src/media/media.service.ts b/apps/api/src/media/media.service.ts index 871d00d..31515a2 100644 --- a/apps/api/src/media/media.service.ts +++ b/apps/api/src/media/media.service.ts @@ -170,7 +170,7 @@ export class MediaService { /** Auth-scoped read: RLS already fences this to the caller's workspace. */ async readAsset(workspaceId: string, id: string): Promise { const asset = await withTenant(workspaceId, async (client) => { - const result = await client.query("SELECT * FROM media_assets WHERE id = $1", [id]); + const result = await client.query("SELECT * FROM media_assets WHERE id = $1 AND workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid", [id]); const row = result.rows[0]; if (row === undefined) throw new NotFoundException(`media asset ${id} not found`); return fromRow(row); @@ -192,7 +192,7 @@ export class MediaService { ): Promise { if (mediaAssetIds.length === 0) return; const result = await client.query( - "SELECT count(*)::integer AS count FROM media_assets WHERE id = ANY($1::uuid[])", + "SELECT count(*)::integer AS count FROM media_assets WHERE id = ANY($1::uuid[]) AND workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid", [mediaAssetIds], ); const count = Number(result.rows[0]?.count ?? 0); diff --git a/apps/api/src/publishing/publishing.service.ts b/apps/api/src/publishing/publishing.service.ts index 278edac..2b7bc01 100644 --- a/apps/api/src/publishing/publishing.service.ts +++ b/apps/api/src/publishing/publishing.service.ts @@ -657,7 +657,7 @@ export class PublishingService { async function assertMediaAssetsExist(client: PoolClient, mediaAssetIds: string[]): Promise { if (mediaAssetIds.length === 0) return; const result = await client.query<{ count: string | number }>( - "SELECT count(*)::integer AS count FROM media_assets WHERE id = ANY($1::uuid[])", + "SELECT count(*)::integer AS count FROM media_assets WHERE id = ANY($1::uuid[]) AND workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid", [mediaAssetIds], ); if (Number(result.rows[0]?.count ?? 0) !== mediaAssetIds.length) {