Skip to content

Commit

Permalink
Chore/show unlock icon next to view entities (#23238)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people committed May 23, 2024
1 parent 2dc39a7 commit aa90e57
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 34 deletions.
10 changes: 10 additions & 0 deletions apps/studio/components/interfaces/Linter/Linter.utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ({
Expand Down
173 changes: 154 additions & 19 deletions apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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)

Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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}>
Expand Down Expand Up @@ -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={
Expand Down Expand Up @@ -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}
/>
)}
</>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Loading

0 comments on commit aa90e57

Please sign in to comment.