From bd1ed2614e8b06aa5b13d92f40087e51687e2866 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Thu, 17 Oct 2024 03:02:26 +0800 Subject: [PATCH] feat: added enforceMfa toggle for orgs --- .../20241016183616_add-org-enforce-mfa.ts | 19 +++++ backend/src/db/schemas/organizations.ts | 3 +- .../src/ee/services/license/license-fns.ts | 3 +- .../src/ee/services/license/license-types.ts | 1 + .../server/routes/v1/organization-router.ts | 3 +- backend/src/services/org/org-service.ts | 5 +- backend/src/services/org/org-types.ts | 1 + .../src/hooks/api/organization/queries.tsx | 13 +++- frontend/src/hooks/api/organization/types.ts | 2 + frontend/src/hooks/api/subscriptions/types.ts | 1 + .../components/OrgAuthTab/OrgAuthTab.tsx | 2 + .../OrgAuthTab/OrgGenericAuthSection.tsx | 73 +++++++++++++++++++ 12 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 backend/src/db/migrations/20241016183616_add-org-enforce-mfa.ts create mode 100644 frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgGenericAuthSection.tsx diff --git a/backend/src/db/migrations/20241016183616_add-org-enforce-mfa.ts b/backend/src/db/migrations/20241016183616_add-org-enforce-mfa.ts new file mode 100644 index 000000000..d01f1698e --- /dev/null +++ b/backend/src/db/migrations/20241016183616_add-org-enforce-mfa.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.Organization, "enforceMfa"))) { + await knex.schema.alterTable(TableName.Organization, (tb) => { + tb.boolean("enforceMfa").defaultTo(false).notNullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.Organization, "enforceMfa")) { + await knex.schema.alterTable(TableName.Organization, (t) => { + t.dropColumn("enforceMfa"); + }); + } +} diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index 7bd20d94d..31de98168 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -20,7 +20,8 @@ export const OrganizationsSchema = z.object({ scimEnabled: z.boolean().default(false).nullable().optional(), kmsDefaultKeyId: z.string().uuid().nullable().optional(), kmsEncryptedDataKey: zodBuffer.nullable().optional(), - defaultMembershipRole: z.string().default("member") + defaultMembershipRole: z.string().default("member"), + enforceMfa: z.boolean().default(false) }); export type TOrganizations = z.infer; diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index fa67b72d1..031c9f1a2 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -46,7 +46,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ writeLimit: 200, secretsLimit: 40 }, - pkiEst: false + pkiEst: false, + enforceMfa: false }); export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 6e671c26f..c331679b5 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -64,6 +64,7 @@ export type TFeatureSet = { secretsLimit: number; }; pkiEst: boolean; + enforceMfa: boolean; }; export type TOrgPlansTableDTO = { diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index 00f039723..f6d5cacef 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -226,7 +226,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { .refine((v) => slugify(v) === v, { message: "Membership role must be a valid slug" }) - .optional() + .optional(), + enforceMfa: z.boolean().optional() }), response: { 200: z.object({ diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index b4b8775f0..f5e15bbda 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -268,7 +268,7 @@ export const orgServiceFactory = ({ actorOrgId, actorAuthMethod, orgId, - data: { name, slug, authEnforced, scimEnabled, defaultMembershipRoleSlug } + data: { name, slug, authEnforced, scimEnabled, defaultMembershipRoleSlug, enforceMfa } }: TUpdateOrgDTO) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); @@ -317,7 +317,8 @@ export const orgServiceFactory = ({ slug: slug ? slugify(slug) : undefined, authEnforced, scimEnabled, - defaultMembershipRole + defaultMembershipRole, + enforceMfa }); if (!org) throw new NotFoundError({ message: "Organization not found" }); return org; diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index d62a2c25b..5b44eeea5 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -64,6 +64,7 @@ export type TUpdateOrgDTO = { authEnforced: boolean; scimEnabled: boolean; defaultMembershipRoleSlug: string; + enforceMfa: boolean; }>; } & TOrgPermission; diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 1de64e058..caab2408d 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -82,13 +82,22 @@ export const useCreateOrg = (options: { invalidate: boolean } = { invalidate: tr export const useUpdateOrg = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, UpdateOrgDTO>({ - mutationFn: ({ name, authEnforced, scimEnabled, slug, orgId, defaultMembershipRoleSlug }) => { + mutationFn: ({ + name, + authEnforced, + scimEnabled, + slug, + orgId, + defaultMembershipRoleSlug, + enforceMfa + }) => { return apiRequest.patch(`/api/v1/organization/${orgId}`, { name, authEnforced, scimEnabled, slug, - defaultMembershipRoleSlug + defaultMembershipRoleSlug, + enforceMfa }); }, onSuccess: () => { diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts index 3d8d5474b..b644cb325 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -11,6 +11,7 @@ export type Organization = { scimEnabled: boolean; slug: string; defaultMembershipRole: string; + enforceMfa: boolean; }; export type UpdateOrgDTO = { @@ -20,6 +21,7 @@ export type UpdateOrgDTO = { scimEnabled?: boolean; slug?: string; defaultMembershipRoleSlug?: string; + enforceMfa?: boolean; }; export type BillingDetails = { diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 6ee571d93..3506295fa 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -42,4 +42,5 @@ export type SubscriptionPlan = { instanceUserManagement: boolean; externalKms: boolean; pkiEst: boolean; + enforceMfa: boolean; }; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgAuthTab.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgAuthTab.tsx index f4c4c0c5d..c0ff17ae6 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgAuthTab.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgAuthTab.tsx @@ -16,6 +16,7 @@ import { LoginMethod } from "@app/hooks/api/admin/types"; import { LDAPModal } from "./LDAPModal"; import { OIDCModal } from "./OIDCModal"; import { OrgGeneralAuthSection } from "./OrgGeneralAuthSection"; +import { OrgGenericAuthSection } from "./OrgGenericAuthSection"; import { OrgLDAPSection } from "./OrgLDAPSection"; import { OrgOIDCSection } from "./OrgOIDCSection"; import { OrgScimSection } from "./OrgSCIMSection"; @@ -161,6 +162,7 @@ export const OrgAuthTab = withPermission( return ( <> + {shouldShowCreateIdentityProviderView ? ( createIdentityProviderView ) : ( diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgGenericAuthSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgGenericAuthSection.tsx new file mode 100644 index 000000000..3a6663dde --- /dev/null +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgGenericAuthSection.tsx @@ -0,0 +1,73 @@ +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { Switch, UpgradePlanModal } from "@app/components/v2"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization, + useSubscription +} from "@app/context"; +import { useUpdateOrg } from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +export const OrgGenericAuthSection = () => { + const { currentOrg } = useOrganization(); + const { subscription } = useSubscription(); + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); + + const { mutateAsync } = useUpdateOrg(); + + const handleEnforceMfaToggle = async (value: boolean) => { + try { + if (!currentOrg?.id) return; + if (!subscription?.enforceMfa) { + handlePopUpOpen("upgradePlan"); + return; + } + + await mutateAsync({ + orgId: currentOrg?.id, + enforceMfa: value + }); + + createNotification({ + text: `Successfully ${value ? "enforced" : "un-enforced"} MFA`, + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: (err as { response: { data: { message: string } } }).response.data.message, + type: "error" + }); + } + }; + + return ( +
+
+
+

Enforce Multi-factor Authentication

+ + {(isAllowed) => ( + handleEnforceMfaToggle(value)} + isChecked={currentOrg?.enforceMfa ?? false} + isDisabled={!isAllowed} + /> + )} + +
+

+ Enforce members to authenticate with MFA in order to access the organization +

+
+ handlePopUpToggle("upgradePlan", isOpen)} + text="You can enforce user MFA if you switch to Infisical's Pro plan." + /> +
+ ); +};