From ee8f901bfd3ccfa104add57b43f006847368d882 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Wed, 26 Jun 2024 13:27:29 +0200 Subject: [PATCH] feat: RLS policies for realtime channels (#27490) * Add queries and mutations for realtime channels. * Change the layout, add a link for policies and modal for creating a channel. * Add a page for policies. * Add templates for realtime tables. * Fix the spacing in the PolicyDetails component. Add better types. * Pass templateData to the template generating functions. * Various CSS fixes. * Add components for rendering the realtime policies page. * Fix the editing policy flow. * Fix refetching when adding or deleting policies. * Minor various fixes. * Remove an old console.log. * More fixes for templates. * Make the realtime policies open by default when clicking on realtime. * Update the realtime policy templates. * Use a dropdown button for the three types of policies. * Fix a copy-paste mistake. * Fix icons. * Fix the toast. * Clean up the previous components. * Clean up more unneeded code. * Remove templateData prop. * Remove unneeded queries and mutations. * More cleanup. * Reuse the components from the Auth Policies. * Minor fixes for the new components. * Add a feature flag for AuthZ. * Revert some changes. * Fix a build error. * Fix all definitions. * Fix an issue with copying large lines which from templates which are word-wrapped. * Addressed various comments by Joshen. * Revert the UI changes in the Policy Editor. * Update the template names. * Small style fix --------- Co-authored-by: Joshen Lim --- .../AIPolicyEditorPanel/PolicyDetailsV2.tsx | 11 +- .../AIPolicyEditorPanel/PolicyTemplates.tsx | 13 +- .../AIPolicyEditorPanel/RLSCodeEditor.tsx | 10 +- .../Policies/AIPolicyEditorPanel/index.tsx | 4 +- .../interfaces/Auth/Policies/Policies.tsx | 2 +- .../PolicyEditorModal.constants.ts | 138 ++++++++++++++++++ .../PolicyTableRow/PolicyTableRowHeader.tsx | 44 +++--- .../interfaces/Realtime/Policies.tsx | 72 +++++++++ .../layouts/RealtimeLayout/RealtimeLayout.tsx | 14 +- .../RealtimeLayout/RealtimeMenu.utils.ts | 22 ++- apps/studio/data/database-policies/keys.ts | 2 +- .../pages/project/[ref]/auth/policies.tsx | 5 +- .../project/[ref]/realtime/inspector.tsx | 4 +- .../pages/project/[ref]/realtime/policies.tsx | 12 ++ 14 files changed, 312 insertions(+), 41 deletions(-) create mode 100644 apps/studio/components/interfaces/Realtime/Policies.tsx create mode 100644 apps/studio/pages/project/[ref]/realtime/policies.tsx diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyDetailsV2.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyDetailsV2.tsx index fa81a9fc59a..14a131c1aa5 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyDetailsV2.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyDetailsV2.tsx @@ -1,9 +1,10 @@ +import { Check, ChevronsUpDown } from 'lucide-react' import { useEffect, useState } from 'react' +import { UseFormReturn } from 'react-hook-form' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query' import { useTablesQuery } from 'data/tables/tables-query' -import { Check, ChevronsUpDown } from 'lucide-react' import { Button, CommandEmpty_Shadcn_, @@ -37,7 +38,13 @@ interface PolicyDetailsV2Props { searchString?: string selectedTable?: string isEditing: boolean - form: any + form: UseFormReturn<{ + name: string + table: string + behavior: string + command: string + roles: string + }> onUpdateCommand: (command: string) => void } diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyTemplates.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyTemplates.tsx index 73fefa60219..31830fc8537 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyTemplates.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/PolicyTemplates.tsx @@ -8,7 +8,10 @@ import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock' import CardButton from 'components/ui/CardButton' import CopyButton from 'components/ui/CopyButton' import NoSearchResults from 'components/ui/NoSearchResults' -import { getGeneralPolicyTemplates } from '../PolicyEditorModal/PolicyEditorModal.constants' +import { + getGeneralPolicyTemplates, + getRealtimePolicyTemplates, +} from '../PolicyEditorModal/PolicyEditorModal.constants' interface PolicyTemplatesProps { schema: string @@ -26,7 +29,11 @@ export const PolicyTemplates = ({ onSelectTemplate, }: PolicyTemplatesProps) => { const [search, setSearch] = useState('') - const templates = getGeneralPolicyTemplates(schema, table.length > 0 ? table : 'table_name') + + const templates = + schema === 'realtime' + ? getRealtimePolicyTemplates() + : getGeneralPolicyTemplates(schema, table.length > 0 ? table : 'table_name') const baseTemplates = selectedPolicy !== undefined @@ -68,7 +75,7 @@ export const PolicyTemplates = ({ title={template.name} titleClass="text-sm" className={cn( - 'transition', + 'transition w-full', template.id === selectedTemplate ? '!border-stronger bg-surface-200 hover:!border-stronger' : '' diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/RLSCodeEditor.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/RLSCodeEditor.tsx index e51b2284519..6b33be84e2c 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/RLSCodeEditor.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/RLSCodeEditor.tsx @@ -4,9 +4,9 @@ import { MutableRefObject, useEffect, useRef } from 'react' import { cn } from 'ui' import { Markdown } from 'components/interfaces/Markdown' -import { noop } from 'lodash' -import { formatQuery } from 'data/sql/format-sql-query' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { formatQuery } from 'data/sql/format-sql-query' +import { noop } from 'lodash' // [Joshen] Is there a way we can just have one single MonacoEditor component that's shared across the dashboard? // Feels like we're creating multiple copies of Editor. I'm keen to make this one the defacto as well so lets make sure @@ -119,6 +119,12 @@ const RLSCodeEditor = ({ onChange() } + // when the value has changed, trigger the onChange callback so that the height of the container can be adjusted. + // Happens when the value wordwraps and is updated via a template. + useEffect(() => { + onChange() + }, [value]) + async function formatPgsql(value: any) { try { const formatted = await formatQuery({ diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx index 6c905ed4de4..39edae2948f 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx @@ -195,9 +195,9 @@ export const AIPolicyEditorPanel = memo(function ({ }, [chatMessages, debugThread]) const { mutate: executeMutation, isLoading: isExecuting } = useExecuteSqlMutation({ - onSuccess: () => { + onSuccess: async () => { // refresh all policies - queryClient.invalidateQueries(databasePoliciesKeys.list(ref)) + await queryClient.invalidateQueries(databasePoliciesKeys.list(ref)) toast.success('Successfully created new policy') onSelectCancel() }, diff --git a/apps/studio/components/interfaces/Auth/Policies/Policies.tsx b/apps/studio/components/interfaces/Auth/Policies/Policies.tsx index a299ea11ac1..24606d04e9a 100644 --- a/apps/studio/components/interfaces/Auth/Policies/Policies.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/Policies.tsx @@ -145,7 +145,7 @@ const Policies = ({
onSelectCreatePolicy(table.name)} onSelectEditPolicy={onSelectEditPolicy} diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts index b71239c9bdb..ddcecc68903 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorModal/PolicyEditorModal.constants.ts @@ -173,3 +173,141 @@ for select using ( roles: [], }, ] + +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: "realtime.messages.extension = 'broadcast'", + check: '', + 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 update +TO authenticated +using ( realtime.messages.extension = 'broadcast' ) +with check ( realtime.messages.extension = 'broadcast' );`.trim(), + name: 'Allow pushing broadcasts for authenticated users only', + definition: "realtime.messages.extension = 'broadcast'", + check: "realtime.messages.extension = 'broadcast'", + command: 'UPDATE', + 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: `realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name'`, + check: '', + 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 update +using ( realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name' ) +with check ( realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name' );`.trim(), + name: 'Allow pushing broadcasts to specific channel', + definition: `realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name'`, + check: `realtime.messages.extension = 'broadcast' AND realtime.topic() = 'channel_name'`, + command: 'UPDATE', + 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: "realtime.messages.extension = 'presence'", + check: '', + 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 update +TO authenticated +using ( realtime.messages.extension = 'presence' ) +with check ( realtime.messages.extension = 'presence' ); + ;`.trim(), + name: 'Allow broadcasting presences on all channels for authenticated users only', + definition: "realtime.messages.extension = 'presence'", + check: "realtime.messages.extension = 'presence'", + command: 'UPDATE', + 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: `realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name'`, + check: '', + 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 update +using ( realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name' ) +with check ( realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name' ); + ;`.trim(), + name: 'Publish presence to a specific channel', + definition: `realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name'`, + check: `realtime.messages.extension = 'presence' AND realtime.topic() = 'channel_name'`, + command: 'UPDATE', + roles: [], + }, + ] as PolicyTemplate[] + return results +} diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx index cfee481bc00..530a2aa9f22 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx @@ -8,7 +8,7 @@ import { useRouter } from 'next/router' import { useIsRLSAIAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { useCheckPermissions } from 'hooks' -import { Badge, Button } from 'ui' +import { Badge, Button, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, Tooltip_Shadcn_ } from 'ui' interface PolicyTableRowHeaderProps { table: PostgresTable @@ -29,6 +29,10 @@ const PolicyTableRowHeader = ({ const isAiAssistantEnabled = useIsRLSAIAssistantEnabled() const canToggleRLS = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') + const isRealtimeSchema = table.schema === 'realtime' + const isRealtimeMessagesTable = isRealtimeSchema && table.name === 'messages' + const isTableLocked = isRealtimeSchema ? !isRealtimeMessagesTable : isLocked + return (
@@ -36,7 +40,7 @@ const PolicyTableRowHeader = ({

{table.name}

- {isLocked && ( + {isTableLocked && ( Locked @@ -48,22 +52,22 @@ const PolicyTableRowHeader = ({
- {!isLocked && ( + {!isTableLocked && (
- - - - - {!canToggleRLS && ( - - + {!isRealtimeMessagesTable ? ( + + + + + {!canToggleRLS && ( +
-
-
- )} -
+ + )} + + ) : null} {!isAiAssistantEnabled && (