mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
1 parent
d2f4e808e5
commit
1bcecb1c42
5 files changed
+82
-33
No files matched your search
+7
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user