mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 12:25:05 +03:00
## Context Previous PR was [here](https://github.com/supabase/supabase/pull/45143) but it got stale with lots of conflicts so figured it'll be easier redo it off the latest master Moves policies page from Auth to Database under an Access Control section along with Roles. This moves all existing files, applies redirects, and updates urls to point to the new route <img width="274" height="412" alt="image" src="https://github.com/user-attachments/assets/7952c185-64ae-4355-ba36-45397efe1787" /> <img width="453" height="471" alt="image" src="https://github.com/user-attachments/assets/04b3dcb3-48a5-4049-9893-d01109fb46a9" /> ## To test - [ ] Verify that policies now live under Database correctly <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a quick navigation shortcut to open **Database > Policies (RLS)**. * **Bug Fixes** * Updated Policies and RLS-related links across the product to open the **Database policies** area (menus, command palette, context actions, alerts, and link-outs). * Added a permanent redirect from the old **auth policies** URL to the new **database policies** URL. * **Documentation** * Updated RLS Dashboard and security checklist instructions to reference **Database > Policies**. * **Tests** * Adjusted automated tests to validate the new Policies route. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
191 lines
7.2 KiB
TypeScript
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
|
|
variant="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
|
|
variant="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>
|
|
)
|
|
}
|