diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx index 54680d64291..aa5065a4f98 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx @@ -133,11 +133,6 @@ export const useIsSqlEditorManualSaveEnabled = () => { return sqlEditorManualSaveEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE] } -export const useIsRLSTesterEnabled = () => { - const { flags } = useFeaturePreviewContext() - return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER] -} - export const useIsMarketplaceEnabled = () => { const { flags } = useFeaturePreviewContext() const isMarketplaceEnabled = useFlag('marketplaceIntegrations') diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx index 49ef243658d..7a03d13ea76 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx @@ -34,12 +34,9 @@ import { IntegrationsLayoutPreview } from './IntegrationsLayoutPreview' import { JitDbAccessPreview } from './JitDbAccessPreview' import { PgDeltaDiffPreview } from './PgDeltaDiffPreview' import { PlatformWebhooksPreview } from './PlatformWebhooksPreview' -import { RLSTesterPreview } from './RLSTesterPreview' import { SqlEditorManualSavePreview } from './SqlEditorManualSavePreview' import { UnifiedLogsPreview } from './UnifiedLogsPreview' import { FeaturePreview, useFeaturePreviews } from './useFeaturePreviews' -import { useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' import { IS_PLATFORM } from '@/lib/constants' import { useTrack } from '@/lib/telemetry/track' @@ -52,7 +49,6 @@ const FEATURE_PREVIEW_KEY_TO_CONTENT: { [LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS]: , [LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS]: , [LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS]: , - [LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER]: , [LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE]: , [LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE]: , } @@ -70,12 +66,6 @@ export const FeaturePreviewModal = () => { const featurePreviewContext = useFeaturePreviewContext() const track = useTrack() - const { dismissBanner } = useBannerStack() - const [, setIsDismissedRlsTesterBanner] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.RLS_TESTER_BANNER_DISMISSED(ref ?? ''), - false - ) - const { flags, onUpdateFlag } = featurePreviewContext const allFeaturePreviews = ( IS_PLATFORM ? featurePreviews : featurePreviews.filter((x) => !x.isPlatformOnly) @@ -94,11 +84,6 @@ export const FeaturePreviewModal = () => { const isEnabling = !isSelectedFeatureEnabled - if (selectedFeature.key === LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER) { - dismissBanner('rls-tester-banner') - setIsDismissedRlsTesterBanner(true) - } - onUpdateFlag(selectedFeature.key, isEnabling) track(isEnabling ? 'feature_preview_enabled' : 'feature_preview_disabled', { feature: selectedFeature.key, diff --git a/apps/studio/components/interfaces/App/FeaturePreview/RLSTesterPreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/RLSTesterPreview.tsx deleted file mode 100644 index 01ea7cffdc5..00000000000 --- a/apps/studio/components/interfaces/App/FeaturePreview/RLSTesterPreview.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useParams } from 'common' -import Image from 'next/image' - -import { InlineLink } from '@/components/ui/InlineLink' -import { BASE_PATH } from '@/lib/constants' - -export const RLSTesterPreview = () => { - const { ref } = useParams() - - return ( -
-

- Verify if your RLS policies have been set up properly by running queries as a specific user. - While role impersonation isn't a new feature on the dashboard, we've built a dedicated UI - for this which will also show what policies are evaluated for the query. -

- rls-tester-preview -
-

Enabling this preview will:

-
    -
  • - Show the "Test" button on the{' '} - - Database Policies page - -
  • -
-
-
- ) -} diff --git a/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts b/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts index c256c2ffac2..dd425cc52f5 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts +++ b/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts @@ -30,16 +30,6 @@ export const useFeaturePreviews = (): FeaturePreview[] => { return useMemo( () => [ - { - key: LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER, - name: 'RLS Tester', - discussionsUrl: 'https://github.com/orgs/supabase/discussions/45233', - enabled: true, - isNew: true, - isPlatformOnly: false, - isDefaultOptIn: false, - getRoute: (ref?: string) => `/project/${ref}/database/policies`, - }, { key: LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS, name: 'Updated Logs interface', diff --git a/apps/studio/components/interfaces/Database/RLSTester/InferredSQLViewer.tsx b/apps/studio/components/interfaces/Database/RLSTester/InferredSQLViewer.tsx deleted file mode 100644 index ecdaf4b6b80..00000000000 --- a/apps/studio/components/interfaces/Database/RLSTester/InferredSQLViewer.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { UntrustedSqlFragment } from '@supabase/pg-meta' -import { Loader2 } from 'lucide-react' -import { Badge, Tooltip, TooltipContent, TooltipTrigger } from 'ui' - -import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor' - -export const InferredSQLViewer = ({ - sql, - isLoading = false, -}: { - sql: UntrustedSqlFragment | undefined - isLoading?: boolean -}) => { - return ( - <> -
-
-

Inferred SQL:

- {isLoading && } -
-
- - - Generated - - - This query is inferred from client library code with the help of the Assistant and may - not guarantee correctness. - - -
-
-
- {isLoading && !sql ? ( -
- -
- ) : ( - - )} -
- - ) -} diff --git a/apps/studio/components/interfaces/Database/RLSTester/RLSTableCard.tsx b/apps/studio/components/interfaces/Database/RLSTester/RLSTableCard.tsx deleted file mode 100644 index 724f8b57cf2..00000000000 --- a/apps/studio/components/interfaces/Database/RLSTester/RLSTableCard.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import { Check, ChevronDown, Edit, X } from 'lucide-react' -import { useMemo } from 'react' -import { cn, Collapsible, CollapsibleContent, CollapsibleTrigger, WarningIcon } from 'ui' - -import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils' -import { ButtonTooltip } from '@/components/ui/ButtonTooltip' -import { type ParseSQLQueryOperations } from '@/data/misc/parse-query-mutation' - -interface RLSTableCardProps { - table: { schema: string; name: string; isRLSEnabled: boolean } - operation: ParseSQLQueryOperations - role?: string - policies: Policy[] - hasError: boolean - handleSelectEditPolicy: (policy: Policy) => void -} - -export const RLSTableCard = ({ - table, - operation, - role, - policies, - hasError, - handleSelectEditPolicy, -}: RLSTableCardProps) => { - const { schema, name, isRLSEnabled } = table - const trueOnlyPolicy = policies.find((x) => x.definition === 'true') - const falseOnlyPolicy = policies.find((x) => x.definition === 'false') - const noPolicies = isRLSEnabled && policies.length === 0 - - const tableAccessDescription = useMemo(() => { - if (!isRLSEnabled) { - return ( -

- RLS is disabled and all data is publicly accessible. We highly recommend enabling RLS and - adding policies to restrict access. -

- ) - } - - if (noPolicies) { - return ( -

- RLS is enabled but no policies exist for the{' '} - {role} role on this table -{' '} - {operation === 'SELECT' - ? 'no data will be returned' - : `no data will be ${operation?.toLowerCase()}${operation?.toLowerCase().endsWith('e') ? 'd' : 'ed'}`} - . -

- ) - } - - if (trueOnlyPolicy) { - return ( - <> -

- The policy "{trueOnlyPolicy.name}" for the{' '} - {role} role on this table evaluates to{' '} - true, so all data from this query is - accessible to this user. -

- - - ) - } - - if (falseOnlyPolicy) { - return ( - <> -

- The policy "{falseOnlyPolicy.name}" for the{' '} - {role} role on this table evaluates to{' '} - false, so no data from this query is - accessible to this user. -

- - - ) - } - - return ( - <> -

- {policies.length} {policies.length > 1 ? 'policies apply' : 'policy applies'} for the{' '} - {role} role on this table.{' '} - {operation === 'SELECT' - ? `Only rows that match ${policies.length > 1 ? 'these conditions' : 'this condition'} are returned.` - : `The ${operation} operation will only be successful if the conditions are matched.`} -

- - - ) - }, [ - isRLSEnabled, - noPolicies, - trueOnlyPolicy, - falseOnlyPolicy, - policies, - role, - operation, - handleSelectEditPolicy, - ]) - - return ( - - -
-
- {!isRLSEnabled ? ( - - ) : (hasError && operation === 'INSERT') || noPolicies || falseOnlyPolicy ? ( - - ) : ( - - )} -

- {schema}.{name} -

-
-
-
- {operation === 'SELECT' && ( -

- {noPolicies || falseOnlyPolicy - ? 'Returns no rows' - : !isRLSEnabled || !!trueOnlyPolicy - ? 'Returns all rows' - : null} -

- )} - -
-
- - {tableAccessDescription} - -
- ) -} - -const TableAccessPolicySummary = ({ - policies, - operation, - handleSelectEditPolicy, -}: { - policies: Policy[] - operation: ParseSQLQueryOperations - handleSelectEditPolicy: (policy: Policy) => void -}) => { - return ( -
-

- {policies.length} {policies.length > 1 ? 'policies' : 'policy'} applied -

-
    - {policies.map((policy) => ( -
  • -
    -

    {policy.name}

    -

    - {operation === 'SELECT' ? 'Show rows' : `Allow ${operation?.toLocaleLowerCase()}s`}{' '} - where:{' '} - - {policy.definition ?? policy.check} - -

    -
    - } - className="w-7" - tooltip={{ content: { side: 'bottom', text: 'Edit policy' } }} - onClick={() => { - handleSelectEditPolicy(policy) - }} - /> -
  • - ))} -
-
- ) -} diff --git a/apps/studio/components/interfaces/Database/RLSTester/RLSTester.types.ts b/apps/studio/components/interfaces/Database/RLSTester/RLSTester.types.ts deleted file mode 100644 index 2948c48f477..00000000000 --- a/apps/studio/components/interfaces/Database/RLSTester/RLSTester.types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils' -import { type User } from '@/data/auth/users-infinite-query' -import { type ParseSQLQueryResponse } from '@/data/misc/parse-query-mutation' - -export type ParseQueryResults = { - tables: { - schema: string - table: string - tablePolicies: Array - isRLSEnabled: boolean - }[] - operation: ParseSQLQueryResponse['operation'] - role?: string - user?: User -} diff --git a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterEmptyState.tsx b/apps/studio/components/interfaces/Database/RLSTester/RLSTesterEmptyState.tsx deleted file mode 100644 index d6eecd60beb..00000000000 --- a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterEmptyState.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { ListTodo } from 'lucide-react' - -export const RLSTesterEmptyState = () => { - return ( -
- -

Test summary and results will be shown here

-

- Verify that the results match what your RLS policies allow -

-
- ) -} diff --git a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.tsx b/apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.tsx deleted file mode 100644 index 5391c6318f8..00000000000 --- a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import { Badge, cn, Tabs, TabsContent, TabsList, TabsTrigger } from 'ui' -import { Admonition } from 'ui-patterns/admonition' - -import { Results } from '../../SQLEditor/UtilityPanel/Results' -import { RLSTableCard } from './RLSTableCard' -import { ParseQueryResults } from './RLSTester.types' -import { deriveRLSTestState } from './RLSTesterResults.utils' -import { useTestQueryRLS } from './useTestQueryRLS' -import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils' -import { type QueryResponseError } from '@/data/sql/execute-sql-mutation' - -interface RLSTesterResultsProps { - results: Object[] - autoLimit: boolean - parseQueryResults: ParseQueryResults - executeSqlError: Error | QueryResponseError | null | undefined - handleSelectEditPolicy: (policy: Policy) => void -} - -export const RLSTesterResults = ({ - results, - autoLimit, - parseQueryResults, - executeSqlError, - handleSelectEditPolicy, -}: RLSTesterResultsProps) => { - const { limit } = useTestQueryRLS() - - const { - isServiceRole, - tableWithRLSEnabledButNoPolicies, - tableWithRLSEnabledWithPolicyFalse, - tableWithRLSEnabledWithPoliciesDontApply, - noAccessToData, - } = deriveRLSTestState(parseQueryResults) - - const { operation, role } = parseQueryResults - const rlsBlockInsert = executeSqlError && operation === 'INSERT' - const noAccess = noAccessToData || rlsBlockInsert - - return ( -
-
-

Summary

- {noAccess ? ( - No access - ) : ( - {results.length > 0 ? 'Can access' : 'Has access'} - )} -
- - - - - Policies applied - - - Data preview - - - - {!!parseQueryResults && ( -
-
-

Ran as

- {!parseQueryResults.role ? ( - postgres - ) : parseQueryResults.user ? ( -

{parseQueryResults.user.email}

- ) : parseQueryResults.role === 'anon' ? ( -

an Anonymous user

- ) : null} -
- - {parseQueryResults.role === 'anon' && ( -

Not logged in user

- )} - {!!parseQueryResults.user && ( - ID: {parseQueryResults.user.id} - )} -
- )} - - - {!isServiceRole && - (!!tableWithRLSEnabledButNoPolicies ? ( - -

- This user{' '} - {operation === 'SELECT' - ? 'has no access to any rows' - : `is unable to ${operation?.toLowerCase()} any rows`}{' '} - from this query -

-

- The table{' '} - - {tableWithRLSEnabledButNoPolicies.schema}. - {tableWithRLSEnabledButNoPolicies.table} - {' '} - has RLS enabled but no policies set up for the{' '} - {parseQueryResults.role}{' '} - role. -

-
- ) : tableWithRLSEnabledWithPolicyFalse ? ( - -

- This user has no access to any rows from this query -

-

- The table{' '} - - {tableWithRLSEnabledWithPolicyFalse.schema}. - {tableWithRLSEnabledWithPolicyFalse.table} - {' '} - has a policy that evaluates to - false for the{' '} - {parseQueryResults.role}{' '} - role. -

-
- ) : rlsBlockInsert && - parseQueryResults.user && - tableWithRLSEnabledWithPoliciesDontApply ? ( - -

- This user is unable to {operation?.toLowerCase()} any rows from this query -

-

- The table{' '} - - {tableWithRLSEnabledWithPoliciesDontApply.schema}. - {tableWithRLSEnabledWithPoliciesDontApply.table} - {' '} - has a policy for the{' '} - {parseQueryResults.role}{' '} - role, but its condition wasn't satisfied for this specific request. -

-
- ) : null)} - - {isServiceRole && ( - -

- The postgres role has access to all rows - for this query -

-

- The postgres role has admin privileges and - bypasses all RLS policies. -

-
- )} - -
-

Table access

- {!isServiceRole && ( -
- {parseQueryResults?.tables.map((x) => { - const { schema, table, tablePolicies, isRLSEnabled } = x - return ( - - ) - })} -
- )} -
-
- -
- -
- {results.length > 0 && ( -

- {results.length} row{results.length > 1 ? 's' : ''} - {autoLimit && results.length >= limit && ` (Limited to only ${limit} rows)`} -

- )} -
-
-
- ) -} diff --git a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.utils.ts b/apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.utils.ts deleted file mode 100644 index 5f0e43025c6..00000000000 --- a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.utils.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { ParseQueryResults } from './RLSTester.types' - -export function deriveRLSTestState(parseQueryResults: ParseQueryResults | undefined) { - const isServiceRole = parseQueryResults?.role === undefined - const tableWithRLSEnabledButNoPolicies = parseQueryResults?.tables.find( - (x) => x.isRLSEnabled && x.tablePolicies.length === 0 - ) - const tableWithRLSEnabledWithPolicyFalse = parseQueryResults?.tables.find( - (x) => x.isRLSEnabled && x.tablePolicies.some((y) => y.definition === 'false') - ) - const tableWithRLSEnabledWithPoliciesDontApply = parseQueryResults?.tables.find( - (x) => x.isRLSEnabled && x.tablePolicies.length !== 0 - ) - - const noAccessToData = - !isServiceRole && (!!tableWithRLSEnabledButNoPolicies || !!tableWithRLSEnabledWithPolicyFalse) - - return { - isServiceRole, - tableWithRLSEnabledButNoPolicies, - tableWithRLSEnabledWithPolicyFalse, - tableWithRLSEnabledWithPoliciesDontApply, - noAccessToData, - } -} diff --git a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterSheet.tsx b/apps/studio/components/interfaces/Database/RLSTester/RLSTesterSheet.tsx deleted file mode 100644 index d5bcc5ed0b7..00000000000 --- a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterSheet.tsx +++ /dev/null @@ -1,387 +0,0 @@ -import { - acceptUntrustedSql, - safeSql, - type SafeSqlFragment, - type UntrustedSqlFragment, -} from '@supabase/pg-meta' -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectTrigger, - SelectValue, -} from '@ui/components/shadcn/ui/select' -import { LOCAL_STORAGE_KEYS, useFlag } from 'common' -import { Code, ExternalLink } from 'lucide-react' -import { useEffect, useRef, useState } from 'react' -import { - Button, - DialogSectionSeparator, - Sheet, - SheetContent, - SheetDescription, - SheetFooter, - SheetHeader, - SheetSection, - SheetTitle, - SheetTrigger, -} from 'ui' -import { Admonition } from 'ui-patterns/admonition' -import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' -import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' - -import { InferredSQLViewer } from './InferredSQLViewer' -import { type ParseQueryResults } from './RLSTester.types' -import { RLSTesterEmptyState } from './RLSTesterEmptyState' -import { RLSTesterResults } from './RLSTesterResults' -import { RoleSelector } from './RoleSelector' -import { SandboxManagement } from './SandboxManagement' -import { UserSelector } from './UserSelector' -import { UserSqlEditor } from './UserSqlEditor' -import { useTestQueryRLS, type TestQueryBlockedReason } from './useTestQueryRLS' -import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils' -import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' -import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown' -import { FeaturePreviewBadge } from '@/components/ui/FeaturePreviewBadge' -import { useTrack } from '@/lib/telemetry/track' -import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state' -import { PostgresSandboxProvider, usePostgresSandbox } from '@/state/postgres-sandbox/sandbox' -import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state' -import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state' - -interface RLSTesterSheetProps { - handleSelectEditPolicy: (policy: Policy) => void -} - -export const RLSTesterSheet = (props: RLSTesterSheetProps) => { - return ( - - - - ) -} - -const RLSTesterSheetContents = ({ handleSelectEditPolicy }: RLSTesterSheetProps) => { - const track = useTrack() - const aiSnap = useAiAssistantStateSnapshot() - const { openSidebar } = useSidebarManagerSnapshot() - const { setRole } = useRoleImpersonationStateSnapshot() - const { startSandbox, status, isSyncing } = usePostgresSandbox() - - const sandboxEnabled = useFlag('rlsTesterSandbox') - const sandboxIsStarting = status === 'loading' - - const [open, setOpen] = useState(false) - const [selectedOption, setSelectedOption] = useState<'anon' | 'authenticated'>('anon') - const [blockedReason, setBlockedReason] = useState() - - const [format, setFormat] = useState<'sql' | 'lib'>('sql') - const [inferredSQL, setInferredSQL] = useState() - - const [value, setValue] = useState(safeSql``) - const [results, setResults] = useState(null) - const [autoLimit, setAutoLimit] = useState(false) - const [parseQueryResults, setParseQueryResults] = useState() - - const { - testQuery, - inferSQLFromLib, - isLoading, - isInferring, - executeSqlError, - parseQueryError, - parseClientCodeError, - } = useTestQueryRLS() - const isErrorDueToRLS = - executeSqlError?.message.includes('violates row-level security policy') ?? false - const mutationOperation = blockedReason?.type === 'mutation' ? blockedReason.operation : undefined - - const debounceRef = useRef | null>(null) - - const handleValueChange = (sql: SafeSqlFragment) => { - setValue(sql) - if (format !== 'lib') return - - if (debounceRef.current !== null) clearTimeout(debounceRef.current) - if (!sql) return - - debounceRef.current = setTimeout(() => inferSQLFromLib(sql, setInferredSQL), 1500) - } - - const executionCallbacks = { - option: selectedOption, - acknowledgeMutation: blockedReason?.type === 'mutation', - onExecuteSQL: ({ result, isAutoLimit }: { result: Object[] | null; isAutoLimit: boolean }) => { - setResults(result) - setAutoLimit(isAutoLimit) - }, - onParseQuery: setParseQueryResults, - onValidationBlocked: setBlockedReason, - } - - const onRunQuery = async () => { - setBlockedReason(undefined) - - if (format === 'lib') { - if (!inferredSQL) return - const blocked = await testQuery({ - value: acceptUntrustedSql(inferredSQL), - ...executionCallbacks, - }) - if (!blocked) track('rls_tester_run_query_clicked', { type: 'inferred' }) - } else { - const blocked = await testQuery({ value, ...executionCallbacks }) - if (!blocked) track('rls_tester_run_query_clicked', { type: 'raw' }) - } - } - - const assistantSql = format === 'lib' && inferredSQL ? acceptUntrustedSql(inferredSQL) : value - - const getDebugPrompt = ({ includeSql = false }: { includeSql?: boolean } = {}) => { - const prompt = `Help me fix my RLS policy based on the attached SQL snippet that gave the following error: \n\n${executeSqlError?.message}\n\nEvaluate if the problem might be query first, before checking my RLS policies.` - - return includeSql ? `${prompt}\n\nSQL Query:\n\`\`\`sql\n${assistantSql}\n\`\`\`` : prompt - } - - const onDebugWithAssistant = () => { - const prompt = getDebugPrompt() - openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) - aiSnap.newChat({ - name: 'Debug RLS policies', - sqlSnippets: [assistantSql], - initialInput: prompt, - }) - setOpen(false) - } - - useEffect(() => { - setRole({ type: 'postgrest', role: 'anon' }) - return () => { - // Flip back to service role - setRole(undefined) - } - // [Joshen] Intentional - to only reset back to service role when navigating away - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - return ( - <> - - - - - - - - - What data can my users access? - - - - See what data a user is allowed to read or modify based on your RLS policies - - - -
- {sandboxEnabled && } - - -
- - {selectedOption === 'authenticated' && } -
- - - -
-

Query

-
- -
-
- -
- { - if (!isInferring && !isLoading) onRunQuery() - }, - }, - }} - /> -
-
- - {format === 'lib' && ( -
- - -
- )} - - - - {blockedReason?.type === 'multiple-statements' ? ( -
- -
- ) : blockedReason?.type === 'unsupported-operation' ? ( -
- -
- ) : parseQueryError ? ( -
- -
- ) : parseClientCodeError ? ( -
- -
- ) : executeSqlError && !isErrorDueToRLS ? ( -
- getDebugPrompt({ includeSql: true })} - onOpenAssistant={onDebugWithAssistant} - />, - ]} - /> -
- ) : isLoading ? ( -
- -
- ) : results === null && !isErrorDueToRLS ? ( - - ) : !!parseQueryResults ? ( - - ) : null} -
- - - -
- - -
-
-
-
- - setBlockedReason(undefined)} - alert={{ - title: `This ${mutationOperation} query will run against your actual database`, - description: 'Your database may be directly modified as a result. Are you sure?', - }} - > - {sandboxEnabled && ( - <> -

- We highly recommend using the sandbox to set up an ephemeral database environment for - testing insert, update, or delete queries. -

- - - )} -
- - ) -} diff --git a/apps/studio/components/interfaces/Database/RLSTester/RoleSelector.tsx b/apps/studio/components/interfaces/Database/RLSTester/RoleSelector.tsx deleted file mode 100644 index 0d23453b36b..00000000000 --- a/apps/studio/components/interfaces/Database/RLSTester/RoleSelector.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { RadioGroupStacked, RadioGroupStackedItem } from 'ui' -import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' - -import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state' - -interface RoleSelectorProps { - onSelectRole: (value: 'anon' | 'authenticated') => void -} - -export const RoleSelector = ({ onSelectRole }: RoleSelectorProps) => { - const { role, setRole } = useRoleImpersonationStateSnapshot() - - return ( - - - { - onSelectRole('anon') - setRole({ type: 'postgrest', role: 'anon' }) - }} - /> - { - onSelectRole('authenticated') - }} - /> - - - ) -} diff --git a/apps/studio/components/interfaces/Database/RLSTester/SandboxManagement.tsx b/apps/studio/components/interfaces/Database/RLSTester/SandboxManagement.tsx deleted file mode 100644 index 1200d4d361e..00000000000 --- a/apps/studio/components/interfaces/Database/RLSTester/SandboxManagement.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { Box, Loader2, LogOut, RefreshCw } from 'lucide-react' -import { Badge, Button } from 'ui' -import { Admonition } from 'ui-patterns/admonition' - -import { ButtonTooltip } from '@/components/ui/ButtonTooltip' -import { usePostgresSandbox } from '@/state/postgres-sandbox/sandbox' - -export const SandboxManagement = () => { - const { status, error, isSyncing, startSandbox, destroySandbox, syncSandbox } = - usePostgresSandbox() - - if (status === 'idle') { - return ( - startSandbox()}> - Set up sandbox - , - ]} - > -
-

Run queries in a sandbox

- Recommended -
-

- Ensure that queries do not affect your actual database -

-
- ) - } - - if (status === 'loading') { - return ( - -
-
- -
-

Setting up sandbox

-
-
- ) - } - - if (status === 'error') { - return ( - startSandbox()}> - Retry set up - , - ]} - /> - ) - } - - return ( - } - className="w-7" - disabled={isSyncing} - tooltip={{ content: { side: 'bottom', text: 'Exit sandbox' } }} - onClick={() => destroySandbox()} - />, - } - className="w-7" - loading={isSyncing} - tooltip={{ content: { side: 'bottom', text: 'Refresh schema' } }} - onClick={() => syncSandbox()} - />, - ]} - > -
-
- -
-

Sandbox active

-

Your database is never modified

-
-
- ) -} diff --git a/apps/studio/components/interfaces/Database/RLSTester/UserSelector.tsx b/apps/studio/components/interfaces/Database/RLSTester/UserSelector.tsx deleted file mode 100644 index c949c2f3fe0..00000000000 --- a/apps/studio/components/interfaces/Database/RLSTester/UserSelector.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { keepPreviousData } from '@tanstack/react-query' -import { useDebounce } from '@uidotdev/usehooks' -import { Check, ChevronsUpDown } from 'lucide-react' -import { useMemo, useState } from 'react' -import { toast } from 'sonner' -import { - Button, - cn, - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, - copyToClipboard, - Popover, - PopoverContent, - PopoverTrigger, - ScrollArea, -} from 'ui' -import { Admonition } from 'ui-patterns/admonition' -import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' - -import { User, useUsersInfiniteQuery } from '@/data/auth/users-infinite-query' -import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' -import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state' -import { ResponseError } from '@/types' - -export const UserSelector = () => { - const { data: project } = useSelectedProjectQuery() - const state = useRoleImpersonationStateSnapshot() - - const [open, setOpen] = useState(false) - const [searchText, setSearchText] = useState('') - - const debouncedSearchText = useDebounce(searchText, 300) - - const { data, error, isSuccess, isPending, isError } = useUsersInfiniteQuery( - { - projectRef: project?.ref, - connectionString: project?.connectionString, - keywords: debouncedSearchText.trim().toLocaleLowerCase(), - }, - { placeholderData: keepPreviousData } - ) - const users = useMemo(() => data?.pages.flatMap((page) => page.result) ?? [], [data?.pages]) - - const impersonatingUser = - state.role?.type === 'postgrest' && - state.role.role === 'authenticated' && - state.role.userType === 'native' - ? state.role.user - : undefined - - const onSelectUser = async (user: User) => { - try { - await state.setRole({ - type: 'postgrest', - role: 'authenticated', - userType: 'native', - user, - aal: 'aal1', - }) - } catch (error) { - toast.error(`Failed to impersonate user: ${(error as ResponseError).message}`) - } - } - - return ( - - ID:{' '} - { - copyToClipboard(impersonatingUser?.id ?? '') - toast('Copied ID to clipboard') - }} - > - {impersonatingUser.id} - -

- ) : undefined - } - > - - - - - - - - - {isError ? ( - - Failed to fetch users: {error.message} - - ) : ( - No user found - )} - - - {isPending && ( -
- -
- )} - - {isSuccess && ( - - 7 ? 'h-full md:h-[210px]' : ''}> - {users.map((user) => { - return ( - { - onSelectUser(user) - setOpen(false) - }} - > -
-

- {user.email} - - {user.id?.slice(0, 8)} - -

- {impersonatingUser?.id === user.id && } -
-
- ) - })} -
-
- )} -
-
-
-
-
- ) -} diff --git a/apps/studio/components/interfaces/Database/RLSTester/UserSqlEditor.tsx b/apps/studio/components/interfaces/Database/RLSTester/UserSqlEditor.tsx deleted file mode 100644 index f920731f49d..00000000000 --- a/apps/studio/components/interfaces/Database/RLSTester/UserSqlEditor.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { rawSql, type SafeSqlFragment } from '@supabase/pg-meta' -import type { ComponentProps } from 'react' - -import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor' - -interface UserSqlEditorProps { - id: string - value: SafeSqlFragment - placeholder?: SafeSqlFragment - actions?: ComponentProps['actions'] - onChange: (sql: SafeSqlFragment) => void -} - -/** - * Wraps CodeEditor for user-authored SQL. The rawSql boundary lives here — any - * text the user types is immediately promoted to SafeSqlFragment so callers - * never handle plain strings. - */ -export const UserSqlEditor = ({ value, onChange, ...props }: UserSqlEditorProps) => { - return ( - onChange(rawSql(val ?? ''))} - {...props} - /> - ) -} diff --git a/apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.test.tsx b/apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.test.tsx deleted file mode 100644 index 8ee7cda9bc8..00000000000 --- a/apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.test.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import type { SafeSqlFragment } from '@supabase/pg-meta' -import { screen } from '@testing-library/react' -import { describe, expect, it, vi } from 'vitest' - -import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils' -import type { ParseQueryResults } from '@/components/interfaces/Database/RLSTester/RLSTester.types' -import { RLSTesterResults } from '@/components/interfaces/Database/RLSTester/RLSTesterResults' -import { render } from '@/tests/helpers' - -vi.mock('@/components/interfaces/Database/RLSTester/useTestQueryRLS', () => ({ - useTestQueryRLS: () => ({ limit: 100 }), -})) - -vi.mock('@/components/interfaces/Database/RLSTester/RLSTableCard', () => ({ - RLSTableCard: () =>
, -})) - -vi.mock('@/components/interfaces/SQLEditor/UtilityPanel/Results', () => ({ - Results: () =>
, -})) - -const sql = (s: string) => s as unknown as SafeSqlFragment - -const makePolicy = (definition: string | null = null): Policy => - ({ definition: definition !== null ? sql(definition) : null }) as Policy - -const makeTable = ( - overrides?: Partial -): ParseQueryResults['tables'][number] => ({ - schema: 'public', - table: 'items', - isRLSEnabled: true, - tablePolicies: [], - ...overrides, -}) - -const defaultProps = { - results: [], - autoLimit: false, - executeSqlError: undefined, - handleSelectEditPolicy: vi.fn(), -} - -describe('RLSTesterResults', () => { - describe('access badge', () => { - it('shows "No access" badge when table has RLS enabled but no policies', () => { - render( - - ) - expect(screen.getByText('No access')).toBeInTheDocument() - }) - - it('shows "No access" badge when a policy definition is false', () => { - render( - - ) - expect(screen.getByText('No access')).toBeInTheDocument() - }) - - it('shows "Has access" badge when results are empty and user has access', () => { - render( - - ) - expect(screen.getByText('Has access')).toBeInTheDocument() - }) - - it('shows "Can access" badge when results are returned', () => { - render( - - ) - expect(screen.getByText('Can access')).toBeInTheDocument() - }) - }) - - describe('policy admonitions', () => { - it('shows service role admonition for postgres role', () => { - render( - - ) - expect(screen.getByText(/bypasses all RLS policies/)).toBeInTheDocument() - }) - - it('shows "no policies" admonition when RLS is enabled but no policies exist', () => { - render( - - ) - expect(screen.getByText(/no policies set up/)).toBeInTheDocument() - expect(screen.getByText(/public.profiles/)).toBeInTheDocument() - }) - - it('shows "policy false" admonition when a policy evaluates to false', () => { - render( - - ) - expect(screen.getByText(/evaluates to/)).toBeInTheDocument() - expect(screen.getByText(/public.secrets/)).toBeInTheDocument() - }) - }) - - describe('"Ran as" section', () => { - it('shows postgres for service role', () => { - render( - - ) - expect(screen.getAllByText('postgres').length).toBeGreaterThan(0) - }) - - it('shows "an Anonymous user" for anon role', () => { - render( - - ) - expect(screen.getByText('an Anonymous user')).toBeInTheDocument() - expect(screen.getByText('Not logged in user')).toBeInTheDocument() - }) - - it('shows user email and ID when a user is present', () => { - render( - - ) - expect(screen.getByText('alice@example.com')).toBeInTheDocument() - expect(screen.getByText('ID: user-123')).toBeInTheDocument() - }) - }) -}) diff --git a/apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.utils.test.ts b/apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.utils.test.ts deleted file mode 100644 index 3dff5da045f..00000000000 --- a/apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.utils.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -import type { SafeSqlFragment } from '@supabase/pg-meta' -import { describe, expect, it } from 'vitest' - -import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils' -import type { ParseQueryResults } from '@/components/interfaces/Database/RLSTester/RLSTester.types' -import { deriveRLSTestState } from '@/components/interfaces/Database/RLSTester/RLSTesterResults.utils' - -const sql = (s: string) => s as unknown as SafeSqlFragment - -const makePolicy = (definition: string | null = null): Policy => - ({ definition: definition !== null ? sql(definition) : null }) as Policy - -const makeTable = ( - overrides?: Partial -): ParseQueryResults['tables'][number] => ({ - schema: 'public', - table: 'items', - isRLSEnabled: true, - tablePolicies: [], - ...overrides, -}) - -const makeResults = (overrides?: Partial): ParseQueryResults => ({ - tables: [], - operation: 'SELECT', - role: 'anon', - ...overrides, -}) - -describe('deriveRLSTestState', () => { - describe('isServiceRole', () => { - it('is true when parseQueryResults is undefined', () => { - const { isServiceRole } = deriveRLSTestState(undefined) - expect(isServiceRole).toBe(true) - }) - - it('is true when role is undefined (postgres / service role)', () => { - const { isServiceRole } = deriveRLSTestState(makeResults({ role: undefined })) - expect(isServiceRole).toBe(true) - }) - - it('is false when role is anon', () => { - const { isServiceRole } = deriveRLSTestState(makeResults({ role: 'anon' })) - expect(isServiceRole).toBe(false) - }) - - it('is false when role is authenticated', () => { - const { isServiceRole } = deriveRLSTestState(makeResults({ role: 'authenticated' })) - expect(isServiceRole).toBe(false) - }) - }) - - describe('noAccessToData', () => { - it('is false when parseQueryResults is undefined', () => { - const { noAccessToData } = deriveRLSTestState(undefined) - expect(noAccessToData).toBe(false) - }) - - it('is false for service role even when tables have no policies', () => { - const { noAccessToData } = deriveRLSTestState( - makeResults({ role: undefined, tables: [makeTable()] }) - ) - expect(noAccessToData).toBe(false) - }) - - it('is false when RLS is disabled on table', () => { - const { noAccessToData } = deriveRLSTestState( - makeResults({ tables: [makeTable({ isRLSEnabled: false, tablePolicies: [] })] }) - ) - expect(noAccessToData).toBe(false) - }) - - it('is true when RLS is enabled and table has no policies', () => { - const { noAccessToData } = deriveRLSTestState( - makeResults({ tables: [makeTable({ isRLSEnabled: true, tablePolicies: [] })] }) - ) - expect(noAccessToData).toBe(true) - }) - - it('is true when RLS is enabled and a policy definition is false', () => { - const { noAccessToData } = deriveRLSTestState( - makeResults({ - tables: [makeTable({ tablePolicies: [makePolicy('false')] })], - }) - ) - expect(noAccessToData).toBe(true) - }) - - it('is false when RLS is enabled and policies are valid (not false)', () => { - const { noAccessToData } = deriveRLSTestState( - makeResults({ - tables: [makeTable({ tablePolicies: [makePolicy('auth.uid() = user_id')] })], - }) - ) - expect(noAccessToData).toBe(false) - }) - - it('is false when all tables have RLS disabled regardless of policy state', () => { - const { noAccessToData } = deriveRLSTestState( - makeResults({ - tables: [ - makeTable({ isRLSEnabled: false, tablePolicies: [] }), - makeTable({ - table: 'other', - isRLSEnabled: false, - tablePolicies: [makePolicy('false')], - }), - ], - }) - ) - expect(noAccessToData).toBe(false) - }) - }) - - describe('tableWithRLSEnabledButNoPolicies', () => { - it('is undefined when no tables', () => { - const { tableWithRLSEnabledButNoPolicies } = deriveRLSTestState(makeResults({ tables: [] })) - expect(tableWithRLSEnabledButNoPolicies).toBeUndefined() - }) - - it('is undefined when RLS disabled', () => { - const { tableWithRLSEnabledButNoPolicies } = deriveRLSTestState( - makeResults({ tables: [makeTable({ isRLSEnabled: false })] }) - ) - expect(tableWithRLSEnabledButNoPolicies).toBeUndefined() - }) - - it('is undefined when table has policies', () => { - const { tableWithRLSEnabledButNoPolicies } = deriveRLSTestState( - makeResults({ tables: [makeTable({ tablePolicies: [makePolicy('true')] })] }) - ) - expect(tableWithRLSEnabledButNoPolicies).toBeUndefined() - }) - - it('returns the matching table when RLS enabled with no policies', () => { - const table = makeTable({ table: 'profiles', tablePolicies: [] }) - const { tableWithRLSEnabledButNoPolicies } = deriveRLSTestState( - makeResults({ tables: [table] }) - ) - expect(tableWithRLSEnabledButNoPolicies).toEqual(table) - }) - - it('returns the first matching table among multiple', () => { - const first = makeTable({ table: 'profiles', tablePolicies: [] }) - const second = makeTable({ table: 'posts', tablePolicies: [] }) - const { tableWithRLSEnabledButNoPolicies } = deriveRLSTestState( - makeResults({ tables: [first, second] }) - ) - expect(tableWithRLSEnabledButNoPolicies).toEqual(first) - }) - }) - - describe('tableWithRLSEnabledWithPolicyFalse', () => { - it('is undefined when no tables', () => { - const { tableWithRLSEnabledWithPolicyFalse } = deriveRLSTestState(makeResults({ tables: [] })) - expect(tableWithRLSEnabledWithPolicyFalse).toBeUndefined() - }) - - it('is undefined when RLS disabled even with false policy', () => { - const { tableWithRLSEnabledWithPolicyFalse } = deriveRLSTestState( - makeResults({ - tables: [makeTable({ isRLSEnabled: false, tablePolicies: [makePolicy('false')] })], - }) - ) - expect(tableWithRLSEnabledWithPolicyFalse).toBeUndefined() - }) - - it('is undefined when no policy has definition of false', () => { - const { tableWithRLSEnabledWithPolicyFalse } = deriveRLSTestState( - makeResults({ - tables: [makeTable({ tablePolicies: [makePolicy('auth.uid() = user_id')] })], - }) - ) - expect(tableWithRLSEnabledWithPolicyFalse).toBeUndefined() - }) - - it('returns the table when a policy definition is exactly "false"', () => { - const table = makeTable({ table: 'secrets', tablePolicies: [makePolicy('false')] }) - const { tableWithRLSEnabledWithPolicyFalse } = deriveRLSTestState( - makeResults({ tables: [table] }) - ) - expect(tableWithRLSEnabledWithPolicyFalse).toEqual(table) - }) - - it('is undefined when policy definition is null (no definition)', () => { - const { tableWithRLSEnabledWithPolicyFalse } = deriveRLSTestState( - makeResults({ tables: [makeTable({ tablePolicies: [makePolicy(null)] })] }) - ) - expect(tableWithRLSEnabledWithPolicyFalse).toBeUndefined() - }) - }) -}) diff --git a/apps/studio/components/interfaces/Database/RLSTester/__tests__/useTestQueryRLS.utils.test.ts b/apps/studio/components/interfaces/Database/RLSTester/__tests__/useTestQueryRLS.utils.test.ts deleted file mode 100644 index a5ccadf1c6d..00000000000 --- a/apps/studio/components/interfaces/Database/RLSTester/__tests__/useTestQueryRLS.utils.test.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils' -import { - filterTablePolicies, - getTestQueryBlockedReason, -} from '@/components/interfaces/Database/RLSTester/useTestQueryRLS.utils' - -const makePolicy = (overrides: Partial): Policy => - ({ - schema: 'public', - table: 'items', - roles: ['anon'], - command: 'SELECT', - ...overrides, - }) as Policy - -const base = { - policies: [] as Policy[], - schema: 'public', - table: 'items', - role: 'anon', - operation: 'SELECT' as const, -} - -describe('filterTablePolicies', () => { - describe('schema / table matching', () => { - it('excludes policies from a different schema', () => { - const policy = makePolicy({ schema: 'private', table: 'items' }) - expect(filterTablePolicies({ ...base, policies: [policy] })).toHaveLength(0) - }) - - it('excludes policies from a different table', () => { - const policy = makePolicy({ schema: 'public', table: 'other' }) - expect(filterTablePolicies({ ...base, policies: [policy] })).toHaveLength(0) - }) - - it('includes a policy matching schema and table', () => { - const policy = makePolicy({ schema: 'public', table: 'items' }) - expect(filterTablePolicies({ ...base, policies: [policy] })).toHaveLength(1) - }) - }) - - describe('role matching', () => { - it('includes policy when role is in the policy roles array', () => { - const policy = makePolicy({ roles: ['anon', 'authenticated'] }) - expect(filterTablePolicies({ ...base, role: 'anon', policies: [policy] })).toHaveLength(1) - }) - - it('excludes policy when role is not in the policy roles array', () => { - const policy = makePolicy({ roles: ['authenticated'] }) - expect(filterTablePolicies({ ...base, role: 'anon', policies: [policy] })).toHaveLength(0) - }) - - it('includes policy when the only role is "public" (applies to all roles)', () => { - const policy = makePolicy({ roles: ['public'] }) - expect(filterTablePolicies({ ...base, role: 'anon', policies: [policy] })).toHaveLength(1) - }) - - it('excludes "public" role shortcut when policy has multiple roles including public', () => { - const policy = makePolicy({ roles: ['public', 'authenticated'] }) - expect(filterTablePolicies({ ...base, role: 'anon', policies: [policy] })).toHaveLength(0) - }) - - it('handles undefined role (service role) — matches nothing unless public', () => { - const rolePolicy = makePolicy({ roles: ['anon'] }) - const publicPolicy = makePolicy({ roles: ['public'] }) - const result = filterTablePolicies({ - ...base, - role: undefined, - policies: [rolePolicy, publicPolicy], - }) - expect(result).toHaveLength(1) - expect(result[0]).toBe(publicPolicy) - }) - }) - - describe('command matching', () => { - it('includes policy when command matches the operation', () => { - const policy = makePolicy({ command: 'SELECT' }) - expect( - filterTablePolicies({ ...base, operation: 'SELECT', policies: [policy] }) - ).toHaveLength(1) - }) - - it('excludes policy when command does not match the operation', () => { - const policy = makePolicy({ command: 'INSERT' }) - expect( - filterTablePolicies({ ...base, operation: 'SELECT', policies: [policy] }) - ).toHaveLength(0) - }) - - it('includes policy with command ALL regardless of operation', () => { - const policy = makePolicy({ command: 'ALL' }) - expect( - filterTablePolicies({ ...base, operation: 'SELECT', policies: [policy] }) - ).toHaveLength(1) - }) - - it('includes ALL command policy for non-SELECT operations too', () => { - const policy = makePolicy({ command: 'ALL' }) - expect( - filterTablePolicies({ ...base, operation: 'INSERT', policies: [policy] }) - ).toHaveLength(1) - }) - - it('does not include a SELECT-only policy when operation is INSERT', () => { - const policy = makePolicy({ command: 'SELECT' }) - expect( - filterTablePolicies({ ...base, operation: 'INSERT', policies: [policy] }) - ).toHaveLength(0) - }) - }) - - describe('combined filters', () => { - it('returns only policies that satisfy all conditions', () => { - const match = makePolicy({ - schema: 'public', - table: 'items', - roles: ['anon'], - command: 'ALL', - }) - const wrongSchema = makePolicy({ - schema: 'private', - table: 'items', - roles: ['anon'], - command: 'ALL', - }) - const wrongRole = makePolicy({ - schema: 'public', - table: 'items', - roles: ['authenticated'], - command: 'ALL', - }) - const wrongCommand = makePolicy({ - schema: 'public', - table: 'items', - roles: ['anon'], - command: 'INSERT', - }) - - const result = filterTablePolicies({ - ...base, - policies: [match, wrongSchema, wrongRole, wrongCommand], - }) - expect(result).toHaveLength(1) - expect(result[0]).toBe(match) - }) - }) -}) - -describe('getTestQueryBlockedReason', () => { - const blockedBase = { - statementCount: 1, - operation: 'SELECT' as const, - hasSandbox: false, - acknowledgeMutation: false, - } - - describe('multiple statements', () => { - it('blocks when statementCount is greater than 1', () => { - expect(getTestQueryBlockedReason({ ...blockedBase, statementCount: 2 })).toStrictEqual({ - type: 'multiple-statements', - }) - }) - - it('takes priority over an unsupported operation', () => { - expect( - getTestQueryBlockedReason({ ...blockedBase, statementCount: 2, operation: 'DELETE' }) - ).toStrictEqual({ type: 'multiple-statements' }) - }) - - it('takes priority over an unacknowledged mutation', () => { - expect( - getTestQueryBlockedReason({ ...blockedBase, statementCount: 2, operation: 'INSERT' }) - ).toStrictEqual({ type: 'multiple-statements' }) - }) - - it('does not block when statementCount is exactly 1', () => { - expect(getTestQueryBlockedReason({ ...blockedBase, statementCount: 1 })).toBeUndefined() - }) - - it('does not block when statementCount is 0', () => { - expect(getTestQueryBlockedReason({ ...blockedBase, statementCount: 0 })).toBeUndefined() - }) - }) - - describe('unsupported operations', () => { - it('blocks UPDATE', () => { - expect(getTestQueryBlockedReason({ ...blockedBase, operation: 'UPDATE' })).toStrictEqual({ - type: 'unsupported-operation', - operation: 'UPDATE', - }) - }) - - it('blocks DELETE', () => { - expect(getTestQueryBlockedReason({ ...blockedBase, operation: 'DELETE' })).toStrictEqual({ - type: 'unsupported-operation', - operation: 'DELETE', - }) - }) - - it('blocks UPDATE even with a sandbox available', () => { - expect( - getTestQueryBlockedReason({ ...blockedBase, operation: 'UPDATE', hasSandbox: true }) - ).toStrictEqual({ type: 'unsupported-operation', operation: 'UPDATE' }) - }) - - it('blocks DELETE even when already acknowledged', () => { - expect( - getTestQueryBlockedReason({ - ...blockedBase, - operation: 'DELETE', - acknowledgeMutation: true, - }) - ).toStrictEqual({ type: 'unsupported-operation', operation: 'DELETE' }) - }) - }) - - describe('INSERT mutation warning', () => { - it('blocks an unacknowledged INSERT with no sandbox', () => { - expect(getTestQueryBlockedReason({ ...blockedBase, operation: 'INSERT' })).toStrictEqual({ - type: 'mutation', - operation: 'INSERT', - }) - }) - - it('does not block an INSERT when a sandbox is available', () => { - expect( - getTestQueryBlockedReason({ ...blockedBase, operation: 'INSERT', hasSandbox: true }) - ).toBeUndefined() - }) - - it('does not block an INSERT once acknowledged', () => { - expect( - getTestQueryBlockedReason({ - ...blockedBase, - operation: 'INSERT', - acknowledgeMutation: true, - }) - ).toBeUndefined() - }) - - it('does not require acknowledgement when a sandbox is available', () => { - expect( - getTestQueryBlockedReason({ - ...blockedBase, - operation: 'INSERT', - hasSandbox: true, - acknowledgeMutation: false, - }) - ).toBeUndefined() - }) - }) - - describe('unblocked operations', () => { - it('does not block SELECT', () => { - expect(getTestQueryBlockedReason({ ...blockedBase, operation: 'SELECT' })).toBeUndefined() - }) - - it('does not block when operation is undefined', () => { - expect(getTestQueryBlockedReason({ ...blockedBase, operation: undefined })).toBeUndefined() - }) - }) -}) diff --git a/apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.ts b/apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.ts deleted file mode 100644 index c94328db294..00000000000 --- a/apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { safeSql, type SafeSqlFragment, type UntrustedSqlFragment } from '@supabase/pg-meta' -import { useState } from 'react' -import { toast } from 'sonner' - -import { checkIfAppendLimitRequired, suffixWithLimit } from '../../SQLEditor/SQLEditor.utils' -import { type ParseQueryResults } from './RLSTester.types' -import { - filterTablePolicies, - getTestQueryBlockedReason, - type TestQueryBlockedReason, -} from './useTestQueryRLS.utils' -import { useParseClientCodeMutation } from '@/data/ai/parse-client-code-mutation' -import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query' -import { useCheckTableRLSStatusMutation } from '@/data/database/table-check-rls-mutation' -import { - useParseSQLQueryMutation, - type ParseSQLQueryOperations, -} from '@/data/misc/parse-query-mutation' -import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation' -import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' -import { wrapWithRoleImpersonation } from '@/lib/role-impersonation' -import { usePostgresSandbox } from '@/state/postgres-sandbox/sandbox' -import { - isRoleImpersonationEnabled, - useGetImpersonatedRoleState, - useImpersonatedUser, - useRoleImpersonationStateSnapshot, -} from '@/state/role-impersonation-state' -import { type ResponseError } from '@/types' - -const limit = 100 - -export type { TestQueryBlockedReason } - -// [Joshen] Pre-requisite work for identifying UPDATE / DELETE failures due to RLS - not yet wired -// in since those operations are currently blocked (see TestQueryBlockedReason's 'unsupported- -// operation'). Exported so it isn't flagged as unused until the follow-up PR wires it back in. -export const wrapReturnRowsAffected = (sql: SafeSqlFragment) => { - return safeSql` - DO $$ -DECLARE - row_count integer; -BEGIN - ${sql}${(sql.endsWith(';') ? '' : ';') as SafeSqlFragment} - GET DIAGNOSTICS row_count = ROW_COUNT; - -- store it somewhere you can read back - PERFORM set_config('rls_tester.rows_affected', row_count::text, true); -END $$; - -SELECT current_setting('rls_tester.rows_affected', true); -` -} - -/** - * [Joshen] Testing a SQL query for its RLS access involves 3 async steps - * 0. (Optional) Inferring client library code to SQL query via the AI Assistant - * 1. Parsing the provided SQL query to retrieve its operation type + tables involved - * 2. Checking for tables involved if they've got RLS enabled - * 3. Actually running the query to retrieve the results - * - * Errors should all be handled as part of the UI instead of toasts, hence the empty onError - * handlers to mute the default error handlers within the react query mutationhooks - */ -export const useTestQueryRLS = () => { - const { data: project } = useSelectedProjectQuery() - const { role } = useRoleImpersonationStateSnapshot() - - const { sandbox } = usePostgresSandbox() - const getImpersonatedRoleState = useGetImpersonatedRoleState() - const impersonatedRoleState = getImpersonatedRoleState() - const user = useImpersonatedUser() - - const [isLoading, setIsLoading] = useState(false) - const [sandboxError, setSandboxError] = useState() - - const { data: policies = [] } = useDatabasePoliciesQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - - const { mutateAsync: executeSql, error: executeSqlMutationError } = useExecuteSqlMutation({ - onError: () => {}, - }) - const executeSqlError = sandbox ? sandboxError : executeSqlMutationError - - const { - mutateAsync: parseClientCode, - isPending: isInferring, - error: parseClientCodeError, - } = useParseClientCodeMutation({ - onError: () => {}, - }) - - const inferSQLFromLib = async ( - value: string, - onInferSQL: (unchecked_sql: UntrustedSqlFragment) => void - ) => { - const { unchecked_sql, valid } = await parseClientCode({ code: value }) - if (valid && unchecked_sql != null) { - onInferSQL(unchecked_sql) - } else { - toast.error('Client library code provided is not valid') - } - } - - const { mutateAsync: parseQuery, error: parseQueryError } = useParseSQLQueryMutation({ - onError: () => {}, - }) - - const { mutateAsync: getTableRLSStatus, error: getTableRLSStatusError } = - useCheckTableRLSStatusMutation({ - onError: () => {}, - }) - - /** - * Returns true if the query was blocked (multiple statements, or an unacknowledged mutation) - * and did not run, false if it ran (successfully or not) - */ - const testQuery = async ({ - value, - option, - acknowledgeMutation = false, - onExecuteSQL, - onParseQuery, - onValidationBlocked, - }: { - value: SafeSqlFragment - option: 'anon' | 'authenticated' - acknowledgeMutation?: boolean - onExecuteSQL: ({ - result, - operation, - isAutoLimit, - }: { - result: Object[] | null - operation: ParseSQLQueryOperations - isAutoLimit: boolean - }) => void - onParseQuery: (results?: ParseQueryResults) => void - onValidationBlocked: (reason: TestQueryBlockedReason) => void - }): Promise => { - if (!project) { - console.error('Project is required') - return true - } - - if (option === 'authenticated' && !user) { - toast('Select which user to test as before running the query') - return true - } - - try { - setIsLoading(true) - setSandboxError(undefined) - - const { appendAutoLimit } = checkIfAppendLimitRequired(value, limit) - const formattedSql = suffixWithLimit(value, limit) - const data = await parseQuery({ sql: formattedSql }) - - const blockedReason = getTestQueryBlockedReason({ - statementCount: data.statementCount, - operation: data.operation, - hasSandbox: !!sandbox, - acknowledgeMutation, - }) - if (blockedReason) { - onValidationBlocked(blockedReason) - return true - } - - const formattedTables = data.tables.map((x) => { - const [schema, table] = x.includes('.') ? x.split('.') : ['public', x] - return { schema, table } - }) - const response = await getTableRLSStatus({ - projectRef: project?.ref, - connectionString: project?.connectionString, - tables: formattedTables, - }) - - const tables = response - .map(({ table, schema, rls_enabled }) => { - const tablePolicies = filterTablePolicies({ - policies, - schema, - table, - role: role?.role, - operation: data.operation, - }) - return { - table, - schema, - isRLSEnabled: rls_enabled, - tablePolicies, - } - }) - .sort((a, b) => { - const aFirst = a.isRLSEnabled && a.tablePolicies.length === 0 - const bFirst = b.isRLSEnabled && b.tablePolicies.length === 0 - return Number(bFirst) - Number(aFirst) - }) - - const autoLimit = appendAutoLimit ? limit : undefined - // UPDATE/DELETE are blocked above, so wrapReturnRowsAffected isn't wired in here yet - - // it's kept for the follow-up PR that adds proper UPDATE/DELETE support - const sql = wrapWithRoleImpersonation(formattedSql, impersonatedRoleState) - - try { - const { result } = sandbox - ? await sandbox.run({ sql }).catch((e) => { - setSandboxError(e instanceof Error ? e : new Error(String(e))) - throw e - }) - : await executeSql({ - sql, - autoLimit, - projectRef: project.ref, - connectionString: project.connectionString, - isRoleImpersonationEnabled: isRoleImpersonationEnabled(impersonatedRoleState.role), - isStatementTimeoutDisabled: true, - handleError: (e) => { - throw e - }, - queryKey: ['rls-tester'], - }) - - onExecuteSQL({ result, operation: data.operation, isAutoLimit: !!autoLimit }) - onParseQuery({ tables, operation: data.operation, role: role?.role, user }) - } catch (error) { - const isRLSInsertError = Boolean( - (error as ResponseError)?.message?.includes('new row violates row-level security policy') - ) - onExecuteSQL({ result: null, operation: data.operation, isAutoLimit: false }) - if (isRLSInsertError) { - onParseQuery({ tables, operation: data.operation, role: role?.role, user }) - } else { - onParseQuery(undefined) - } - } - } catch (error) { - onExecuteSQL({ result: null, operation: undefined, isAutoLimit: false }) - onParseQuery(undefined) - } finally { - setIsLoading(false) - } - - return false - } - - return { - limit, - testQuery, - inferSQLFromLib, - isLoading, - isInferring, - executeSqlError, - parseQueryError, - parseClientCodeError, - getTableRLSStatusError, - } -} diff --git a/apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.utils.ts b/apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.utils.ts deleted file mode 100644 index b0e698dc43b..00000000000 --- a/apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.utils.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils' -import type { ParseSQLQueryResponse } from '@/data/misc/parse-query-mutation' - -export type TestQueryBlockedReason = - | { type: 'multiple-statements' } - | { type: 'unsupported-operation'; operation: 'UPDATE' | 'DELETE' } - | { type: 'mutation'; operation: 'INSERT' } - -/** - * Decides whether a query should be blocked from running, and why. Checked in this order: - * multiple statements first (regardless of operation), then unsupported operations (UPDATE/ - * DELETE aren't testable yet - RLS blocks them silently instead of raising an error), then - * INSERT mutations against the real database that haven't been acknowledged yet. - */ -export function getTestQueryBlockedReason({ - statementCount, - operation, - hasSandbox, - acknowledgeMutation, -}: { - statementCount: number - operation: ParseSQLQueryResponse['operation'] - hasSandbox: boolean - acknowledgeMutation: boolean -}): TestQueryBlockedReason | undefined { - if (statementCount > 1) return { type: 'multiple-statements' } - if (operation === 'UPDATE' || operation === 'DELETE') { - return { type: 'unsupported-operation', operation } - } - if (operation === 'INSERT' && !hasSandbox && !acknowledgeMutation) { - return { type: 'mutation', operation } - } - return undefined -} - -export function filterTablePolicies({ - policies, - schema, - table, - role, - operation, -}: { - policies: Policy[] - schema: string - table: string - role: string | undefined - operation: ParseSQLQueryResponse['operation'] -}): Policy[] { - return policies.filter( - (x) => - x.schema === schema && - x.table === table && - (x.roles.includes(role ?? '') || (x.roles.length === 1 && x.roles[0] === 'public')) && - (x.command === 'ALL' || x.command === operation) - ) -} diff --git a/apps/studio/components/ui/BannerStack/BannerStackProvider.tsx b/apps/studio/components/ui/BannerStack/BannerStackProvider.tsx index b8aa2e24b7a..75a441a86e9 100644 --- a/apps/studio/components/ui/BannerStack/BannerStackProvider.tsx +++ b/apps/studio/components/ui/BannerStack/BannerStackProvider.tsx @@ -5,7 +5,6 @@ export const BANNER_ID = { INDEX_ADVISOR: 'index-advisor-banner', TABLE_EDITOR_QUEUE_OPERATIONS: 'table-editor-queue-operations-banner', RLS_EVENT_TRIGGER: 'rls-event-trigger-banner', - RLS_TESTER: 'rls-tester-banner', FREE_MICRO_UPGRADE: 'free-micro-upgrade-banner', TOS_UPDATE: 'tos-update-banner', UNIFIED_LOGS: 'unified-logs-banner', diff --git a/apps/studio/components/ui/BannerStack/Banners/BannerRlsTester.tsx b/apps/studio/components/ui/BannerStack/Banners/BannerRlsTester.tsx deleted file mode 100644 index 61b1dbca19d..00000000000 --- a/apps/studio/components/ui/BannerStack/Banners/BannerRlsTester.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { LOCAL_STORAGE_KEYS } from 'common' -import { useParams } from 'common/hooks' -import { AnimatePresence, motion } from 'framer-motion' -import { Check, Loader2, Terminal } from 'lucide-react' -import { useEffect, useState } from 'react' -import { Badge, Button, cn } from 'ui' - -import { BannerCard } from '../BannerCard' -import { useBannerStack } from '../BannerStackProvider' -import { useFeaturePreviewModal } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' - -const text = 'select * from colors' - -export const BannerRlsTester = () => { - const { ref } = useParams() - const { selectFeaturePreview } = useFeaturePreviewModal() - - const [runQueryAnimate, setRunQueryAnimate] = useState(false) - const [showSummary, setShowSummary] = useState(false) - - const { dismissBanner } = useBannerStack() - const [, setIsDismissed] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.RLS_TESTER_BANNER_DISMISSED(ref ?? ''), - false - ) - - useEffect(() => { - setTimeout(() => setRunQueryAnimate(true), 2400) - }, []) - - useEffect(() => { - if (runQueryAnimate) { - setTimeout(() => setShowSummary(true), 1700) - } - }, [runQueryAnimate]) - - return ( - { - setIsDismissed(true) - dismissBanner('rls-tester-banner') - }} - > -
-
- - Preview - - -
-
- {runQueryAnimate && !showSummary ? ( - - ) : ( - - )} -

- {text} -

-
- - - {showSummary && ( - -
- -

- Can access public.colors -

-
-
-

2 policies applied

-
-
- )} -
-
-
-
-

Row Level Security (RLS) Tester

-

- Verify your RLS policies are correct by running queries as a specific user -

-
- -
-
- ) -} diff --git a/apps/studio/data/rls-tester/get-schema-ddl.ts b/apps/studio/data/rls-tester/get-schema-ddl.ts deleted file mode 100644 index 815d1d51ab4..00000000000 --- a/apps/studio/data/rls-tester/get-schema-ddl.ts +++ /dev/null @@ -1,180 +0,0 @@ -import pgMeta, { - getEntityDefinitionsSql, - joinSqlFragments, - literal, - safeSql, - type PGPolicy, -} from '@supabase/pg-meta' -import { z } from 'zod' - -import { executeSql } from '@/data/sql/execute-sql-mutation' -import { INTERNAL_SCHEMAS } from '@/hooks/useProtectedSchemas' - -export interface RlsTableStatus { - schema: string - table: string - rls_enabled: boolean - rls_forced: boolean -} - -export interface CustomRole { - name: string -} - -export interface DatabaseSchemaDDL { - schemas: string[] - typeDefinitions: string[] - entityDefinitions: string[] - functionDefinitions: string[] - policies: PGPolicy[] - rlsStatuses: RlsTableStatus[] - customRoles: CustomRole[] -} - -const pgMetaRolesList = pgMeta.roles.list() -const pgMetaFunctionsZod = pgMeta.functions.list().zod -const pgMetaPoliciesZod = pgMeta.policies.list().zod -const pgMetaTablesZod = pgMeta.tables.list().zod - -// Extension-owned / platform-specific schemas whose DDL depends on C extensions, -// custom operators, and platform functions that PGlite cannot replicate. -// We skip entity/function/type DDL for these but still fetch their policies — -// those may reference user tables we do load. -const SUPABASE_INTERNAL_SCHEMAS = new Set([...INTERNAL_SCHEMAS, '_realtime']) - -const SYSTEM_ROLES = new Set([ - 'postgres', - 'anon', - 'authenticated', - 'service_role', - 'supabase_admin', - 'supabase_auth_admin', - 'supabase_storage_admin', - 'supabase_replication_admin', - 'supabase_read_only_user', - 'pg_monitor', - 'pg_read_all_settings', - 'pg_read_all_stats', - 'pg_stat_scan_tables', - 'pg_read_server_files', - 'pg_write_server_files', - 'pg_execute_server_program', - 'pg_signal_backend', - 'dashboard_user', - 'pgbouncer', -]) - -function getTypeDefinitionsSql(schemas: string[]) { - return safeSql` - SELECT - CASE t.typtype - WHEN 'e' THEN - 'CREATE TYPE ' || quote_ident(n.nspname) || '.' || quote_ident(t.typname) || - ' AS ENUM (' || - (SELECT string_agg(quote_literal(e.enumlabel), ', ' ORDER BY e.enumsortorder) - FROM pg_enum e WHERE e.enumtypid = t.oid) || - ')' - WHEN 'c' THEN - 'CREATE TYPE ' || quote_ident(n.nspname) || '.' || quote_ident(t.typname) || - ' AS (' || - (SELECT string_agg(quote_ident(a.attname) || ' ' || pg_catalog.format_type(a.atttypid, a.atttypmod), ', ' ORDER BY a.attnum) - FROM pg_attribute a WHERE a.attrelid = t.typrelid AND a.attnum > 0 AND NOT a.attisdropped) || - ')' - WHEN 'd' THEN - 'CREATE DOMAIN ' || quote_ident(n.nspname) || '.' || quote_ident(t.typname) || - ' AS ' || pg_catalog.format_type(t.typbasetype, t.typtypmod) - END AS definition - FROM pg_type t - JOIN pg_namespace n ON n.oid = t.typnamespace - LEFT JOIN pg_class c ON c.oid = t.typrelid - LEFT JOIN pg_depend d ON d.objid = t.oid AND d.deptype = 'e' - WHERE n.nspname IN (${joinSqlFragments(schemas.map(literal), ', ')}) - AND t.typtype IN ('e', 'c', 'd') - AND d.objid IS NULL - AND (t.typtype != 'c' OR c.relkind = 'c') - ORDER BY t.typtype, n.nspname, t.typname - ` -} - -type Variables = { - projectRef?: string - connectionString?: string | null - schemas: string[] -} - -export async function getDatabaseSchemaDDL( - { projectRef, connectionString, schemas }: Variables, - signal?: AbortSignal -): Promise { - const userSchemas = schemas.filter((s) => !SUPABASE_INTERNAL_SCHEMAS.has(s)) - - const entitySql = getEntityDefinitionsSql({ schemas: userSchemas }) - const functionsSql = pgMeta.functions.list({ includedSchemas: userSchemas }).sql - const policiesSql = pgMeta.policies.list({ includedSchemas: schemas }).sql - const tablesSql = pgMeta.tables.list({ includedSchemas: userSchemas }).sql - - const [entityResult, policiesResult, rlsResult, rolesResult, functionsResult, typesResult] = - await Promise.all([ - executeSql( - { projectRef, connectionString, sql: entitySql, queryKey: ['rls-sandbox-ddl'] }, - signal - ), - executeSql( - { projectRef, connectionString, sql: policiesSql, queryKey: ['rls-sandbox-policies'] }, - signal - ), - executeSql( - { projectRef, connectionString, sql: tablesSql, queryKey: ['rls-sandbox-rls'] }, - signal - ), - executeSql( - { projectRef, connectionString, sql: pgMetaRolesList.sql, queryKey: ['rls-sandbox-roles'] }, - signal - ), - executeSql( - { - projectRef, - connectionString, - sql: functionsSql, - queryKey: ['rls-sandbox-functions'], - }, - signal - ), - executeSql( - { - projectRef, - connectionString, - sql: getTypeDefinitionsSql(userSchemas), - queryKey: ['rls-sandbox-types'], - }, - signal - ), - ]) - - const roles = (rolesResult.result as z.infer).filter( - (r) => !SYSTEM_ROLES.has(r.name) && !r.name.startsWith('pg_') && !r.name.startsWith('supabase_') - ) - - const functions = (functionsResult.result as z.infer).filter( - (f) => (f.language === 'sql' || f.language === 'plpgsql') && f.return_type !== 'trigger' - ) - - return { - schemas: userSchemas, - typeDefinitions: (typesResult.result as { definition: string }[]).map((r) => r.definition), - entityDefinitions: (entityResult.result[0]?.data?.definitions ?? []).map( - (d: { sql: string }) => d.sql - ), - functionDefinitions: functions.map((f) => f.complete_statement), - policies: policiesResult.result as z.infer as PGPolicy[], - rlsStatuses: (rlsResult.result as z.infer).map((t) => ({ - schema: t.schema, - table: t.name, - rls_enabled: t.rls_enabled, - rls_forced: t.rls_forced, - })), - customRoles: roles, - } -} - -export type DatabaseSchemaDDLData = Awaited> diff --git a/apps/studio/data/rls-tester/get-seed-data.ts b/apps/studio/data/rls-tester/get-seed-data.ts deleted file mode 100644 index 1d4dd3318df..00000000000 --- a/apps/studio/data/rls-tester/get-seed-data.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { ident, joinSqlFragments, literal, safeSql } from '@supabase/pg-meta' - -import { RlsTableStatus } from './get-schema-ddl' -import { executeSql } from '@/data/sql/execute-sql-mutation' - -export interface TableSeedData { - schema: string - table: string - rows: Record[] -} - -// Each entry can optionally restrict which columns are fetched. Used by the -// sandbox to avoid pulling secrets (e.g. auth.users encrypted_password / tokens) -// into the browser-side PGlite instance. -export type SeedTableEntry = RlsTableStatus & { columns?: readonly string[] } - -type Variables = { - projectRef?: string - connectionString?: string | null - tables: SeedTableEntry[] - rowLimit: number -} - -async function fetchTableSeed( - { - projectRef, - connectionString, - schema, - table, - columns, - rowLimit, - }: Omit & { - schema: string - table: string - columns?: readonly string[] - }, - signal?: AbortSignal -): Promise { - try { - const projection = - columns && columns.length > 0 ? joinSqlFragments(columns.map(ident), ', ') : safeSql`*` - const { result } = await executeSql( - { - projectRef, - connectionString, - sql: safeSql`SELECT ${projection} FROM ${ident(schema)}.${ident(table)} LIMIT ${literal(Number(rowLimit))}`, - queryKey: ['rls-sandbox-seed', schema, table], - }, - signal - ) - return { schema, table, rows: (result ?? []) as Record[] } - } catch { - return { schema, table, rows: [] } - } -} - -const SEED_CONCURRENCY = 8 - -export async function getProjectSeedData( - { projectRef, connectionString, tables, rowLimit }: Variables, - signal?: AbortSignal -): Promise { - const results: TableSeedData[] = [] - const queue = tables.slice() - const workers = Array.from({ length: Math.min(SEED_CONCURRENCY, queue.length) }, async () => { - while (queue.length > 0) { - const entry = queue.shift() - if (!entry) break - results.push( - await fetchTableSeed( - { - projectRef, - connectionString, - schema: entry.schema, - table: entry.table, - columns: entry.columns, - rowLimit, - }, - signal - ) - ) - } - }) - await Promise.all(workers) - return results.filter((t) => t.rows.length > 0) -} - -export type ProjectSeedDataData = TableSeedData[] diff --git a/apps/studio/package.json b/apps/studio/package.json index 442d76c48d2..547bc0a3c54 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -51,8 +51,6 @@ "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", - "@electric-sql/pglite": "0.4.5", - "@electric-sql/pglite-tools": "^0.3.4", "@graphiql/react": "^0.37.3", "@graphiql/toolkit": "^0.11.3", "@hcaptcha/react-hcaptcha": "^1.12.0", diff --git a/apps/studio/pages/project/[ref]/database/policies.tsx b/apps/studio/pages/project/[ref]/database/policies.tsx index 766d3ac6afa..34823209990 100644 --- a/apps/studio/pages/project/[ref]/database/policies.tsx +++ b/apps/studio/pages/project/[ref]/database/policies.tsx @@ -1,5 +1,5 @@ -import { ident, safeSql } from '@supabase/pg-meta' import type { PGTable } from '@supabase/pg-meta' +import { ident, safeSql } from '@supabase/pg-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { LOCAL_STORAGE_KEYS, useParams } from 'common' import { Search, X } from 'lucide-react' @@ -21,7 +21,6 @@ import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' 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' @@ -30,15 +29,12 @@ import { generatePolicyUpdateSQL, type Policy, } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils' -import { RLSTesterSheet } from '@/components/interfaces/Database/RLSTester/RLSTesterSheet' import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout' import { DefaultLayout } from '@/components/layouts/DefaultLayout' import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import { getExposedSchemas } from '@/components/layouts/ProjectNeedsSecuring/ProjectNeedsSecuring.utils' import { AlertError } from '@/components/ui/AlertError' import { AutoEnableRLSNotice } from '@/components/ui/AutoEnableRLSNotice' -import { BannerRlsTester } from '@/components/ui/BannerStack/Banners/BannerRlsTester' -import { useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider' import { DocsButton } from '@/components/ui/DocsButton' import { NoPermission } from '@/components/ui/NoPermission' import { SchemaSelector } from '@/components/ui/SchemaSelector' @@ -121,7 +117,6 @@ const DatabasePoliciesPage: NextPageWithLayout = () => { ) const isInlineEditorEnabled = useIsInlineEditorEnabled() - const rlsTesterEnabled = useIsRLSTesterEnabled() const { openSidebar } = useSidebarManagerSnapshot() const { @@ -150,18 +145,12 @@ const DatabasePoliciesPage: NextPageWithLayout = () => { useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => setSearchString('')) const { isSchemaLocked } = useIsProtectedSchema({ schema: schema, excludedSchemas: ['realtime'] }) - const { addBanner, dismissBanner } = useBannerStack() const [isAutoEnableRLSMinimized] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.RLS_EVENT_TRIGGER_BANNER_DISMISSED(projectRef ?? ''), false ) - const [isRlsTesterBannerDismissed] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.RLS_TESTER_BANNER_DISMISSED(projectRef ?? ''), - false - ) - const { data: policies = [], isPending: isLoadingPolicies, @@ -249,25 +238,6 @@ const DatabasePoliciesPage: NextPageWithLayout = () => { const handleResetSearch = useCallback(() => setSearchString(''), [setSearchString]) - useEffect(() => { - if (rlsTesterEnabled) return - - if (!isRlsTesterBannerDismissed) { - addBanner({ - id: 'rls-tester-banner', - isDismissed: false, - content: , - priority: 3, - }) - } else { - dismissBanner('rls-tester-banner') - } - - return () => { - dismissBanner('rls-tester-banner') - } - }, [addBanner, dismissBanner, isRlsTesterBannerDismissed, rlsTesterEnabled]) - useEffect(() => { if (selectedIdToEdit && isPoliciesSuccess && !selectedPolicyToEdit) { toast(`Policy ID ${selectedIdToEdit} cannot be found`) @@ -294,7 +264,6 @@ const DatabasePoliciesPage: NextPageWithLayout = () => { {isAutoEnableRLSMinimized && } - {rlsTesterEnabled && } @@ -343,6 +312,7 @@ const DatabasePoliciesPage: NextPageWithLayout = () => {