Files
supabase/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx
T
Charis 0433eeb5f5 feat(studio): mark sql provenance for safety (#45336)
Mark provenance of SQL via the branded types SafeSqlFragment and
UntrustedSqlFragment. Only SafeSqlFragment should be executed;
UntrustedSqlFragments require some kind of implicit user approval (show
on screen + user has to click something) before they are promoted to
SafeSqlFragment.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Editor and RLS tester show loading states for inferred/generated SQL
and include a dedicated user SQL editor for safer edits.

* **Refactor**
* Platform-wide SQL handling tightened: snippets and AI-generated SQL
are treated as untrusted/display-only until promoted, improving safety
and consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-04 13:08:06 -04:00

191 lines
7.2 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { noop } from 'lodash'
import { Edit, MoreVertical, Trash } from 'lucide-react'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
TableCell,
TableRow,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { generatePolicyUpdateSQL } from './PolicyTableRow.utils'
import type { Policy } from './PolicyTableRow.utils'
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip'
import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
interface PolicyRowProps {
policy: Policy
onSelectEditPolicy: (policy: Policy) => void
onSelectDeletePolicy: (policy: Policy) => void
isLocked?: boolean
}
export const PolicyRow = ({
policy,
isLocked: isLockedDefault = false,
onSelectEditPolicy = noop,
onSelectDeletePolicy = noop,
}: PolicyRowProps) => {
const aiSnap = useAiAssistantStateSnapshot()
const { openSidebar } = useSidebarManagerSnapshot()
const { can: canUpdatePolicies } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'policies'
)
const { data: project } = useSelectedProjectQuery()
const { data: authConfig } = useAuthConfigQuery({ projectRef: project?.ref })
// override islocked for Realtime messages table
const isLocked =
policy.schema === 'realtime' && policy.table === 'messages' ? false : isLockedDefault
// TODO(km): Simple check for roles that allow authenticated access.
// In the future, we'll use splinter to return proper warnings for policies that allow anonymous user access.
const appliesToAnonymousUsers =
authConfig?.EXTERNAL_ANONYMOUS_USERS_ENABLED &&
(policy.roles.includes('authenticated') || policy.roles.includes('public'))
const displayedRoles = (() => {
const rolesWithAnonymous = appliesToAnonymousUsers
? [...policy.roles, 'anonymous sign-ins']
: policy.roles
return rolesWithAnonymous
})()
return (
<TableRow>
<TableCell className="w-[40%] truncate">
<div className="flex items-center gap-x-2 min-w-0">
<Button
type="text"
className="text-foreground text-sm p-0 hover:bg-transparent w-full truncate justify-start"
onClick={() => onSelectEditPolicy(policy)}
>
{policy.name}
</Button>
</div>
</TableCell>
<TableCell className="w-[20%] truncate">
<code className="text-code-inline">{policy.command}</code>
</TableCell>
<TableCell className="w-[30%] truncate">
<div className="flex items-center gap-x-1">
<div className="text-foreground-lighter text-sm truncate">
{displayedRoles.slice(0, 2).map((role, i) => (
<span key={`policy-${role}-${i}`}>
<code className="text-code-inline">{role}</code>
{i < Math.min(displayedRoles.length, 2) - 1 ? ', ' : ' '}
</span>
))}
</div>
{displayedRoles.length > 2 && (
<Tooltip>
<TooltipTrigger asChild>
<div>
<span key="policy-etc" className="text-foreground-light text-xs">
+ {displayedRoles.length - 2} more
</span>
</div>
</TooltipTrigger>
<TooltipContent
side="bottom"
align="center"
className="max-w-80 font-mono flex flex-wrap justify-center gap-y-1"
>
{displayedRoles.slice(2).map((role, i, arr) => (
<>
<code key={role} className="text-code-inline break-keep!">
{role}
</code>
{i < arr.length - 1 && ', '}
</>
))}
</TooltipContent>
</Tooltip>
)}
</div>
</TableCell>
<TableCell className="text-right whitespace-nowrap">
{!isLocked && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="default"
className="px-1.5"
icon={<MoreVertical />}
data-testid={`policy-${policy.name}-actions-button`}
/>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-52">
<DropdownMenuItem className="gap-x-2" onClick={() => onSelectEditPolicy(policy)}>
<Edit size={14} />
<p>Edit policy</p>
</DropdownMenuItem>
<DropdownMenuItem
className="space-x-2"
onClick={() => {
const sql = generatePolicyUpdateSQL(policy)
openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
aiSnap.newChat({
name: `Update policy ${policy.name}`,
sqlSnippets: [sql],
initialInput: `Update the policy with name \"${policy.name}\" in the ${policy.schema} schema on the ${policy.table} table. It should...`,
suggestions: {
title: `I can help you make a change to the policy \"${policy.name}\" in the ${policy.schema} schema on the ${policy.table} table, here are a few example prompts to get you started:`,
prompts: [
{
label: 'Improve Policy',
description: 'Tell me how I can improve this policy...',
},
{
label: 'Duplicate Policy',
description: 'Duplicate this policy for another table...',
},
{
label: 'Add Conditions',
description: 'Add extra conditions to this policy...',
},
],
},
})
}}
>
<Edit size={14} />
<p>Edit policy with Assistant</p>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItemTooltip
className="gap-x-2"
disabled={!canUpdatePolicies}
onClick={() => onSelectDeletePolicy(policy)}
tooltip={{
content: {
side: 'left',
text: 'You need additional permissions to delete policies',
},
}}
>
<Trash size={14} />
<p>Delete policy</p>
</DropdownMenuItemTooltip>
</DropdownMenuContent>
</DropdownMenu>
)}
</TableCell>
</TableRow>
)
}