diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx index 341c60d06d6..a65470a3d96 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRowHeader.tsx @@ -15,6 +15,7 @@ import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' interface PolicyTableRowHeaderProps { table: PolicyTable isLocked: boolean + hasApiAccess: boolean onSelectToggleRLS: (table: PolicyTable) => void onSelectCreatePolicy: (table: PolicyTable) => void } @@ -22,6 +23,7 @@ interface PolicyTableRowHeaderProps { export const PolicyTableRowHeader = ({ table, isLocked, + hasApiAccess, onSelectToggleRLS = noop, onSelectCreatePolicy, }: PolicyTableRowHeaderProps) => { @@ -57,6 +59,11 @@ export const PolicyTableRowHeader = ({ RLS Disabled )} + {!hasApiAccess && ( + + API Disabled + + )} {isTableLocked && ( diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx index 02c34be3d63..19c0031b096 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx @@ -73,27 +73,39 @@ const PolicyTableRowComponent = ({ }) const hasAnonAuthenticatedRolesAccess = tablesWithAnonAuthAccess.has(table.name) - const isPubliclyReadableWritable = - !isRLSEnabled && isTableExposedThroughAPI && hasAnonAuthenticatedRolesAccess - const rlsEnabledNoPolicies = isRLSEnabled && policies.length === 0 + const hasApiAccess = isTableExposedThroughAPI && hasAnonAuthenticatedRolesAccess + const isPubliclyReadableWritable = !isRLSEnabled && hasApiAccess + const rlsEnabledNoPolicies = isRLSEnabled && hasApiAccess && policies.length === 0 + const isApiDisabledDueToRoles = isTableExposedThroughAPI && !hasAnonAuthenticatedRolesAccess const isRealtimeSchema = table.schema === 'realtime' const isRealtimeMessagesTable = isRealtimeSchema && table.name === 'messages' const isTableLocked = isRealtimeSchema ? !isRealtimeMessagesTable : isLocked const showPolicies = !isPoliciesLoading && !isPoliciesError + const shouldHideHeaderBorder = + isPubliclyReadableWritable || + rlsEnabledNoPolicies || + !isTableExposedThroughAPI || + isApiDisabledDueToRoles + + const admonitionMessage = useMemo(() => { + if (isPubliclyReadableWritable) { + return 'This table can be accessed by anyone via the Data API as RLS is disabled.' + } + if (isApiDisabledDueToRoles) { + return 'This table cannot be accessed via the Data API as no permissions exist for the anon or authenticated roles.' + } + return 'This table can be accessed via the Data API but no RLS policies exist so no data will be returned.' + }, [isPubliclyReadableWritable, isApiDisabledDueToRoles]) + return ( - + @@ -113,19 +125,16 @@ const PolicyTableRowComponent = ({ )} - {(isPubliclyReadableWritable || rlsEnabledNoPolicies) && isTableExposedThroughAPI && ( - -

- {isPubliclyReadableWritable - ? 'Anyone with your project’s anonymous key can read, modify, or delete your data.' - : 'No data will be selectable via Supabase APIs because RLS is enabled but no policies have been created yet.'} -

-
- )} + {(isPubliclyReadableWritable || rlsEnabledNoPolicies || isApiDisabledDueToRoles) && + isTableExposedThroughAPI && ( + +

{admonitionMessage}

+
+ )} {isPoliciesLoading && ( diff --git a/apps/studio/components/interfaces/TableGridEditor/TableEntity.utils.ts b/apps/studio/components/interfaces/TableGridEditor/TableEntity.utils.ts index c3fa6d403ca..d561cddae27 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableEntity.utils.ts +++ b/apps/studio/components/interfaces/TableGridEditor/TableEntity.utils.ts @@ -4,7 +4,7 @@ import { Lint } from '../../../data/lint/lint-query' export const getEntityLintDetails = ( entityName: string, lintName: string, - lintLevels: ('ERROR' | 'WARN')[], + lintLevels: ('ERROR' | 'WARN' | 'INFO')[], lints: Lint[], schema: string ): { hasLint: boolean; count: number; matchingLint: Lint | null } => { @@ -14,7 +14,7 @@ export const getEntityLintDetails = ( lint?.metadata?.name === entityName && lint?.metadata?.schema === schema && lint?.name === lintName && - lintLevels.includes(lint?.level as 'ERROR' | 'WARN') + lintLevels.includes(lint?.level) ) || null return { diff --git a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx index e2b986201e7..5b9dcb15732 100644 --- a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx @@ -104,7 +104,7 @@ export const EntityListItem = ({ projectRef: project?.ref, }) - const tableHasLints: boolean = getEntityLintDetails( + const tableHasRlsDisabledLint: boolean = getEntityLintDetails( entity.name, 'rls_disabled_in_public', ['ERROR'], @@ -112,6 +112,14 @@ export const EntityListItem = ({ selectedSchema ).hasLint + const tableHasRlsEnabledNoPolicyLint: boolean = getEntityLintDetails( + entity.name, + 'rls_enabled_no_policy', + ['ERROR', 'WARN', 'INFO'], + lints, + selectedSchema + ).hasLint + const viewHasLints: boolean = getEntityLintDetails( entity.name, 'security_definer_view', @@ -212,7 +220,8 @@ export const EntityListItem = ({ - {accessWarning} as RLS is disabled. {learnMoreCTA}. + This table can be accessed by anyone via the Data API as RLS is disabled. {learnMoreCTA} + . ) } @@ -460,13 +472,34 @@ const EntityTooltipTrigger = ({ ) } + // Show warning for tables with RLS enabled but no policies + if ( + isDataApiExposedBadgeEnabled && + entity.type === ENTITY_TYPE.TABLE && + apiAccessData?.hasApiAccess && + tableHasRlsEnabledNoPolicyLint + ) { + return ( + + + + + + This table can be accessed via the Data API but no RLS policies exist so no data will be + returned + + + ) + } + + // Show globe icon for tables with API access, RLS enabled and policies if (isDataApiExposedBadgeEnabled && apiAccessData?.hasApiAccess) { return ( - This table is exposed via the Data API + This table can be accessed via the Data API ) } diff --git a/e2e/studio/features/rls-policies.spec.ts b/e2e/studio/features/rls-policies.spec.ts index 98b55f0f096..495c4a41377 100644 --- a/e2e/studio/features/rls-policies.spec.ts +++ b/e2e/studio/features/rls-policies.spec.ts @@ -234,7 +234,7 @@ test.describe.serial('RLS Policies', () => { // Hover the badge to verify the tooltip explains the risk await unrestrictedBadge.hover() await expect( - page.getByText(/Data is publicly accessible via API/i), + page.getByText(/This table can be accessed by anyone via the Data API as RLS is disabled/i), 'Tooltip should describe unrestricted public access when RLS is disabled' ).toBeVisible({ timeout: 10000 }) })