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:
authored and GitHub committed 2024-05-23 16:52:03 -02:30
1 parent 2dc39a75a0
commit aa90e578ca
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)
},