chore(db): force row-level security on media_assets
Migration 042 enables and forces the tenant_isolation policy on media_assets with an explicit WITH CHECK; media and publishing queries also scope by app.workspace_id.
This commit is contained in:
parent
ad0f1be6cf
commit
fb81fab7bb
3 changed files with 14 additions and 3 deletions
11
apps/api/migrations/042_media_assets_rls.sql
Normal file
11
apps/api/migrations/042_media_assets_rls.sql
Normal file
|
|
@ -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);
|
||||||
|
|
@ -170,7 +170,7 @@ export class MediaService {
|
||||||
/** Auth-scoped read: RLS already fences this to the caller's workspace. */
|
/** Auth-scoped read: RLS already fences this to the caller's workspace. */
|
||||||
async readAsset(workspaceId: string, id: string): Promise<MediaAssetRecord & { bytes: Buffer }> {
|
async readAsset(workspaceId: string, id: string): Promise<MediaAssetRecord & { bytes: Buffer }> {
|
||||||
const asset = await withTenant(workspaceId, async (client) => {
|
const asset = await withTenant(workspaceId, async (client) => {
|
||||||
const result = await client.query<MediaRow>("SELECT * FROM media_assets WHERE id = $1", [id]);
|
const result = await client.query<MediaRow>("SELECT * FROM media_assets WHERE id = $1 AND workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid", [id]);
|
||||||
const row = result.rows[0];
|
const row = result.rows[0];
|
||||||
if (row === undefined) throw new NotFoundException(`media asset ${id} not found`);
|
if (row === undefined) throw new NotFoundException(`media asset ${id} not found`);
|
||||||
return fromRow(row);
|
return fromRow(row);
|
||||||
|
|
@ -192,7 +192,7 @@ export class MediaService {
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (mediaAssetIds.length === 0) return;
|
if (mediaAssetIds.length === 0) return;
|
||||||
const result = await client.query(
|
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],
|
[mediaAssetIds],
|
||||||
);
|
);
|
||||||
const count = Number(result.rows[0]?.count ?? 0);
|
const count = Number(result.rows[0]?.count ?? 0);
|
||||||
|
|
|
||||||
|
|
@ -657,7 +657,7 @@ export class PublishingService {
|
||||||
async function assertMediaAssetsExist(client: PoolClient, mediaAssetIds: string[]): Promise<void> {
|
async function assertMediaAssetsExist(client: PoolClient, mediaAssetIds: string[]): Promise<void> {
|
||||||
if (mediaAssetIds.length === 0) return;
|
if (mediaAssetIds.length === 0) return;
|
||||||
const result = await client.query<{ count: string | number }>(
|
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],
|
[mediaAssetIds],
|
||||||
);
|
);
|
||||||
if (Number(result.rows[0]?.count ?? 0) !== mediaAssetIds.length) {
|
if (Number(result.rows[0]?.count ?? 0) !== mediaAssetIds.length) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue