mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
## Context Back to working on the [RLS Tester](https://github.com/orgs/supabase/discussions/45233), slowly adding support for mutation queries. First part here will be to add support for testing `INSERT` based queries (Note that there's no changes to the sandbox stuff in this PR) ## Changes involved - If testing an `INSERT` query, we show a big warning first that the query will be ran on the actual DB - Note that we skip the warning if the sandbox is used <img width="534" height="231" alt="image" src="https://github.com/user-attachments/assets/ef75a0c9-61e4-49b0-9d78-458e8e5f7f4f" /> - If the testing as an anon user + RLS enabled <img width="601" height="386" alt="image" src="https://github.com/user-attachments/assets/b21f048d-bac1-4ddd-b84b-c231ae9f9e3e" /> - If testing as an auth-ed user + RLS enabled, but the INSERT violates RLS (conditions don't meet) <img width="604" height="489" alt="image" src="https://github.com/user-attachments/assets/41c40486-48d5-4eee-b7cd-8f993edc47be" /> - Else if testing as an auth-ed user + RLS enabled and INSERT matches RLS <img width="612" height="402" alt="image" src="https://github.com/user-attachments/assets/41854b40-b351-408b-8d23-cc5e0fa40813" /> - Minor cosmetic layout change here - Use layout horizontal - Also added the user ID below the dropdown with click to copy action for convenience <img width="615" height="528" alt="image" src="https://github.com/user-attachments/assets/b9c04395-5435-474a-b3c5-640143faa782" /> - Added inline guard againsts some conditions - Should not be able to run UPDATE or DELETE queries <img width="622" height="319" alt="image" src="https://github.com/user-attachments/assets/351af7c6-8f1e-47ae-8651-3b9b0b512490" /> - Should not be able to run multiple queries <img width="612" height="317" alt="image" src="https://github.com/user-attachments/assets/603d9a1f-1d1f-40f2-806d-93aea6b6cf8e" /> ## To test - [ ] Verify that the RLS Tester works as expected for an insert query - Against actual DB - Against sandbox (only available on staging) - [ ] Verify that inline guards are all working as expected - Let me know if there's any edge cases I might have missed! <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * RLS Tester results are now operation-aware (SELECT vs mutations), with clearer “no rows/all rows” and policy evaluation explanations. * Added copy-to-clipboard for the impersonated user ID. * Query parsing now surfaces richer context, including WHERE clause details and statement count, and SELECT-only previews. * **Bug Fixes** * Improved handling of blocked mutation queries and RLS-related error messaging. * Updated RLS Tester navigation to the correct policies page. * Refined sandbox-assisted execution flow and empty/error states. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
208 lines
6.7 KiB
TypeScript
208 lines
6.7 KiB
TypeScript
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 (
|
|
<p>
|
|
RLS is disabled and all data is publicly accessible. We highly recommend enabling RLS and
|
|
adding policies to restrict access.
|
|
</p>
|
|
)
|
|
}
|
|
|
|
if (noPolicies) {
|
|
return (
|
|
<p>
|
|
RLS is enabled but no policies exist for the{' '}
|
|
<code className="text-code-inline">{role}</code> role on this table -{' '}
|
|
{operation === 'SELECT'
|
|
? 'no data will be returned'
|
|
: `no data will be ${operation?.toLowerCase()}${operation?.toLowerCase().endsWith('e') ? 'd' : 'ed'}`}
|
|
.
|
|
</p>
|
|
)
|
|
}
|
|
|
|
if (trueOnlyPolicy) {
|
|
return (
|
|
<>
|
|
<p>
|
|
The policy "{trueOnlyPolicy.name}" for the{' '}
|
|
<code className="text-code-inline">{role}</code> role on this table evaluates to{' '}
|
|
<code className="text-code-inline">true</code>, so all data from this query is
|
|
accessible to this user.
|
|
</p>
|
|
<TableAccessPolicySummary
|
|
policies={policies}
|
|
operation={operation}
|
|
handleSelectEditPolicy={handleSelectEditPolicy}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
if (falseOnlyPolicy) {
|
|
return (
|
|
<>
|
|
<p>
|
|
The policy "{falseOnlyPolicy.name}" for the{' '}
|
|
<code className="text-code-inline">{role}</code> role on this table evaluates to{' '}
|
|
<code className="text-code-inline">false</code>, so no data from this query is
|
|
accessible to this user.
|
|
</p>
|
|
<TableAccessPolicySummary
|
|
policies={policies}
|
|
operation={operation}
|
|
handleSelectEditPolicy={handleSelectEditPolicy}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<p>
|
|
{policies.length} {policies.length > 1 ? 'policies apply' : 'policy applies'} for the{' '}
|
|
<code className="text-code-inline">{role}</code> 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.`}
|
|
</p>
|
|
<TableAccessPolicySummary
|
|
policies={policies}
|
|
operation={operation}
|
|
handleSelectEditPolicy={handleSelectEditPolicy}
|
|
/>
|
|
</>
|
|
)
|
|
}, [
|
|
isRLSEnabled,
|
|
noPolicies,
|
|
trueOnlyPolicy,
|
|
falseOnlyPolicy,
|
|
policies,
|
|
role,
|
|
operation,
|
|
handleSelectEditPolicy,
|
|
])
|
|
|
|
return (
|
|
<Collapsible
|
|
className={cn('border rounded-sm', !isRLSEnabled && 'bg-warning-300 border-warning-500')}
|
|
>
|
|
<CollapsibleTrigger className="flex items-center justify-between px-3 py-2 w-full [&[data-state=open]>div>svg]:-rotate-180!">
|
|
<div className="w-full flex items-center justify-between">
|
|
<div className="flex items-center gap-x-2">
|
|
{!isRLSEnabled ? (
|
|
<WarningIcon />
|
|
) : (hasError && operation === 'INSERT') || noPolicies || falseOnlyPolicy ? (
|
|
<X size={16} className="text-destructive" />
|
|
) : (
|
|
<Check size={16} className="text-brand" />
|
|
)}
|
|
<p className={cn('text-xs font-mono', !isRLSEnabled && 'font-medium text-foreground')}>
|
|
{schema}.{name}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-x-2">
|
|
{operation === 'SELECT' && (
|
|
<p
|
|
className={cn(
|
|
'text-xs text-foreground-light w-max',
|
|
!isRLSEnabled && 'text-foreground'
|
|
)}
|
|
>
|
|
{noPolicies || falseOnlyPolicy
|
|
? 'Returns no rows'
|
|
: !isRLSEnabled || !!trueOnlyPolicy
|
|
? 'Returns all rows'
|
|
: null}
|
|
</p>
|
|
)}
|
|
<ChevronDown className="transition-transform duration-200" strokeWidth={1.5} size={14} />
|
|
</div>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent
|
|
className={cn(
|
|
'border-t p-3 text-sm text-foreground-light',
|
|
!isRLSEnabled && 'border-warning-500'
|
|
)}
|
|
>
|
|
{tableAccessDescription}
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
)
|
|
}
|
|
|
|
const TableAccessPolicySummary = ({
|
|
policies,
|
|
operation,
|
|
handleSelectEditPolicy,
|
|
}: {
|
|
policies: Policy[]
|
|
operation: ParseSQLQueryOperations
|
|
handleSelectEditPolicy: (policy: Policy) => void
|
|
}) => {
|
|
return (
|
|
<div className="border rounded-sm mt-4">
|
|
<p className="text-xs font-mono text-foreground-light uppercase border-b px-3 py-2">
|
|
{policies.length} {policies.length > 1 ? 'policies' : 'policy'} applied
|
|
</p>
|
|
<ul>
|
|
{policies.map((policy) => (
|
|
<li key={policy.id} className="px-3 py-2 flex justify-between items-center">
|
|
<div>
|
|
<p>{policy.name}</p>
|
|
<p className="text-foreground-lighter">
|
|
{operation === 'SELECT' ? 'Show rows' : `Allow ${operation?.toLocaleLowerCase()}s`}{' '}
|
|
where:{' '}
|
|
<code className="text-code-inline text-foreground">
|
|
{policy.definition ?? policy.check}
|
|
</code>
|
|
</p>
|
|
</div>
|
|
<ButtonTooltip
|
|
variant="text"
|
|
icon={<Edit />}
|
|
className="w-7"
|
|
tooltip={{ content: { side: 'bottom', text: 'Edit policy' } }}
|
|
onClick={() => {
|
|
handleSelectEditPolicy(policy)
|
|
}}
|
|
/>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)
|
|
}
|