-- Task #423: tasks.organization_id NOT NULL + RLS policies -- ============================================================ -- Step 1: backfill any NULL organization_id in tasks from parent form UPDATE tasks SET organization_id = forms.organization_id FROM forms WHERE tasks.form_id = forms.id AND tasks.organization_id IS NULL; -- Step 2: enforce NOT NULL (safe after backfill above) ALTER TABLE tasks ALTER COLUMN organization_id SET NOT NULL; -- ============================================================ -- Step 3: Row-Level Security policies for tenant tables. -- -- Policy expression for DIRECT organization_id tables: -- USING ( -- current_setting('app.is_superadmin', true) = 'true' -- OR organization_id = NULLIF(current_setting('app.current_org_id', true), '')::int -- ) -- -- Policy expression for JOIN-based tables (no direct organization_id): -- USING ( -- current_setting('app.is_superadmin', true) = 'true' -- OR IN (SELECT id FROM WHERE organization_id = ...) -- ) -- -- Policies are INACTIVE until `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` -- is also issued (done at startup when ENABLE_RLS=true). -- ============================================================ -- Direct organization_id tables DO $$ DECLARE t text; DECLARE tbls text[] := ARRAY[ 'tasks','users','forms','task_relations','user_notifications', 'message_reads','bookmarks','bookmark_folders','bots','bot_subscriptions', 'data_tables','invitations','user_custom_fields','device_tokens', 'web_push_subscriptions','organization_api_keys','automations', 'task_reminders','task_audit_log','conversations','global_fields', 'custom_tab_modules','external_services','notification_subscriptions' ]; BEGIN FOREACH t IN ARRAY tbls LOOP IF EXISTS (SELECT FROM pg_tables WHERE schemaname='public' AND tablename=t) THEN EXECUTE format('DROP POLICY IF EXISTS tenant_iso ON %I', t); EXECUTE format( 'CREATE POLICY tenant_iso ON %I USING (' ' current_setting(''app.is_superadmin'', true) = ''true''' ' OR organization_id = NULLIF(current_setting(''app.current_org_id'', true), '''')::int' ')', t); END IF; END LOOP; END $$; -- Join-based: form_fields, form_statuses, form_tabs, status_transitions -- (no direct organization_id — join through forms) DO $$ DECLARE t text; DECLARE tbls text[] := ARRAY['form_fields','form_statuses','form_tabs','status_transitions']; BEGIN FOREACH t IN ARRAY tbls LOOP IF EXISTS (SELECT FROM pg_tables WHERE schemaname='public' AND tablename=t) THEN EXECUTE format('DROP POLICY IF EXISTS tenant_iso ON %I', t); EXECUTE format( 'CREATE POLICY tenant_iso ON %I USING (' ' current_setting(''app.is_superadmin'', true) = ''true''' ' OR form_id IN (' ' SELECT id FROM forms' ' WHERE organization_id = NULLIF(current_setting(''app.current_org_id'', true), '''')::int' ' )' ')', t); END IF; END LOOP; END $$; -- Join-based: task_messages, task_field_values, field_history -- (no direct organization_id — join through tasks, which is now NOT NULL) DO $$ DECLARE t text; DECLARE tbls text[] := ARRAY['task_messages','task_field_values','field_history']; BEGIN FOREACH t IN ARRAY tbls LOOP IF EXISTS (SELECT FROM pg_tables WHERE schemaname='public' AND tablename=t) THEN EXECUTE format('DROP POLICY IF EXISTS tenant_iso ON %I', t); EXECUTE format( 'CREATE POLICY tenant_iso ON %I USING (' ' current_setting(''app.is_superadmin'', true) = ''true''' ' OR task_id IN (' ' SELECT id FROM tasks' ' WHERE organization_id = NULLIF(current_setting(''app.current_org_id'', true), '''')::int' ' )' ')', t); END IF; END LOOP; END $$; -- NOTE: ENABLE ROW LEVEL SECURITY is NOT issued here. -- It is applied at runtime by server/index.ts when ENABLE_RLS=true. -- This keeps the migration safe on environments not yet using withTenant.