mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 19:35:06 +03:00
## Context The dashboard has an RQ hook that fetches all table privileges in the database `useTablePrivilegesQuery` [here](https://github.com/supabase/supabase/blob/master/apps/studio/data/privileges/table-privileges-query.ts#L21) which can potentially be a resource heavy query on the database, especially if the database has a large number of relations. A recent UI that was added `ProjectNeedsSecuring` uses that query, and has become a common entry point for all projects as it's rendered when the user lands on the project's home page, and the project has tables with RLS issues, in which case if the project has a large number of tables, the database will face run into resource issues, resulting in statement timeouts. ## Changes involved Opting to pass in `includedSchemas` parameter wherever we're calling `useTablePrivilegesQuery`, which includes: - `ProjectNeedsSecuring` - `QueueSettings` - `column-privileges` In which we'll hence only fetch the table privileges for the provided schemas only (rather than the whole DB) Also did a similar fix for `useColumnPrivilegesQuery` as well as it likely runs into the same problem ## To test - [ ] Verify that those 3 UIs are still working as expected (should not have any visual changes) - [ ] Verify in the network tab that table / column privileges are now filtered to the schema provided, rather than fetching for all schemas in the DB <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved permission-save failure messaging by generating clearer toast errors from unexpected failures. * Prevented privilege-related UI from loading until required configuration is successfully retrieved. * **Performance** * Faster, more targeted privilege loading by scoping both table and column privilege queries to the selected/relevant schema(s), reducing unnecessary client-side filtering. * Switched privilege retrieval to schema-aware database metadata queries for more efficient results. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Ali Waseem <waseema393@gmail.com>
196 lines
6.2 KiB
TypeScript
196 lines
6.2 KiB
TypeScript
import { LOCAL_STORAGE_KEYS, useFlag, useParams } from 'common'
|
|
import { AnimatePresence, motion } from 'framer-motion'
|
|
import { useRouter } from 'next/router'
|
|
import { PropsWithChildren, useMemo } from 'react'
|
|
|
|
import type {
|
|
ProjectSecurityActionDetails,
|
|
ProjectSecurityActionType,
|
|
} from './ProjectNeedsSecuring.types'
|
|
import { getExposedSchemas, getTableKey, sortTables } from './ProjectNeedsSecuring.utils'
|
|
import { ProjectNeedsSecuringView } from './ProjectNeedsSecuringView'
|
|
import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query'
|
|
import { useProjectLintsQuery } from '@/data/lint/lint-query'
|
|
import { useTablePrivilegesQuery } from '@/data/privileges/table-privileges-query'
|
|
import { useTablesQuery } from '@/data/tables/tables-query'
|
|
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { isApiAccessRole, isApiPrivilegeType } from '@/lib/data-api-types'
|
|
import { useTrack } from '@/lib/telemetry/track'
|
|
|
|
const PROJECT_SECURITY_FEATURE_FLAG = 'projectNeedsSecuring'
|
|
const PROJECT_HOME_PATHNAME = '/project/[ref]'
|
|
|
|
const ProjectNeedsSecuringGate = ({ children }: PropsWithChildren) => {
|
|
const router = useRouter()
|
|
const track = useTrack()
|
|
const { ref: projectRef } = useParams()
|
|
const { data: project } = useSelectedProjectQuery()
|
|
|
|
const [securityDismissedAt, setSecurityDismissedAt, { isLoading: isLoadingDismissedAt }] =
|
|
useLocalStorageQuery<string | null>(
|
|
projectRef
|
|
? LOCAL_STORAGE_KEYS.PROJECT_SECURITY_DISMISSED_AT(projectRef)
|
|
: 'project-security-dismissed-at-unknown',
|
|
null
|
|
)
|
|
|
|
const isProjectHomeRoute = router.pathname === PROJECT_HOME_PATHNAME
|
|
|
|
const { data: lints = [], isPending: isLoadingLints } = useProjectLintsQuery(
|
|
{ projectRef },
|
|
{ enabled: isProjectHomeRoute && !!projectRef }
|
|
)
|
|
|
|
const rlsIssueKeys = useMemo(() => {
|
|
return new Set(
|
|
lints
|
|
.filter((lint) => lint.name === 'rls_disabled_in_public' && lint.level === 'ERROR')
|
|
.map((lint) => {
|
|
const schema = typeof lint.metadata?.schema === 'string' ? lint.metadata.schema : null
|
|
const name = typeof lint.metadata?.name === 'string' ? lint.metadata.name : null
|
|
|
|
return schema && name ? getTableKey({ schema, name }) : null
|
|
})
|
|
.filter((value): value is string => value !== null)
|
|
)
|
|
}, [lints])
|
|
|
|
const hasRlsIssues = rlsIssueKeys.size > 0
|
|
const shouldRenderGate =
|
|
isProjectHomeRoute &&
|
|
!!projectRef &&
|
|
!isLoadingDismissedAt &&
|
|
hasRlsIssues &&
|
|
securityDismissedAt === null
|
|
|
|
const {
|
|
data: tables,
|
|
error: tablesError,
|
|
isPending: isLoadingTables,
|
|
} = useTablesQuery(
|
|
{
|
|
projectRef,
|
|
connectionString: project?.connectionString,
|
|
includeColumns: false,
|
|
},
|
|
{ enabled: shouldRenderGate }
|
|
)
|
|
|
|
const handleTrackAction = (
|
|
type: ProjectSecurityActionType,
|
|
details?: ProjectSecurityActionDetails
|
|
) => {
|
|
track('project_security_cta_clicked', {
|
|
type,
|
|
...details,
|
|
})
|
|
}
|
|
|
|
const {
|
|
data: dbSchema,
|
|
error: postgrestConfigError,
|
|
isPending: isLoadingPostgrestConfig,
|
|
isSuccess: isSuccessPostgrestConfig,
|
|
} = useProjectPostgrestConfigQuery(
|
|
{ projectRef },
|
|
{
|
|
enabled: shouldRenderGate,
|
|
select: ({ db_schema }) => db_schema,
|
|
}
|
|
)
|
|
|
|
const exposedSchemas = getExposedSchemas(dbSchema)
|
|
|
|
const {
|
|
data: tablePrivileges,
|
|
error: tablePrivilegesError,
|
|
isPending: isLoadingTablePrivileges,
|
|
} = useTablePrivilegesQuery(
|
|
{ projectRef, connectionString: project?.connectionString, includedSchemas: exposedSchemas },
|
|
{ enabled: shouldRenderGate && isSuccessPostgrestConfig }
|
|
)
|
|
|
|
const tableRows = useMemo(() => {
|
|
if (!tables) return []
|
|
|
|
const dataApiAccessByTable = new Map<string, boolean>()
|
|
|
|
for (const entry of tablePrivileges ?? []) {
|
|
const key = getTableKey(entry)
|
|
const hasDataApiAccess = entry.privileges.some(
|
|
(privilege) =>
|
|
isApiAccessRole(privilege.grantee) && isApiPrivilegeType(privilege.privilege_type)
|
|
)
|
|
|
|
if (hasDataApiAccess) {
|
|
dataApiAccessByTable.set(key, true)
|
|
}
|
|
}
|
|
|
|
return sortTables(
|
|
tables
|
|
.filter((table) => !table.rls_enabled && rlsIssueKeys.has(getTableKey(table)))
|
|
.map((table) => {
|
|
const key = getTableKey(table)
|
|
|
|
return {
|
|
id: table.id,
|
|
name: table.name,
|
|
schema: table.schema,
|
|
rlsEnabled: table.rls_enabled,
|
|
dataApiAccessible: dataApiAccessByTable.get(key) === true,
|
|
hasRlsIssue: rlsIssueKeys.has(key),
|
|
}
|
|
})
|
|
)
|
|
}, [rlsIssueKeys, tablePrivileges, tables])
|
|
|
|
if (!isProjectHomeRoute || !projectRef || isLoadingLints || !hasRlsIssues) {
|
|
return <>{children}</>
|
|
}
|
|
|
|
return (
|
|
<AnimatePresence mode="wait">
|
|
{shouldRenderGate ? (
|
|
<motion.div
|
|
key="project-needs-securing"
|
|
className="flex flex-1 min-h-0 w-full"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.2, ease: 'easeOut' }}
|
|
>
|
|
<ProjectNeedsSecuringView
|
|
projectRef={projectRef}
|
|
issueCount={rlsIssueKeys.size}
|
|
tables={tableRows}
|
|
isLoading={isLoadingTables || isLoadingPostgrestConfig || isLoadingTablePrivileges}
|
|
error={tablesError ?? postgrestConfigError ?? tablePrivilegesError}
|
|
onDismiss={() => setSecurityDismissedAt(new Date().toISOString())}
|
|
onTrackAction={handleTrackAction}
|
|
/>
|
|
</motion.div>
|
|
) : (
|
|
<motion.div
|
|
key="project-needs-securing-children"
|
|
className="flex flex-1 min-h-0 w-full"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.2, ease: 'easeOut' }}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
)
|
|
}
|
|
|
|
export const ProjectNeedsSecuring = ({ children }: PropsWithChildren) => {
|
|
const isEnabled = useFlag(PROJECT_SECURITY_FEATURE_FLAG)
|
|
|
|
if (!isEnabled) return <>{children}</>
|
|
return <ProjectNeedsSecuringGate>{children}</ProjectNeedsSecuringGate>
|
|
}
|