Files
Alaister YoungandAlaister Young 995f6f65c7 [FE-4483] fix(studio): disable network bans for v3 projects (#50997)
Disables network bans for v3 (`AWS_K8S`) projects and shows a specific
unsupported notice. The shared banned-IP query waits for project details
and skips unsupported projects, covering both Database Settings and
Advisor for v3 and High Availability projects.

The hook returns the standard query result and uses `skipToken` to
prevent unsupported requests, including manual refetches. Database
Settings handles project-detail errors at the call site. Open unban
confirmations are cleared when the section becomes disabled, and
submission checks eligibility.

Addresses
[FE-4483](https://linear.app/supabase/issue/FE-4483/disable-network-bans-for-v3-aws-k8s-projects).

## To test

- Open Database Settings on a v3 project. Check that Network bans shows
the v3 notice, hides the IP list and unban controls, and makes no
network-bans retrieval request on initial load or reload, including
while Advisor is mounted.
- Check that an HA project still shows its existing notice and makes no
network-bans retrieval request on initial load or reload.
- Navigate from a supported project to a v3 or HA project and check that
no banned-IP request is sent for the unsupported project and no
banned-IP signals from the previous project appear in Advisor.
- If project details fail without cached data, check that Network bans
shows an error after retries finish and does not retrieve bans. A
successful retry should restore normal behavior.
- Open an unban confirmation on a supported project, then navigate to a
v3 or HA project. Check that the dialog closes without sending an unban
request and stays closed when returning. A newly opened confirmation
should still work.
- On a supported project, check the empty state and banned IP list.
Confirm that users with permission can unban an IP and users without
permission see a disabled button with the permissions tooltip.

Validation: 17 focused tests passed, covering automatic and manual
request suppression, project-detail error display and recovery, and
navigation between supported and unsupported projects. Changed-file
ESLint, Prettier, and full Studio typecheck (without the incremental
cache) passed. Earlier local browser checks on `9912c6c` confirmed no
retrieval requests for an HA project on AWS_K8S across reloads and
Advisor, and a successful empty state on a supported project. The local
failed-project case redirected to the organization after retries, so the
inline error remains verified by the component test only. The latest
preview, standalone v3 notice, populated bans/unban, and no-permission
tooltip still need browser verification.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Banned IP settings now show an unsupported-project notice for AWS
Kubernetes projects and hide ban lists and unban actions for AWS
Kubernetes and High Availability projects.
* Banned IP data loads only after project details are available and only
for supported projects; unsupported projects do not display cached ban
data.
  * Project-detail errors are shown separately from ban-list errors.
* Unban confirmations close when a project becomes unsupported or an
unban succeeds. Unbanning is unavailable when you lack update permission
or the project is unsupported.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
2026-09-29 17:51:14 +00:00

197 lines
7.3 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { Globe } from 'lucide-react'
import { useMemo, useState } from 'react'
import { toast } from 'sonner'
import { Badge, Card, CardContent, Skeleton } from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import {
PageSection,
PageSectionContent,
PageSectionDescription,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { AlertError } from '@/components/ui/AlertError'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { DocsButton } from '@/components/ui/DocsButton'
import { HighAvailabilityDisabledSectionNotice } from '@/components/ui/HighAvailability/HighAvailabilityDisabledSectionNotice'
import { useBannedIPsDeleteMutation } from '@/data/banned-ips/banned-ips-delete-mutations'
import { useBannedIPsQuery } from '@/data/banned-ips/banned-ips-query'
import { useUserIPAddressQuery } from '@/data/misc/user-ip-address-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useHighAvailability } from '@/hooks/misc/useHighAvailability'
import { useIsAwsK8sCloudProvider, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { DOCS_URL } from '@/lib/constants'
const HA_DISABLED_TITLE = 'Network bans unavailable on High Availability projects'
const HA_DISABLED_DESCRIPTION =
"We're working to bring network bans to High Availability projects. Contact support if this is blocking your work."
const V3_DISABLED_TITLE = 'Network bans unavailable on v3 projects'
export const BannedIPs = () => {
const { ref } = useParams()
const { data: project, error: projectError } = useSelectedProjectQuery()
const { isHighAvailability } = useHighAvailability()
const isAwsK8s = useIsAwsK8sCloudProvider()
const [selectedIPToUnban, setSelectedIPToUnban] = useState<string | null>(null)
const {
isPending: isLoadingIPList,
isFetching: isFetchingIPList,
data: ipList,
error: ipListError,
} = useBannedIPsQuery({ projectRef: ref })
const { data: userIPAddress } = useUserIPAddressQuery()
const hasProjectError = !project && !!projectError
const ipListLoading = isLoadingIPList || isFetchingIPList
const { can: canUnbanNetworks } = useAsyncCheckPermissions(PermissionAction.UPDATE, 'projects', {
resource: {
project_id: project?.id,
},
})
const isSectionDisabled = isHighAvailability || isAwsK8s || !canUnbanNetworks
if (isSectionDisabled && selectedIPToUnban !== null) {
setSelectedIPToUnban(null)
}
const sectionDisabledReason = useMemo(() => {
if (isHighAvailability) return HA_DISABLED_TITLE
if (isAwsK8s) return V3_DISABLED_TITLE
return 'You need additional permissions to unban networks'
}, [isHighAvailability, isAwsK8s])
const { mutate: unbanIPs, isPending: isUnbanning } = useBannedIPsDeleteMutation({
onSuccess: () => {
toast.success('IP address successfully unbanned')
setSelectedIPToUnban(null)
},
onError: (error) => {
toast.error(`Failed to unban IP: ${error?.message}`)
},
})
const onConfirmUnbanIP = () => {
if (selectedIPToUnban === null || !ref || isSectionDisabled) return
unbanIPs({
projectRef: ref,
ips: [selectedIPToUnban], // Pass the IP as an array
})
}
return (
<>
<PageSection id="banned-ips">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Network bans</PageSectionTitle>
<PageSectionDescription>
IP addresses temporarily blocked due to suspicious traffic
</PageSectionDescription>
</PageSectionSummary>
<DocsButton href={`${DOCS_URL}/reference/cli/supabase-network-bans`} />
</PageSectionMeta>
<PageSectionContent>
{hasProjectError && (
<AlertError
error={projectError}
subject="Failed to retrieve project details"
projectRef={ref}
/>
)}
{isHighAvailability && (
<div className="mb-4">
<HighAvailabilityDisabledSectionNotice
title={HA_DISABLED_TITLE}
description={HA_DISABLED_DESCRIPTION}
/>
</div>
)}
{!isHighAvailability && isAwsK8s && (
<Admonition
type="default"
title={V3_DISABLED_TITLE}
description="Fail2Ban is not supported on v3 projects."
/>
)}
{!hasProjectError && !isHighAvailability && !isAwsK8s && (
<>
{ipListError && (
<AlertError
error={ipListError}
subject="Failed to retrieve banned IP addresses"
projectRef={ref}
/>
)}
{!ipListError && ipListLoading && (
<Card>
<CardContent className="space-y-4">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
</CardContent>
</Card>
)}
{!ipListError && !ipListLoading && ipList && (
<Card>
{ipList.banned_ipv4_addresses.length > 0 ? (
ipList.banned_ipv4_addresses.map((ip) => (
<CardContent key={ip} className="flex items-center justify-between">
<div className="flex items-center space-x-5">
<Globe size={16} className="text-foreground-lighter" />
<p className="text-sm font-mono">{ip}</p>
{ip === userIPAddress && <Badge>Your IP address</Badge>}
</div>
<ButtonTooltip
disabled={isSectionDisabled}
onClick={() => setSelectedIPToUnban(ip)}
tooltip={{
content: {
side: 'bottom',
text: isSectionDisabled ? sectionDisabledReason : undefined,
},
}}
>
Unban IP
</ButtonTooltip>
</CardContent>
))
) : (
<CardContent className="text-foreground text-sm">
There are no banned IP addresses for your project
</CardContent>
)}
</Card>
)}
</>
)}
</PageSectionContent>
</PageSection>
<ConfirmationModal
variant="destructive"
size="medium"
loading={isUnbanning}
visible={selectedIPToUnban !== null && !isSectionDisabled}
title="Confirm Unban IP"
confirmLabel="Confirm Unban"
confirmLabelLoading="Unbanning..."
onCancel={() => setSelectedIPToUnban(null)}
onConfirm={onConfirmUnbanIP}
alert={{
title: 'This action cannot be undone',
description: `Are you sure you want to unban this IP address ${selectedIPToUnban}?`,
}}
/>
</>
)
}