diff --git a/backend/src/db/migrations/20240626115035_admin-login-method-config.ts b/backend/src/db/migrations/20240626115035_admin-login-method-config.ts new file mode 100644 index 0000000000..8748fe753e --- /dev/null +++ b/backend/src/db/migrations/20240626115035_admin-login-method-config.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.SuperAdmin, "enabledLoginMethods"))) { + await knex.schema.alterTable(TableName.SuperAdmin, (tb) => { + tb.specificType("enabledLoginMethods", "text[]"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.SuperAdmin, "enabledLoginMethods")) { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + t.dropColumn("enabledLoginMethods"); + }); + } +} diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts index 29e41c78ef..b676b81a80 100644 --- a/backend/src/db/schemas/super-admin.ts +++ b/backend/src/db/schemas/super-admin.ts @@ -18,7 +18,8 @@ export const SuperAdminSchema = z.object({ trustSamlEmails: z.boolean().default(false).nullable().optional(), trustLdapEmails: z.boolean().default(false).nullable().optional(), trustOidcEmails: z.boolean().default(false).nullable().optional(), - defaultAuthOrgId: z.string().uuid().nullable().optional() + defaultAuthOrgId: z.string().uuid().nullable().optional(), + enabledLoginMethods: z.string().array().nullable().optional() }); export type TSuperAdmin = z.infer; diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index 22b64c16e6..a7e6dbcec1 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -34,6 +34,7 @@ import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; +import { LoginMethod } from "@app/services/super-admin/super-admin-types"; import { TUserDALFactory } from "@app/services/user/user-dal"; import { normalizeUsername } from "@app/services/user/user-fns"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; @@ -417,6 +418,13 @@ export const ldapConfigServiceFactory = ({ }: TLdapLoginDTO) => { const appCfg = getConfig(); const serverCfg = await getServerCfg(); + + if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.LDAP)) { + throw new BadRequestError({ + message: "Login with LDAP is disabled by administrator." + }); + } + let userAlias = await userAliasDAL.findOne({ externalId, orgId, diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index b983492bc2..55c929b9a9 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -26,6 +26,7 @@ import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; +import { LoginMethod } from "@app/services/super-admin/super-admin-types"; import { TUserDALFactory } from "@app/services/user/user-dal"; import { normalizeUsername } from "@app/services/user/user-fns"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; @@ -157,6 +158,13 @@ export const oidcConfigServiceFactory = ({ const oidcLogin = async ({ externalId, email, firstName, lastName, orgId, callbackPort }: TOidcLoginDTO) => { const serverCfg = await getServerCfg(); + + if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) { + throw new BadRequestError({ + message: "Login with OIDC is disabled by administrator." + }); + } + const appCfg = getConfig(); const userAlias = await userAliasDAL.findOne({ externalId, diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index 08a13bf067..c147b7e275 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -28,6 +28,7 @@ import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; +import { LoginMethod } from "@app/services/super-admin/super-admin-types"; import { TUserDALFactory } from "@app/services/user/user-dal"; import { normalizeUsername } from "@app/services/user/user-fns"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; @@ -335,6 +336,13 @@ export const samlConfigServiceFactory = ({ }: TSamlLoginDTO) => { const appCfg = getConfig(); const serverCfg = await getServerCfg(); + + if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.SAML)) { + throw new BadRequestError({ + message: "Login with SAML is disabled by administrator." + }); + } + const userAlias = await userAliasDAL.findOne({ externalId, orgId, diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 24c7e2a6e4..73d6cc7e78 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -8,6 +8,7 @@ import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; +import { LoginMethod } from "@app/services/super-admin/super-admin-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; export const registerAdminRouter = async (server: FastifyZodProvider) => { @@ -54,7 +55,14 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { trustSamlEmails: z.boolean().optional(), trustLdapEmails: z.boolean().optional(), trustOidcEmails: z.boolean().optional(), - defaultAuthOrgId: z.string().optional().nullable() + defaultAuthOrgId: z.string().optional().nullable(), + enabledLoginMethods: z + .nativeEnum(LoginMethod) + .array() + .optional() + .refine((methods) => !methods || methods.length > 0, { + message: "At least one login method should be enabled." + }) }), response: { 200: z.object({ @@ -70,7 +78,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }); }, handler: async (req) => { - const config = await server.services.superAdmin.updateServerCfg(req.body); + const config = await server.services.superAdmin.updateServerCfg(req.body, req.permission.id); return { config }; } }); diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 8090b937ab..2ce900b471 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -17,6 +17,7 @@ import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; import { TOrgDALFactory } from "../org/org-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; +import { LoginMethod } from "../super-admin/super-admin-types"; import { TUserDALFactory } from "../user/user-dal"; import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns"; import { @@ -158,9 +159,22 @@ export const authLoginServiceFactory = ({ const userEnc = await userDAL.findUserEncKeyByUsername({ username: email }); + const serverCfg = await getServerCfg(); + + if ( + serverCfg.enabledLoginMethods && + !serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) && + !providerAuthToken + ) { + throw new BadRequestError({ + message: "Login with email is disabled by administrator." + }); + } + if (!userEnc || (userEnc && !userEnc.isAccepted)) { throw new Error("Failed to find user"); } + if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) { validateProviderAuthToken(providerAuthToken as string, email); } @@ -507,6 +521,40 @@ export const authLoginServiceFactory = ({ let user = await userDAL.findUserByUsername(email); const serverCfg = await getServerCfg(); + if (serverCfg.enabledLoginMethods) { + switch (authMethod) { + case AuthMethod.GITHUB: { + if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITHUB)) { + throw new BadRequestError({ + message: "Login with Github is disabled by administrator.", + name: "Oauth 2 login" + }); + } + break; + } + case AuthMethod.GOOGLE: { + if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GOOGLE)) { + throw new BadRequestError({ + message: "Login with Google is disabled by administrator.", + name: "Oauth 2 login" + }); + } + break; + } + case AuthMethod.GITLAB: { + if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITLAB)) { + throw new BadRequestError({ + message: "Login with Gitlab is disabled by administrator.", + name: "Oauth 2 login" + }); + } + break; + } + default: + break; + } + } + const appCfg = getConfig(); if (!user) { diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 41b97efa42..70330c4ef7 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -12,7 +12,7 @@ import { AuthMethod } from "../auth/auth-type"; import { TOrgServiceFactory } from "../org/org-service"; import { TUserDALFactory } from "../user/user-dal"; import { TSuperAdminDALFactory } from "./super-admin-dal"; -import { TAdminSignUpDTO } from "./super-admin-types"; +import { LoginMethod, TAdminSignUpDTO } from "./super-admin-types"; type TSuperAdminServiceFactoryDep = { serverCfgDAL: TSuperAdminDALFactory; @@ -79,7 +79,37 @@ export const superAdminServiceFactory = ({ return newCfg; }; - const updateServerCfg = async (data: TSuperAdminUpdate) => { + const updateServerCfg = async (data: TSuperAdminUpdate, userId: string) => { + if (data.enabledLoginMethods) { + const superAdminUser = await userDAL.findById(userId); + const loginMethodToAuthMethod = { + [LoginMethod.EMAIL]: [AuthMethod.EMAIL], + [LoginMethod.GOOGLE]: [AuthMethod.GOOGLE], + [LoginMethod.GITLAB]: [AuthMethod.GITLAB], + [LoginMethod.GITHUB]: [AuthMethod.GITHUB], + [LoginMethod.LDAP]: [AuthMethod.LDAP], + [LoginMethod.OIDC]: [AuthMethod.OIDC], + [LoginMethod.SAML]: [ + AuthMethod.AZURE_SAML, + AuthMethod.GOOGLE_SAML, + AuthMethod.JUMPCLOUD_SAML, + AuthMethod.KEYCLOAK_SAML, + AuthMethod.OKTA_SAML + ] + }; + + if ( + !data.enabledLoginMethods.some((loginMethod) => + loginMethodToAuthMethod[loginMethod as LoginMethod].some( + (authMethod) => superAdminUser.authMethods?.includes(authMethod) + ) + ) + ) { + throw new BadRequestError({ + message: "You must configure at least one auth method to prevent account lockout" + }); + } + } const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, data); await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg)); @@ -167,7 +197,7 @@ export const superAdminServiceFactory = ({ orgName: initialOrganizationName }); - await updateServerCfg({ initialized: true }); + await updateServerCfg({ initialized: true }, userInfo.user.id); const token = await authService.generateUserTokens({ user: userInfo.user, authMethod: AuthMethod.EMAIL, diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index e444c8843a..f622c8f174 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -15,3 +15,13 @@ export type TAdminSignUpDTO = { ip: string; userAgent: string; }; + +export enum LoginMethod { + EMAIL = "email", + GOOGLE = "google", + GITHUB = "github", + GITLAB = "gitlab", + SAML = "saml", + LDAP = "ldap", + OIDC = "oidc" +} diff --git a/frontend/src/components/signup/InitialSignupStep.tsx b/frontend/src/components/signup/InitialSignupStep.tsx index e5c23f3334..3a7c6db045 100644 --- a/frontend/src/components/signup/InitialSignupStep.tsx +++ b/frontend/src/components/signup/InitialSignupStep.tsx @@ -4,6 +4,9 @@ import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons import { faEnvelope } from "@fortawesome/free-regular-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useServerConfig } from "@app/context"; +import { LoginMethod } from "@app/hooks/api/admin/types"; + import { Button } from "../v2"; export default function InitialSignupStep({ @@ -12,67 +15,79 @@ export default function InitialSignupStep({ setIsSignupWithEmail: (value: boolean) => void; }) { const { t } = useTranslation(); + const { config } = useServerConfig(); + + const shouldDisplaySignupMethod = (method: LoginMethod) => + !config.enabledLoginMethods || config.enabledLoginMethods.includes(method); return (

{t("signup.initial-title")}

-
- -
-
- -
-
- -
-
- -
+ {shouldDisplaySignupMethod(LoginMethod.GOOGLE) && ( +
+ +
+ )} + {shouldDisplaySignupMethod(LoginMethod.GITHUB) && ( +
+ +
+ )} + {shouldDisplaySignupMethod(LoginMethod.GITLAB) && ( +
+ +
+ )} + {shouldDisplaySignupMethod(LoginMethod.EMAIL) && ( +
+ +
+ )}
{t("signup.create-policy")}
diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 524bc6ace0..4d5add1e0e 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -1,3 +1,13 @@ +export enum LoginMethod { + EMAIL = "email", + GOOGLE = "google", + GITHUB = "github", + GITLAB = "gitlab", + SAML = "saml", + LDAP = "ldap", + OIDC = "oidc" +} + export type TServerConfig = { initialized: boolean; allowSignUp: boolean; @@ -9,6 +19,7 @@ export type TServerConfig = { isSecretScanningDisabled: boolean; defaultAuthOrgSlug: string | null; defaultAuthOrgId: string | null; + enabledLoginMethods: LoginMethod[]; }; export type TCreateAdminUserDTO = { diff --git a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx index 0616ee58d1..b2df75865b 100644 --- a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx +++ b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx @@ -15,6 +15,7 @@ import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config"; import { Button, Input } from "@app/components/v2"; import { useServerConfig } from "@app/context"; import { useFetchServerStatus } from "@app/hooks/api"; +import { LoginMethod } from "@app/hooks/api/admin/types"; import { useNavigateToSelectOrganization } from "../../Login.utils"; @@ -61,6 +62,9 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: } }, []); + const shouldDisplayLoginMethod = (method: LoginMethod) => + !config.enabledLoginMethods || config.enabledLoginMethods.includes(method); + const handleLogin = async (e: FormEvent) => { e.preventDefault(); try { @@ -162,156 +166,179 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:

Login to Infisical

-
- -
-
- +
+ )} + {shouldDisplayLoginMethod(LoginMethod.GITHUB) && ( +
+ -
-
- +
+ )} + {shouldDisplayLoginMethod(LoginMethod.GITLAB) && ( +
+ -
-
- -
-
- -
-
- -
-
-
- or -
-
-
- setEmail(e.target.value)} - type="email" - placeholder="Enter your email..." - isRequired - autoComplete="username" - className="h-10" - /> -
-
- setPassword(e.target.value)} - type="password" - placeholder="Enter your password..." - isRequired - autoComplete="current-password" - id="current-password" - className="select:-webkit-autofill:focus h-10" - /> -
- {shouldShowCaptcha && ( -
- setCaptchaToken(token)} - ref={captchaRef} - /> + window.close(); + }} + leftIcon={} + className="mx-0 h-10 w-full" + > + Continue with GitLab +
)} -
- -
+ {shouldDisplayLoginMethod(LoginMethod.SAML) && ( +
+ +
+ )} + {shouldDisplayLoginMethod(LoginMethod.OIDC) && ( +
+ +
+ )} + {shouldDisplayLoginMethod(LoginMethod.LDAP) && ( +
+ +
+ )} + {(!config.enabledLoginMethods || + (shouldDisplayLoginMethod(LoginMethod.EMAIL) && config.enabledLoginMethods.length > 1)) && ( +
+
+ or +
+
+ )} + {shouldDisplayLoginMethod(LoginMethod.EMAIL) && ( + <> +
+ setEmail(e.target.value)} + type="email" + placeholder="Enter your email..." + isRequired + autoComplete="username" + className="h-10" + /> +
+
+ setPassword(e.target.value)} + type="password" + placeholder="Enter your password..." + isRequired + autoComplete="current-password" + id="current-password" + className="select:-webkit-autofill:focus h-10" + /> +
+ {shouldShowCaptcha && ( +
+ setCaptchaToken(token)} + ref={captchaRef} + /> +
+ )} +
+ +
+ + )} {!isLoading && loginError && } - {config.allowSignUp ? ( + {config.allowSignUp && + (shouldDisplayLoginMethod(LoginMethod.EMAIL) || + shouldDisplayLoginMethod(LoginMethod.GOOGLE) || + shouldDisplayLoginMethod(LoginMethod.GITHUB) || + shouldDisplayLoginMethod(LoginMethod.GITLAB)) ? (
@@ -322,13 +349,15 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: ) : (
)} -
- - - Forgot password? Recover your account - - -
+ {shouldDisplayLoginMethod(LoginMethod.EMAIL) && ( +
+ + + Forgot password? Recover your account + + +
+ )} ); }; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgAuthTab.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgAuthTab.tsx index 31d6987bd8..d344b8a28e 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgAuthTab.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgAuthTab.tsx @@ -1,5 +1,6 @@ -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { OrgPermissionActions, OrgPermissionSubjects, useServerConfig } from "@app/context"; import { withPermission } from "@app/hoc"; +import { LoginMethod } from "@app/hooks/api/admin/types"; import { OrgGeneralAuthSection } from "./OrgGeneralAuthSection"; import { OrgLDAPSection } from "./OrgLDAPSection"; @@ -9,12 +10,23 @@ import { OrgSSOSection } from "./OrgSSOSection"; export const OrgAuthTab = withPermission( () => { + const { + config: { enabledLoginMethods } + } = useServerConfig(); + + const shouldDisplaySection = (method: LoginMethod) => + !enabledLoginMethods || enabledLoginMethods.includes(method); + return (
- - - - + {shouldDisplaySection(LoginMethod.SAML) && ( + <> + + + + )} + {shouldDisplaySection(LoginMethod.OIDC) && } + {shouldDisplaySection(LoginMethod.LDAP) && }
); diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgGeneralAuthSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgGeneralAuthSection.tsx index 7c3af9493f..6e4eca456b 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgGeneralAuthSection.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgGeneralAuthSection.tsx @@ -11,7 +11,6 @@ import { useLogoutUser, useUpdateOrg } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; export const OrgGeneralAuthSection = () => { - const { currentOrg } = useOrganization(); const { subscription } = useSubscription(); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); @@ -88,6 +87,7 @@ export const OrgGeneralAuthSection = () => { Enforce members to authenticate via SAML to access this organization

+
handlePopUpToggle("upgradePlan", isOpen)} diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgLDAPSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgLDAPSection.tsx index 458dc0a102..74d35d199c 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgLDAPSection.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgLDAPSection.tsx @@ -95,7 +95,6 @@ export const OrgLDAPSection = (): JSX.Element => { return ( <> -

LDAP

@@ -152,6 +151,7 @@ export const OrgLDAPSection = (): JSX.Element => {

)} +
{ return ( <> -

OIDC

@@ -103,6 +102,7 @@ export const OrgOIDCSection = (): JSX.Element => {

)} +
{ - const { currentOrg } = useOrganization(); const { subscription } = useSubscription(); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ @@ -59,7 +58,6 @@ export const OrgScimSection = () => { return ( <> -

SCIM

diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgSSOSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgSSOSection.tsx index 67daf0fca0..bcb4f88378 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgSSOSection.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgSSOSection.tsx @@ -15,7 +15,7 @@ import { SSOModal } from "./SSOModal"; export const OrgSSOSection = (): JSX.Element => { const { currentOrg } = useOrganization(); const { subscription } = useSubscription(); - + const { data, isLoading } = useGetSSOConfig(currentOrg?.id ?? ""); const { mutateAsync } = useUpdateSSOConfig(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ @@ -115,6 +115,7 @@ export const OrgSSOSection = (): JSX.Element => { Allow members to authenticate into Infisical with SAML

+
; export const AuthMethodSection = () => { - const { user } = useUser(); + const { config } = useServerConfig(); const { mutateAsync } = useUpdateUserAuthMethods(); const { reset, setValue, watch } = useForm({ @@ -102,6 +103,14 @@ export const AuthMethodSection = () => {
{user && authMethodOpts.map((authMethodOpt) => { + // only filter when enabledLoginMethods is explicitly configured by admin + if ( + config.enabledLoginMethods && + !config.enabledLoginMethods.includes(authMethodOpt.loginMethod) + ) { + return null; + } + return (
diff --git a/frontend/src/views/admin/DashboardPage/AuthPanel.tsx b/frontend/src/views/admin/DashboardPage/AuthPanel.tsx new file mode 100644 index 0000000000..84e48ea0f6 --- /dev/null +++ b/frontend/src/views/admin/DashboardPage/AuthPanel.tsx @@ -0,0 +1,252 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Switch } from "@app/components/v2"; +import { useServerConfig } from "@app/context"; +import { useUpdateServerConfig } from "@app/hooks/api"; +import { LoginMethod } from "@app/hooks/api/admin/types"; + +const formSchema = z.object({ + isEmailEnabled: z.boolean(), + isGoogleEnabled: z.boolean(), + isGithubEnabled: z.boolean(), + isGitlabEnabled: z.boolean(), + isSamlEnabled: z.boolean(), + isLdapEnabled: z.boolean(), + isOidcEnabled: z.boolean() +}); + +type TAuthForm = z.infer; + +export const AuthPanel = () => { + const { config } = useServerConfig(); + const { enabledLoginMethods } = config; + const { mutateAsync: updateServerConfig } = useUpdateServerConfig(); + + const { + control, + handleSubmit, + formState: { isSubmitting, isDirty } + } = useForm({ + resolver: zodResolver(formSchema), + // if not yet explicitly defined by the admin, all login methods should be enabled by default + values: enabledLoginMethods + ? { + isEmailEnabled: enabledLoginMethods.includes(LoginMethod.EMAIL), + isGoogleEnabled: enabledLoginMethods.includes(LoginMethod.GOOGLE), + isGithubEnabled: enabledLoginMethods.includes(LoginMethod.GITHUB), + isGitlabEnabled: enabledLoginMethods.includes(LoginMethod.GITLAB), + isSamlEnabled: enabledLoginMethods.includes(LoginMethod.SAML), + isLdapEnabled: enabledLoginMethods.includes(LoginMethod.LDAP), + isOidcEnabled: enabledLoginMethods.includes(LoginMethod.OIDC) + } + : { + isEmailEnabled: true, + isGoogleEnabled: true, + isGithubEnabled: true, + isGitlabEnabled: true, + isSamlEnabled: true, + isLdapEnabled: true, + isOidcEnabled: true + } + }); + + const onAuthFormSubmit = async (formData: TAuthForm) => { + try { + const enabledMethods: LoginMethod[] = []; + if (formData.isEmailEnabled) { + enabledMethods.push(LoginMethod.EMAIL); + } + + if (formData.isGoogleEnabled) { + enabledMethods.push(LoginMethod.GOOGLE); + } + + if (formData.isGithubEnabled) { + enabledMethods.push(LoginMethod.GITHUB); + } + + if (formData.isGitlabEnabled) { + enabledMethods.push(LoginMethod.GITLAB); + } + + if (formData.isSamlEnabled) { + enabledMethods.push(LoginMethod.SAML); + } + + if (formData.isLdapEnabled) { + enabledMethods.push(LoginMethod.LDAP); + } + + if (formData.isOidcEnabled) { + enabledMethods.push(LoginMethod.OIDC); + } + + if (!enabledMethods.length) { + createNotification({ + type: "error", + text: "At least one login method should be enabled." + }); + return; + } + + await updateServerConfig({ + enabledLoginMethods: enabledMethods + }); + + createNotification({ + text: "Login methods have been successfully updated.", + type: "success" + }); + } catch (e) { + console.error(e); + createNotification({ + type: "error", + text: "Failed to update login methods." + }); + } + }; + + return ( +
+
+
Login Methods
+
+ Select the login methods you wish to allow for all users of this instance. +
+ { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

Email

+
+
+ ); + }} + /> + { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

Google SSO

+
+
+ ); + }} + /> + { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

Github SSO

+
+
+ ); + }} + /> + { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

Gitlab SSO

+
+
+ ); + }} + /> + { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

SAML SSO

+
+
+ ); + }} + /> + { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

OIDC SSO

+
+
+ ); + }} + /> +
+ { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

LDAP

+
+
+ ); + }} + /> + + + ); +}; diff --git a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx index 134a67f8cd..42d3cdc1b4 100644 --- a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx @@ -24,10 +24,12 @@ import { import { useOrganization, useServerConfig, useUser } from "@app/context"; import { useGetOrganizations, useUpdateServerConfig } from "@app/hooks/api"; +import { AuthPanel } from "./AuthPanel"; import { RateLimitPanel } from "./RateLimitPanel"; enum TabSections { Settings = "settings", + Auth = "auth", RateLimit = "rate-limit" } @@ -120,7 +122,7 @@ export const AdminDashboardPage = () => {

Admin Dashboard

-

Manage your Infisical instance.

+

Manage your instance level configurations.

{isUserLoading || isNotAllowed ? ( @@ -131,6 +133,7 @@ export const AdminDashboardPage = () => {
General + Authentication Rate Limit
@@ -203,7 +206,8 @@ export const AdminDashboardPage = () => { Default organization
- Select the default organization you want to set for SAML/LDAP based logins. When selected, user logins will be automatically scoped to the selected organization. + Select the default organization you want to set for SAML/LDAP based logins. When + selected, user logins will be automatically scoped to the selected organization.
{ + + +