From bb4eaef0df8173ec5f32cd5b8bf6b80f346a28d8 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 1 Jul 2026 23:34:14 +0800 Subject: [PATCH] Shift old policies UI into storage since its only being used there (#47497) ## Context This is one chonky boy of a PR, but it's just re-organizing files and folders to clean things up Storage Policies have been using the old Database policy UI (the one with the Dialog), so it makes most sense to shift those files under the `Storage` folder instead of keeping them under `Database`, so it's clearer which files are being consumed by whom, and easier to clean things up as well As part of this clean up, also tore out all the RLS generation logic from the Table Editor which are no longer used as they were affected by the change in files. Deprecated + deleted any unused code too ## Summary by CodeRabbit * **New Features** * Enhanced the policy editor with a centralized set of reusable templates, including general table templates plus predefined Realtime and queue access templates. * Updated the table creation flow so it no longer auto-generates additional RLS policy drafts. * **Bug Fixes** * Improved the policy table header badge layout for clearer RLS/API/lock indicators. * Simplified policy preview/save behavior so only meaningful edits are reflected in the applied SQL. * Streamlined the table-creation success messaging to remove conditional failure details. * **Tests** * Updated/removal of policy and table-creation test coverage to match the new behavior. --------- Co-authored-by: Ivan Vasilov --- .../Database/Policies/Policies.constants.ts | 345 ++++++++++++- .../Database/Policies/Policies.utils.test.ts | 450 ----------------- .../Database/Policies/Policies.utils.ts | 477 ------------------ .../PolicyEditorModal.constants.ts | 329 ------------ .../PolicyEditorPanel/PolicyTemplates.tsx | 2 +- .../PolicyTableRow/PolicyTableRowHeader.tsx | 22 +- .../Policies/PolicyTableRow/index.tsx | 4 +- .../PolicyTemplates.constants.ts | 14 - .../interfaces/Storage/Storage.types.ts | 2 +- .../PolicyEditor/PolicyAllowedOperation.tsx | 0 .../PolicyEditor/PolicyDefinition.tsx | 0 .../PolicyEditor/PolicyEditorFooter.tsx | 0 .../PolicyEditor/PolicyName.tsx | 0 .../PolicyEditor/PolicyRoles.tsx | 0 .../StoragePolicies}/PolicyEditor/index.tsx | 0 .../PolicyEditorModal.constants.ts | 6 + .../PolicyEditorModalTitle.tsx | 6 +- .../PolicyEditorModal/index.tsx | 22 +- .../StoragePolicies}/PolicyReview.tsx | 6 +- .../StoragePolicies}/PolicySelection.tsx | 4 +- .../PolicyTemplates/TemplatePreview.tsx | 2 +- .../PolicyTemplates/TemplatesList.tsx | 2 +- .../PolicyTemplates/index.tsx | 6 +- .../StoragePolicies/StoragePolicies.tsx | 10 +- .../StoragePolicies/StoragePolicies.types.ts} | 5 + .../StoragePolicies/StoragePolicies.utils.ts | 158 ++++++ .../StoragePoliciesEditPolicyModal.tsx | 10 +- .../StoragePolicies/StoragePoliciesEditor.tsx | 8 +- .../StoragePolicies/StoragePoliciesReview.tsx | 4 +- .../StoragePolicyEditorModalTitle.tsx | 2 +- .../SidePanelEditor/SidePanelEditor.tsx | 37 +- .../SidePanelEditor.utils.createTable.test.ts | 2 - .../SidePanelEditor/SidePanelEditor.utils.tsx | 51 +- .../TableEditor/TableEditor.tsx | 3 - .../pages/project/[ref]/database/policies.tsx | 2 +- 35 files changed, 567 insertions(+), 1424 deletions(-) delete mode 100644 apps/studio/components/interfaces/Database/Policies/Policies.utils.test.ts delete mode 100644 apps/studio/components/interfaces/Database/Policies/Policies.utils.ts delete mode 100644 apps/studio/components/interfaces/Database/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts delete mode 100644 apps/studio/components/interfaces/Database/Policies/PolicyTemplates/PolicyTemplates.constants.ts rename apps/studio/components/interfaces/{Database/Policies => Storage/StoragePolicies}/PolicyEditor/PolicyAllowedOperation.tsx (100%) rename apps/studio/components/interfaces/{Database/Policies => Storage/StoragePolicies}/PolicyEditor/PolicyDefinition.tsx (100%) rename apps/studio/components/interfaces/{Database/Policies => Storage/StoragePolicies}/PolicyEditor/PolicyEditorFooter.tsx (100%) rename apps/studio/components/interfaces/{Database/Policies => Storage/StoragePolicies}/PolicyEditor/PolicyName.tsx (100%) rename apps/studio/components/interfaces/{Database/Policies => Storage/StoragePolicies}/PolicyEditor/PolicyRoles.tsx (100%) rename apps/studio/components/interfaces/{Database/Policies => Storage/StoragePolicies}/PolicyEditor/index.tsx (100%) create mode 100644 apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditorModal/PolicyEditorModal.constants.ts rename apps/studio/components/interfaces/{Database/Policies => Storage/StoragePolicies}/PolicyEditorModal/PolicyEditorModalTitle.tsx (93%) rename apps/studio/components/interfaces/{Database/Policies => Storage/StoragePolicies}/PolicyEditorModal/index.tsx (95%) rename apps/studio/components/interfaces/{Database/Policies => Storage/StoragePolicies}/PolicyReview.tsx (92%) rename apps/studio/components/interfaces/{Database/Policies => Storage/StoragePolicies}/PolicySelection.tsx (98%) rename apps/studio/components/interfaces/{Database/Policies => Storage/StoragePolicies}/PolicyTemplates/TemplatePreview.tsx (92%) rename apps/studio/components/interfaces/{Database/Policies => Storage/StoragePolicies}/PolicyTemplates/TemplatesList.tsx (93%) rename apps/studio/components/interfaces/{Database/Policies => Storage/StoragePolicies}/PolicyTemplates/index.tsx (91%) rename apps/studio/components/interfaces/{Database/Policies/Policies.types.ts => Storage/StoragePolicies/StoragePolicies.types.ts} (95%) create mode 100644 apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.utils.ts diff --git a/apps/studio/components/interfaces/Database/Policies/Policies.constants.ts b/apps/studio/components/interfaces/Database/Policies/Policies.constants.ts index 060d9ee81c3..2e1e7dcb2a7 100644 --- a/apps/studio/components/interfaces/Database/Policies/Policies.constants.ts +++ b/apps/studio/components/interfaces/Database/Policies/Policies.constants.ts @@ -1,6 +1,341 @@ -export const POLICY_MODAL_VIEWS = { - SELECTION: 'SELECTION', - TEMPLATES: 'TEMPLATES', - EDITOR: 'EDITOR', - REVIEW: 'REVIEW', +import { safeSql } from '@supabase/pg-meta/src/pg-format' +import type { SafeSqlFragment } from '@supabase/pg-meta/src/pg-format' + +export interface PolicyTemplate { + id: string + preview: boolean + templateName: string + description: string + name: string + statement: string + definition: SafeSqlFragment + check: SafeSqlFragment + command: 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | 'ALL' + roles: Array +} + +/** + * ---------------------------------------------------------------- + * PostgreSQL policy templates for the auth policies page + * ---------------------------------------------------------------- + * id: Unique identifier for the monaco editor to dynamically refresh + * templateName: As a display for a more descriptive title for the policy + * description: Additional details about the template and how to make it yours + * statement: SQL statement template for the policy + * + * name: Actual policy name that will be used in the editor + * definition: Actual policy using expression that will be used in the editor + * check: Actual policy with check expression that will be used in the editor + * command: Operation to create policy for + */ + +export const getGeneralPolicyTemplates = (schema: string, table: string): PolicyTemplate[] => [ + { + id: 'policy-1', + preview: false, + templateName: 'Enable read access to everyone', + description: + 'This policy gives read access to your table for all users via the SELECT operation.', + statement: ` +create policy "Enable read access for all users" +on "${schema}"."${table}" +for select using (true);`.trim(), + name: 'Enable read access for all users', + definition: safeSql`true`, + check: safeSql``, + command: 'SELECT', + roles: [], + }, + { + id: 'policy-2', + preview: false, + templateName: 'Enable insert access for authenticated users only', + description: 'This policy gives insert access to your table for all authenticated users only.', + statement: ` +create policy "Enable insert for authenticated users only" +on "${schema}"."${table}" +for insert to authenticated +with check (true);`.trim(), + name: 'Enable insert for authenticated users only', + definition: safeSql``, + check: safeSql`true`, + command: 'INSERT', + roles: ['authenticated'], + }, + { + id: 'policy-3', + preview: false, + templateName: 'Enable delete access for users based on their user ID *', + description: + 'This policy assumes that your table has a column "user_id", and allows users to delete rows which the "user_id" column matches their ID', + statement: ` +create policy "Enable delete for users based on user_id" +on "${schema}"."${table}" +for delete using ( + (select auth.uid()) = user_id +);`.trim(), + name: 'Enable delete for users based on user_id', + definition: safeSql`(select auth.uid()) = user_id`, + check: safeSql``, + command: 'DELETE', + roles: [], + }, + { + id: 'policy-4', + preview: false, + templateName: 'Enable insert access for users based on their user ID *', + description: + 'This policy assumes that your table has a column "user_id", and allows users to insert rows which the "user_id" column matches their ID', + statement: ` +create policy "Enable insert for users based on user_id" +on "${schema}"."${table}" +for insert with check ( + (select auth.uid()) = user_id +);`.trim(), + name: 'Enable insert for users based on user_id', + definition: safeSql``, + check: safeSql`(select auth.uid()) = user_id`, + command: 'INSERT', + roles: [], + }, + { + id: 'policy-5', + preview: true, + name: 'Policy with table joins', + templateName: 'Policy with table joins', + description: ` +Query across tables to build more advanced RLS rules + +Assuming 2 tables called \`teams\` and \`members\`, you can query both tables in the policy to control access to the members table.`, + statement: ` +create policy "Members can update team details if they belong to the team" +on teams for update using ( + (select auth.uid()) in ( + select user_id from members where team_id = id + ) +); +`.trim(), + definition: safeSql`(select auth.uid()) in (select user_id from members where team_id = id)`, + check: safeSql``, + command: 'UPDATE', + roles: [], + }, + { + id: 'policy-6', + preview: true, + templateName: 'Policy with security definer functions', + description: ` +Useful in a many-to-many relationship where you want to restrict access to the linking table. + +Assuming 2 tables called \`teams\` and \`members\`, you can use a security definer function in combination with a policy to control access to the members table.`.trim(), + statement: ` +create or replace function get_teams_for_user(user_id uuid) +returns setof bigint as $$ + select team_id from members where user_id = $1 +$$ stable language sql security definer; + +create policy "Team members can update team members if they belong to the team" +on members +for all using ( + team_id in (select get_teams_for_user(auth.uid())) +); +`.trim(), + name: 'Policy with security definer functions', + definition: safeSql`team_id in (select get_teams_for_user(auth.uid()))`, + check: safeSql``, + command: 'ALL', + roles: [], + }, + { + id: 'policy-7', + preview: true, + name: 'Policy to implement Time To Live (TTL)', + templateName: 'Policy to implement Time To Live (TTL)', + description: ` +Implement a TTL-like feature that you see in Instagram stories or Snapchat where messages expire after a day. + +Rows under the table are available only if they have been created within the last 24 hours.`, + statement: ` +create policy "Stories are live for a day" +on "${schema}"."${table}" +for select using ( + created_at > (current_timestamp - interval '1 day') +); +`.trim(), + definition: safeSql`created_at > (current_timestamp - interval '1 day')`, + check: safeSql``, + command: 'SELECT', + roles: [], + }, + { + id: 'policy-8', + preview: false, + templateName: 'Allow users to only view their own data', + description: 'Restrict users to reading only their own data.', + statement: ` +create policy "Enable users to view their own data only" +on "${schema}"."${table}" +for select +to authenticated +using ( + (select auth.uid()) = user_id +);`.trim(), + name: 'Enable users to view their own data only', + definition: safeSql`(select auth.uid()) = user_id`, + check: safeSql``, + command: 'SELECT', + roles: ['authenticated'], + }, +] + +export const getRealtimePolicyTemplates = (): PolicyTemplate[] => { + const results = [ + { + id: 'policy-broadcast-1', + preview: false, + templateName: 'Allow listening for broadcasts for authenticated users only', + description: 'This policy allows listening for broadcasts for authenticated users only.', + statement: ` +create policy "Allow listening for broadcasts for authenticated users only" +on realtime.messages for select +to authenticated +using ( realtime.messages.extension = 'broadcast' );`.trim(), + name: 'Allow listening for broadcasts for authenticated users only', + definition: safeSql`realtime.messages.extension = 'broadcast'`, + check: safeSql``, + command: 'SELECT', + roles: ['authenticated'], + }, + { + id: 'policy-broadcast-2', + preview: false, + templateName: 'Allow pushing broadcasts for authenticated users only', + description: 'This policy allows pushing broadcasts for authenticated users only.', + statement: ` +create policy "Allow pushing broadcasts for authenticated users only" +ON realtime.messages for insert +TO authenticated +with check ( realtime.messages.extension = 'broadcast' );`.trim(), + name: 'Allow pushing broadcasts for authenticated users only', + definition: safeSql`realtime.messages.extension = 'broadcast'`, + check: safeSql`realtime.messages.extension = 'broadcast'`, + command: 'INSERT', + roles: ['authenticated'], + }, + { + id: 'policy-broadcast-3', + preview: false, + templateName: 'Allow listening for broadcasts from a specific channel', + description: 'This policy allows listening for broadcasts from a specific channel.', + statement: ` +create policy "Allow listening for broadcasts from a specific channel" +on realtime.messages for select +using ( realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name' );`.trim(), + name: 'Allow listening for broadcasts from a specific channel', + definition: safeSql`realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name'`, + check: safeSql``, + command: 'SELECT', + roles: [], + }, + { + id: 'policy-broadcast-4', + preview: false, + templateName: 'Allow pushing broadcasts to specific channel', + description: 'This policy allow pushing broadcasts to specific channel.', + statement: ` +create policy "Allow pushing broadcasts to specific channel" +ON realtime.messages for insert +with check ( realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name' );`.trim(), + name: 'Allow pushing broadcasts to specific channel', + definition: safeSql`realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name'`, + check: safeSql`realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name'`, + command: 'INSERT', + roles: [], + }, + { + id: 'policy-presences-1', + preview: false, + templateName: 'Allow listening for presences on all channels for authenticated users only', + description: + 'This policy enables listening for presences on all channels for all authenticated users only.', + statement: ` +create policy "Allow listening for presences on all channels for authenticated users only" +on realtime.messages for select +to authenticated +using ( realtime.messages.extension = 'presence' );`.trim(), + name: 'Allow listening for presences on all channels for authenticated users only', + definition: safeSql`realtime.messages.extension = 'presence'`, + check: safeSql``, + command: 'SELECT', + roles: ['authenticated'], + }, + { + id: 'policy-presences-2', + preview: false, + templateName: 'Allow broadcasting presences on all channels for authenticated users only', + description: + 'This policy enables broadcasting presences on all channels for all authenticated users only.', + statement: ` +create policy "Allow broadcasting presences on all channels for authenticated users only" +ON realtime.messages for insert +TO authenticated +with check ( realtime.messages.extension = 'presence' ); + ;`.trim(), + name: 'Allow broadcasting presences on all channels for authenticated users only', + definition: safeSql`realtime.messages.extension = 'presence'`, + check: safeSql`realtime.messages.extension = 'presence'`, + command: 'INSERT', + roles: ['authenticated'], + }, + { + id: 'policy-presences-3', + preview: false, + templateName: 'Allow listening for presences from a specific channel', + description: 'This policy enables listening for presences from a specific channel.', + statement: ` +create policy "Allow listening for presences from a specific channel" +on realtime.messages for select +using ( realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name' );`.trim(), + name: 'Allow listening for presences from a specific channel', + definition: safeSql`realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name'`, + check: safeSql``, + command: 'SELECT', + roles: [], + }, + { + id: 'policy-presences-4', + preview: false, + templateName: 'Publish presence to a specific channel', + description: 'This policy allows publishing presence to a specific channel.', + statement: ` +create policy "Publish presence to a specific channel" +ON realtime.messages for insert +with check ( realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name' ); + ;`.trim(), + name: 'Publish presence to a specific channel', + definition: safeSql`realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name'`, + check: safeSql`realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name'`, + command: 'INSERT', + roles: [], + }, + ] as PolicyTemplate[] + return results +} + +export const getQueuePolicyTemplates = (): PolicyTemplate[] => { + return [ + { + id: 'policy-queues-1', + preview: false, + templateName: 'Allow access to queue', + statement: ``.trim(), + name: 'Allow anon and authenticated to access messages from queue', + description: + 'Base policy to ensure that anon and authenticated can only access appropriate rows. USING and CHECK statements will need to be adjusted accordingly', + definition: safeSql`true`, + check: safeSql`true`, + command: 'ALL', + roles: ['anon', 'authenticated'], + }, + ] } diff --git a/apps/studio/components/interfaces/Database/Policies/Policies.utils.test.ts b/apps/studio/components/interfaces/Database/Policies/Policies.utils.test.ts deleted file mode 100644 index e785b11c2b9..00000000000 --- a/apps/studio/components/interfaces/Database/Policies/Policies.utils.test.ts +++ /dev/null @@ -1,450 +0,0 @@ -import { safeSql } from '@supabase/pg-meta' -import { beforeEach, describe, expect, it, vi } from 'vitest' - -import { - generateAiPoliciesForTable, - generateProgrammaticPoliciesForTable, - generateStartingPoliciesForTable, - type GeneratedPolicy, -} from './Policies.utils' -import type { ForeignKeyConstraint } from '@/data/database/foreign-key-constraints-query' - -// Mock generateSqlPolicy for AI tests -const mockGenerateSqlPolicy = vi.fn() -vi.mock('@/data/ai/sql-policy-mutation', () => ({ - generateSqlPolicy: (...args: unknown[]) => mockGenerateSqlPolicy(...args), -})) - -// Helper to create a foreign key constraint -const createForeignKey = (overrides: Partial = {}): ForeignKeyConstraint => ({ - id: 1, - constraint_name: 'fk_constraint', - source_id: 100, - source_schema: 'public', - source_table: 'posts', - source_columns: ['user_id'], - target_id: 200, - target_schema: 'auth', - target_table: 'users', - target_columns: ['id'], - deletion_action: 'NO ACTION', - update_action: 'NO ACTION', - ...overrides, -}) - -describe('Policies.utils - Policy Generation', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - describe('generateProgrammaticPoliciesForTable', () => { - it('should generate 4 CRUD policies for direct FK to auth.users', () => { - const foreignKeyConstraints: ForeignKeyConstraint[] = [ - createForeignKey({ - source_schema: 'public', - source_table: 'posts', - source_columns: ['user_id'], - target_schema: 'auth', - target_table: 'users', - target_columns: ['id'], - }), - ] - - const policies = generateProgrammaticPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - foreignKeyConstraints, - }) - - expect(policies).toHaveLength(4) - - const commands = policies.map((p) => p.command) - expect(commands).toContain('SELECT') - expect(commands).toContain('INSERT') - expect(commands).toContain('UPDATE') - expect(commands).toContain('DELETE') - }) - - it('should return empty array when no FK path to auth.users exists', () => { - const foreignKeyConstraints: ForeignKeyConstraint[] = [ - createForeignKey({ - source_schema: 'public', - source_table: 'posts', - source_columns: ['category_id'], - target_schema: 'public', - target_table: 'categories', - target_columns: ['id'], - }), - ] - - const policies = generateProgrammaticPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - foreignKeyConstraints, - }) - - expect(policies).toHaveLength(0) - }) - - it('should return empty array when foreignKeyConstraints is empty', () => { - const policies = generateProgrammaticPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - foreignKeyConstraints: [], - }) - - expect(policies).toHaveLength(0) - }) - - it('should generate policies with EXISTS clause for indirect FK path (2 hops)', () => { - // posts -> profiles -> auth.users - const foreignKeyConstraints: ForeignKeyConstraint[] = [ - createForeignKey({ - id: 1, - source_schema: 'public', - source_table: 'posts', - source_columns: ['profile_id'], - target_schema: 'public', - target_table: 'profiles', - target_columns: ['id'], - }), - createForeignKey({ - id: 2, - source_schema: 'public', - source_table: 'profiles', - source_columns: ['user_id'], - target_schema: 'auth', - target_table: 'users', - target_columns: ['id'], - }), - ] - - const policies = generateProgrammaticPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - foreignKeyConstraints, - }) - - expect(policies).toHaveLength(4) - - // Check that the expression contains EXISTS for indirect path - const selectPolicy = policies.find((p) => p.command === 'SELECT') - expect(selectPolicy?.definition).toContain('exists') - expect(selectPolicy?.sql).toContain('exists') - }) - - describe('policy structure validation', () => { - const foreignKeyConstraints: ForeignKeyConstraint[] = [ - createForeignKey({ - source_schema: 'public', - source_table: 'posts', - source_columns: ['user_id'], - target_schema: 'auth', - target_table: 'users', - target_columns: ['id'], - }), - ] - - it('should include all required fields in generated policies', () => { - const policies = generateProgrammaticPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - foreignKeyConstraints, - }) - - for (const policy of policies) { - expect(policy).toHaveProperty('name') - expect(policy).toHaveProperty('sql') - expect(policy).toHaveProperty('command') - expect(policy).toHaveProperty('table', 'posts') - expect(policy).toHaveProperty('schema', 'public') - expect(policy).toHaveProperty('action', 'PERMISSIVE') - expect(policy).toHaveProperty('roles') - expect(policy.roles).toContain('authenticated') - } - }) - - it('SELECT policy should have definition but no check', () => { - const policies = generateProgrammaticPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - foreignKeyConstraints, - }) - - const selectPolicy = policies.find((p) => p.command === 'SELECT') - expect(selectPolicy?.definition).toBeDefined() - expect(selectPolicy?.check).toBeUndefined() - }) - - it('DELETE policy should have definition but no check', () => { - const policies = generateProgrammaticPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - foreignKeyConstraints, - }) - - const deletePolicy = policies.find((p) => p.command === 'DELETE') - expect(deletePolicy?.definition).toBeDefined() - expect(deletePolicy?.check).toBeUndefined() - }) - - it('INSERT policy should have check but no definition', () => { - const policies = generateProgrammaticPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - foreignKeyConstraints, - }) - - const insertPolicy = policies.find((p) => p.command === 'INSERT') - expect(insertPolicy?.definition).toBeUndefined() - expect(insertPolicy?.check).toBeDefined() - }) - - it('UPDATE policy should have both definition and check', () => { - const policies = generateProgrammaticPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - foreignKeyConstraints, - }) - - const updatePolicy = policies.find((p) => p.command === 'UPDATE') - expect(updatePolicy?.definition).toBeDefined() - expect(updatePolicy?.check).toBeDefined() - }) - - it('should generate correct SQL syntax for direct FK', () => { - const policies = generateProgrammaticPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - foreignKeyConstraints, - }) - - const selectPolicy = policies.find((p) => p.command === 'SELECT') - expect(selectPolicy?.sql).toContain('CREATE POLICY') - expect(selectPolicy?.sql).toContain('public.posts') - expect(selectPolicy?.sql).toContain('AS PERMISSIVE FOR SELECT') - expect(selectPolicy?.sql).toContain('TO authenticated') - expect(selectPolicy?.sql).toContain('USING') - expect(selectPolicy?.sql).toContain('auth.uid()') - }) - }) - - it('should handle non-public schema', () => { - const foreignKeyConstraints: ForeignKeyConstraint[] = [ - createForeignKey({ - source_schema: 'private', - source_table: 'documents', - source_columns: ['owner_id'], - target_schema: 'auth', - target_table: 'users', - target_columns: ['id'], - }), - ] - - const policies = generateProgrammaticPoliciesForTable({ - table: { name: 'documents', schema: 'private' }, - foreignKeyConstraints, - }) - - expect(policies).toHaveLength(4) - expect(policies[0].schema).toBe('private') - expect(policies[0].sql).toContain('private.documents') - }) - }) - - describe('generateAiPoliciesForTable', () => { - const mockAiPolicies: GeneratedPolicy[] = [ - { - name: 'ai_select_policy', - sql: 'CREATE POLICY "ai_select_policy" ON public.posts FOR SELECT USING (true);', - command: 'SELECT', - table: 'posts', - schema: 'public', - definition: safeSql`true`, - action: 'PERMISSIVE', - roles: ['public'], - }, - ] - - it('should return policies from AI when called with valid inputs', async () => { - mockGenerateSqlPolicy.mockResolvedValue(mockAiPolicies) - - const policies = await generateAiPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - columns: [{ name: 'id' }, { name: 'title' }], - projectRef: 'test-project', - connectionString: 'postgresql://localhost:5432/test', - }) - - expect(mockGenerateSqlPolicy).toHaveBeenCalledWith({ - tableName: 'posts', - schema: 'public', - columns: ['id', 'title'], - projectRef: 'test-project', - connectionString: 'postgresql://localhost:5432/test', - }) - expect(policies).toEqual(mockAiPolicies) - }) - - it('should return empty array when connectionString is null', async () => { - const policies = await generateAiPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - columns: [{ name: 'id' }], - projectRef: 'test-project', - connectionString: null, - }) - - expect(mockGenerateSqlPolicy).not.toHaveBeenCalled() - expect(policies).toEqual([]) - }) - - it('should return empty array when connectionString is undefined', async () => { - const policies = await generateAiPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - columns: [{ name: 'id' }], - projectRef: 'test-project', - connectionString: undefined, - }) - - expect(mockGenerateSqlPolicy).not.toHaveBeenCalled() - expect(policies).toEqual([]) - }) - - it('should handle API errors gracefully and return empty array', async () => { - const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - mockGenerateSqlPolicy.mockRejectedValue(new Error('API error')) - - const policies = await generateAiPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - columns: [{ name: 'id' }], - projectRef: 'test-project', - connectionString: 'postgresql://localhost:5432/test', - }) - - expect(policies).toEqual([]) - expect(consoleLogSpy).toHaveBeenCalledWith('AI policy generation failed:', expect.any(Error)) - - consoleLogSpy.mockRestore() - }) - - it('should trim column names before sending to API', async () => { - mockGenerateSqlPolicy.mockResolvedValue([]) - - await generateAiPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - columns: [{ name: ' id ' }, { name: ' title ' }], - projectRef: 'test-project', - connectionString: 'postgresql://localhost:5432/test', - }) - - expect(mockGenerateSqlPolicy).toHaveBeenCalledWith( - expect.objectContaining({ - columns: ['id', 'title'], - }) - ) - }) - }) - - describe('generateStartingPoliciesForTable', () => { - const mockAiPolicies: GeneratedPolicy[] = [ - { - name: 'ai_policy', - sql: 'CREATE POLICY "ai_policy" ON public.posts FOR SELECT USING (true);', - command: 'SELECT', - table: 'posts', - schema: 'public', - definition: safeSql`true`, - action: 'PERMISSIVE', - roles: ['public'], - }, - ] - - it('should use programmatic policies when FK path exists (does not call AI)', async () => { - const foreignKeyConstraints: ForeignKeyConstraint[] = [ - createForeignKey({ - source_schema: 'public', - source_table: 'posts', - source_columns: ['user_id'], - target_schema: 'auth', - target_table: 'users', - target_columns: ['id'], - }), - ] - - const policies = await generateStartingPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - foreignKeyConstraints, - columns: [{ name: 'id' }], - projectRef: 'test-project', - connectionString: 'postgresql://localhost:5432/test', - enableAi: true, - }) - - expect(policies).toHaveLength(4) - expect(mockGenerateSqlPolicy).not.toHaveBeenCalled() - }) - - it('should fall back to AI when no FK path exists and enableAi is true', async () => { - mockGenerateSqlPolicy.mockResolvedValue(mockAiPolicies) - - const policies = await generateStartingPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - foreignKeyConstraints: [], - columns: [{ name: 'id' }], - projectRef: 'test-project', - connectionString: 'postgresql://localhost:5432/test', - enableAi: true, - }) - - expect(mockGenerateSqlPolicy).toHaveBeenCalled() - expect(policies).toEqual(mockAiPolicies) - }) - - it('should return empty array when no FK path exists and enableAi is false', async () => { - const policies = await generateStartingPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - foreignKeyConstraints: [], - columns: [{ name: 'id' }], - projectRef: 'test-project', - connectionString: 'postgresql://localhost:5432/test', - enableAi: false, - }) - - expect(mockGenerateSqlPolicy).not.toHaveBeenCalled() - expect(policies).toEqual([]) - }) - - it('should return empty array when no FK path and AI returns empty', async () => { - mockGenerateSqlPolicy.mockResolvedValue([]) - - const policies = await generateStartingPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - foreignKeyConstraints: [], - columns: [{ name: 'id' }], - projectRef: 'test-project', - connectionString: 'postgresql://localhost:5432/test', - enableAi: true, - }) - - expect(policies).toEqual([]) - }) - - it('should prioritize programmatic over AI even when both could generate policies', async () => { - mockGenerateSqlPolicy.mockResolvedValue(mockAiPolicies) - - const foreignKeyConstraints: ForeignKeyConstraint[] = [ - createForeignKey({ - source_schema: 'public', - source_table: 'posts', - source_columns: ['user_id'], - target_schema: 'auth', - target_table: 'users', - target_columns: ['id'], - }), - ] - - const policies = await generateStartingPoliciesForTable({ - table: { name: 'posts', schema: 'public' }, - foreignKeyConstraints, - columns: [{ name: 'id' }], - projectRef: 'test-project', - connectionString: 'postgresql://localhost:5432/test', - enableAi: true, - }) - - // Should return 4 programmatic policies, not 1 AI policy - expect(policies).toHaveLength(4) - expect(mockGenerateSqlPolicy).not.toHaveBeenCalled() - }) - }) -}) diff --git a/apps/studio/components/interfaces/Database/Policies/Policies.utils.ts b/apps/studio/components/interfaces/Database/Policies/Policies.utils.ts deleted file mode 100644 index d98c9322558..00000000000 --- a/apps/studio/components/interfaces/Database/Policies/Policies.utils.ts +++ /dev/null @@ -1,477 +0,0 @@ -import { - acceptUntrustedSql, - ident, - safeSql, - untrustedSql, - type DisplayableSqlFragment, - type SafeSqlFragment, -} from '@supabase/pg-meta' -import type { PGPolicy } from '@supabase/pg-meta' -import { has, isEmpty, isEqual } from 'lodash' - -import { - DraftPostgresPolicyCreatePayload, - DraftPostgresPolicyUpdatePayload, - PolicyFormField, - PolicyForReview, -} from './Policies.types' -import { generateSqlPolicy } from '@/data/ai/sql-policy-mutation' -import type { CreatePolicyBody } from '@/data/database-policies/database-policy-create-mutation' -import type { ForeignKeyConstraint } from '@/data/database/foreign-key-constraints-query' - -/** - * Returns an array of SQL statements that will preview in the review step of the policy editor - * @param {*} policyFormFields { name, using, check, command } - */ - -export const createSQLPolicy = ( - policyFormFields: PolicyFormField, - originalPolicyFormFields?: PGPolicy -) => { - const { definition, check } = policyFormFields - const formattedPolicyFormFields = { - ...policyFormFields, - definition: definition - ? definition.replace(/\s+/g, ' ').trim() - : definition === undefined - ? null - : definition, - check: check ? check.replace(/\s+/g, ' ').trim() : check === undefined ? null : check, - } - - if (!originalPolicyFormFields || isEmpty(originalPolicyFormFields)) { - return createSQLStatementForCreatePolicy(formattedPolicyFormFields) - } - - // If there are no changes, return an empty object - if (isEqual(policyFormFields, originalPolicyFormFields)) { - return {} - } - - // Extract out all the fields that updated - const fieldsToUpdate: any = {} - if (!isEqual(formattedPolicyFormFields.name, originalPolicyFormFields.name)) { - fieldsToUpdate.name = formattedPolicyFormFields.name - } - if (!isEqual(formattedPolicyFormFields.definition, originalPolicyFormFields.definition)) { - fieldsToUpdate.definition = formattedPolicyFormFields.definition - } - if (!isEqual(formattedPolicyFormFields.check, originalPolicyFormFields.check)) { - fieldsToUpdate.check = formattedPolicyFormFields.check - } - if (!isEqual(formattedPolicyFormFields.roles, originalPolicyFormFields.roles)) { - fieldsToUpdate.roles = formattedPolicyFormFields.roles - } - - if (!isEmpty(fieldsToUpdate)) { - return createSQLStatementForUpdatePolicy(formattedPolicyFormFields, fieldsToUpdate) - } - - return {} -} - -const createSQLStatementForCreatePolicy = (policyFormFields: PolicyFormField): PolicyForReview => { - const { name, definition, check, command, schema, table } = policyFormFields - const roles = policyFormFields.roles.length === 0 ? ['public'] : policyFormFields.roles - const description = `Add policy for the ${command} operation under the policy "${name}"` - const statement = [ - `CREATE POLICY "${name}" ON "${schema}"."${table}"`, - `AS PERMISSIVE FOR ${command}`, - `TO ${roles.join(', ')}`, - `${definition ? `USING (${definition})` : ''}`, - `${check ? `WITH CHECK (${check})` : ''}`, - ].join('\n') - - return { description, statement } -} - -const createSQLStatementForUpdatePolicy = ( - policyFormFields: PolicyFormField, - fieldsToUpdate: Partial -): PolicyForReview => { - const { name, schema, table } = policyFormFields - - const definitionChanged = has(fieldsToUpdate, ['definition']) - const checkChanged = has(fieldsToUpdate, ['check']) - const nameChanged = has(fieldsToUpdate, ['name']) - const rolesChanged = has(fieldsToUpdate, ['roles']) - - const parameters = Object.keys(fieldsToUpdate) - const description = `Update policy's ${ - parameters.length === 1 - ? parameters[0] - : `${parameters.slice(0, parameters.length - 1).join(', ')} and ${ - parameters[parameters.length - 1] - }` - } ` - const roles = - (fieldsToUpdate?.roles ?? []).length === 0 ? ['public'] : (fieldsToUpdate.roles as string[]) - - const alterStatement = `ALTER POLICY "${name}" ON "${schema}"."${table}"` - const statement = [ - 'BEGIN;', - ...(definitionChanged ? [` ${alterStatement} USING (${fieldsToUpdate.definition});`] : []), - ...(checkChanged ? [` ${alterStatement} WITH CHECK (${fieldsToUpdate.check});`] : []), - ...(rolesChanged ? [` ${alterStatement} TO ${roles.join(', ')};`] : []), - ...(nameChanged ? [` ${alterStatement} RENAME TO "${fieldsToUpdate.name}";`] : []), - 'COMMIT;', - ].join('\n') - - return { description, statement } -} - -// These constructors return DRAFT payloads — `definition`/`check` are still -// `DisplayableSqlFragment`. Promotion to `SafeSqlFragment` must happen at the user gesture -// (the Save click in `PolicyEditorModal`), not here, since this module has no guarantee that -// it was reached via a deliberate user action. -export const createPayloadForCreatePolicy = ( - policyFormFields: PolicyFormField -): DraftPostgresPolicyCreatePayload => { - const { name, schema, table, command, definition, check, roles } = policyFormFields - return { - name, - schema, - table, - action: 'PERMISSIVE', - command: command || undefined, - definition: !definition ? undefined : untrustedSql(definition), - check: !check ? undefined : untrustedSql(check), - roles: roles.length > 0 ? roles : undefined, - } -} - -export const createPayloadForUpdatePolicy = ( - policyFormFields: PolicyFormField, - originalPolicyFormFields: PGPolicy -): DraftPostgresPolicyUpdatePayload => { - const { definition, check } = policyFormFields - const formattedDefinition = definition ? definition.replace(/\s+/g, ' ').trim() : definition - const formattedCheck = check ? check.replace(/\s+/g, ' ').trim() : check - - const payload: DraftPostgresPolicyUpdatePayload = { id: originalPolicyFormFields.id } - - if (!isEqual(policyFormFields.name, originalPolicyFormFields.name)) { - payload.name = policyFormFields.name - } - if (!isEqual(formattedDefinition, originalPolicyFormFields.definition)) { - payload.definition = !formattedDefinition ? undefined : untrustedSql(formattedDefinition) - } - if (!isEqual(formattedCheck, originalPolicyFormFields.check)) { - payload.check = !formattedCheck ? undefined : untrustedSql(formattedCheck) - } - if (!isEqual(policyFormFields.roles, originalPolicyFormFields.roles)) { - if (policyFormFields.roles.length === 0) payload.roles = ['public'] - else payload.roles = policyFormFields.roles || undefined - } - - return payload -} - -// --- Policy Generation --- - -/** - * A policy generated for display/staging in the table editor. - * `definition`/`check` are `DisplayableSqlFragment` because generators have different provenance: - * programmatic generation produces `SafeSqlFragment` (composed via `safeSql`), AI generation - * produces `UntrustedSqlFragment` (third-party output). Consumers must promote via - * `acceptUntrustedSql` at a user gesture before executing. - */ -export type GeneratedPolicy = Required< - Pick -> & - Pick & { - definition?: DisplayableSqlFragment - check?: DisplayableSqlFragment - sql: string - } - -/** - * A {@link GeneratedPolicy} whose `definition`/`check` have already been promoted to - * `SafeSqlFragment`. Producing one of these is the contract that says: the user gesture - * required to execute this SQL has already happened. - */ -export type AcceptedGeneratedPolicy = Omit & { - definition?: SafeSqlFragment - check?: SafeSqlFragment -} - -/** - * Promotes a {@link GeneratedPolicy} to an {@link AcceptedGeneratedPolicy}. - * ONLY call from an event handler tied to a deliberate user action (e.g. the Save click - * on the table editor). Never call from useEffect, render, or any path that runs without - * a user gesture. - */ -export const acceptGeneratedPolicy = (policy: GeneratedPolicy): AcceptedGeneratedPolicy => ({ - ...policy, - definition: policy.definition === undefined ? undefined : acceptUntrustedSql(policy.definition), - check: policy.check === undefined ? undefined : acceptUntrustedSql(policy.check), -}) - -type Relationship = { - source_schema: string - source_table_name: string - source_column_name: string - target_table_schema: string - target_table_name: string - target_column_name: string -} - -/** - * Gets relationships for a specific table from FK constraints. - * Returns relationships where the table is the source. - */ -const getRelationshipsForTable = ({ - schema, - table, - fkConstraints, -}: { - schema: string - table: string - fkConstraints: ForeignKeyConstraint[] -}): Relationship[] => { - return fkConstraints - .filter((fk) => fk.source_schema === schema && fk.source_table === table) - .flatMap((fk) => - fk.source_columns.map((sourceCol, i) => ({ - source_schema: fk.source_schema, - source_table_name: fk.source_table, - source_column_name: sourceCol, - target_table_schema: fk.target_schema, - target_table_name: fk.target_table, - target_column_name: fk.target_columns[i], - })) - ) -} - -/** - * BFS to find shortest path from table to auth.users via foreign key relationships. - * Returns null if no path exists within maxDepth. - */ -const findPathToAuthUsers = ( - startTable: { schema: string; name: string }, - allForeignKeyConstraints: ForeignKeyConstraint[], - maxDepth = 3 -): Relationship[] | null => { - const startRelationships = getRelationshipsForTable({ - schema: startTable.schema, - table: startTable.name, - fkConstraints: allForeignKeyConstraints, - }) - - const queue: { table: { schema: string; name: string }; path: Relationship[] }[] = [ - { table: startTable, path: [] }, - ] - const visited = new Set() - visited.add(`${startTable.schema}.${startTable.name}`) - - while (queue.length > 0) { - const queueItem = queue.shift() - if (!queueItem) continue - - const { table, path } = queueItem - if (path.length >= maxDepth) continue - - const relationships = - path.length === 0 - ? startRelationships - : getRelationshipsForTable({ - schema: table.schema, - table: table.name, - fkConstraints: allForeignKeyConstraints, - }) - - for (const rel of relationships) { - // Found path to auth.users - if ( - rel.target_table_schema === 'auth' && - rel.target_table_name === 'users' && - rel.target_column_name === 'id' - ) { - return [...path, rel] - } - - const targetId = `${rel.target_table_schema}.${rel.target_table_name}` - if (visited.has(targetId)) continue - - // Add target table to queue for further exploration - queue.push({ - table: { schema: rel.target_table_schema, name: rel.target_table_name }, - path: [...path, rel], - }) - visited.add(targetId) - } - } - - return null -} - -/** Generates SQL expression for RLS policy based on FK path to auth.users */ -const buildPolicyExpression = (path: Relationship[]): SafeSqlFragment => { - if (path.length === 0) return safeSql`` - - // Direct FK to auth.users - if (path.length === 1) { - return safeSql`(select auth.uid()) = ${ident(path[0].source_column_name)}` - } - - // Indirect path - build EXISTS with JOINs - const [first, ...rest] = path - const firstTarget = safeSql`${ident(first.target_table_schema)}.${ident(first.target_table_name)}` - const source = safeSql`${ident(first.source_schema)}.${ident(first.source_table_name)}` - const last = path[path.length - 1] - - const joins = rest.slice(0, -1).reduce( - (acc, r) => { - const targetSchema = ident(r.target_table_schema) - const targetTable = ident(r.target_table_name) - const targetColumn = ident(r.target_column_name) - - const sourceSchema = ident(r.source_schema) - const sourceTable = ident(r.source_table_name) - const sourceColumn = ident(r.source_column_name) - const join = safeSql`join ${targetSchema}.${targetTable} on ${targetSchema}.${targetTable}.${targetColumn} = ${sourceSchema}.${sourceTable}.${sourceColumn}` - return acc.length === 0 ? join : safeSql`${acc}\n ${join}` - }, - safeSql`` - ) - - return safeSql`exists ( - select 1 from ${firstTarget} - ${joins} - where ${firstTarget}.${ident(first.target_column_name)} = ${source}.${ident(first.source_column_name)} - and ${ident(last.source_schema)}.${ident(last.source_table_name)}.${ident(last.source_column_name)} = (select auth.uid()) -)` -} - -/** Builds policy SQL for all CRUD operations */ -const buildPoliciesForPath = ( - table: { name: string; schema: string }, - path: Relationship[] -): GeneratedPolicy[] => { - const expression = buildPolicyExpression(path) - const targetCol = path[0].source_column_name - - return (['SELECT', 'INSERT', 'UPDATE', 'DELETE'] as const).map((command) => { - const name = `Enable ${command.toLowerCase()} access for users based on ${ident(targetCol)}` - const base = `CREATE POLICY "${name}" ON ${ident(table.schema)}.${ident(table.name)} AS PERMISSIVE FOR ${command} TO authenticated` - - const sql = - command === 'INSERT' - ? `${base} WITH CHECK (${expression});` - : command === 'UPDATE' - ? `${base} USING (${expression}) WITH CHECK (${expression});` - : `${base} USING (${expression});` - - // Structured data for mutation API - const definition = command === 'INSERT' ? undefined : expression - const check = command === 'SELECT' || command === 'DELETE' ? undefined : expression - - return { - name, - sql, - command, - table: table.name, - schema: table.schema, - definition, - check, - action: 'PERMISSIVE' as const, - roles: ['authenticated'], - } - }) -} - -/** - * Generates RLS policies programmatically based on FK relationships to auth.users. - */ -export const generateProgrammaticPoliciesForTable = ({ - table, - foreignKeyConstraints, -}: { - table: { name: string; schema: string } - foreignKeyConstraints: ForeignKeyConstraint[] -}): GeneratedPolicy[] => { - try { - const path = findPathToAuthUsers(table, foreignKeyConstraints) - - if (path?.length) { - return buildPoliciesForPath(table, path) - } - } catch (error) { - // Silently fail - caller will handle empty result - } - - return [] -} - -/** - * Generates RLS policies using AI. - */ -export const generateAiPoliciesForTable = async ({ - table, - columns, - projectRef, - connectionString, -}: { - table: { name: string; schema: string } - columns: { name: string }[] - projectRef: string - connectionString?: string | null -}): Promise => { - if (!connectionString) return [] - - try { - return await generateSqlPolicy({ - tableName: table.name, - schema: table.schema, - columns: columns.map((col) => col.name.trim()), - projectRef, - connectionString: connectionString ?? '', - }) - } catch (error) { - console.log('AI policy generation failed:', error) - return [] - } -} - -/** - * Generates RLS policies for a table. - * First tries programmatic generation based on FK relationships to auth.users. - * Falls back to AI generation if no path exists. - */ -export const generateStartingPoliciesForTable = async ({ - table, - foreignKeyConstraints, - columns, - projectRef, - connectionString, - enableAi, -}: { - table: { name: string; schema: string } - foreignKeyConstraints: ForeignKeyConstraint[] - columns: { name: string }[] - projectRef: string - connectionString?: string | null - enableAi: boolean -}): Promise => { - // Try programmatic generation first - const programmaticPolicies = generateProgrammaticPoliciesForTable({ - table, - foreignKeyConstraints, - }) - - if (programmaticPolicies.length > 0) { - return programmaticPolicies - } - - // Fall back to AI generation - if (enableAi) { - return await generateAiPoliciesForTable({ - table, - columns, - projectRef, - connectionString, - }) - } - - return [] -} diff --git a/apps/studio/components/interfaces/Database/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts b/apps/studio/components/interfaces/Database/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts deleted file mode 100644 index 9e918f6cc11..00000000000 --- a/apps/studio/components/interfaces/Database/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts +++ /dev/null @@ -1,329 +0,0 @@ -import { safeSql } from '@supabase/pg-meta/src/pg-format' - -import { PolicyTemplate } from '../PolicyTemplates/PolicyTemplates.constants' - -/** - * ---------------------------------------------------------------- - * PostgreSQL policy templates for the auth policies page - * ---------------------------------------------------------------- - * id: Unique identifier for the monaco editor to dynamically refresh - * templateName: As a display for a more descriptive title for the policy - * description: Additional details about the template and how to make it yours - * statement: SQL statement template for the policy - * - * name: Actual policy name that will be used in the editor - * definition: Actual policy using expression that will be used in the editor - * check: Actual policy with check expression that will be used in the editor - * command: Operation to create policy for - */ - -export const getGeneralPolicyTemplates = (schema: string, table: string): PolicyTemplate[] => [ - { - id: 'policy-1', - preview: false, - templateName: 'Enable read access to everyone', - description: - 'This policy gives read access to your table for all users via the SELECT operation.', - statement: ` -create policy "Enable read access for all users" -on "${schema}"."${table}" -for select using (true);`.trim(), - name: 'Enable read access for all users', - definition: safeSql`true`, - check: safeSql``, - command: 'SELECT', - roles: [], - }, - { - id: 'policy-2', - preview: false, - templateName: 'Enable insert access for authenticated users only', - description: 'This policy gives insert access to your table for all authenticated users only.', - statement: ` -create policy "Enable insert for authenticated users only" -on "${schema}"."${table}" -for insert to authenticated -with check (true);`.trim(), - name: 'Enable insert for authenticated users only', - definition: safeSql``, - check: safeSql`true`, - command: 'INSERT', - roles: ['authenticated'], - }, - { - id: 'policy-3', - preview: false, - templateName: 'Enable delete access for users based on their user ID *', - description: - 'This policy assumes that your table has a column "user_id", and allows users to delete rows which the "user_id" column matches their ID', - statement: ` -create policy "Enable delete for users based on user_id" -on "${schema}"."${table}" -for delete using ( - (select auth.uid()) = user_id -);`.trim(), - name: 'Enable delete for users based on user_id', - definition: safeSql`(select auth.uid()) = user_id`, - check: safeSql``, - command: 'DELETE', - roles: [], - }, - { - id: 'policy-4', - preview: false, - templateName: 'Enable insert access for users based on their user ID *', - description: - 'This policy assumes that your table has a column "user_id", and allows users to insert rows which the "user_id" column matches their ID', - statement: ` -create policy "Enable insert for users based on user_id" -on "${schema}"."${table}" -for insert with check ( - (select auth.uid()) = user_id -);`.trim(), - name: 'Enable insert for users based on user_id', - definition: safeSql``, - check: safeSql`(select auth.uid()) = user_id`, - command: 'INSERT', - roles: [], - }, - { - id: 'policy-5', - preview: true, - name: 'Policy with table joins', - templateName: 'Policy with table joins', - description: ` -Query across tables to build more advanced RLS rules - -Assuming 2 tables called \`teams\` and \`members\`, you can query both tables in the policy to control access to the members table.`, - statement: ` -create policy "Members can update team details if they belong to the team" -on teams for update using ( - (select auth.uid()) in ( - select user_id from members where team_id = id - ) -); -`.trim(), - definition: safeSql`(select auth.uid()) in (select user_id from members where team_id = id)`, - check: safeSql``, - command: 'UPDATE', - roles: [], - }, - { - id: 'policy-6', - preview: true, - templateName: 'Policy with security definer functions', - description: ` -Useful in a many-to-many relationship where you want to restrict access to the linking table. - -Assuming 2 tables called \`teams\` and \`members\`, you can use a security definer function in combination with a policy to control access to the members table.`.trim(), - statement: ` -create or replace function get_teams_for_user(user_id uuid) -returns setof bigint as $$ - select team_id from members where user_id = $1 -$$ stable language sql security definer; - -create policy "Team members can update team members if they belong to the team" -on members -for all using ( - team_id in (select get_teams_for_user(auth.uid())) -); -`.trim(), - name: 'Policy with security definer functions', - definition: safeSql`team_id in (select get_teams_for_user(auth.uid()))`, - check: safeSql``, - command: 'ALL', - roles: [], - }, - { - id: 'policy-7', - preview: true, - name: 'Policy to implement Time To Live (TTL)', - templateName: 'Policy to implement Time To Live (TTL)', - description: ` -Implement a TTL-like feature that you see in Instagram stories or Snapchat where messages expire after a day. - -Rows under the table are available only if they have been created within the last 24 hours.`, - statement: ` -create policy "Stories are live for a day" -on "${schema}"."${table}" -for select using ( - created_at > (current_timestamp - interval '1 day') -); -`.trim(), - definition: safeSql`created_at > (current_timestamp - interval '1 day')`, - check: safeSql``, - command: 'SELECT', - roles: [], - }, - { - id: 'policy-8', - preview: false, - templateName: 'Allow users to only view their own data', - description: 'Restrict users to reading only their own data.', - statement: ` -create policy "Enable users to view their own data only" -on "${schema}"."${table}" -for select -to authenticated -using ( - (select auth.uid()) = user_id -);`.trim(), - name: 'Enable users to view their own data only', - definition: safeSql`(select auth.uid()) = user_id`, - check: safeSql``, - command: 'SELECT', - roles: ['authenticated'], - }, -] - -export const getRealtimePolicyTemplates = (): PolicyTemplate[] => { - const results = [ - { - id: 'policy-broadcast-1', - preview: false, - templateName: 'Allow listening for broadcasts for authenticated users only', - description: 'This policy allows listening for broadcasts for authenticated users only.', - statement: ` -create policy "Allow listening for broadcasts for authenticated users only" -on realtime.messages for select -to authenticated -using ( realtime.messages.extension = 'broadcast' );`.trim(), - name: 'Allow listening for broadcasts for authenticated users only', - definition: safeSql`realtime.messages.extension = 'broadcast'`, - check: safeSql``, - command: 'SELECT', - roles: ['authenticated'], - }, - { - id: 'policy-broadcast-2', - preview: false, - templateName: 'Allow pushing broadcasts for authenticated users only', - description: 'This policy allows pushing broadcasts for authenticated users only.', - statement: ` -create policy "Allow pushing broadcasts for authenticated users only" -ON realtime.messages for insert -TO authenticated -with check ( realtime.messages.extension = 'broadcast' );`.trim(), - name: 'Allow pushing broadcasts for authenticated users only', - definition: safeSql`realtime.messages.extension = 'broadcast'`, - check: safeSql`realtime.messages.extension = 'broadcast'`, - command: 'INSERT', - roles: ['authenticated'], - }, - { - id: 'policy-broadcast-3', - preview: false, - templateName: 'Allow listening for broadcasts from a specific channel', - description: 'This policy allows listening for broadcasts from a specific channel.', - statement: ` -create policy "Allow listening for broadcasts from a specific channel" -on realtime.messages for select -using ( realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name' );`.trim(), - name: 'Allow listening for broadcasts from a specific channel', - definition: safeSql`realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name'`, - check: safeSql``, - command: 'SELECT', - roles: [], - }, - { - id: 'policy-broadcast-4', - preview: false, - templateName: 'Allow pushing broadcasts to specific channel', - description: 'This policy allow pushing broadcasts to specific channel.', - statement: ` -create policy "Allow pushing broadcasts to specific channel" -ON realtime.messages for insert -with check ( realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name' );`.trim(), - name: 'Allow pushing broadcasts to specific channel', - definition: safeSql`realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name'`, - check: safeSql`realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name'`, - command: 'INSERT', - roles: [], - }, - { - id: 'policy-presences-1', - preview: false, - templateName: 'Allow listening for presences on all channels for authenticated users only', - description: - 'This policy enables listening for presences on all channels for all authenticated users only.', - statement: ` -create policy "Allow listening for presences on all channels for authenticated users only" -on realtime.messages for select -to authenticated -using ( realtime.messages.extension = 'presence' );`.trim(), - name: 'Allow listening for presences on all channels for authenticated users only', - definition: safeSql`realtime.messages.extension = 'presence'`, - check: safeSql``, - command: 'SELECT', - roles: ['authenticated'], - }, - { - id: 'policy-presences-2', - preview: false, - templateName: 'Allow broadcasting presences on all channels for authenticated users only', - description: - 'This policy enables broadcasting presences on all channels for all authenticated users only.', - statement: ` -create policy "Allow broadcasting presences on all channels for authenticated users only" -ON realtime.messages for insert -TO authenticated -with check ( realtime.messages.extension = 'presence' ); - ;`.trim(), - name: 'Allow broadcasting presences on all channels for authenticated users only', - definition: safeSql`realtime.messages.extension = 'presence'`, - check: safeSql`realtime.messages.extension = 'presence'`, - command: 'INSERT', - roles: ['authenticated'], - }, - { - id: 'policy-presences-3', - preview: false, - templateName: 'Allow listening for presences from a specific channel', - description: 'This policy enables listening for presences from a specific channel.', - statement: ` -create policy "Allow listening for presences from a specific channel" -on realtime.messages for select -using ( realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name' );`.trim(), - name: 'Allow listening for presences from a specific channel', - definition: safeSql`realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name'`, - check: safeSql``, - command: 'SELECT', - roles: [], - }, - { - id: 'policy-presences-4', - preview: false, - templateName: 'Publish presence to a specific channel', - description: 'This policy allows publishing presence to a specific channel.', - statement: ` -create policy "Publish presence to a specific channel" -ON realtime.messages for insert -with check ( realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name' ); - ;`.trim(), - name: 'Publish presence to a specific channel', - definition: safeSql`realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name'`, - check: safeSql`realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name'`, - command: 'INSERT', - roles: [], - }, - ] as PolicyTemplate[] - return results -} - -export const getQueuePolicyTemplates = (): PolicyTemplate[] => { - return [ - { - id: 'policy-queues-1', - preview: false, - templateName: 'Allow access to queue', - statement: ``.trim(), - name: 'Allow anon and authenticated to access messages from queue', - description: - 'Base policy to ensure that anon and authenticated can only access appropriate rows. USING and CHECK statements will need to be adjusted accordingly', - definition: safeSql`true`, - check: safeSql`true`, - command: 'ALL', - roles: ['anon', 'authenticated'], - }, - ] -} diff --git a/apps/studio/components/interfaces/Database/Policies/PolicyEditorPanel/PolicyTemplates.tsx b/apps/studio/components/interfaces/Database/Policies/PolicyEditorPanel/PolicyTemplates.tsx index ea7da926e6f..77d54208dad 100644 --- a/apps/studio/components/interfaces/Database/Policies/PolicyEditorPanel/PolicyTemplates.tsx +++ b/apps/studio/components/interfaces/Database/Policies/PolicyEditorPanel/PolicyTemplates.tsx @@ -17,7 +17,7 @@ import { getGeneralPolicyTemplates, getQueuePolicyTemplates, getRealtimePolicyTemplates, -} from '../PolicyEditorModal/PolicyEditorModal.constants' +} from '../Policies.constants' import { Markdown } from '@/components/interfaces/Markdown' import CardButton from '@/components/ui/CardButton' import CopyButton from '@/components/ui/CopyButton' diff --git a/apps/studio/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRowHeader.tsx b/apps/studio/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRowHeader.tsx index fc4df51adea..36ef1d8dc4d 100644 --- a/apps/studio/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRowHeader.tsx +++ b/apps/studio/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRowHeader.tsx @@ -56,24 +56,26 @@ export const PolicyTableRowHeader = ({ > {table.name} + +
{!table.rls_enabled && ( - + RLS Disabled )} {!isLoadingApiAccess && !hasApiAccess && ( - + API Disabled )} - - {isTableLocked && ( - - - Locked - - - )} + {isTableLocked && ( + + + Locked + + + )} +
{!isTableLocked && (
diff --git a/apps/studio/components/interfaces/Database/Policies/PolicyTableRow/index.tsx b/apps/studio/components/interfaces/Database/Policies/PolicyTableRow/index.tsx index 8098b16c72e..2e60637e59b 100644 --- a/apps/studio/components/interfaces/Database/Policies/PolicyTableRow/index.tsx +++ b/apps/studio/components/interfaces/Database/Policies/PolicyTableRow/index.tsx @@ -94,7 +94,9 @@ const PolicyTableRowComponent = ({ return ( - + -} diff --git a/apps/studio/components/interfaces/Storage/Storage.types.ts b/apps/studio/components/interfaces/Storage/Storage.types.ts index d18cf5f3228..3b55265bca9 100644 --- a/apps/studio/components/interfaces/Storage/Storage.types.ts +++ b/apps/studio/components/interfaces/Storage/Storage.types.ts @@ -1,5 +1,5 @@ import { STORAGE_ROW_STATUS, STORAGE_ROW_TYPES } from './Storage.constants' -import type { PolicyFormField } from '@/components/interfaces/Database/Policies/Policies.types' +import { PolicyFormField } from './StoragePolicies/StoragePolicies.types' export interface StoragePolicyFormField extends PolicyFormField { allowedOperations: string[] diff --git a/apps/studio/components/interfaces/Database/Policies/PolicyEditor/PolicyAllowedOperation.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditor/PolicyAllowedOperation.tsx similarity index 100% rename from apps/studio/components/interfaces/Database/Policies/PolicyEditor/PolicyAllowedOperation.tsx rename to apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditor/PolicyAllowedOperation.tsx diff --git a/apps/studio/components/interfaces/Database/Policies/PolicyEditor/PolicyDefinition.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditor/PolicyDefinition.tsx similarity index 100% rename from apps/studio/components/interfaces/Database/Policies/PolicyEditor/PolicyDefinition.tsx rename to apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditor/PolicyDefinition.tsx diff --git a/apps/studio/components/interfaces/Database/Policies/PolicyEditor/PolicyEditorFooter.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditor/PolicyEditorFooter.tsx similarity index 100% rename from apps/studio/components/interfaces/Database/Policies/PolicyEditor/PolicyEditorFooter.tsx rename to apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditor/PolicyEditorFooter.tsx diff --git a/apps/studio/components/interfaces/Database/Policies/PolicyEditor/PolicyName.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditor/PolicyName.tsx similarity index 100% rename from apps/studio/components/interfaces/Database/Policies/PolicyEditor/PolicyName.tsx rename to apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditor/PolicyName.tsx diff --git a/apps/studio/components/interfaces/Database/Policies/PolicyEditor/PolicyRoles.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditor/PolicyRoles.tsx similarity index 100% rename from apps/studio/components/interfaces/Database/Policies/PolicyEditor/PolicyRoles.tsx rename to apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditor/PolicyRoles.tsx diff --git a/apps/studio/components/interfaces/Database/Policies/PolicyEditor/index.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditor/index.tsx similarity index 100% rename from apps/studio/components/interfaces/Database/Policies/PolicyEditor/index.tsx rename to apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditor/index.tsx diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditorModal/PolicyEditorModal.constants.ts b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditorModal/PolicyEditorModal.constants.ts new file mode 100644 index 00000000000..060d9ee81c3 --- /dev/null +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditorModal/PolicyEditorModal.constants.ts @@ -0,0 +1,6 @@ +export const POLICY_MODAL_VIEWS = { + SELECTION: 'SELECTION', + TEMPLATES: 'TEMPLATES', + EDITOR: 'EDITOR', + REVIEW: 'REVIEW', +} diff --git a/apps/studio/components/interfaces/Database/Policies/PolicyEditorModal/PolicyEditorModalTitle.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditorModal/PolicyEditorModalTitle.tsx similarity index 93% rename from apps/studio/components/interfaces/Database/Policies/PolicyEditorModal/PolicyEditorModalTitle.tsx rename to apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditorModal/PolicyEditorModalTitle.tsx index ba194252cb3..044a4acaffd 100644 --- a/apps/studio/components/interfaces/Database/Policies/PolicyEditorModal/PolicyEditorModalTitle.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditorModal/PolicyEditorModalTitle.tsx @@ -2,7 +2,7 @@ import { noop } from 'lodash' import { ChevronLeft, FlaskConical } from 'lucide-react' import { Button } from 'ui' -import { POLICY_MODAL_VIEWS } from '../Policies.constants' +import { POLICY_MODAL_VIEWS } from './PolicyEditorModal.constants' import { DocsButton } from '@/components/ui/DocsButton' import { DOCS_URL } from '@/lib/constants' @@ -16,7 +16,7 @@ interface PolicyEditorModalTitleProps { onToggleFeaturePreviewModal: () => void } -const PolicyEditorModalTitle = ({ +export const PolicyEditorModalTitle = ({ view, schema, table, @@ -65,5 +65,3 @@ const PolicyEditorModalTitle = ({
) } - -export default PolicyEditorModalTitle diff --git a/apps/studio/components/interfaces/Database/Policies/PolicyEditorModal/index.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditorModal/index.tsx similarity index 95% rename from apps/studio/components/interfaces/Database/Policies/PolicyEditorModal/index.tsx rename to apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditorModal/index.tsx index d7b09c6416b..7a82ff90e73 100644 --- a/apps/studio/components/interfaces/Database/Policies/PolicyEditorModal/index.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyEditorModal/index.tsx @@ -4,7 +4,10 @@ import { useCallback, useEffect, useState } from 'react' import { toast } from 'sonner' import { Dialog, DialogContent, DialogHeader, DialogTitle } from 'ui' -import { POLICY_MODAL_VIEWS } from '../Policies.constants' +import { PolicyEditor } from '../PolicyEditor' +import { PolicyReview } from '../PolicyReview' +import { PolicySelection } from '../PolicySelection' +import { PolicyTemplates } from '../PolicyTemplates' import { DraftPostgresPolicyCreatePayload, DraftPostgresPolicyUpdatePayload, @@ -12,20 +15,19 @@ import { PolicyForReview, PostgresPolicyCreatePayload, PostgresPolicyUpdatePayload, -} from '../Policies.types' +} from '../StoragePolicies.types' import { createPayloadForCreatePolicy, createPayloadForUpdatePolicy, createSQLPolicy, -} from '../Policies.utils' -import { PolicyEditor } from '../PolicyEditor' -import { PolicyReview } from '../PolicyReview' -import PolicySelection from '../PolicySelection' -import PolicyTemplates from '../PolicyTemplates' -import { PolicyTemplate } from '../PolicyTemplates/PolicyTemplates.constants' -import { getGeneralPolicyTemplates } from './PolicyEditorModal.constants' -import PolicyEditorModalTitle from './PolicyEditorModalTitle' +} from '../StoragePolicies.utils' +import { POLICY_MODAL_VIEWS } from './PolicyEditorModal.constants' +import { PolicyEditorModalTitle } from './PolicyEditorModalTitle' import { useFeaturePreviewModal } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import { + getGeneralPolicyTemplates, + type PolicyTemplate, +} from '@/components/interfaces/Database/Policies/Policies.constants' import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import { useLatest } from '@/hooks/misc/useLatest' import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose' diff --git a/apps/studio/components/interfaces/Database/Policies/PolicyReview.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyReview.tsx similarity index 92% rename from apps/studio/components/interfaces/Database/Policies/PolicyReview.tsx rename to apps/studio/components/interfaces/Storage/StoragePolicies/PolicyReview.tsx index 77f27382bb0..a22c7c18ab2 100644 --- a/apps/studio/components/interfaces/Database/Policies/PolicyReview.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyReview.tsx @@ -2,7 +2,7 @@ import { isEmpty, noop } from 'lodash' import { useState } from 'react' import { Button, DialogFooter, DialogSection } from 'ui' -import type { PolicyForReview } from './Policies.types' +import type { PolicyForReview } from './StoragePolicies.types' import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor' interface PolicyReviewProps { @@ -11,8 +11,6 @@ interface PolicyReviewProps { onSelectSave: () => void } -// [Joshen] This seems like dead code atm, clean up separately - export const PolicyReview = ({ policy = {}, onSelectBack = noop, @@ -24,7 +22,7 @@ export const PolicyReview = ({ onSelectSave() } - let formattedSQLStatement = policy.statement || '' + const formattedSQLStatement = policy.statement || '' return ( <> diff --git a/apps/studio/components/interfaces/Database/Policies/PolicySelection.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicySelection.tsx similarity index 98% rename from apps/studio/components/interfaces/Database/Policies/PolicySelection.tsx rename to apps/studio/components/interfaces/Storage/StoragePolicies/PolicySelection.tsx index c1a7fc666cf..06dc7a89ade 100644 --- a/apps/studio/components/interfaces/Database/Policies/PolicySelection.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicySelection.tsx @@ -12,7 +12,7 @@ interface PolicySelectionProps { onToggleFeaturePreviewModal?: () => void } -const PolicySelection = ({ +export const PolicySelection = ({ description = '', showAssistantPreview, onViewTemplates = noop, @@ -89,5 +89,3 @@ const PolicySelection = ({ ) } - -export default PolicySelection diff --git a/apps/studio/components/interfaces/Database/Policies/PolicyTemplates/TemplatePreview.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyTemplates/TemplatePreview.tsx similarity index 92% rename from apps/studio/components/interfaces/Database/Policies/PolicyTemplates/TemplatePreview.tsx rename to apps/studio/components/interfaces/Storage/StoragePolicies/PolicyTemplates/TemplatePreview.tsx index 62520e18639..4e4effeaa91 100644 --- a/apps/studio/components/interfaces/Database/Policies/PolicyTemplates/TemplatePreview.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyTemplates/TemplatePreview.tsx @@ -1,6 +1,6 @@ import { isEmpty } from 'lodash' -import { PolicyTemplate } from './PolicyTemplates.constants' +import { type PolicyTemplate } from '@/components/interfaces/Database/Policies/Policies.constants' import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor' interface TemplatePreviewProps { diff --git a/apps/studio/components/interfaces/Database/Policies/PolicyTemplates/TemplatesList.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyTemplates/TemplatesList.tsx similarity index 93% rename from apps/studio/components/interfaces/Database/Policies/PolicyTemplates/TemplatesList.tsx rename to apps/studio/components/interfaces/Storage/StoragePolicies/PolicyTemplates/TemplatesList.tsx index 9e7afbcb273..4ece27c1cc1 100644 --- a/apps/studio/components/interfaces/Database/Policies/PolicyTemplates/TemplatesList.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyTemplates/TemplatesList.tsx @@ -1,7 +1,7 @@ import { noop } from 'lodash' import { Menu } from 'ui' -import { PolicyTemplate } from './PolicyTemplates.constants' +import { type PolicyTemplate } from '@/components/interfaces/Database/Policies/Policies.constants' interface TemplatesListProps { templates: PolicyTemplate[] diff --git a/apps/studio/components/interfaces/Database/Policies/PolicyTemplates/index.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyTemplates/index.tsx similarity index 91% rename from apps/studio/components/interfaces/Database/Policies/PolicyTemplates/index.tsx rename to apps/studio/components/interfaces/Storage/StoragePolicies/PolicyTemplates/index.tsx index 201e0adbb9f..4c1157a19df 100644 --- a/apps/studio/components/interfaces/Database/Policies/PolicyTemplates/index.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/PolicyTemplates/index.tsx @@ -2,9 +2,9 @@ import { isEmpty, noop } from 'lodash' import { useState } from 'react' import { Button, DialogSectionSeparator } from 'ui' -import { PolicyTemplate } from './PolicyTemplates.constants' import TemplatePreview from './TemplatePreview' import TemplatesList from './TemplatesList' +import { type PolicyTemplate } from '@/components/interfaces/Database/Policies/Policies.constants' interface PolicyTemplatesProps { templates?: PolicyTemplate[] @@ -12,7 +12,7 @@ interface PolicyTemplatesProps { onUseTemplate?: (template: PolicyTemplate) => void } -const PolicyTemplates = ({ +export const PolicyTemplates = ({ templates = [], templatesNote = '', onUseTemplate = noop, @@ -45,5 +45,3 @@ const PolicyTemplates = ({ ) } - -export default PolicyTemplates diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx index 64dfb572535..79a7110b4b7 100644 --- a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx @@ -16,14 +16,14 @@ import { import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { formatPoliciesForStorage, UNGROUPED_POLICY_SYMBOL } from '../Storage.utils' -import { StoragePoliciesBucketRow } from './StoragePoliciesBucketRow' -import { BucketsPolicies, type SelectBucketPolicyForAction } from './StoragePoliciesBucketsSection' -import { StoragePoliciesEditPolicyModal } from './StoragePoliciesEditPolicyModal' +import { PolicyEditorModal } from './PolicyEditorModal' import type { PostgresPolicyCreatePayload, PostgresPolicyUpdatePayload, -} from '@/components/interfaces/Database/Policies/Policies.types' -import { PolicyEditorModal } from '@/components/interfaces/Database/Policies/PolicyEditorModal' +} from './StoragePolicies.types' +import { StoragePoliciesBucketRow } from './StoragePoliciesBucketRow' +import { BucketsPolicies, type SelectBucketPolicyForAction } from './StoragePoliciesBucketsSection' +import { StoragePoliciesEditPolicyModal } from './StoragePoliciesEditPolicyModal' import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils' import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query' import { useDatabasePolicyCreateMutation } from '@/data/database-policies/database-policy-create-mutation' diff --git a/apps/studio/components/interfaces/Database/Policies/Policies.types.ts b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.types.ts similarity index 95% rename from apps/studio/components/interfaces/Database/Policies/Policies.types.ts rename to apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.types.ts index 911fb6426fc..4461bfd9fbb 100644 --- a/apps/studio/components/interfaces/Database/Policies/Policies.types.ts +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.types.ts @@ -58,3 +58,8 @@ export interface DraftPostgresPolicyUpdatePayload extends Omit< definition?: DisplayableSqlFragment check?: DisplayableSqlFragment } + +export interface PolicyForReview { + description?: string + statement?: string +} diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.utils.ts b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.utils.ts new file mode 100644 index 00000000000..a9131a949ad --- /dev/null +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicies.utils.ts @@ -0,0 +1,158 @@ +import type { PGPolicy } from '@supabase/pg-meta' +import { untrustedSql } from '@supabase/pg-meta' +import { has, isEmpty, isEqual } from 'lodash' + +import { + DraftPostgresPolicyCreatePayload, + DraftPostgresPolicyUpdatePayload, + PolicyFormField, + PolicyForReview, +} from './StoragePolicies.types' + +/** + * Returns an array of SQL statements that will preview in the review step of the policy editor + * @param {*} policyFormFields { name, using, check, command } + */ + +export const createSQLPolicy = ( + policyFormFields: PolicyFormField, + originalPolicyFormFields?: PGPolicy +) => { + const { definition, check } = policyFormFields + const formattedPolicyFormFields = { + ...policyFormFields, + definition: definition + ? definition.replace(/\s+/g, ' ').trim() + : definition === undefined + ? null + : definition, + check: check ? check.replace(/\s+/g, ' ').trim() : check === undefined ? null : check, + } + + if (!originalPolicyFormFields || isEmpty(originalPolicyFormFields)) { + return createSQLStatementForCreatePolicy(formattedPolicyFormFields) + } + + // If there are no changes, return an empty object + if (isEqual(policyFormFields, originalPolicyFormFields)) { + return {} + } + + // Extract out all the fields that updated + const fieldsToUpdate: any = {} + if (!isEqual(formattedPolicyFormFields.name, originalPolicyFormFields.name)) { + fieldsToUpdate.name = formattedPolicyFormFields.name + } + if (!isEqual(formattedPolicyFormFields.definition, originalPolicyFormFields.definition)) { + fieldsToUpdate.definition = formattedPolicyFormFields.definition + } + if (!isEqual(formattedPolicyFormFields.check, originalPolicyFormFields.check)) { + fieldsToUpdate.check = formattedPolicyFormFields.check + } + if (!isEqual(formattedPolicyFormFields.roles, originalPolicyFormFields.roles)) { + fieldsToUpdate.roles = formattedPolicyFormFields.roles + } + + if (!isEmpty(fieldsToUpdate)) { + return createSQLStatementForUpdatePolicy(formattedPolicyFormFields, fieldsToUpdate) + } + + return {} +} + +const createSQLStatementForCreatePolicy = (policyFormFields: PolicyFormField): PolicyForReview => { + const { name, definition, check, command, schema, table } = policyFormFields + const roles = policyFormFields.roles.length === 0 ? ['public'] : policyFormFields.roles + const description = `Add policy for the ${command} operation under the policy "${name}"` + const statement = [ + `CREATE POLICY "${name}" ON "${schema}"."${table}"`, + `AS PERMISSIVE FOR ${command}`, + `TO ${roles.join(', ')}`, + `${definition ? `USING (${definition})` : ''}`, + `${check ? `WITH CHECK (${check})` : ''}`, + ].join('\n') + + return { description, statement } +} + +const createSQLStatementForUpdatePolicy = ( + policyFormFields: PolicyFormField, + fieldsToUpdate: Partial +): PolicyForReview => { + const { name, schema, table } = policyFormFields + + const definitionChanged = has(fieldsToUpdate, ['definition']) + const checkChanged = has(fieldsToUpdate, ['check']) + const nameChanged = has(fieldsToUpdate, ['name']) + const rolesChanged = has(fieldsToUpdate, ['roles']) + + const parameters = Object.keys(fieldsToUpdate) + const description = `Update policy's ${ + parameters.length === 1 + ? parameters[0] + : `${parameters.slice(0, parameters.length - 1).join(', ')} and ${ + parameters[parameters.length - 1] + }` + } ` + const roles = + (fieldsToUpdate?.roles ?? []).length === 0 ? ['public'] : (fieldsToUpdate.roles as string[]) + + const alterStatement = `ALTER POLICY "${name}" ON "${schema}"."${table}"` + const statement = [ + 'BEGIN;', + ...(definitionChanged ? [` ${alterStatement} USING (${fieldsToUpdate.definition});`] : []), + ...(checkChanged ? [` ${alterStatement} WITH CHECK (${fieldsToUpdate.check});`] : []), + ...(rolesChanged ? [` ${alterStatement} TO ${roles.join(', ')};`] : []), + ...(nameChanged ? [` ${alterStatement} RENAME TO "${fieldsToUpdate.name}";`] : []), + 'COMMIT;', + ].join('\n') + + return { description, statement } +} + +// These constructors return DRAFT payloads — `definition`/`check` are still +// `DisplayableSqlFragment`. Promotion to `SafeSqlFragment` must happen at the user gesture +// (the Save click in `PolicyEditorModal`), not here, since this module has no guarantee that +// it was reached via a deliberate user action. +export const createPayloadForCreatePolicy = ( + policyFormFields: PolicyFormField +): DraftPostgresPolicyCreatePayload => { + const { name, schema, table, command, definition, check, roles } = policyFormFields + return { + name, + schema, + table, + action: 'PERMISSIVE', + command: command || undefined, + definition: !definition ? undefined : untrustedSql(definition), + check: !check ? undefined : untrustedSql(check), + roles: roles.length > 0 ? roles : undefined, + } +} + +export const createPayloadForUpdatePolicy = ( + policyFormFields: PolicyFormField, + originalPolicyFormFields: PGPolicy +): DraftPostgresPolicyUpdatePayload => { + const { definition, check } = policyFormFields + const formattedDefinition = definition ? definition.replace(/\s+/g, ' ').trim() : definition + const formattedCheck = check ? check.replace(/\s+/g, ' ').trim() : check + + const payload: DraftPostgresPolicyUpdatePayload = { id: originalPolicyFormFields.id } + + if (!isEqual(policyFormFields.name, originalPolicyFormFields.name)) { + payload.name = policyFormFields.name + } + if (!isEqual(formattedDefinition, originalPolicyFormFields.definition)) { + payload.definition = !formattedDefinition ? undefined : untrustedSql(formattedDefinition) + } + if (!isEqual(formattedCheck, originalPolicyFormFields.check)) { + payload.check = !formattedCheck ? undefined : untrustedSql(formattedCheck) + } + if (!isEqual(policyFormFields.roles, originalPolicyFormFields.roles)) { + if (policyFormFields.roles.length === 0) payload.roles = ['public'] + else payload.roles = policyFormFields.roles || undefined + } + + return payload +} diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditPolicyModal.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditPolicyModal.tsx index e254b2d4cd0..070b1c814e4 100644 --- a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditPolicyModal.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditPolicyModal.tsx @@ -8,13 +8,13 @@ import { createPayloadsForAddPolicy, createSQLPolicies, } from '../Storage.utils' +import { POLICY_MODAL_VIEWS } from './PolicyEditorModal/PolicyEditorModal.constants' +import { PolicySelection } from './PolicySelection' +import { PolicyTemplates } from './PolicyTemplates' import { STORAGE_POLICY_TEMPLATES } from './StoragePolicies.constants' -import StoragePoliciesEditor from './StoragePoliciesEditor' -import StoragePoliciesReview from './StoragePoliciesReview' +import { StoragePoliciesEditor } from './StoragePoliciesEditor' +import { StoragePoliciesReview } from './StoragePoliciesReview' import { StoragePolicyEditorModalTitle } from './StoragePolicyEditorModalTitle' -import { POLICY_MODAL_VIEWS } from '@/components/interfaces/Database/Policies/Policies.constants' -import PolicySelection from '@/components/interfaces/Database/Policies/PolicySelection' -import PolicyTemplates from '@/components/interfaces/Database/Policies/PolicyTemplates' const newPolicyTemplate: any = { name: '', diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditor.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditor.tsx index 74fa9482b89..981c35abacd 100644 --- a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditor.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditor.tsx @@ -3,8 +3,8 @@ import { Button, Checkbox, cn, DialogSection, DialogSectionSeparator } from 'ui' import { STORAGE_CLIENT_LIBRARY_MAPPINGS } from '../Storage.constants' import { deriveAllowedClientLibraryMethods } from '../Storage.utils' -import { PolicyName } from '@/components/interfaces/Database/Policies/PolicyEditor/PolicyName' -import { PolicyRoles } from '@/components/interfaces/Database/Policies/PolicyEditor/PolicyRoles' +import { PolicyName } from './PolicyEditor/PolicyName' +import { PolicyRoles } from './PolicyEditor/PolicyRoles' import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor' import { DOCS_URL } from '@/lib/constants' @@ -149,7 +149,7 @@ const PolicyEditorFooter = ({ onViewTemplates = () => {}, onReviewPolicy = () => // [Refactor] All these update methods could be summarised into one single function probably -const StoragePoliciesEditor = ({ +export const StoragePoliciesEditor = ({ policyFormFields = {}, onViewTemplates = noop, onUpdatePolicyName = noop, @@ -194,5 +194,3 @@ const StoragePoliciesEditor = ({ ) } - -export default StoragePoliciesEditor diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesReview.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesReview.tsx index 02e3d301596..d4b687a3c5b 100644 --- a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesReview.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesReview.tsx @@ -17,7 +17,7 @@ interface StoragePoliciesReviewProps { onSelectSave: any } -const StoragePoliciesReview = ({ +export const StoragePoliciesReview = ({ policyStatements = [], onSelectBack = () => {}, onSelectSave = () => {}, @@ -74,5 +74,3 @@ const StoragePoliciesReview = ({ ) } - -export default StoragePoliciesReview diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicyEditorModalTitle.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicyEditorModalTitle.tsx index 6278433c9a6..7138302232b 100644 --- a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicyEditorModalTitle.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicyEditorModalTitle.tsx @@ -3,7 +3,7 @@ import { ChevronLeft, X } from 'lucide-react' import { Dialog as DialogPrimitive } from 'radix-ui' import { cn } from 'ui' -import { POLICY_MODAL_VIEWS } from '@/components/interfaces/Database/Policies/Policies.constants' +import { POLICY_MODAL_VIEWS } from './PolicyEditorModal/PolicyEditorModal.constants' import { DocsButton } from '@/components/ui/DocsButton' import { DOCS_URL } from '@/lib/constants' diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx index ec146b9d980..05a0d70b445 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx @@ -37,10 +37,6 @@ import type { ImportContent } from './TableEditor/TableEditor.types' import { useTableRowOperations } from '@/components/grid/hooks/useTableRowOperations' import { getStableRowIdentifiers } from '@/components/grid/utils/queueOperationUtils' import { useIsQueueOperationsEnabled } from '@/components/interfaces/Account/Preferences/useDashboardSettings' -import { - acceptGeneratedPolicy, - type GeneratedPolicy, -} from '@/components/interfaces/Database/Policies/Policies.utils' import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import { databasePoliciesKeys } from '@/data/database-policies/keys' import { useDatabasePublicationCreateMutation } from '@/data/database-publications/database-publications-create-mutation' @@ -83,7 +79,6 @@ type SaveTableParamsBase = { columns: ColumnField[] foreignKeyRelations: ForeignKey[] resolve: () => void - generatedPolicies?: GeneratedPolicy[] } type SaveTableParamsNew = SaveTableParamsBase & { @@ -598,7 +593,6 @@ export const SidePanelEditor = ({ configuration, columns, foreignKeyRelations, - generatedPolicies = [], resolve, }: SaveTableParams) => { let toastId @@ -663,7 +657,6 @@ export const SidePanelEditor = ({ 'table.has_rls': isRLSEnabled ? 1 : 0, 'table.has_foreign_keys': foreignKeyRelations.length > 0 ? 1 : 0, 'table.has_import': importContent !== undefined ? 1 : 0, - 'table.generated_policies_count': generatedPolicies.length, 'project.region': project?.region ?? 'local', ...(project?.cloud_provider && { 'project.cloud_provider': project.cloud_provider, @@ -674,13 +667,7 @@ export const SidePanelEditor = ({ }) try { - // The Save click is the explicit user gesture that promotes generated policy - // SQL (programmatic or AI) to executable. Programmatic fragments are already - // SafeSqlFragment; AI fragments are UntrustedSqlFragment — both are accepted - // here before being passed into createTable. - const acceptedPolicies = generatedPolicies.map(acceptGeneratedPolicy) - - const { table, failedPolicies } = await createTable({ + const { table } = await createTable({ projectRef: project?.ref!, connectionString: project?.connectionString, toastId, @@ -689,13 +676,10 @@ export const SidePanelEditor = ({ foreignKeyRelations, isRLSEnabled, importContent, - generatedPolicies: acceptedPolicies, - onCreatePoliciesSuccess: () => track('rls_generated_policies_created'), track, }) createTableSpan.setAttribute('table.created', 1) - createTableSpan.setAttribute('table.failed_policies', failedPolicies.length) await Sentry.startSpan( { name: 'create_table.post_creation', op: 'db.table.post_creation' }, @@ -736,24 +720,7 @@ export const SidePanelEditor = ({ } ) - // Show success toast after everything is complete - if (failedPolicies.length > 0) { - toast.success( - `Table ${table.name} is created successfully, but we ran into issues creating ${failedPolicies.length} policie${failedPolicies.length > 1 ? 's' : ''}`, - { - id: toastId, - description: ( -
    - {failedPolicies.map((x) => ( -
  • {x.name}
  • - ))} -
- ), - } - ) - } else { - toast.success(`Table ${table.name} is good to go!`, { id: toastId }) - } + toast.success(`Table ${table.name} is good to go!`, { id: toastId }) onTableCreated(table) } catch (error) { diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.createTable.test.ts b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.createTable.test.ts index 02e843e0f2c..2f5b366797a 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.createTable.test.ts +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.createTable.test.ts @@ -132,7 +132,6 @@ describe('createTable', () => { }) expect(mockTrack).toHaveBeenCalledWith('table_created', { - has_generated_policies: false, method: 'table_editor', schema_name: 'public', table_name: 'test_table', @@ -148,7 +147,6 @@ describe('createTable', () => { ) expect(result).toStrictEqual({ - failedPolicies: [], table: mockTableResult, }) }) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx index 5221b41011d..63b72069d9d 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/nextjs' +import type { PGTablePrimaryKey } from '@supabase/pg-meta' import pgMeta, { getAddForeignKeySQL, getAddPrimaryKeySQL, @@ -11,7 +12,6 @@ import pgMeta, { getUpdateIdentitySequenceSQL, type ForeignKey, } from '@supabase/pg-meta' -import type { PGTablePrimaryKey } from '@supabase/pg-meta' import { joinSqlFragments, safeSql, type SafeSqlFragment } from '@supabase/pg-meta/src/pg-format' import { Query } from '@supabase/pg-meta/src/query' import { chunk, find, isEmpty, isEqual } from 'lodash' @@ -26,12 +26,10 @@ import type { ColumnField, CreateColumnPayload, UpdateColumnPayload } from './Si import { checkIfRelationChanged } from './TableEditor/ForeignKeysManagement/ForeignKeysManagement.utils' import type { ImportContent } from './TableEditor/TableEditor.types' import type { SupaRow } from '@/components/grid/types' -import { type AcceptedGeneratedPolicy } from '@/components/interfaces/Database/Policies/Policies.utils' import { SparkBar } from '@/components/ui/SparkBar' import { createDatabaseColumn } from '@/data/database-columns/database-column-create-mutation' import { deleteDatabaseColumn } from '@/data/database-columns/database-column-delete-mutation' import { updateDatabaseColumn } from '@/data/database-columns/database-column-update-mutation' -import { createDatabasePolicy } from '@/data/database-policies/database-policy-create-mutation' import type { Constraint } from '@/data/database/constraints-query' import { ForeignKeyConstraint } from '@/data/database/foreign-key-constraints-query' import { databaseKeys } from '@/data/database/keys' @@ -443,8 +441,6 @@ export const createTable = async ({ foreignKeyRelations, isRLSEnabled, importContent, - generatedPolicies = [], - onCreatePoliciesSuccess, track, }: { projectRef: string @@ -459,8 +455,6 @@ export const createTable = async ({ foreignKeyRelations: ForeignKey[] isRLSEnabled: boolean importContent?: ImportContent - generatedPolicies?: AcceptedGeneratedPolicy[] - onCreatePoliciesSuccess?: () => void track: Track }) => { const queryClient = getQueryClient() @@ -545,51 +539,10 @@ export const createTable = async ({ } ) - // 6. Create generated RLS policies if any - // [Joshen] Possible area for optimization to create all policies in a single query call - // Can be subsequently added to the table creation SQL as well for a single transaction - - const failedPolicies: AcceptedGeneratedPolicy[] = [] - if (generatedPolicies.length > 0 && isRLSEnabled) { - await Sentry.startSpan( - { name: 'create_table.create_policies', op: 'db.policies.create' }, - async (span) => { - span.setAttribute('policies.count', generatedPolicies.length) - toast.loading(`Creating ${generatedPolicies.length} policies for table...`, { id: toastId }) - await Promise.all( - generatedPolicies.map(async (policy) => { - try { - return await createDatabasePolicy({ - projectRef, - connectionString, - payload: { - name: policy.name, - table: policy.table, - schema: policy.schema, - definition: policy.definition, - check: policy.check, - action: policy.action, - command: policy.command, - roles: policy.roles, - }, - }) - } catch (error: any) { - console.error('Failed to generate policy', error.message) - failedPolicies.push(policy) - } - }) - ) - span.setAttribute('policies.failed_count', failedPolicies.length) - onCreatePoliciesSuccess?.() - } - ) - } - track('table_created', { method: 'table_editor', schema_name: payload.schema, table_name: payload.name, - has_generated_policies: generatedPolicies.length > 0 && isRLSEnabled, }) if (isRLSEnabled) { @@ -726,7 +679,7 @@ export const createTable = async ({ ) // Finally, return the created table - return { table, failedPolicies } + return { table } } /** TODO: Refactor to do in a single transaction */ diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx index 86d0a076fd0..a8313d0cd1a 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx @@ -242,7 +242,6 @@ export const TableEditor = ({ columns, foreignKeyRelations: fkRelations, resolve, - generatedPolicies: [], }) } else if (isDuplicating) { const payload: SaveTablePayloadFor<'duplicate'> = { @@ -256,7 +255,6 @@ export const TableEditor = ({ columns, foreignKeyRelations: fkRelations, resolve, - generatedPolicies: [], }) } else { const payload: SaveTablePayloadFor<'update'> = { @@ -271,7 +269,6 @@ export const TableEditor = ({ columns, foreignKeyRelations: fkRelations, resolve, - generatedPolicies: [], }) } } else { diff --git a/apps/studio/pages/project/[ref]/database/policies.tsx b/apps/studio/pages/project/[ref]/database/policies.tsx index fec4d768ee9..654a48de3fb 100644 --- a/apps/studio/pages/project/[ref]/database/policies.tsx +++ b/apps/studio/pages/project/[ref]/database/policies.tsx @@ -23,8 +23,8 @@ import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { useIsInlineEditorEnabled } from '@/components/interfaces/Account/Preferences/useDashboardSettings' import { useIsRLSTesterEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { Policies } from '@/components/interfaces/Database/Policies/Policies' +import { getGeneralPolicyTemplates } from '@/components/interfaces/Database/Policies/Policies.constants' import { PoliciesDataProvider } from '@/components/interfaces/Database/Policies/PoliciesDataContext' -import { getGeneralPolicyTemplates } from '@/components/interfaces/Database/Policies/PolicyEditorModal/PolicyEditorModal.constants' import { PolicyEditorPanel } from '@/components/interfaces/Database/Policies/PolicyEditorPanel' import { generatePolicyUpdateSQL,