mirror of
https://github.com/supabase/supabase.git
synced 2026-09-09 03:19:36 +08:00
High Availability (Multigres) projects always enforce SSL, and the management API now rejects any attempt to read or change the setting (supabase/platform#37484). This makes the Database Settings toggle reflect that instead of surfacing an error. **Changed:** - `SSLConfiguration`: skip the `ssl-enforcement` query for HA projects (via `useHighAvailability`) and render the "Enforce SSL on incoming connections" switch checked + disabled with the tooltip "SSL is always enforced on High Availability projects". Non-HA projects are unchanged. - `SSLEnforcementConfirmDialog`: add a controlled `open`/`onOpenChange` mode. The switch now opens the dialog from its own `onCheckedChange` rather than a wrapping `AlertDialogTrigger`, so a disabled switch can no longer open the dialog by clicking the row wrapper beside it (this was reachable for every disabled state, and for HA would have PUT into the new 400 guardrail). The JIT section's existing trigger-with-children usage is untouched. **Added:** - `SSLConfiguration.test.tsx` (MSW): HA → checked/disabled, tooltip, no `ssl-enforcement` request, no dialog from switch/wrapper clicks; non-HA → reflects fetched config, switch opens the dialog and Cancel leaves it unchanged. ## To test On an HA project → Project Settings → Database → SSL configuration: - Switch is on and disabled, hovering shows "SSL is always enforced on High Availability projects" - No request to `/v1/projects/{ref}/ssl-enforcement` fires, no spinner sticks, no error toast - Clicking the disabled switch or the empty area beside it does **not** open the "brief downtime" dialog On a non-HA project: - Switch reflects the current config and the GET fires once - Clicking the switch opens the confirm dialog with Enable/Disable SSL; Cancel and Escape close it without changing the switch or sending a PUT - Clicking beside the switch (not on it) does not open the dialog Linear: https://linear.app/supabase/issue/MUL-1417/database-settings-disable-ssl-enforcement-toggle <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - High Availability projects now show SSL as always enabled, with an explanatory tooltip. - SSL settings are protected from changes on High Availability projects. - SSL confirmation dialogs now open and close reliably when changing settings. - Added accessible announcements for SSL configuration loading and updates. - **Bug Fixes** - Improved SSL state handling for standard and High Availability projects. - Prevented unnecessary SSL enforcement checks for High Availability projects. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
249 lines
10 KiB
TypeScript
249 lines
10 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { useParams } from 'common'
|
|
import { template } from 'lodash'
|
|
import { Download, Loader2 } from 'lucide-react'
|
|
import { useMemo, useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
import { Button, Card, CardContent, Switch, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
|
import { Admonition } from 'ui-patterns/Admonition'
|
|
import { FormLayout } from 'ui-patterns/form/Layout/FormLayout'
|
|
import {
|
|
PageSection,
|
|
PageSectionContent,
|
|
PageSectionMeta,
|
|
PageSectionSummary,
|
|
PageSectionTitle,
|
|
} from 'ui-patterns/PageSection'
|
|
|
|
import { SSLEnforcementConfirmDialog } from './SSLEnforcementConfirmDialog'
|
|
import { SupportLink } from '@/components/interfaces/Support/SupportLink'
|
|
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
|
import { DocsButton } from '@/components/ui/DocsButton'
|
|
import { InlineLinkClassName } from '@/components/ui/InlineLink'
|
|
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
|
|
import { useJitDbAccessQuery } from '@/data/jit-db-access/jit-db-access-query'
|
|
import { useSSLEnforcementQuery } from '@/data/ssl-enforcement/ssl-enforcement-query'
|
|
import { useSSLEnforcementUpdateMutation } from '@/data/ssl-enforcement/ssl-enforcement-update-mutation'
|
|
import { useCustomContent } from '@/hooks/custom-content/useCustomContent'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
import { useHighAvailability } from '@/hooks/misc/useHighAvailability'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { DOCS_URL } from '@/lib/constants'
|
|
|
|
export const SSLConfiguration = () => {
|
|
const { ref } = useParams()
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false)
|
|
|
|
const { data: settings } = useProjectSettingsV2Query({ projectRef: ref })
|
|
|
|
// High Availability projects always enforce SSL and the API rejects any
|
|
// attempt to read or change the setting, so skip the query for them and
|
|
// show the setting as always on.
|
|
const { isHighAvailability, isPending: isHighAvailabilityPending } = useHighAvailability()
|
|
const canLoadSSLEnforcement = !isHighAvailability && !isHighAvailabilityPending
|
|
const {
|
|
data: sslEnforcementConfiguration,
|
|
isPending: isSSLEnforcementPending,
|
|
isSuccess: isSSLEnforcementSuccess,
|
|
} = useSSLEnforcementQuery({ projectRef: ref }, { enabled: canLoadSSLEnforcement })
|
|
|
|
const isLoading = isHighAvailabilityPending || (!isHighAvailability && isSSLEnforcementPending)
|
|
const isSuccess = isHighAvailability || isSSLEnforcementSuccess
|
|
const { data: jitDbAccessConfiguration } = useJitDbAccessQuery({ projectRef: ref })
|
|
const { mutateAsync: updateSSLEnforcement, isPending: isSubmitting } =
|
|
useSSLEnforcementUpdateMutation({
|
|
onSuccess: () => {
|
|
toast.success('Successfully updated SSL configuration')
|
|
},
|
|
onError: (error) => {
|
|
toast.error(`Failed to update SSL enforcement: ${error.message}`)
|
|
},
|
|
})
|
|
|
|
const { can: canUpdateSSLEnforcement } = useAsyncCheckPermissions(
|
|
PermissionAction.UPDATE,
|
|
'projects',
|
|
{
|
|
resource: {
|
|
project_id: project?.id,
|
|
},
|
|
}
|
|
)
|
|
|
|
// Derived directly from the query so a refetch triggered elsewhere (e.g.
|
|
// enabling SSL enforcement from the JIT DB access unavailable banner) is
|
|
// reflected here too, instead of relying on a mirrored local state that
|
|
// would only resync on the initial load.
|
|
const isEnforced =
|
|
isHighAvailability ||
|
|
(isSSLEnforcementSuccess &&
|
|
sslEnforcementConfiguration.appliedSuccessfully &&
|
|
sslEnforcementConfiguration.currentConfig.database)
|
|
|
|
const hasAccessToSSLEnforcement = !(
|
|
sslEnforcementConfiguration !== undefined &&
|
|
'isNotAllowed' in sslEnforcementConfiguration &&
|
|
sslEnforcementConfiguration.isNotAllowed
|
|
)
|
|
|
|
// Temporary access requires SSL enforcement to be enabled, so SSL enforcement
|
|
// can't be turned off again while temporary access is still enabled.
|
|
const isTemporaryAccessEnabled =
|
|
jitDbAccessConfiguration?.state === 'enabled' && jitDbAccessConfiguration.appliedSuccessfully
|
|
|
|
const isSwitchDisabled =
|
|
isLoading ||
|
|
isSubmitting ||
|
|
isHighAvailability ||
|
|
!canUpdateSSLEnforcement ||
|
|
!hasAccessToSSLEnforcement ||
|
|
isTemporaryAccessEnabled
|
|
|
|
let switchTooltipMessage: string | undefined
|
|
if (isHighAvailability) {
|
|
switchTooltipMessage = 'SSL is always enforced on High Availability projects'
|
|
} else if (!canUpdateSSLEnforcement) {
|
|
switchTooltipMessage =
|
|
'You need additional permissions to update SSL enforcement for your project'
|
|
} else if (!hasAccessToSSLEnforcement) {
|
|
switchTooltipMessage = 'Your project does not have access to SSL enforcement'
|
|
} else if (isTemporaryAccessEnabled) {
|
|
switchTooltipMessage =
|
|
'Temporary access must first be disabled before SSL enforcement can be disabled'
|
|
}
|
|
|
|
let loadingAnnouncement = ''
|
|
if (isSubmitting) {
|
|
loadingAnnouncement = 'Updating SSL enforcement'
|
|
} else if (isLoading) {
|
|
loadingAnnouncement = 'Loading SSL configuration'
|
|
}
|
|
|
|
const env = process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod' ? 'prod' : 'staging'
|
|
const hasSSLCertificate =
|
|
settings?.inserted_at !== undefined && new Date(settings.inserted_at) >= new Date('2021-04-30')
|
|
|
|
const { sslCertificateUrl: sslCertificateUrlTemplate } = useCustomContent(['ssl:certificate_url'])
|
|
const sslCertificateUrl = useMemo(
|
|
() => template(sslCertificateUrlTemplate ?? '')({ env }),
|
|
[sslCertificateUrlTemplate, env]
|
|
)
|
|
|
|
const toggleSSLEnforcement = async () => {
|
|
if (!ref) return console.error('Project ref is required')
|
|
await updateSSLEnforcement({ projectRef: ref, requestedConfig: { database: !isEnforced } })
|
|
}
|
|
|
|
return (
|
|
<PageSection id="ssl-configuration">
|
|
<PageSectionMeta>
|
|
<PageSectionSummary>
|
|
<PageSectionTitle>SSL configuration</PageSectionTitle>
|
|
</PageSectionSummary>
|
|
<DocsButton href={`${DOCS_URL}/guides/platform/ssl-enforcement`} />
|
|
</PageSectionMeta>
|
|
<PageSectionContent>
|
|
<Card>
|
|
<CardContent className="space-y-4">
|
|
<FormLayout
|
|
layout="flex-row-reverse"
|
|
label="Enforce SSL on incoming connections"
|
|
description="Reject non-SSL connections to your database"
|
|
>
|
|
<div className="flex items-center justify-end mt-2.5 space-x-2">
|
|
{(isLoading || isSubmitting) && (
|
|
<Loader2
|
|
aria-hidden="true"
|
|
className="animate-spin motion-reduce:animate-none"
|
|
strokeWidth={1.5}
|
|
size={16}
|
|
/>
|
|
)}
|
|
{isSuccess && (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
{/* [Joshen] Added div as tooltip is messing with data state property of toggle */}
|
|
{/* A disabled switch can't take focus, so the wrapper becomes the focus
|
|
target to keep the tooltip reachable by keyboard */}
|
|
<div tabIndex={isSwitchDisabled ? 0 : undefined}>
|
|
{/* The dialog is opened from the switch itself rather than a wrapping
|
|
trigger, so a disabled switch can never open it. */}
|
|
<Switch
|
|
size="large"
|
|
checked={isEnforced}
|
|
disabled={isSwitchDisabled}
|
|
onCheckedChange={() => setIsConfirmDialogOpen(true)}
|
|
/>
|
|
</div>
|
|
</TooltipTrigger>
|
|
{switchTooltipMessage && (
|
|
<TooltipContent side="bottom" className="w-64 text-center">
|
|
{switchTooltipMessage}
|
|
</TooltipContent>
|
|
)}
|
|
</Tooltip>
|
|
)}
|
|
{/* Kept mounted so screen readers announce loading state changes */}
|
|
<span className="sr-only" role="status">
|
|
{loadingAnnouncement}
|
|
</span>
|
|
</div>
|
|
<SSLEnforcementConfirmDialog
|
|
open={isConfirmDialogOpen}
|
|
onOpenChange={setIsConfirmDialogOpen}
|
|
isTargetEnforced={!isEnforced}
|
|
isSubmitting={isSubmitting}
|
|
onConfirm={toggleSSLEnforcement}
|
|
/>
|
|
</FormLayout>
|
|
{isSSLEnforcementSuccess && !sslEnforcementConfiguration.appliedSuccessfully && (
|
|
<Admonition
|
|
type="warning"
|
|
layout="horizontal"
|
|
title="SSL enforcement was not updated successfully"
|
|
description={
|
|
<>
|
|
Please try updating again, or contact{' '}
|
|
<SupportLink className={InlineLinkClassName}>support</SupportLink> if this error
|
|
persists
|
|
</>
|
|
}
|
|
/>
|
|
)}
|
|
</CardContent>
|
|
<CardContent>
|
|
<FormLayout
|
|
layout="flex-row-reverse"
|
|
label="SSL Certificate"
|
|
description="Use this certificate when connecting to your database to prevent snooping and man-in-the-middle attacks."
|
|
>
|
|
<div className="flex items-end justify-end">
|
|
{!hasSSLCertificate ? (
|
|
<ButtonTooltip
|
|
disabled
|
|
variant="default"
|
|
icon={<Download />}
|
|
tooltip={{
|
|
content: {
|
|
side: 'bottom',
|
|
text: 'Projects before 15:08 (GMT+08), 29th April 2021 do not have SSL certificates installed',
|
|
},
|
|
}}
|
|
>
|
|
Download certificate
|
|
</ButtonTooltip>
|
|
) : (
|
|
<Button variant="default" icon={<Download />}>
|
|
<a href={sslCertificateUrl}>Download certificate</a>
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</FormLayout>
|
|
</CardContent>
|
|
</Card>
|
|
</PageSectionContent>
|
|
</PageSection>
|
|
)
|
|
}
|