mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
Improve auth policies RLS warnings granularity (#35579)
This commit is contained in:
1 parent
9139418ef0
commit
227bb54853
3 files changed
+129
-13
No files matched your search
@@ -5,7 +5,9 @@ import { Info } from 'lucide-react'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
|
||||
import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query'
|
||||
import { useTableRolesAccessQuery } from 'data/tables/table-roles-access-query'
|
||||
import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
|
||||
import PolicyRow from './PolicyRow'
|
||||
@@ -39,6 +41,30 @@ export const PolicyTableRow = ({
|
||||
onSelectDeletePolicy = noop,
|
||||
}: PolicyTableRowProps) => {
|
||||
const { project } = useProjectContext()
|
||||
|
||||
// [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
|
||||
// - If the schema is not exposed through the API via Postgrest
|
||||
// - If the anon and authenticated roles do not have access to the table
|
||||
// Ideally we should just rely on the security lints as the source of truth, but the security lints currently have limitations
|
||||
// - 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 { data: roles = [] } = useTableRolesAccessQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
schema: table.schema,
|
||||
table: table.name,
|
||||
})
|
||||
const hasAnonAuthenticatedRolesAccess = roles.length !== 0
|
||||
const isPubliclyReadableWritable =
|
||||
!isRLSEnabled && isTableExposedThroughAPI && hasAnonAuthenticatedRolesAccess
|
||||
|
||||
const { data, error, isLoading, isError, isSuccess } = useDatabasePoliciesQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
@@ -46,6 +72,7 @@ export const PolicyTableRow = ({
|
||||
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
|
||||
|
||||
return (
|
||||
<Panel
|
||||
@@ -59,27 +86,40 @@ export const PolicyTableRow = ({
|
||||
/>
|
||||
}
|
||||
>
|
||||
{!table.rls_enabled && !isLocked && (
|
||||
{(isPubliclyReadableWritable || rlsEnabledNoPolicies) && (
|
||||
<div
|
||||
className={cn(
|
||||
'dark:bg-alternative-200 bg-surface-200 px-6 py-2 text-xs flex items-center gap-2',
|
||||
policies.length === 0 ? '' : 'border-b'
|
||||
)}
|
||||
>
|
||||
<div className="w-1.5 h-1.5 bg-warning-600 rounded-full" />
|
||||
<span className="font-bold text-warning-600">Warning:</span>{' '}
|
||||
<div
|
||||
className={cn(
|
||||
'w-1.5 h-1.5 rounded-full bg-warning-600 ',
|
||||
rlsEnabledNoPolicies && 'bg-selection'
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn('font-bold text-warning-600', rlsEnabledNoPolicies && 'text-foreground')}
|
||||
>
|
||||
{isPubliclyReadableWritable ? 'Warning' : 'Note'}:
|
||||
</span>{' '}
|
||||
<span className="text-foreground-light">
|
||||
Row Level Security is disabled. Your table is publicly readable and writable.
|
||||
{isPubliclyReadableWritable
|
||||
? 'Row Level Security is disabled. Your table is publicly readable and writable.'
|
||||
: 'Row Level Security is enabled, but no policies exist. No data will be selectable via Supabase APIs.'}
|
||||
</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="w-3 h-3" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="w-[400px]">
|
||||
Anyone with the project's anonymous key can modify or delete your data. Enable RLS and
|
||||
create access policies to keep your data secure.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{isPubliclyReadableWritable && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="w-3 h-3" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="w-[400px]">
|
||||
Anyone with the project's anonymous key can modify or delete your data. Enable RLS
|
||||
and create access policies to keep your data secure.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
export const tableKeys = {
|
||||
list: (projectRef: string | undefined, schema?: string, includeColumns?: boolean) =>
|
||||
['projects', projectRef, 'tables', schema, includeColumns].filter(Boolean),
|
||||
rolesAccess: (projectRef: string | undefined, schema: string, table: string) => [
|
||||
'projects',
|
||||
projectRef,
|
||||
'roles-access',
|
||||
{ schema, table },
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
|
||||
import { tableKeys } from './keys'
|
||||
|
||||
type TableRolesAccessArgs = {
|
||||
schema: string
|
||||
table: string
|
||||
}
|
||||
|
||||
/**
|
||||
* [Joshen] Specifically just checking for anon and authenticated roles since this is
|
||||
* just to verify if the table is exposed via the Supabase API
|
||||
*/
|
||||
export const getTableRolesAccessSql = ({ schema, table }: TableRolesAccessArgs) => {
|
||||
const sql = /* SQL */ `
|
||||
SELECT grantee, privilege_type
|
||||
FROM information_schema.role_table_grants
|
||||
WHERE table_schema = '${schema}'
|
||||
AND table_name = '${table}'
|
||||
AND grantee IN ('anon', 'authenticated');
|
||||
`.trim()
|
||||
|
||||
return sql
|
||||
}
|
||||
|
||||
export type TableRolesAccessVariables = TableRolesAccessArgs & {
|
||||
projectRef?: string
|
||||
connectionString?: string | null
|
||||
}
|
||||
|
||||
export async function getTableRolesAccess(
|
||||
{ schema, table, projectRef, connectionString }: TableRolesAccessVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!schema) {
|
||||
throw new Error('schema is required')
|
||||
}
|
||||
|
||||
const sql = getTableRolesAccessSql({ schema, table })
|
||||
|
||||
const { result } = (await executeSql(
|
||||
{ projectRef, connectionString, sql, queryKey: ['TableRolesAccess', schema] },
|
||||
signal
|
||||
)) as { result: { grantee: string; privilege_type: string }[] }
|
||||
|
||||
const res = []
|
||||
if (result.some((x) => x.grantee === 'anon')) res.push('anon')
|
||||
if (result.some((x) => x.grantee === 'authenticated')) res.push('authenticated')
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
export type TableRolesAccessData = Awaited<ReturnType<typeof getTableRolesAccess>>
|
||||
export type TableRolesAccessError = ExecuteSqlError
|
||||
|
||||
export const useTableRolesAccessQuery = <TData = TableRolesAccessData>(
|
||||
{ projectRef, connectionString, schema, table }: TableRolesAccessVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<TableRolesAccessData, TableRolesAccessError, TData> = {}
|
||||
) =>
|
||||
useQuery<TableRolesAccessData, TableRolesAccessError, TData>(
|
||||
tableKeys.rolesAccess(projectRef, schema, table),
|
||||
({ signal }) => getTableRolesAccess({ projectRef, connectionString, schema, table }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined' && typeof schema !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
Reference in new issue
Block a user