mirror of
https://github.com/supabase/supabase.git
synced 2026-07-05 13:14:36 +08:00
* Add settings for queues: toggle expose through postgrest + permissions via table privileges * Ensure appropriate grants are granted when toggling, and revoked when disabling * Update to use queues_public schema * Update queue schema to pgmq_public and add/remove from data api when enabling/disabling * Fix query for retrieving toggle state * Add schema invalidation * Remove hard code * Use QueuesSettings from Queues folder, remove from NewQueues * Update SQL for toggling exposure + support RLS enabling * Support toggling RLS for a queue * Update admonition copy in queues for enabling/disable postgrest exposure * Add custom RLS policy for queue * Minor style fixes * Fix * Remove hard code * Update RLS to add message regarding relevancy only if exposure to PostgREST is enabled * Update message in exposing queues to postgREST * Address feedback * Address feedback * Don't revoke postgres role stuff * Remove hard code * Update copy * Update * Address Oli's feedback, ensure that queues ALL have RLS enabled prior to allowing exposure to PostgREST * Address remaining feedback * Remove hardcode * Update * Address feedback
444 lines
17 KiB
TypeScript
444 lines
17 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { Lock, MousePointer2, PlusCircle, Unlock } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
|
|
import { useParams } from 'common'
|
|
import { useTrackedState } from 'components/grid/store/Store'
|
|
import { getEntityLintDetails } from 'components/interfaces/TableGridEditor/TableEntity.utils'
|
|
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
|
import APIDocsButton from 'components/ui/APIDocsButton'
|
|
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
|
import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query'
|
|
import { useDatabasePublicationsQuery } from 'data/database-publications/database-publications-query'
|
|
import { useDatabasePublicationUpdateMutation } from 'data/database-publications/database-publications-update-mutation'
|
|
import { useProjectLintsQuery } from 'data/lint/lint-query'
|
|
import {
|
|
Entity,
|
|
isTableLike,
|
|
isForeignTable as isTableLikeForeignTable,
|
|
isMaterializedView as isTableLikeMaterializedView,
|
|
isView as isTableLikeView,
|
|
} from 'data/table-editor/table-editor-types'
|
|
import { useTableUpdateMutation } from 'data/tables/table-update-mutation'
|
|
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
|
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
|
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
|
|
import {
|
|
Button,
|
|
PopoverContent_Shadcn_,
|
|
PopoverTrigger_Shadcn_,
|
|
Popover_Shadcn_,
|
|
TooltipContent_Shadcn_,
|
|
TooltipTrigger_Shadcn_,
|
|
Tooltip_Shadcn_,
|
|
cn,
|
|
} from 'ui'
|
|
import ConfirmModal from 'ui-patterns/Dialogs/ConfirmDialog'
|
|
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
|
import { RoleImpersonationPopover } from '../RoleImpersonationSelector'
|
|
|
|
export interface GridHeaderActionsProps {
|
|
table: Entity
|
|
canEditViaTableEditor: boolean
|
|
}
|
|
|
|
const GridHeaderActions = ({ table }: GridHeaderActionsProps) => {
|
|
const { ref } = useParams()
|
|
const { project } = useProjectContext()
|
|
|
|
// need project lints to get security status for views
|
|
const { data: lints = [] } = useProjectLintsQuery({
|
|
projectRef: project?.ref,
|
|
})
|
|
|
|
const isTable = isTableLike(table)
|
|
const isForeignTable = isTableLikeForeignTable(table)
|
|
const isView = isTableLikeView(table)
|
|
const isMaterializedView = isTableLikeMaterializedView(table)
|
|
|
|
const realtimeEnabled = useIsFeatureEnabled('realtime:all')
|
|
const isLocked = PROTECTED_SCHEMAS.includes(table.schema)
|
|
|
|
const { mutate: updateTable } = useTableUpdateMutation({
|
|
onError: (error) => {
|
|
toast.error(`Failed to toggle RLS: ${error.message}`)
|
|
},
|
|
onSettled: () => {
|
|
closeConfirmModal()
|
|
},
|
|
})
|
|
|
|
const [showEnableRealtime, setShowEnableRealtime] = useState(false)
|
|
const [open, setOpen] = useState(false)
|
|
const [rlsConfirmModalOpen, setRlsConfirmModalOpen] = useState(false)
|
|
|
|
const state = useTrackedState()
|
|
const { selectedRows } = state
|
|
const showHeaderActions = selectedRows.size === 0
|
|
|
|
const projectRef = project?.ref
|
|
const { data } = useDatabasePoliciesQuery({
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
})
|
|
const policies = (data ?? []).filter(
|
|
(policy) => policy.schema === table.schema && policy.table === table.name
|
|
)
|
|
|
|
const { data: publications } = useDatabasePublicationsQuery({
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
})
|
|
const realtimePublication = (publications ?? []).find(
|
|
(publication) => publication.name === 'supabase_realtime'
|
|
)
|
|
const realtimeEnabledTables = realtimePublication?.tables ?? []
|
|
const isRealtimeEnabled = realtimeEnabledTables.some((t: any) => t.id === table?.id)
|
|
|
|
const { mutate: updatePublications, isLoading: isTogglingRealtime } =
|
|
useDatabasePublicationUpdateMutation({
|
|
onSuccess: () => {
|
|
setShowEnableRealtime(false)
|
|
},
|
|
onError: (error) => {
|
|
toast.error(`Failed to toggle realtime for ${table.name}: ${error.message}`)
|
|
},
|
|
})
|
|
|
|
const canSqlWriteTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
|
|
const canSqlWriteColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns')
|
|
const isReadOnly = !canSqlWriteTables && !canSqlWriteColumns
|
|
// This will change when we allow autogenerated API docs for schemas other than `public`
|
|
const doesHaveAutoGeneratedAPIDocs = table.schema === 'public'
|
|
|
|
const { hasLint: viewHasLints, matchingLint: matchingViewLint } = getEntityLintDetails(
|
|
table.name,
|
|
'security_definer_view',
|
|
['ERROR', 'WARN'],
|
|
lints,
|
|
table.schema
|
|
)
|
|
|
|
const { hasLint: materializedViewHasLints, matchingLint: matchingMaterializedViewLint } =
|
|
getEntityLintDetails(
|
|
table.name,
|
|
'materialized_view_in_api',
|
|
['ERROR', 'WARN'],
|
|
lints,
|
|
table.schema
|
|
)
|
|
|
|
const toggleRealtime = async () => {
|
|
if (!project) return console.error('Project is required')
|
|
if (!realtimePublication) return console.error('Unable to find realtime publication')
|
|
|
|
const exists = realtimeEnabledTables.some((x: any) => x.id == table.id)
|
|
const tables = !exists
|
|
? [`${table.schema}.${table.name}`].concat(
|
|
realtimeEnabledTables.map((t: any) => `${t.schema}.${t.name}`)
|
|
)
|
|
: realtimeEnabledTables
|
|
.filter((x: any) => x.id != table.id)
|
|
.map((x: any) => `${x.schema}.${x.name}`)
|
|
|
|
updatePublications({
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
id: realtimePublication.id,
|
|
tables,
|
|
})
|
|
}
|
|
|
|
const closeConfirmModal = () => {
|
|
setRlsConfirmModalOpen(false)
|
|
}
|
|
const onToggleRLS = async () => {
|
|
const payload = {
|
|
id: table.id,
|
|
rls_enabled: !(isTable && table.rls_enabled),
|
|
}
|
|
|
|
updateTable({
|
|
projectRef: project?.ref!,
|
|
connectionString: project?.connectionString,
|
|
id: payload.id,
|
|
schema: table.schema,
|
|
payload: payload,
|
|
})
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{showHeaderActions && (
|
|
<div className="flex items-center gap-x-2">
|
|
{isReadOnly && (
|
|
<Tooltip_Shadcn_>
|
|
<TooltipTrigger_Shadcn_ asChild>
|
|
<div className="border border-strong rounded bg-overlay-hover px-3 py-1 text-xs">
|
|
Viewing as read-only
|
|
</div>
|
|
</TooltipTrigger_Shadcn_>
|
|
<TooltipContent_Shadcn_ side="bottom">
|
|
You need additional permissions to manage your project's data
|
|
</TooltipContent_Shadcn_>
|
|
</Tooltip_Shadcn_>
|
|
)}
|
|
{isTable ? (
|
|
table.rls_enabled ? (
|
|
<>
|
|
{policies.length < 1 && !isLocked ? (
|
|
<ButtonTooltip
|
|
asChild
|
|
type="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 table, but no policies are set. Select queries may return 0 results.',
|
|
},
|
|
}}
|
|
>
|
|
<Link
|
|
passHref
|
|
href={`/project/${projectRef}/auth/policies?search=${table.id}&schema=${table.schema}`}
|
|
>
|
|
Add RLS policy
|
|
</Link>
|
|
</ButtonTooltip>
|
|
) : (
|
|
<Button
|
|
asChild
|
|
type={policies.length < 1 && !isLocked ? 'warning' : 'default'}
|
|
className="group"
|
|
icon={
|
|
isLocked || policies.length > 0 ? (
|
|
<div
|
|
className={cn(
|
|
'flex items-center justify-center rounded-full bg-border-stronger h-[16px]',
|
|
policies.length > 9 ? ' px-1' : 'w-[16px]',
|
|
''
|
|
)}
|
|
>
|
|
<span className="text-[11px] text-foreground font-mono text-center">
|
|
{policies.length}
|
|
</span>
|
|
</div>
|
|
) : (
|
|
<PlusCircle strokeWidth={1.5} />
|
|
)
|
|
}
|
|
>
|
|
<Link
|
|
passHref
|
|
href={`/project/${projectRef}/auth/policies?search=${table.id}&schema=${table.schema}`}
|
|
>
|
|
Auth {policies.length > 1 ? 'policies' : 'policy'}
|
|
</Link>
|
|
</Button>
|
|
)}
|
|
</>
|
|
) : (
|
|
<Popover_Shadcn_ open={open} onOpenChange={() => setOpen(!open)} modal={false}>
|
|
<PopoverTrigger_Shadcn_ asChild>
|
|
<Button type="warning" icon={<Lock strokeWidth={1.5} />}>
|
|
RLS disabled
|
|
</Button>
|
|
</PopoverTrigger_Shadcn_>
|
|
<PopoverContent_Shadcn_ className="min-w-[395px] text-sm" align="end">
|
|
<h3 className="flex items-center gap-2">
|
|
<Lock size={16} /> Row Level Security (RLS)
|
|
</h3>
|
|
<div className="grid gap-2 mt-4 text-foreground-light text-sm">
|
|
<p>
|
|
You can restrict and control who can read, write and update data in this table
|
|
using Row Level Security.
|
|
</p>
|
|
<p>
|
|
With RLS enabled, anonymous users will not be able to read/write data in the
|
|
table.
|
|
</p>
|
|
{!isLocked && (
|
|
<div className="mt-2">
|
|
<Button
|
|
type="default"
|
|
onClick={() => setRlsConfirmModalOpen(!rlsConfirmModalOpen)}
|
|
>
|
|
Enable RLS for this table
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</PopoverContent_Shadcn_>
|
|
</Popover_Shadcn_>
|
|
)
|
|
) : null}
|
|
{isView && viewHasLints && (
|
|
<Popover_Shadcn_ open={open} onOpenChange={() => setOpen(!open)} modal={false}>
|
|
<PopoverTrigger_Shadcn_ asChild>
|
|
<Button type="warning" icon={<Unlock strokeWidth={1.5} />}>
|
|
Security Definer view
|
|
</Button>
|
|
</PopoverTrigger_Shadcn_>
|
|
<PopoverContent_Shadcn_ className="min-w-[395px] text-sm" align="end">
|
|
<h3 className="flex items-center gap-2">
|
|
<Unlock size={16} /> Secure your View
|
|
</h3>
|
|
<div className="grid gap-2 mt-4 text-foreground-light text-sm">
|
|
<p>
|
|
This view is defined with the Security Definer property, giving it permissions
|
|
of the view's creator (Postgres), rather than the permissions of the querying
|
|
user.
|
|
</p>
|
|
|
|
<p>
|
|
Since this view is in the public schema, it is accessible via your project's
|
|
APIs.
|
|
</p>
|
|
|
|
<div className="mt-2">
|
|
<Button type="default" asChild>
|
|
<Link
|
|
target="_blank"
|
|
href={`/project/${ref}/advisors/security?preset=${matchingViewLint?.level}&id=${matchingViewLint?.cache_key}`}
|
|
>
|
|
Learn more
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</PopoverContent_Shadcn_>
|
|
</Popover_Shadcn_>
|
|
)}
|
|
{isMaterializedView && materializedViewHasLints && (
|
|
<Popover_Shadcn_ open={open} onOpenChange={() => setOpen(!open)} modal={false}>
|
|
<PopoverTrigger_Shadcn_ asChild>
|
|
<Button type="warning" icon={<Unlock strokeWidth={1.5} />}>
|
|
Security Definer view
|
|
</Button>
|
|
</PopoverTrigger_Shadcn_>
|
|
<PopoverContent_Shadcn_ className="min-w-[395px] text-sm" align="end">
|
|
<h3 className="flex items-center gap-2">
|
|
<Unlock size={16} /> Secure your View
|
|
</h3>
|
|
<div className="grid gap-2 mt-4 text-foreground-light text-sm">
|
|
<p>
|
|
This view is defined with the Security Definer property, giving it permissions
|
|
of the view's creator (Postgres), rather than the permissions of the querying
|
|
user.
|
|
</p>
|
|
|
|
<p>
|
|
Since this view is in the public schema, it is accessible via your project's
|
|
APIs.
|
|
</p>
|
|
|
|
<div className="mt-2">
|
|
<Button type="default" asChild>
|
|
<Link
|
|
target="_blank"
|
|
href={`/project/${ref}/advisors/security?preset=${matchingMaterializedViewLint?.level}&id=${matchingMaterializedViewLint?.cache_key}`}
|
|
>
|
|
Learn more
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</PopoverContent_Shadcn_>
|
|
</Popover_Shadcn_>
|
|
)}
|
|
{isForeignTable && table.schema === 'public' && (
|
|
<Popover_Shadcn_ open={open} onOpenChange={() => setOpen(!open)} modal={false}>
|
|
<PopoverTrigger_Shadcn_ asChild>
|
|
<Button type="warning" icon={<Unlock strokeWidth={1.5} />}>
|
|
Foreign table is accessible via your project's APIs
|
|
</Button>
|
|
</PopoverTrigger_Shadcn_>
|
|
<PopoverContent_Shadcn_ className="min-w-[395px] text-sm" align="end">
|
|
<h3 className="flex items-center gap-2">
|
|
<Unlock size={16} /> Secure Foreign table
|
|
</h3>
|
|
<div className="grid gap-2 mt-4 text-foreground-light text-sm">
|
|
<p>
|
|
Foreign tables do not enforce RLS. Move them to a private schema not exposed to
|
|
Postgrest or disable Postgrest.
|
|
</p>
|
|
|
|
<div className="mt-2">
|
|
<Button type="default" asChild>
|
|
<Link
|
|
target="_blank"
|
|
href="https://github.com/orgs/supabase/discussions/21647"
|
|
>
|
|
Learn more
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</PopoverContent_Shadcn_>
|
|
</Popover_Shadcn_>
|
|
)}
|
|
<RoleImpersonationPopover serviceRoleLabel="postgres" />
|
|
{isTable && realtimeEnabled && (
|
|
<Button
|
|
type="default"
|
|
icon={
|
|
<MousePointer2
|
|
strokeWidth={1.5}
|
|
className={isRealtimeEnabled ? 'text-brand' : 'text-foreground-muted'}
|
|
/>
|
|
}
|
|
onClick={() => setShowEnableRealtime(true)}
|
|
>
|
|
Realtime {isRealtimeEnabled ? 'on' : 'off'}
|
|
</Button>
|
|
)}
|
|
{doesHaveAutoGeneratedAPIDocs && <APIDocsButton section={['entities', table.name]} />}
|
|
</div>
|
|
)}
|
|
<ConfirmationModal
|
|
visible={showEnableRealtime}
|
|
loading={isTogglingRealtime}
|
|
title={`${isRealtimeEnabled ? 'Disable' : 'Enable'} realtime for ${table.name}`}
|
|
confirmLabel={`${isRealtimeEnabled ? 'Disable' : 'Enable'} realtime`}
|
|
confirmLabelLoading={`${isRealtimeEnabled ? 'Disabling' : 'Enabling'} realtime`}
|
|
onCancel={() => setShowEnableRealtime(false)}
|
|
onConfirm={() => toggleRealtime()}
|
|
>
|
|
<div className="space-y-2">
|
|
<p className="text-sm">
|
|
Once realtime has been {isRealtimeEnabled ? 'disabled' : 'enabled'}, the table will{' '}
|
|
{isRealtimeEnabled ? 'no longer ' : ''}broadcast any changes to authorized subscribers.
|
|
</p>
|
|
{!isRealtimeEnabled && (
|
|
<p className="text-sm">
|
|
You may also select which events to broadcast to subscribers on the{' '}
|
|
<Link href={`/project/${ref}/database/publications`} className="text-brand">
|
|
database publications
|
|
</Link>{' '}
|
|
settings.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</ConfirmationModal>
|
|
{isTable && (
|
|
<ConfirmModal
|
|
danger={table.rls_enabled}
|
|
visible={rlsConfirmModalOpen}
|
|
title="Confirm to enable Row Level Security"
|
|
description="Are you sure you want to enable Row Level Security for this table?"
|
|
buttonLabel="Enable RLS"
|
|
buttonLoadingLabel="Updating"
|
|
onSelectCancel={closeConfirmModal}
|
|
onSelectConfirm={onToggleRLS}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default GridHeaderActions
|