Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2160,6 +2160,16 @@ export const platform: NavMenuConstant = {
{
name: 'Multi-factor Authentication',
url: '/guides/platform/multi-factor-authentication',
items: [
{
name: 'Enable MFA',
url: '/guides/platform/multi-factor-authentication',
},
{
name: 'Require MFA for organization members',
url: '/guides/platform/org-mfa-enforcement',
},
],
},
{
name: 'Transfer Project',
Expand Down
1 change: 1 addition & 0 deletions apps/docs/content/guides/deployment/going-into-prod.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ After developing your project and deciding it's Production Ready, you should run
- Ensure that your Supabase Account is protected with multi-factor authentication (MFA).
- If using a GitHub signin, [enable 2FA on GitHub](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication). Since your GitHub account gives you administrative rights to your Supabase org, you should protect it with a strong password and 2FA using a U2F key or a TOTP app.
- If using email+password signin, set up [MFA for your Supabase account](https://supabase.com/docs/guides/platform/multi-factor-authentication#enable-mfa).
- Enable [MFA enforcement on your organization](/docs/guides/platform/network-restrictions). This ensures all users must have a valid MFA backed session to interact with organization and project resources.
- Consider [adding multiple owners on your Supabase org](https://supabase.com/dashboard/org/_/team). This ensures that if one of the owners is unreachable or loses access to their account, you still have Owner access to your org.
- Ensure email confirmations are [enabled](https://supabase.com/dashboard/project/_/auth/providers) in the `Settings > Auth` page.
- Ensure that you've [set the expiry](https://supabase.com/dashboard/project/_/auth/providers) for one-time passwords (OTPs) to a reasonable value that you are comfortable with. We recommend setting this to 3600 seconds (1 hour) or lower.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ You can use Supabase to store and process Protected Health Information (PHI). Yo
- Signing a Business Associate Agreement (BAA) with Supabase. Submit a [HIPAA add-on request](https://forms.supabase.com/hipaa2) to get started. You will need to be at least on the [Team Plan](https://supabase.com/pricing) to sign a BAA with us.
- [Marking specific projects as HIPAA projects](/docs/guides/platform/hipaa-projects) and addressing security issues raised by the advisor.
- Ensuring [MFA is enabled](/docs/guides/platform/multi-factor-authentication) on all Supabase accounts.
- [Enforce MFA](/docs/guides/platform/org-mfa-enforcement) as a requirement to access the organization
- Enabling [Point in Time Recovery](/docs/guides/platform/backups#point-in-time-recovery) which requires at least a [small compute add-on](/docs/guides/platform/compute-add-ons).
- Turning on [SSL Enforcement](/docs/guides/platform/ssl-enforcement).
- Enabling [Network Restrictions](/docs/guides/platform/network-restrictions).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ For security reasons, we will not be able to restore access to your account if y

Once you've enabled MFA for your Supabase user account, you will be prompted to enter your second factor challenge code as seen in your preferred TOTP app.

If you are an organization owner and on the Pro, Team or Enterprise plan, you can enforce that all organization members [must have MFA enabled](/docs/guides/platform/org-mfa-enforcement).

## Disable MFA

You can disable MFA for your user account under your [Supabase account settings](/dashboard/account/security). On subsequent login attempts, you will not be prompted to enter a MFA code.
Expand Down
32 changes: 32 additions & 0 deletions apps/docs/content/guides/platform/org-mfa-enforcement.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
title: 'Enforce MFA on Organization'
description: 'All users in an organization must have a valid MFA session to interact with organization resources'
---

Supabase provides multi-factor authentication (MFA) enforcement on the organization level. With MFA enforcement, you can ensure that all organization members use MFA. Members cannot interact with your organization or your organization's projects without a valid MFA-backed session.

<Admonition type="note">

MFA enforcement is only available on the [Pro, Team and Enterprise plans](https://supabase.com/pricing).

This feature is currently in limited preview. If you would like to opt-in to try it, contact support.

</Admonition>

## Manage MFA enforcement

To enable MFA on an organization, visit the [security settings](/dashboard/org/_/security) page and toggle `Require MFA to access organization` on.

- Only organization **owners** can modify this setting
- The owner must have [MFA on their own account](/docs/guides/platform/multi-factor-authentication)
- Supabase recommends creating two distinct MFA apps on your user account

<Admonition type="caution">

When MFA enforcement is enabled, users without MFA will immediately lose access all resources in the organization. The users will still be members of the organization and will regain their original permissions once they enable MFA on their account.

</Admonition>

## Personal access tokens

Personal access tokens are not affected by MFA enforcement. Personal access tokens are designed for programmatic access and issuing of these require a valid Supabase session backed by MFA, if enabled on the account.
1 change: 1 addition & 0 deletions apps/docs/public/humans.txt
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ Qiao Han
Raminder Singh
Riccardo Busetti
Rodrigo Mansueli
Ronan Lehane
Rory Wilding
Sam Rose
Sergio Cioban Filho
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ export const OrgNotFound = ({ slug }: { slug?: string }) => {
<AlertError error={organizationsError} subject="Failed to load organizations" />
)}
{isOrganizationsSuccess &&
organizations?.map((org) => <OrganizationCard key={org.slug} organization={org} />)}
organizations?.map((org) => (
<OrganizationCard key={org.slug} organization={org} href={`/new/${org.slug}`} />
))}
</div>
</>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ import { ActionCard } from 'components/ui/ActionCard'
import { useProjectsQuery } from 'data/projects/projects-query'
import { Organization } from 'types'

export const OrganizationCard = ({ organization }: { organization: Organization }) => {
export const OrganizationCard = ({
organization,
href,
}: {
organization: Organization
href?: string
}) => {
const { data: allProjects = [] } = useProjectsQuery()
const numProjects = allProjects.filter((x) => x.organization_slug === organization.slug).length

return (
<Link href={`/new/${organization.slug}`}>
<Link href={href ?? `/org/${organization.slug}`}>
<ActionCard
bgColor="bg border"
className="[&>div]:items-center"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,15 @@ import { toast } from 'sonner'
import * as z from 'zod'

import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import InformationBox from 'components/ui/InformationBox'
import { useOrganizationCreateInvitationMutation } from 'data/organization-members/organization-invitation-create-mutation'
import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query'
import { useOrganizationMembersQuery } from 'data/organizations/organization-members-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import { useHasAccessToProjectLevelPermissions } from 'data/subscriptions/org-subscription-query'
import {
doPermissionsCheck,
useCheckPermissions,
useGetPermissions,
} from 'hooks/misc/useCheckPermissions'
import { doPermissionsCheck, useGetPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useProfile } from 'lib/profile'
import {
Expand Down Expand Up @@ -50,9 +48,6 @@ import {
SelectTrigger_Shadcn_,
Select_Shadcn_,
Switch,
Tooltip,
TooltipContent,
TooltipTrigger,
cn,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
Expand All @@ -64,6 +59,10 @@ export const InviteMemberButton = () => {
const organization = useSelectedOrganization()
const { permissions: permissions } = useGetPermissions()

const { organizationMembersCreate: organizationMembersCreationEnabled } = useIsFeatureEnabled([
'organization_members:create',
])

const [isOpen, setIsOpen] = useState(false)
const [projectDropdownOpen, setProjectDropdownOpen] = useState(false)

Expand All @@ -75,10 +74,6 @@ export const InviteMemberButton = () => {
const orgProjects = (projects ?? [])
.filter((project) => project.organization_id === organization?.id)
.sort((a, b) => a.name.localeCompare(b.name))
const canReadSubscriptions = useCheckPermissions(
PermissionAction.BILLING_READ,
'stripe.subscriptions'
)

const currentPlan = organization?.plan
const hasAccessToProjectLevelPermissions = useHasAccessToProjectLevelPermissions(slug as string)
Expand All @@ -96,6 +91,7 @@ export const InviteMemberButton = () => {

const canInviteMembers =
hasOrgRole &&
rolesAddable.length > 0 &&
orgScopedRoles.some(({ id: role_id }) =>
doPermissionsCheck(
permissions,
Expand Down Expand Up @@ -182,22 +178,24 @@ export const InviteMemberButton = () => {
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<Button
disabled={!canInviteMembers}
className="pointer-events-auto flex-grow md:flex-grow-0"
onClick={() => setIsOpen(true)}
>
Invite member
</Button>
</TooltipTrigger>
{!canInviteMembers && (
<TooltipContent side="bottom">
You need additional permissions to invite a member to this organization
</TooltipContent>
)}
</Tooltip>
<ButtonTooltip
type="primary"
disabled={!canInviteMembers}
className="pointer-events-auto flex-grow md:flex-grow-0"
onClick={() => setIsOpen(true)}
tooltip={{
content: {
side: 'bottom',
text: !organizationMembersCreationEnabled
? 'Inviting members is currently disabled'
: !canInviteMembers
? 'You need additional permissions to invite a member to this organization'
: undefined,
},
}}
>
Invite member
</ButtonTooltip>
</DialogTrigger>
<DialogContent size="medium">
<DialogHeader>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
} from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { LeaveTeamButton } from './LeaveTeamButton'
import { hasMultipleOwners, useGetRolesManagementPermissions } from './TeamSettings.utils'
import { useGetRolesManagementPermissions } from './TeamSettings.utils'
import { UpdateRolesPanel } from './UpdateRolesPanel/UpdateRolesPanel'

interface MemberActionsProps {
Expand All @@ -50,10 +50,6 @@ export const MemberActions = ({ member }: MemberActionsProps) => {
const { data: allRoles } = useOrganizationRolesV2Query({ slug })

const memberIsUser = member.gotrue_id == profile?.gotrue_id
const isOwner = selectedOrganization?.is_owner
const roles = allRoles?.org_scoped_roles ?? []
const canLeave = !isOwner || (isOwner && hasMultipleOwners(members, roles))

const orgScopedRoles = allRoles?.org_scoped_roles ?? []
const projectScopedRoles = allRoles?.project_scoped_roles ?? []
const isPendingInviteAcceptance = !!member.invited_id
Expand Down Expand Up @@ -151,25 +147,6 @@ export const MemberActions = ({ member }: MemberActionsProps) => {
)
}

if (!canRemoveMember || (isPendingInviteAcceptance && !canResendInvite && !canRevokeInvite)) {
return (
<div className="flex items-center justify-end">
<ButtonTooltip
disabled
type="text"
className="px-1.5"
icon={<MoreVertical size={18} />}
tooltip={{
content: {
side: 'bottom',
text: 'You need additional permissions to manage this team member',
},
}}
/>
</div>
)
}

if (memberIsUser) {
return (
<div className="flex items-center justify-end">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Search } from 'lucide-react'
import { useState } from 'react'

import { useParams } from 'common'
import {
ScaffoldActionsContainer,
ScaffoldActionsGroup,
Expand All @@ -10,40 +9,13 @@ import {
ScaffoldSectionContent,
ScaffoldTitle,
} from 'components/layouts/Scaffold'
import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query'
import { usePermissionsQuery } from 'data/permissions/permissions-query'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useProfile } from 'lib/profile'
import { Input } from 'ui-patterns/DataInputs/Input'
import { InviteMemberButton } from './InviteMemberButton'
import MembersView from './MembersView'
import { useGetRolesManagementPermissions } from './TeamSettings.utils'

export const TeamSettings = () => {
const { organizationMembersCreate: organizationMembersCreationEnabled } = useIsFeatureEnabled([
'organization_members:create',
])

const { slug } = useParams()
const { profile } = useProfile()
const selectedOrganization = useSelectedOrganization()

const { data: permissions } = usePermissionsQuery()
const { data: rolesData } = useOrganizationRolesV2Query({ slug })

const roles = rolesData?.org_scoped_roles ?? []

const { rolesAddable } = useGetRolesManagementPermissions(
selectedOrganization?.slug,
roles,
permissions ?? []
)

const [searchString, setSearchString] = useState('')

const canAddMembers = rolesAddable.length > 0

return (
<ScaffoldContainerLegacy>
<ScaffoldTitle>Team</ScaffoldTitle>
Expand All @@ -60,10 +32,7 @@ export const TeamSettings = () => {
placeholder="Filter members"
/>
<ScaffoldActionsGroup className="w-full md:w-auto">
{organizationMembersCreationEnabled &&
canAddMembers &&
profile !== undefined &&
selectedOrganization !== undefined && <InviteMemberButton />}
<InviteMemberButton />
</ScaffoldActionsGroup>
</ScaffoldActionsContainer>
<ScaffoldSectionContent className="w-full">
Expand Down
2 changes: 1 addition & 1 deletion packages/ui-patterns/src/Dialogs/ConfirmationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const ConfirmationModal = forwardRef<
e.preventDefault()
e.stopPropagation()
onConfirm()
if (loading === undefined) setLoading(true)
if (loading_ === undefined) setLoading(true)
}

useEffect(() => {
Expand Down
Loading
Loading