Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 46 additions & 28 deletions apps/studio/components/interfaces/Auth/Policies/Policies.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { PostgresPolicy } from '@supabase/postgres-meta'
import { isEmpty } from 'lodash'
import Link from 'next/link'
import { useState } from 'react'
import { useCallback, useState } from 'react'
import { toast } from 'sonner'

import { useParams } from 'common'
Expand All @@ -23,6 +23,7 @@ interface PoliciesProps {
tables: PolicyTableRowProps['table'][]
hasTables: boolean
isLocked: boolean
visibleTableIds: Set<number>
onSelectCreatePolicy: (table: string) => void
onSelectEditPolicy: (policy: PostgresPolicy) => void
onResetSearch?: () => void
Expand All @@ -34,6 +35,7 @@ export const Policies = ({
tables,
hasTables,
isLocked,
visibleTableIds,
onSelectCreatePolicy,
onSelectEditPolicy: onSelectEditPolicyAI,
onResetSearch,
Expand Down Expand Up @@ -66,27 +68,28 @@ export const Policies = ({
},
})

const closeConfirmModal = () => {
const closeConfirmModal = useCallback(() => {
setSelectedPolicyToDelete({})
setSelectedTableToToggleRLS(undefined)
}
}, [])

const onSelectToggleRLS = (table: {
id: number
schema: string
name: string
rls_enabled: boolean
}) => {
setSelectedTableToToggleRLS(table)
}
const onSelectToggleRLS = useCallback(
(table: { id: number; schema: string; name: string; rls_enabled: boolean }) => {
setSelectedTableToToggleRLS(table)
},
[]
)

const onSelectEditPolicy = (policy: any) => {
onSelectEditPolicyAI(policy)
}
const onSelectEditPolicy = useCallback(
(policy: PostgresPolicy) => {
onSelectEditPolicyAI(policy)
},
[onSelectEditPolicyAI]
)

const onSelectDeletePolicy = (policy: any) => {
const onSelectDeletePolicy = useCallback((policy: PostgresPolicy) => {
setSelectedPolicyToDelete(policy)
}
}, [])

// Methods that involve some API
const onToggleRLS = async () => {
Expand Down Expand Up @@ -116,6 +119,13 @@ export const Policies = ({
})
}

const handleCreatePolicy = useCallback(
(tableData: PolicyTableRowProps['table']) => {
onSelectCreatePolicy(tableData.name)
},
[onSelectCreatePolicy]
)

if (!hasTables) {
return (
<Card className="w-full bg-transparent">
Expand All @@ -139,18 +149,26 @@ export const Policies = ({
<div className="flex flex-col gap-y-4 pb-4">
{isLocked && <ProtectedSchemaWarning schema={schema} entity="policies" />}
{tables.length > 0 ? (
tables.map((table) => (
<section key={table.id}>
<PolicyTableRow
table={table}
isLocked={schema === 'realtime' ? true : isLocked}
onSelectToggleRLS={onSelectToggleRLS}
onSelectCreatePolicy={() => onSelectCreatePolicy(table.name)}
onSelectEditPolicy={onSelectEditPolicy}
onSelectDeletePolicy={onSelectDeletePolicy}
/>
</section>
))
<>
{tables.map((table) => {
const isVisible = visibleTableIds.has(table.id)
return (
<section key={table.id} hidden={!isVisible} aria-hidden={!isVisible}>
<PolicyTableRow
table={table}
isLocked={schema === 'realtime' ? true : isLocked}
onSelectToggleRLS={onSelectToggleRLS}
onSelectCreatePolicy={handleCreatePolicy}
onSelectEditPolicy={onSelectEditPolicy}
onSelectDeletePolicy={onSelectDeletePolicy}
/>
</section>
)
})}
{!!search && visibleTableIds.size === 0 && (
<NoSearchResults searchString={search ?? ''} onResetFilter={onResetSearch} />
)}
</>
) : hasTables ? (
<NoSearchResults searchString={search ?? ''} onResetFilter={onResetSearch} />
) : null}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { PostgresPolicy } from '@supabase/postgres-meta'
import type { PropsWithChildren } from 'react'
import { createContext, useCallback, useContext, useMemo } from 'react'

import type { ResponseError } from 'types'

type TableKey = `${string}.${string}`

type PoliciesDataContextValue = {
getPoliciesForTable: (schema: string, table: string) => PostgresPolicy[]
isPoliciesLoading: boolean
isPoliciesError: boolean
policiesError?: ResponseError | Error
exposedSchemas: Set<string>
}

const PoliciesDataContext = createContext<PoliciesDataContextValue | null>(null)

export const usePoliciesData = () => {
const context = useContext(PoliciesDataContext)
if (!context) throw new Error('usePoliciesData must be used within PoliciesDataProvider')
return context
}

type PoliciesDataProviderProps = {
policies: PostgresPolicy[]
isPoliciesLoading: boolean
isPoliciesError: boolean
policiesError?: ResponseError | Error
exposedSchemas: string[]
}

export const PoliciesDataProvider = ({
children,
policies,
isPoliciesLoading,
isPoliciesError,
policiesError,
exposedSchemas,
}: PropsWithChildren<PoliciesDataProviderProps>) => {
const policiesByTable = useMemo(() => {
const map = new Map<TableKey, PostgresPolicy[]>()

for (const policy of policies) {
const key = `${policy.schema}.${policy.table}` satisfies TableKey
const existing = map.get(key)
if (existing) {
existing.push(policy)
} else {
map.set(key, [policy])
}
}

for (const list of map.values()) {
list.sort((a, b) => a.name.localeCompare(b.name))
}

return map
}, [policies])

const getPoliciesForTable = useCallback(
(schema: string, table: string) => policiesByTable.get(`${schema}.${table}`) ?? [],
[policiesByTable]
)

const exposedSchemasSet = useMemo(() => new Set(exposedSchemas), [exposedSchemas])

const contextValue = useMemo(
() => ({
getPoliciesForTable,
isPoliciesLoading,
isPoliciesError,
policiesError,
exposedSchemas: exposedSchemasSet,
}),
[getPoliciesForTable, isPoliciesLoading, isPoliciesError, policiesError, exposedSchemasSet]
)

return (
<PoliciesDataContext.Provider value={contextValue}>{children}</PoliciesDataContext.Provider>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type PolicyTable = {
id: number
schema: string
name: string
rls_enabled: boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,13 @@ import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { AiIconAnimation, Badge, CardTitle } from 'ui'
import type { PolicyTable } from './PolicyTableRow.types'

interface PolicyTableRowHeaderProps {
table: {
id: number
schema: string
name: string
rls_enabled: boolean
}
table: PolicyTable
isLocked: boolean
onSelectToggleRLS: (table: {
id: number
schema: string
name: string
rls_enabled: boolean
}) => void
onSelectCreatePolicy: () => void
onSelectToggleRLS: (table: PolicyTable) => void
onSelectCreatePolicy: (table: PolicyTable) => void
}

export const PolicyTableRowHeader = ({
Expand Down Expand Up @@ -91,7 +82,7 @@ export const PolicyTableRowHeader = ({
<ButtonTooltip
type="default"
disabled={!canToggleRLS || !canCreatePolicies}
onClick={() => onSelectCreatePolicy()}
onClick={() => onSelectCreatePolicy(table)}
tooltip={{
content: {
side: 'bottom',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { PostgresPolicy } from '@supabase/postgres-meta'
import { noop } from 'lodash'
import { memo, useMemo } from 'react'

import { useParams } from 'common'
import AlertError from 'components/ui/AlertError'
import { InlineLink } from 'components/ui/InlineLink'
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query'
import { useTablesRolesAccessQuery } from 'data/tables/tables-roles-access-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import {
Expand All @@ -23,38 +22,37 @@ import {
} from 'ui'
import { Admonition } from 'ui-patterns'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import { usePoliciesData } from '../PoliciesDataContext'
import { PolicyRow } from './PolicyRow'
import type { PolicyTable } from './PolicyTableRow.types'
import { PolicyTableRowHeader } from './PolicyTableRowHeader'

export interface PolicyTableRowProps {
table: {
id: number
schema: string
name: string
rls_enabled: boolean
}
table: PolicyTable
isLocked: boolean
onSelectToggleRLS: (table: {
id: number
schema: string
name: string
rls_enabled: boolean
}) => void
onSelectCreatePolicy: () => void
onSelectToggleRLS: (table: PolicyTable) => void
onSelectCreatePolicy: (table: PolicyTable) => void
onSelectEditPolicy: (policy: PostgresPolicy) => void
onSelectDeletePolicy: (policy: PostgresPolicy) => void
}

export const PolicyTableRow = ({
const PolicyTableRowComponent = ({
table,
isLocked,
onSelectToggleRLS = noop,
onSelectCreatePolicy,
onSelectCreatePolicy = noop,
onSelectEditPolicy = noop,
onSelectDeletePolicy = noop,
}: PolicyTableRowProps) => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const { getPoliciesForTable, isPoliciesLoading, isPoliciesError, policiesError, exposedSchemas } =
usePoliciesData()

const policies = useMemo(
() => getPoliciesForTable(table.schema, table.name),
[getPoliciesForTable, table.schema, table.name]
)

// [Joshen] Changes here are so that warnings are more accurate and granular instead of purely relying if RLS is disabled or enabled
// The following scenarios are technically okay if the table has RLS disabled, in which it won't be publicly readable / writable
Expand All @@ -64,10 +62,11 @@ export const PolicyTableRow = ({
// - They only consider the public schema
// - They do not consider roles
// Eventually if the security lints are able to cover those, we can look to using them as the source of truth instead then
const { data: config } = useProjectPostgrestConfigQuery({ projectRef: project?.ref })
const exposedSchemas = config?.db_schema ? config?.db_schema.replace(/ /g, '').split(',') : []
const isRLSEnabled = table.rls_enabled
const isTableExposedThroughAPI = exposedSchemas.includes(table.schema)
const isTableExposedThroughAPI = useMemo(
() => exposedSchemas.has(table.schema),
[exposedSchemas, table.schema]
)

const { data: tablesWithAnonAuthAccess = new Set() } = useTablesRolesAccessQuery({
projectRef: project?.ref,
Expand All @@ -78,19 +77,13 @@ export const PolicyTableRow = ({
const hasAnonAuthenticatedRolesAccess = tablesWithAnonAuthAccess.has(table.name)
const isPubliclyReadableWritable =
!isRLSEnabled && isTableExposedThroughAPI && hasAnonAuthenticatedRolesAccess

const { data, error, isLoading, isError, isSuccess } = useDatabasePoliciesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const policies = (data ?? [])
.filter((policy) => policy.schema === table.schema && policy.table === table.name)
.sort((a, b) => a.name.localeCompare(b.name))
const rlsEnabledNoPolicies = isRLSEnabled && policies.length === 0
const isRealtimeSchema = table.schema === 'realtime'
const isRealtimeMessagesTable = isRealtimeSchema && table.name === 'messages'
const isTableLocked = isRealtimeSchema ? !isRealtimeMessagesTable : isLocked

const showPolicies = !isPoliciesLoading && !isPoliciesError

return (
<Card className={cn(isPubliclyReadableWritable && 'border-warning-500')}>
<CardHeader
Expand Down Expand Up @@ -133,23 +126,23 @@ export const PolicyTableRow = ({
</Alert_Shadcn_>
)}

{isLoading && (
{isPoliciesLoading && (
<CardContent>
<ShimmeringLoader />
</CardContent>
)}

{isError && (
{isPoliciesError && (
<CardContent>
<AlertError
className="border-0 rounded-none"
error={error}
error={policiesError}
subject="Failed to retrieve policies"
/>
</CardContent>
)}

{isSuccess && (
{showPolicies && (
<CardContent className="p-0">
{policies.length === 0 ? (
<p className="text-foreground-lighter text-sm p-4">No policies created yet</p>
Expand Down Expand Up @@ -183,3 +176,6 @@ export const PolicyTableRow = ({
</Card>
)
}

export const PolicyTableRow = memo(PolicyTableRowComponent)
PolicyTableRow.displayName = 'PolicyTableRow'
Loading
Loading