mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Chore/show unlock icon next to view entities (#23238)
* Show unlock icon next to views and foreign tables * Temp header actions for all entities * Add warnings for views and foreign tables * Add labels for each entity type * Cleanup * Unneeded comma * Remove unneeded useEffect * Check lints on the entities menu too * Pass exposed schemas to lint query * Type cleanup * Update * Update lint, add 0016 * Fix materialized view logic * Cleanup * Grab lint count * Update apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx Co-authored-by: Inian <inian1234@gmail.com> * Update apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx Co-authored-by: Inian <inian1234@gmail.com> * Update apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx Co-authored-by: Inian <inian1234@gmail.com> * Update apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx Co-authored-by: Inian <inian1234@gmail.com> * Language changes * Use lints for gridheaderactions * Types cleanup --------- Co-authored-by: Inian <inian1234@gmail.com> Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
This commit is contained in:
6 files changed
+281
-34
No files matched your search
@@ -158,6 +158,16 @@ export const lintInfoMap: LintInfo[] = [
|
||||
docsLink:
|
||||
'https://supabase.com/docs/guides/database/database-linter?queryGroups=lint&lint=0015_rls_references_user_metadata',
|
||||
},
|
||||
{
|
||||
name: 'materialized_view_in_api',
|
||||
title: 'Materialized View in API',
|
||||
icon: <Eye className="text-foreground-muted" size={15} strokeWidth={1.5} />,
|
||||
link: () =>
|
||||
`https://supabase.com/docs/guides/database/database-advisors?lint=0016_materialized_view_in_api`,
|
||||
linkText: 'View docs',
|
||||
docsLink:
|
||||
'https://supabase.com/docs/guides/database/database-advisors?lint=0016_materialized_view_in_api',
|
||||
},
|
||||
]
|
||||
|
||||
export const LintCTA = ({
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import type { PostgresTable } from '@supabase/postgres-meta'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useParams } from 'common'
|
||||
import { Lock, MousePointer2, PlusCircle } from 'lucide-react'
|
||||
import { Lock, MousePointer2, PlusCircle, Unlock } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
@@ -19,17 +19,35 @@ import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import ConfirmModal from 'ui-patterns/Dialogs/ConfirmDialog'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { RoleImpersonationPopover } from '../RoleImpersonationSelector'
|
||||
import useEntityType from 'hooks/misc/useEntityType'
|
||||
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
|
||||
import { useProjectLintsQuery } from 'data/lint/lint-query'
|
||||
import { TableLike } from 'hooks/misc/useTable'
|
||||
import { checkEntityForLints } from 'components/interfaces/TableGridEditor/TableEntity.utils'
|
||||
|
||||
export interface GridHeaderActionsProps {
|
||||
table: PostgresTable
|
||||
table: TableLike
|
||||
canEditViaTableEditor: boolean
|
||||
isViewSelected: boolean
|
||||
isTableSelected: boolean
|
||||
}
|
||||
|
||||
const GridHeaderActions = ({ table, isViewSelected, isTableSelected }: GridHeaderActionsProps) => {
|
||||
const GridHeaderActions = ({ table }: GridHeaderActionsProps) => {
|
||||
const entityType = useEntityType(table?.id)
|
||||
const { ref } = useParams()
|
||||
const { project } = useProjectContext()
|
||||
|
||||
// need project lints to get security status for views
|
||||
const { data: lints = [] } = useProjectLintsQuery({
|
||||
projectRef: project?.ref,
|
||||
})
|
||||
|
||||
const isTable = entityType?.type === ENTITY_TYPE.TABLE
|
||||
const isMaterializedView = entityType?.type === ENTITY_TYPE.MATERIALIZED_VIEW
|
||||
const isView = entityType?.type === ENTITY_TYPE.VIEW
|
||||
|
||||
// check if current entity is a view and has an associated security definer lint
|
||||
|
||||
const isForeignTable = entityType?.type === ENTITY_TYPE.FOREIGN_TABLE
|
||||
|
||||
const realtimeEnabled = useIsFeatureEnabled('realtime:all')
|
||||
const isLocked = EXCLUDED_SCHEMAS.includes(table.schema)
|
||||
|
||||
@@ -81,6 +99,20 @@ const GridHeaderActions = ({ table, isViewSelected, isTableSelected }: GridHeade
|
||||
// This will change when we allow autogenerated API docs for schemas other than `public`
|
||||
const doesHaveAutoGeneratedAPIDocs = table.schema === 'public'
|
||||
|
||||
const viewHasLints = checkEntityForLints(
|
||||
table.name,
|
||||
'security_definer_view',
|
||||
['ERROR', 'WARN'],
|
||||
lints,
|
||||
table.schema
|
||||
)
|
||||
const materializedViewHasLints = checkEntityForLints(
|
||||
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')
|
||||
@@ -108,7 +140,7 @@ const GridHeaderActions = ({ table, isViewSelected, isTableSelected }: GridHeade
|
||||
const onToggleRLS = async () => {
|
||||
const payload = {
|
||||
id: table.id,
|
||||
rls_enabled: !table.rls_enabled,
|
||||
rls_enabled: !(table as PostgresTable).rls_enabled,
|
||||
}
|
||||
|
||||
updateTable({
|
||||
@@ -148,8 +180,8 @@ const GridHeaderActions = ({ table, isViewSelected, isTableSelected }: GridHeade
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
|
||||
{isTableSelected ? (
|
||||
table.rls_enabled ? (
|
||||
{isTable ? (
|
||||
(table as PostgresTable).rls_enabled ? (
|
||||
<>
|
||||
{policies.length < 1 && !isLocked ? (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
@@ -247,9 +279,110 @@ const GridHeaderActions = ({ table, isViewSelected, isTableSelected }: GridHeade
|
||||
)
|
||||
) : 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="https://supabase.com/docs/guides/database/database-advisors?lint=0010_security_definer_view"
|
||||
>
|
||||
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="https://supabase.com/docs/guides/database/database-linter?lint=0016_materialized_view_in_api"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
)}
|
||||
|
||||
{isForeignTable && entityType.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" />
|
||||
|
||||
{realtimeEnabled && !isViewSelected && (
|
||||
{isTable && realtimeEnabled && (
|
||||
<Button
|
||||
type="default"
|
||||
icon={
|
||||
@@ -293,16 +426,18 @@ const GridHeaderActions = ({ table, isViewSelected, isTableSelected }: GridHeade
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<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}
|
||||
/>
|
||||
{entityType?.type === ENTITY_TYPE.TABLE && (
|
||||
<ConfirmModal
|
||||
danger={(table as PostgresTable).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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Lint } from '../../../data/lint/lint-query'
|
||||
|
||||
export const checkEntityForLints = (
|
||||
entityName: string,
|
||||
lintName: string,
|
||||
lintLevels: ('ERROR' | 'WARN')[],
|
||||
lints: Lint[],
|
||||
schema: string
|
||||
): { hasLint: boolean; count: number } => {
|
||||
const matchingLints = lints?.filter(
|
||||
(lint) =>
|
||||
lint?.metadata?.name === entityName &&
|
||||
lint?.metadata?.schema === schema &&
|
||||
lint?.name === lintName &&
|
||||
lintLevels.includes(lint?.level as 'ERROR' | 'WARN')
|
||||
)
|
||||
|
||||
return {
|
||||
hasLint: matchingLints.length > 0,
|
||||
count: matchingLints.length,
|
||||
}
|
||||
}
|
||||
@@ -260,14 +260,10 @@ const TableGridEditor = ({
|
||||
schema={selectedTable.schema}
|
||||
table={gridTable}
|
||||
headerActions={
|
||||
isTableSelected || isViewSelected || canEditViaTableEditor ? (
|
||||
<GridHeaderActions
|
||||
table={selectedTable as PostgresTable}
|
||||
canEditViaTableEditor={canEditViaTableEditor}
|
||||
isViewSelected={isViewSelected}
|
||||
isTableSelected={isTableSelected}
|
||||
/>
|
||||
) : null
|
||||
<GridHeaderActions
|
||||
table={selectedTable as TableLike}
|
||||
canEditViaTableEditor={canEditViaTableEditor}
|
||||
/>
|
||||
}
|
||||
onAddColumn={snap.onAddColumn}
|
||||
onEditColumn={onSelectEditColumn}
|
||||
|
||||
@@ -27,6 +27,9 @@ import {
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { useProjectContext } from '../ProjectLayout/ProjectContext'
|
||||
import { useProjectLintsQuery } from 'data/lint/lint-query'
|
||||
import { Lint } from '../../../data/lint/lint-query'
|
||||
import { checkEntityForLints } from 'components/interfaces/TableGridEditor/TableEntity.utils'
|
||||
|
||||
export interface EntityListItemProps {
|
||||
id: number
|
||||
@@ -45,6 +48,33 @@ const EntityListItem: ItemRenderer<Entity, EntityListItemProps> = ({
|
||||
|
||||
const isActive = Number(id) === entity.id
|
||||
|
||||
const { data: lints = [] } = useProjectLintsQuery({
|
||||
projectRef: project?.ref,
|
||||
})
|
||||
|
||||
const tableHasLints = checkEntityForLints(
|
||||
entity.name,
|
||||
'rls_disabled_in_public',
|
||||
['ERROR'],
|
||||
lints,
|
||||
snap.selectedSchemaName
|
||||
)
|
||||
|
||||
const viewHasLints = checkEntityForLints(
|
||||
entity.name,
|
||||
'security_definer_view',
|
||||
['ERROR', 'WARN'],
|
||||
lints,
|
||||
snap.selectedSchemaName
|
||||
)
|
||||
const materializedViewHasLints = checkEntityForLints(
|
||||
entity.name,
|
||||
'materialized_view_in_api',
|
||||
['ERROR', 'WARN'],
|
||||
lints,
|
||||
snap.selectedSchemaName
|
||||
)
|
||||
|
||||
const formatTooltipText = (entityType: string) => {
|
||||
return Object.entries(ENTITY_TYPE)
|
||||
.find(([, value]) => value === entityType)?.[0]
|
||||
@@ -105,6 +135,64 @@ const EntityListItem: ItemRenderer<Entity, EntityListItemProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const EntityTooltipTrigger = ({ entity }: { entity: Entity }) => {
|
||||
let tooltipContent = null
|
||||
|
||||
switch (entity.type) {
|
||||
case ENTITY_TYPE.TABLE:
|
||||
if (tableHasLints.hasLint) {
|
||||
tooltipContent = 'RLS Disabled'
|
||||
}
|
||||
break
|
||||
case ENTITY_TYPE.VIEW:
|
||||
if (viewHasLints.hasLint) {
|
||||
tooltipContent = 'Security Definer view'
|
||||
}
|
||||
break
|
||||
case ENTITY_TYPE.MATERIALIZED_VIEW:
|
||||
if (materializedViewHasLints.hasLint) {
|
||||
tooltipContent = 'Security Definer view'
|
||||
}
|
||||
|
||||
break
|
||||
case ENTITY_TYPE.FOREIGN_TABLE:
|
||||
tooltipContent = 'RLS is not enforced on foreign tables'
|
||||
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if (tooltipContent) {
|
||||
return (
|
||||
<Tooltip.Root delayDuration={0} disableHoverableContent={true}>
|
||||
<Tooltip.Trigger className="min-w-4" asChild>
|
||||
<Unlock
|
||||
size={14}
|
||||
strokeWidth={2}
|
||||
className={cn('min-w-4', isActive ? 'text-warning-600' : 'text-warning-500')}
|
||||
/>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
side="bottom"
|
||||
className={[
|
||||
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
||||
'border border-background',
|
||||
'text-xs text-foreground',
|
||||
].join(' ')}
|
||||
>
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
{tooltipContent}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
title={entity.name}
|
||||
@@ -175,13 +263,7 @@ const EntityListItem: ItemRenderer<Entity, EntityListItemProps> = ({
|
||||
>
|
||||
{entity.name}
|
||||
</span>
|
||||
{entity.type === ENTITY_TYPE.TABLE && !entity.rls_enabled && (
|
||||
<Unlock
|
||||
size={14}
|
||||
strokeWidth={2}
|
||||
className={cn('min-w-4', isActive ? 'text-warning-600' : 'text-warning-500')}
|
||||
/>
|
||||
)}
|
||||
<EntityTooltipTrigger entity={entity} />
|
||||
</div>
|
||||
|
||||
{entity.type === ENTITY_TYPE.TABLE && isActive && !isLocked && (
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { components } from 'data/api'
|
||||
import { handleError, patch } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { tableKeys } from './keys'
|
||||
import { lintKeys } from 'data/lint/keys'
|
||||
|
||||
export type UpdateTableBody = components['schemas']['UpdateTableBody']
|
||||
|
||||
@@ -59,6 +60,7 @@ export const useTableUpdateMutation = ({
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries(tableKeys.list(projectRef, schema)),
|
||||
queryClient.invalidateQueries(tableKeys.table(projectRef, id)),
|
||||
queryClient.invalidateQueries(lintKeys.lint(projectRef)),
|
||||
])
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user