From 98a15a901ec17c38c9312ce0e5196b45524475ba Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 26 Jun 2024 22:45:14 +0800 Subject: [PATCH] feat: allowed toggling login options as admin --- ...0240626115035_admin-login-method-config.ts | 19 ++ backend/src/db/schemas/super-admin.ts | 3 +- backend/src/server/routes/v1/admin-router.ts | 10 +- .../services/super-admin/super-admin-types.ts | 10 + frontend/src/hooks/api/admin/types.ts | 11 + .../components/InitialStep/InitialStep.tsx | 321 ++++++++++-------- .../views/admin/DashboardPage/AuthPanel.tsx | 251 ++++++++++++++ .../admin/DashboardPage/DashboardPage.tsx | 9 +- 8 files changed, 488 insertions(+), 146 deletions(-) create mode 100644 backend/src/db/migrations/20240626115035_admin-login-method-config.ts create mode 100644 frontend/src/views/admin/DashboardPage/AuthPanel.tsx 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 000000000..8748fe753 --- /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 29e41c78e..b676b81a8 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/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 24c7e2a6e..4557152bd 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({ diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index e444c8843..f622c8f17 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/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 524bc6ace..4d5add1e0 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 0616ee58d..83615ccf5 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"; @@ -162,154 +163,188 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:

Login to Infisical

-
- -
-
- +
+ ))} + {!config.enabledLoginMethods || + (config.enabledLoginMethods.includes(LoginMethod.GITHUB) && ( +
+ -
-
- +
+ ))} + {!config.enabledLoginMethods || + (config.enabledLoginMethods.includes(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 + +
+ ))} + {!config.enabledLoginMethods || + (config.enabledLoginMethods.includes(LoginMethod.SAML) && ( +
+ +
+ ))} + {!config.enabledLoginMethods || + (config.enabledLoginMethods.includes(LoginMethod.OIDC) && ( +
+ +
+ ))} + {!config.enabledLoginMethods || + (config.enabledLoginMethods.includes(LoginMethod.LDAP) && ( +
+ +
+ ))} + + {!config.enabledLoginMethods || + (config.enabledLoginMethods.length > 1 && + config.enabledLoginMethods.includes(LoginMethod.EMAIL) && ( +
+
+ or +
+
+ ))} + {!config.enabledLoginMethods || + (config.enabledLoginMethods.includes(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 ? (
diff --git a/frontend/src/views/admin/DashboardPage/AuthPanel.tsx b/frontend/src/views/admin/DashboardPage/AuthPanel.tsx new file mode 100644 index 000000000..0060cb095 --- /dev/null +++ b/frontend/src/views/admin/DashboardPage/AuthPanel.tsx @@ -0,0 +1,251 @@ +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), + 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 available to users +
+ { + 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 134a67f8c..bf8a5a4a9 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" } @@ -131,6 +133,7 @@ export const AdminDashboardPage = () => {
General + Auth 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.
{ + + +