mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 19:35:06 +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 -->
291 lines
11 KiB
TypeScript
291 lines
11 KiB
TypeScript
import { useParams } from 'common'
|
|
import { Lock, Paintbrush, PlusCircle, Trash2 } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { parseAsBoolean, useQueryState } from 'nuqs'
|
|
import { useMemo, useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
import { Button, cn, LoadingLine, Popover, PopoverContent, PopoverTrigger, Separator } from 'ui'
|
|
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
|
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import { pgmqQueueTable } from './Queues.utils'
|
|
import { DeleteQueue } from '@/components/interfaces/Integrations/Queues/SingleQueue/DeleteQueue'
|
|
import { PurgeQueue } from '@/components/interfaces/Integrations/Queues/SingleQueue/PurgeQueue'
|
|
import { QUEUE_MESSAGE_TYPE } from '@/components/interfaces/Integrations/Queues/SingleQueue/Queue.utils'
|
|
import { QueueMessagesDataGrid } from '@/components/interfaces/Integrations/Queues/SingleQueue/QueueDataGrid'
|
|
import { QueueFilters } from '@/components/interfaces/Integrations/Queues/SingleQueue/QueueFilters'
|
|
import { QueueSettings } from '@/components/interfaces/Integrations/Queues/SingleQueue/QueueSettings'
|
|
import { SendMessageModal } from '@/components/interfaces/Integrations/Queues/SingleQueue/SendMessageModal'
|
|
import { Markdown } from '@/components/interfaces/Markdown'
|
|
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
|
import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query'
|
|
import { useQueueMessagesInfiniteQuery } from '@/data/database-queues/database-queue-messages-infinite-query'
|
|
import { useQueuesExposePostgrestStatusQuery } from '@/data/database-queues/database-queues-expose-postgrest-status-query'
|
|
import { useTableUpdateMutation } from '@/data/tables/table-update-mutation'
|
|
import { useTablesQuery } from '@/data/tables/tables-query'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
|
|
export const QueueTab = () => {
|
|
const { childId: queueName, ref } = useParams()
|
|
const { data: project } = useSelectedProjectQuery()
|
|
|
|
const [openRlsPopover, setOpenRlsPopover] = useState(false)
|
|
const [rlsConfirmModalOpen, setRlsConfirmModalOpen] = useState(false)
|
|
const [sendMessageModalShown, setSendMessageModalShown] = useQueryState(
|
|
'new-message',
|
|
parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
|
|
)
|
|
const [purgeQueueModalShown, setPurgeQueueModalShown] = useState(false)
|
|
const [deleteQueueModalShown, setDeleteQueueModalShown] = useState(false)
|
|
const [selectedTypes, setSelectedTypes] = useState<QUEUE_MESSAGE_TYPE[]>([])
|
|
|
|
const { data: tables, isPending: isLoadingTables } = useTablesQuery({
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
schema: 'pgmq',
|
|
})
|
|
const queueRelname = queueName ? pgmqQueueTable(queueName) : undefined
|
|
const queueTable = tables?.find((x) => x.name === queueRelname)
|
|
const isRlsEnabled = queueTable?.rls_enabled ?? false
|
|
|
|
const { data: policies } = useDatabasePoliciesQuery({
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
schema: 'pgmq',
|
|
})
|
|
const queuePolicies = (policies ?? []).filter((policy) => policy.table === queueRelname)
|
|
|
|
const { data: isExposed } = useQueuesExposePostgrestStatusQuery({
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
})
|
|
|
|
const {
|
|
data,
|
|
error,
|
|
isPending: isLoading,
|
|
fetchNextPage,
|
|
isFetching,
|
|
} = useQueueMessagesInfiniteQuery(
|
|
{
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
queueName: queueName!,
|
|
// when no types are selected, include all types of messages
|
|
status: selectedTypes.length === 0 ? ['archived', 'available', 'scheduled'] : selectedTypes,
|
|
},
|
|
{ staleTime: 30 }
|
|
)
|
|
const messages = useMemo(() => data?.pages.flatMap((p) => p), [data?.pages])
|
|
|
|
const { mutate: updateTable, isPending: isUpdatingTable } = useTableUpdateMutation({
|
|
onSettled: () => {
|
|
toast.success(`Successfully enabled RLS for ${queueName}`)
|
|
setRlsConfirmModalOpen(false)
|
|
},
|
|
})
|
|
|
|
const onToggleRLS = async () => {
|
|
if (!project) return console.error('Project is required')
|
|
if (!queueTable) return toast.error('Unable to toggle RLS: Queue table not found')
|
|
const payload = {
|
|
id: queueTable.id,
|
|
rls_enabled: true,
|
|
}
|
|
updateTable({
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
id: queueTable.id,
|
|
name: queueTable.name,
|
|
schema: 'pgmq',
|
|
payload: payload,
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div className="h-full flex flex-col">
|
|
<div className="flex items-center justify-between gap-x-4 py-1.5 px-10 mb-0 bg-surface-200">
|
|
<QueueFilters selectedTypes={selectedTypes} setSelectedTypes={setSelectedTypes} />
|
|
<div className="flex gap-x-2">
|
|
<QueueSettings />
|
|
|
|
<ButtonTooltip
|
|
variant="text"
|
|
className="px-1.5"
|
|
onClick={() => setPurgeQueueModalShown(true)}
|
|
icon={<Paintbrush />}
|
|
title="Purge messages"
|
|
aria-label="Purge messages"
|
|
tooltip={{ content: { side: 'bottom', text: 'Purge messages' } }}
|
|
/>
|
|
|
|
<ButtonTooltip
|
|
variant="text"
|
|
className="px-1.5"
|
|
onClick={() => setDeleteQueueModalShown(true)}
|
|
icon={<Trash2 />}
|
|
title="Delete queue"
|
|
aria-label="Delete queue"
|
|
tooltip={{ content: { side: 'bottom', text: 'Delete queue' } }}
|
|
/>
|
|
|
|
<Separator orientation="vertical" className="h-[26px]" />
|
|
|
|
{isLoadingTables ? (
|
|
<ShimmeringLoader className="w-[123px]" />
|
|
) : isRlsEnabled ? (
|
|
<>
|
|
{queuePolicies.length === 0 ? (
|
|
<ButtonTooltip
|
|
asChild
|
|
variant="default"
|
|
className="group"
|
|
icon={<PlusCircle strokeWidth={1.5} className="text-foreground-muted" />}
|
|
tooltip={{
|
|
content: {
|
|
side: 'bottom',
|
|
className: 'w-[280px]',
|
|
text: 'RLS is enabled for this queue, but no policies are set. Queue will not be accessible.',
|
|
},
|
|
}}
|
|
>
|
|
<Link
|
|
passHref
|
|
href={`/project/${ref}/database/policies?search=${queueTable?.id}&schema=pgmq`}
|
|
>
|
|
Add RLS policy
|
|
</Link>
|
|
</ButtonTooltip>
|
|
) : (
|
|
<Button
|
|
asChild
|
|
variant="default"
|
|
className="group"
|
|
icon={
|
|
<div
|
|
className={cn(
|
|
'flex items-center justify-center rounded-full bg-border-stronger h-[16px]',
|
|
queuePolicies.length > 9 ? ' px-1' : 'w-[16px]'
|
|
)}
|
|
>
|
|
<span className="text-[11px] text-foreground font-mono text-center">
|
|
{queuePolicies.length}
|
|
</span>
|
|
</div>
|
|
}
|
|
>
|
|
<Link
|
|
passHref
|
|
href={`/project/${ref}/database/policies?search=${queueTable?.id}&schema=pgmq`}
|
|
>
|
|
Auth {queuePolicies.length > 1 ? 'policies' : 'policy'}
|
|
</Link>
|
|
</Button>
|
|
)}
|
|
</>
|
|
) : (
|
|
<Popover
|
|
modal={false}
|
|
open={openRlsPopover}
|
|
onOpenChange={() => setOpenRlsPopover(!openRlsPopover)}
|
|
>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant={isExposed ? 'warning' : 'default'}
|
|
icon={<Lock strokeWidth={1.5} />}
|
|
>
|
|
RLS disabled
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-80 text-sm" align="end">
|
|
<h3 className="text-xs flex items-center gap-x-2">
|
|
<Lock size={14} /> Row Level Security (RLS)
|
|
</h3>
|
|
<div className="grid gap-2 mt-2 text-foreground-light text-xs">
|
|
{isExposed ? (
|
|
<>
|
|
<p>
|
|
You can restrict and control who can manage this queue using Row Level
|
|
Security.
|
|
</p>
|
|
<p>With RLS enabled, anonymous users will not have access to this queue.</p>
|
|
<Button
|
|
variant="default"
|
|
className="w-min"
|
|
onClick={() => setRlsConfirmModalOpen(!rlsConfirmModalOpen)}
|
|
>
|
|
Enable RLS for this queue
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Markdown
|
|
className="[&>p]:leading-normal! text-xs [&>p]:m-0! flex flex-col gap-y-2"
|
|
content={`
|
|
RLS for queues is only relevant if exposure through PostgREST has been enabled, in which you can restrict and control who can manage this queue using Row Level Security.
|
|
|
|
You may opt to manage your queues via any Supabase client libraries or PostgREST endpoints by enabling this in the [queues settings](/project/${project?.ref}/integrations/queues/settings).`}
|
|
/>
|
|
<Button
|
|
variant="default"
|
|
className="w-min"
|
|
onClick={() => setRlsConfirmModalOpen(!rlsConfirmModalOpen)}
|
|
>
|
|
Enable RLS for this queue
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)}
|
|
|
|
<Button variant="primary" onClick={() => setSendMessageModalShown(true)}>
|
|
Add message
|
|
</Button>
|
|
|
|
{/* <DocsButton href={docsUrl} />} */}
|
|
</div>
|
|
</div>
|
|
|
|
<LoadingLine loading={isFetching} />
|
|
|
|
<QueueMessagesDataGrid
|
|
error={error}
|
|
messages={messages || []}
|
|
isLoading={isLoading}
|
|
showMessageModal={() => setSendMessageModalShown(true)}
|
|
fetchNextPage={fetchNextPage}
|
|
/>
|
|
<SendMessageModal
|
|
visible={sendMessageModalShown}
|
|
onClose={() => setSendMessageModalShown(false)}
|
|
/>
|
|
<DeleteQueue
|
|
queueName={queueName!}
|
|
visible={deleteQueueModalShown}
|
|
onClose={() => setDeleteQueueModalShown(false)}
|
|
/>
|
|
<PurgeQueue
|
|
queueName={queueName!}
|
|
visible={purgeQueueModalShown}
|
|
onClose={() => setPurgeQueueModalShown(false)}
|
|
/>
|
|
|
|
<ConfirmationModal
|
|
visible={rlsConfirmModalOpen}
|
|
title="Enable Row Level Security"
|
|
confirmLabel="Enable RLS"
|
|
confirmLabelLoading="Enabling RLS"
|
|
loading={isUpdatingTable}
|
|
onCancel={() => setRlsConfirmModalOpen(false)}
|
|
onConfirm={() => onToggleRLS()}
|
|
>
|
|
<p className="text-sm text-foreground-light">
|
|
Are you sure you want to enable Row Level Security for the queue "{queueName}"?
|
|
</p>
|
|
</ConfirmationModal>
|
|
</div>
|
|
)
|
|
}
|