mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat: iso27001 certificate (#44963)
Edit: Can be merged, mgmt api deployed Dashboard addition to frontend for access to the ISO 27001 certificate. View for Team customers: <img width="1737" height="1151" alt="image" src="https://github.com/user-attachments/assets/cd62d24f-8b6e-4600-9ded-943a170cd124" /> Resolves SEC-799 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * ISO 27001 certificate added to Documents with a Download action, confirmation modal, new-tab open on success, and error toast on failure. * Users without billing permission see a no-permission view; users missing entitlement see an “Upgrade to Team” prompt. * **Refactor** * Upgrade-to-Team flows for SOC2 and related upgrade UI standardized to use the shared upgrade component. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
6b8772a33e
commit
bd55ad23a6
9 files changed
+248
-16
No files matched your search
@@ -3,6 +3,7 @@ import { Fragment } from 'react'
|
||||
import { CustomDocument } from './CustomDocument'
|
||||
import { DPA } from './DPA'
|
||||
import { HIPAA } from './HIPAA'
|
||||
import { ISO27001 } from './ISO27001'
|
||||
import { SecurityQuestionnaire } from './SecurityQuestionnaire'
|
||||
import { SOC2 } from './SOC2'
|
||||
import { TIA } from './TIA'
|
||||
@@ -50,6 +51,12 @@ export const Documents = () => {
|
||||
|
||||
<ScaffoldDivider />
|
||||
|
||||
<ScaffoldContainer id="iso27001" className="px-6 xl:px-10">
|
||||
<ISO27001 />
|
||||
</ScaffoldContainer>
|
||||
|
||||
<ScaffoldDivider />
|
||||
|
||||
<ScaffoldContainer id="hipaa" className="px-6 xl:px-10">
|
||||
<HIPAA />
|
||||
</ScaffoldContainer>
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { Download } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import {
|
||||
ScaffoldSection,
|
||||
ScaffoldSectionContent,
|
||||
ScaffoldSectionDetail,
|
||||
} from '@/components/layouts/Scaffold'
|
||||
import NoPermission from '@/components/ui/NoPermission'
|
||||
import { UpgradePlanButton } from '@/components/ui/UpgradePlanButton'
|
||||
import { getDocument } from '@/data/documents/document-query'
|
||||
import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
|
||||
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
|
||||
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
|
||||
export const ISO27001 = () => {
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
const slug = organization?.slug
|
||||
|
||||
const { mutate: sendEvent } = useSendEventMutation()
|
||||
const { can: canReadSubscriptions, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
|
||||
PermissionAction.BILLING_READ,
|
||||
'stripe.subscriptions'
|
||||
)
|
||||
const { hasAccess: hasAccessToISO27001, isLoading: isLoadingEntitlement } = useCheckEntitlements(
|
||||
'security.iso27001_certificate'
|
||||
)
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const fetchISO27001 = async (orgSlug: string) => {
|
||||
try {
|
||||
const link = await getDocument({ orgSlug, docType: 'iso27001-certificate' })
|
||||
if (link?.fileUrl) window.open(link.fileUrl, '_blank')
|
||||
setIsOpen(false)
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
toast.error(`Failed to download ISO 27001 certificate: ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDownloadClick = () => {
|
||||
if (!slug) return
|
||||
|
||||
sendEvent({
|
||||
action: 'document_view_button_clicked',
|
||||
properties: { documentName: 'ISO27001' },
|
||||
groups: { organization: slug },
|
||||
})
|
||||
setIsOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<ScaffoldSection className="py-12">
|
||||
<ScaffoldSectionDetail>
|
||||
<h4 className="mb-5">ISO 27001</h4>
|
||||
<div className="space-y-2 text-sm text-foreground-light [&_p]:m-0">
|
||||
<p>
|
||||
Organizations on Team Plan or above have access to our most recent ISO 27001
|
||||
certificate.
|
||||
</p>
|
||||
</div>
|
||||
</ScaffoldSectionDetail>
|
||||
<ScaffoldSectionContent>
|
||||
{isLoadingPermissions || isLoadingEntitlement ? (
|
||||
<div className="@lg:flex items-center justify-center h-full">
|
||||
<ShimmeringLoader className="w-24" />
|
||||
</div>
|
||||
) : !canReadSubscriptions ? (
|
||||
<NoPermission resourceText="access our ISO 27001 certificate" />
|
||||
) : !hasAccessToISO27001 ? (
|
||||
<div className="@lg:flex items-center justify-center h-full">
|
||||
<UpgradePlanButton
|
||||
variant="default"
|
||||
plan="Team"
|
||||
source="org-documents-iso27001"
|
||||
featureProposition="download the ISO 27001 certificate"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="@lg:flex items-center justify-center h-full">
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Download />}
|
||||
onClick={handleDownloadClick}
|
||||
disabled={!slug}
|
||||
>
|
||||
Download ISO 27001 Certificate
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<ConfirmationModal
|
||||
visible={isOpen}
|
||||
size="large"
|
||||
title="Non-Disclosure Agreement to access Supabase's ISO 27001 Certificate"
|
||||
confirmLabel="I agree"
|
||||
confirmLabelLoading="Downloading"
|
||||
onCancel={() => setIsOpen(false)}
|
||||
onConfirm={() => {
|
||||
if (slug) fetchISO27001(slug)
|
||||
}}
|
||||
>
|
||||
<ol className="list-decimal list-inside text-sm text-foreground-light pl-30">
|
||||
<li>The information that you are about to access is confidential.</li>
|
||||
<li>
|
||||
Your access to our ISO 27001 materials is governed by confidentiality obligations
|
||||
contained in the agreement between Supabase, Inc ("Supabase", "we", "our" or "us") and
|
||||
the Supabase customer that has authorized you to access our platform to obtain this
|
||||
information (our "Customer").
|
||||
</li>
|
||||
<li>
|
||||
You must ensure that you treat the information in our ISO 27001 materials in
|
||||
accordance with those confidentiality obligations, as communicated to you by the
|
||||
Customer.
|
||||
</li>
|
||||
<li>
|
||||
By clicking "I agree" below or otherwise accessing our ISO 27001 materials, you:
|
||||
<ol className="list-[lower-roman] list-inside pl-4">
|
||||
<li>acknowledge that you have read and understood this Confidentiality Notice;</li>
|
||||
<li>
|
||||
confirm that you have been authorized by the Customer to access this information,
|
||||
and your use of our ISO 27001 materials is subject to the confidentiality
|
||||
obligations owed by the Customer to us.
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>
|
||||
This Confidentiality Notice does not substitute or supersede any agreement between us
|
||||
and the Customer, or any internal rules or policies that the Customer requires you to
|
||||
comply with in your access to and use of confidential information. However, your
|
||||
failure to comply with this Confidentiality Notice may be used to determine whether
|
||||
the Customer has complied with its confidentiality obligations to us.
|
||||
</li>
|
||||
</ol>
|
||||
</ConfirmationModal>
|
||||
</ScaffoldSectionContent>
|
||||
</ScaffoldSection>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { Download } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from 'ui'
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
ScaffoldSectionDetail,
|
||||
} from '@/components/layouts/Scaffold'
|
||||
import NoPermission from '@/components/ui/NoPermission'
|
||||
import { UpgradePlanButton } from '@/components/ui/UpgradePlanButton'
|
||||
import { getDocument } from '@/data/documents/document-query'
|
||||
import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
|
||||
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
|
||||
@@ -74,11 +74,12 @@ export const SOC2 = () => {
|
||||
<NoPermission resourceText="access our SOC2 Type 2 report" />
|
||||
) : !hasAccessToSoc2Report ? (
|
||||
<div className="@lg:flex items-center justify-center h-full">
|
||||
<Button asChild type="default">
|
||||
<Link href={`/org/${slug}/billing?panel=subscriptionPlan&source=soc2`}>
|
||||
Upgrade to Team
|
||||
</Link>
|
||||
</Button>
|
||||
<UpgradePlanButton
|
||||
variant="default"
|
||||
plan="Team"
|
||||
source="org-documents-soc2"
|
||||
featureProposition="download the SOC2 Type 2report"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="@lg:flex items-center justify-center h-full">
|
||||
|
||||
@@ -48,6 +48,7 @@ interface RequestUpgradeToBillingOwnersProps {
|
||||
/** Used in the default message template, e.g: "Upgrade to ..." */
|
||||
featureProposition?: string
|
||||
className?: string
|
||||
type?: 'primary' | 'default'
|
||||
}
|
||||
|
||||
export const RequestUpgradeToBillingOwners = ({
|
||||
@@ -57,6 +58,7 @@ export const RequestUpgradeToBillingOwners = ({
|
||||
featureProposition,
|
||||
children,
|
||||
className,
|
||||
type = 'primary',
|
||||
}: PropsWithChildren<RequestUpgradeToBillingOwnersProps>) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const track = useTrack()
|
||||
@@ -121,10 +123,8 @@ export const RequestUpgradeToBillingOwners = ({
|
||||
|
||||
const defaultValues = {
|
||||
note: !!addon
|
||||
? addon === 'spendCap'
|
||||
? `We'd like to ${isFreePlan ? 'upgrade to Pro and ' : ''}${action} ${target} so that we can ${featureProposition}`
|
||||
: `We'd like to ${isFreePlan ? 'upgrade to Pro and ' : ''}${action} ${target} so that we can ${featureProposition}`
|
||||
: `We'd like to upgrade to the ${plan} plan ${!!featureProposition ? ` to ${featureProposition} ` : ''}${target}`,
|
||||
? `We'd like to ${isFreePlan ? 'upgrade to Pro and ' : ''}${action} ${target} so that we can ${featureProposition}`
|
||||
: `We'd like to upgrade to the ${plan} plan ${!!featureProposition ? `to ${featureProposition} ` : ''}${target}`,
|
||||
}
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
@@ -162,7 +162,7 @@ export const RequestUpgradeToBillingOwners = ({
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<Button block={block} type="primary" className={className}>
|
||||
<Button block={block} type={type} className={className}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
@@ -35,7 +35,7 @@ interface UpgradePlanButtonProps {
|
||||
*/
|
||||
export const UpgradePlanButton = ({
|
||||
source,
|
||||
variant = 'primary',
|
||||
variant: type = 'primary',
|
||||
plan = 'Pro',
|
||||
addon,
|
||||
featureProposition,
|
||||
@@ -97,6 +97,7 @@ export const UpgradePlanButton = ({
|
||||
addon={addon}
|
||||
featureProposition={featureProposition}
|
||||
className={className}
|
||||
type={type}
|
||||
>
|
||||
{children}
|
||||
</RequestUpgradeToBillingOwners>
|
||||
@@ -107,7 +108,7 @@ export const UpgradePlanButton = ({
|
||||
return (
|
||||
<ButtonTooltip
|
||||
disabled
|
||||
type={variant}
|
||||
type={type}
|
||||
className={className}
|
||||
tooltip={{
|
||||
content: {
|
||||
@@ -122,7 +123,7 @@ export const UpgradePlanButton = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Button asChild type={variant} disabled={disabled} className={className} onClick={onClick}>
|
||||
<Button asChild type={type} disabled={disabled} className={className} onClick={onClick}>
|
||||
{link}
|
||||
</Button>
|
||||
)
|
||||
|
||||
@@ -4,7 +4,10 @@ import { documentKeys } from './keys'
|
||||
import { get, handleError } from '@/data/fetchers'
|
||||
import type { ResponseError, UseCustomQueryOptions } from '@/types'
|
||||
|
||||
export type DocType = 'standard-security-questionnaire' | 'soc2-type-2-report'
|
||||
export type DocType =
|
||||
| 'standard-security-questionnaire'
|
||||
| 'soc2-type-2-report'
|
||||
| 'iso27001-certificate'
|
||||
|
||||
export type DocumentVariables = {
|
||||
orgSlug?: string
|
||||
@@ -39,6 +42,19 @@ export async function getDocument({ orgSlug, docType }: DocumentVariables, signa
|
||||
|
||||
return data as { fileUrl: string }
|
||||
}
|
||||
|
||||
if (docType === 'iso27001-certificate') {
|
||||
const { data, error } = await get(
|
||||
`/platform/organizations/{slug}/documents/iso27001-certificate`,
|
||||
{
|
||||
params: { path: { slug: orgSlug } },
|
||||
signal,
|
||||
}
|
||||
)
|
||||
if (error) throw error
|
||||
|
||||
return data as { fileUrl: string }
|
||||
}
|
||||
}
|
||||
|
||||
export type DocumentData = Awaited<ReturnType<typeof getDocument>>
|
||||
|
||||
Vendored
+1
@@ -4896,6 +4896,7 @@ export interface components {
|
||||
| 'security.audit_logs_days'
|
||||
| 'security.questionnaire'
|
||||
| 'security.soc2_report'
|
||||
| 'security.iso27001_certificate'
|
||||
| 'security.private_link'
|
||||
| 'security.enforce_mfa'
|
||||
| 'log.retention_days'
|
||||
|
||||
+61
@@ -1372,6 +1372,23 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/platform/organizations/{slug}/documents/iso27001-certificate': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
/** Get ISO 27001 certificate URL */
|
||||
get: operations['OrgDocumentsController_getIso27001CertificateUrl']
|
||||
put?: never
|
||||
post?: never
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/platform/organizations/{slug}/documents/soc2-type-2-report': {
|
||||
parameters: {
|
||||
query?: never
|
||||
@@ -7263,6 +7280,7 @@ export interface components {
|
||||
| 'security.audit_logs_days'
|
||||
| 'security.questionnaire'
|
||||
| 'security.soc2_report'
|
||||
| 'security.iso27001_certificate'
|
||||
| 'security.private_link'
|
||||
| 'security.enforce_mfa'
|
||||
| 'log.retention_days'
|
||||
@@ -15414,6 +15432,49 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
OrgDocumentsController_getIso27001CertificateUrl: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path: {
|
||||
/** @description Organization slug */
|
||||
slug: string
|
||||
}
|
||||
cookie?: never
|
||||
}
|
||||
requestBody?: never
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['OrgDocumentUrlResponse']
|
||||
}
|
||||
}
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Forbidden action */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Rate limit exceeded */
|
||||
429: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
}
|
||||
}
|
||||
OrgDocumentsController_getSoc2Type2ReportUrl: {
|
||||
parameters: {
|
||||
query?: never
|
||||
|
||||
@@ -2016,7 +2016,7 @@ export interface DocumentViewButtonClickedEvent {
|
||||
/**
|
||||
* The name of the document being viewed, e.g. TIA, SOC2, Standard Security Questionnaire
|
||||
*/
|
||||
documentName: 'TIA' | 'SOC2' | 'Standard Security Questionnaire'
|
||||
documentName: 'TIA' | 'SOC2' | 'ISO27001' | 'Standard Security Questionnaire'
|
||||
}
|
||||
groups: Omit<TelemetryGroups, 'project'>
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user