Table list entity api badge copy (#41547)

* api badge copy

* ts

* test

* API badge policy row (#41548)

api badge policy row

* refine

* fix tiny nits

---------

Co-authored-by: Alaister Young <a@alaisteryoung.com>
This commit is contained in:
Saxon FletcherandAlaister Young authored and GitHub committed 2026-01-06 08:04:40 +10:00
1 parent d2f4e808e5
commit 1bcecb1c42
5 files changed
+82 -33

No files matched your search

@@ -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
</Badge>
)}
{!hasApiAccess && (
<Badge variant="default" className="shrink-0">
API Disabled
</Badge>
)}
</EditorTablePageLink>
{isTableLocked && (
<Badge>
@@ -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 (
<Card className={cn(isPubliclyReadableWritable && 'border-warning-500')}>
<CardHeader
className={cn(
'py-3 px-4',
(isPubliclyReadableWritable || rlsEnabledNoPolicies || !isTableExposedThroughAPI) &&
'border-b-0'
)}
>
<CardHeader className={cn('py-3 px-4', shouldHideHeaderBorder && 'border-b-0')}>
<PolicyTableRowHeader
table={table}
isLocked={isLocked}
hasApiAccess={hasApiAccess}
onSelectToggleRLS={onSelectToggleRLS}
onSelectCreatePolicy={onSelectCreatePolicy}
/>
@@ -113,19 +125,16 @@ const PolicyTableRowComponent = ({
</Admonition>
)}
{(isPubliclyReadableWritable || rlsEnabledNoPolicies) && isTableExposedThroughAPI && (
<Admonition
showIcon={false}
type={isPubliclyReadableWritable ? 'warning' : 'default'}
className="border-0 border-y rounded-none min-h-12 flex items-center"
>
<p>
{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.'}
</p>
</Admonition>
)}
{(isPubliclyReadableWritable || rlsEnabledNoPolicies || isApiDisabledDueToRoles) &&
isTableExposedThroughAPI && (
<Admonition
showIcon={false}
type={isPubliclyReadableWritable ? 'warning' : 'default'}
className="border-0 border-y rounded-none min-h-12 flex items-center"
>
<p>{admonitionMessage}</p>
</Admonition>
)}
{isPoliciesLoading && (
<CardContent>
@@ -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 {
@@ -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 = ({
</span>
<EntityTooltipTrigger
entity={entity}
tableHasLints={tableHasLints}
tableHasRlsDisabledLint={tableHasRlsDisabledLint}
tableHasRlsEnabledNoPolicyLint={tableHasRlsEnabledNoPolicyLint}
viewHasLints={viewHasLints}
materializedViewHasLints={materializedViewHasLints}
foreignTableHasLints={foreignTableHasLints}
@@ -380,14 +389,16 @@ export const EntityListItem = ({
const EntityTooltipTrigger = ({
entity,
tableHasLints,
tableHasRlsDisabledLint,
tableHasRlsEnabledNoPolicyLint,
viewHasLints,
materializedViewHasLints,
foreignTableHasLints,
apiAccessData,
}: {
entity: Entity
tableHasLints: boolean
tableHasRlsDisabledLint: boolean
tableHasRlsEnabledNoPolicyLint: boolean
viewHasLints: boolean
materializedViewHasLints: boolean
foreignTableHasLints: boolean
@@ -408,10 +419,11 @@ const EntityTooltipTrigger = ({
switch (entity.type) {
case ENTITY_TYPE.TABLE:
if (tableHasLints) {
if (tableHasRlsDisabledLint) {
tooltipContent = (
<>
{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 (
<Tooltip>
<TooltipTrigger className="min-w-4" aria-label="Table exposed via Data API">
<Globe size={14} strokeWidth={1} className="text-foreground-lighter" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-52">
This table can be accessed via the Data API but no RLS policies exist so no data will be
returned
</TooltipContent>
</Tooltip>
)
}
// Show globe icon for tables with API access, RLS enabled and policies
if (isDataApiExposedBadgeEnabled && apiAccessData?.hasApiAccess) {
return (
<Tooltip>
<TooltipTrigger className="min-w-4" aria-label="Table exposed via Data API">
<Globe size={14} strokeWidth={1} className="text-foreground-lighter" />
</TooltipTrigger>
<TooltipContent side="right">This table is exposed via the Data API</TooltipContent>
<TooltipContent side="right">This table can be accessed via the Data API</TooltipContent>
</Tooltip>
)
}
+1 -1
View File
@@ -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 })
})