From ab19e7df6df2357f066642ff9ab427a21f04b4b2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:16:19 +0200 Subject: [PATCH 01/19] Feat: Default SAML/LDAP organization slug --- .../20240620142418_default-saml-ldap-org.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 backend/src/db/migrations/20240620142418_default-saml-ldap-org.ts diff --git a/backend/src/db/migrations/20240620142418_default-saml-ldap-org.ts b/backend/src/db/migrations/20240620142418_default-saml-ldap-org.ts new file mode 100644 index 000000000..67753b679 --- /dev/null +++ b/backend/src/db/migrations/20240620142418_default-saml-ldap-org.ts @@ -0,0 +1,27 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +const DEFAULT_ORG_ID_FIELD = "defaultOrgId"; + +export async function up(knex: Knex): Promise { + const hasDefaultOrgColumn = await knex.schema.hasColumn(TableName.SuperAdmin, DEFAULT_ORG_ID_FIELD); + + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + if (!hasDefaultOrgColumn) { + t.uuid(DEFAULT_ORG_ID_FIELD).nullable(); + t.foreign(DEFAULT_ORG_ID_FIELD).references("id").inTable(TableName.Organization).onDelete("SET NULL"); + } + }); +} + +export async function down(knex: Knex): Promise { + const hasDefaultOrgColumn = await knex.schema.hasColumn(TableName.SuperAdmin, DEFAULT_ORG_ID_FIELD); + + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + if (hasDefaultOrgColumn) { + t.dropForeign([DEFAULT_ORG_ID_FIELD]); + t.dropColumn(DEFAULT_ORG_ID_FIELD); + } + }); +} From dfb53dd3330106d99772002ff888e23cc3ee6867 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:16:32 +0200 Subject: [PATCH 02/19] Helper omit function --- backend/src/lib/fn/object.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/backend/src/lib/fn/object.ts b/backend/src/lib/fn/object.ts index 87db80343..8e9ede8db 100644 --- a/backend/src/lib/fn/object.ts +++ b/backend/src/lib/fn/object.ts @@ -13,6 +13,22 @@ export const pick = (obj: T, keys: TKey ); }; +/** + * Omit a list of properties from an object + * into a new object + */ +export const omit = (obj: T, keys: TKeys[]): Omit => { + if (!obj) return {} as Omit; + return (Object.keys(obj) as TKeys[]).reduce( + (acc, key) => { + if (!keys.includes(key)) { + (acc as T)[key] = obj[key]; + } + return acc; + }, + {} as Omit + ); +}; /** * Removes (shakes out) undefined entries from an * object. Optional second argument shakes out values From 04dee70a55e8708dd5379fc7a7fb4482fc772d6e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:17:02 +0200 Subject: [PATCH 03/19] Type changes --- backend/src/db/schemas/super-admin.ts | 3 ++- backend/src/server/routes/v1/admin-router.ts | 8 ++++++-- backend/src/services/super-admin/super-admin-types.ts | 6 ++++++ frontend/src/hooks/api/admin/types.ts | 2 ++ frontend/src/hooks/api/serverDetails/types.ts | 2 +- 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts index 87ba35c83..1f2b6820b 100644 --- a/backend/src/db/schemas/super-admin.ts +++ b/backend/src/db/schemas/super-admin.ts @@ -17,7 +17,8 @@ export const SuperAdminSchema = z.object({ instanceId: z.string().uuid().default("00000000-0000-0000-0000-000000000000"), trustSamlEmails: z.boolean().default(false).nullable().optional(), trustLdapEmails: z.boolean().default(false).nullable().optional(), - trustOidcEmails: z.boolean().default(false).nullable().optional() + trustOidcEmails: z.boolean().default(false).nullable().optional(), + defaultOrgId: z.string().uuid().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 ea701c828..59a9f064e 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -22,6 +22,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { 200: z.object({ config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }).extend({ isMigrationModeOn: z.boolean(), + defaultOrgSlug: z.string().nullable(), isSecretScanningDisabled: z.boolean() }) }) @@ -52,11 +53,14 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { allowedSignUpDomain: z.string().optional().nullable(), trustSamlEmails: z.boolean().optional(), trustLdapEmails: z.boolean().optional(), - trustOidcEmails: z.boolean().optional() + trustOidcEmails: z.boolean().optional(), + defaultOrgSlug: z.string().optional().nullable() }), response: { 200: z.object({ - config: SuperAdminSchema + config: SuperAdminSchema.extend({ + defaultOrgSlug: z.string().nullable() + }) }) } }, diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index e444c8843..0c3789a37 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -1,3 +1,5 @@ +import { TSuperAdminUpdate } from "@app/db/schemas"; + export type TAdminSignUpDTO = { email: string; password: string; @@ -15,3 +17,7 @@ export type TAdminSignUpDTO = { ip: string; userAgent: string; }; + +export type TUpdateServerCfgDTO = Omit & { + defaultOrgSlug?: string | null; +}; diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 0d06a2aa1..f1abf1a7a 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -7,6 +7,8 @@ export type TServerConfig = { trustLdapEmails: boolean; trustOidcEmails: boolean; isSecretScanningDisabled: boolean; + defaultOrgSlug: string | null; + defaultOrgId: string | null; }; export type TCreateAdminUserDTO = { diff --git a/frontend/src/hooks/api/serverDetails/types.ts b/frontend/src/hooks/api/serverDetails/types.ts index 911526404..3e22c2684 100644 --- a/frontend/src/hooks/api/serverDetails/types.ts +++ b/frontend/src/hooks/api/serverDetails/types.ts @@ -4,5 +4,5 @@ export type ServerStatus = { emailConfigured: boolean; secretScanningConfigured: boolean; redisConfigured: boolean; - samlDefaultOrgSlug: boolean + samlDefaultOrgSlug: string; }; From aa6cca738e48d16fde45c6e4e9222c49e33915b6 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:17:14 +0200 Subject: [PATCH 04/19] Update index.ts --- backend/src/server/routes/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 265fc18fd..21e867da4 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -458,6 +458,7 @@ export const registerRoutes = async ( userDAL, authService: loginService, serverCfgDAL: superAdminDAL, + orgDAL, orgService, keyStore }); From 30b8d597963de25d522bcc53dc881f016ba6089b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:17:23 +0200 Subject: [PATCH 05/19] Feat: Default SAML/LDAP organization slug --- .../services/super-admin/super-admin-dal.ts | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/backend/src/services/super-admin/super-admin-dal.ts b/backend/src/services/super-admin/super-admin-dal.ts index 64133ed7e..90f1004da 100644 --- a/backend/src/services/super-admin/super-admin-dal.ts +++ b/backend/src/services/super-admin/super-admin-dal.ts @@ -1,7 +1,30 @@ import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { TableName, TSuperAdmin } from "@app/db/schemas"; import { ormify } from "@app/lib/knex"; export type TSuperAdminDALFactory = ReturnType; -export const superAdminDALFactory = (db: TDbClient) => ormify(db, TableName.SuperAdmin, {}); +export const superAdminDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.SuperAdmin); + + const findById = async (id: string) => { + const config = await db(TableName.SuperAdmin) + .where(`${TableName.SuperAdmin}.id`, id) + .leftJoin(TableName.Organization, `${TableName.SuperAdmin}.defaultOrgId`, `${TableName.Organization}.id`) + .select( + db.ref("*").withSchema(TableName.SuperAdmin) as unknown as keyof TSuperAdmin, + db.ref("slug").withSchema(TableName.Organization).as("defaultOrgSlug") + ) + .first(); + + return { + defaultOrgSlug: config?.defaultOrgSlug || null, + ...config + } as TSuperAdmin & { defaultOrgSlug: string | null }; + }; + + return { + ...orm, + findById + }; +}; From 1930d40be80be3ffc4faa5d3e0503899527c8be1 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:17:33 +0200 Subject: [PATCH 06/19] Feat: Default SAML/LDAP organization slug --- .../super-admin/super-admin-service.ts | 45 +++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 27e198d85..b06757984 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -1,21 +1,24 @@ import bcrypt from "bcrypt"; -import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; +import { TOrganizations, TSuperAdmin } from "@app/db/schemas"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError } from "@app/lib/errors"; +import { omit } from "@app/lib/fn"; import { TAuthLoginFactory } from "../auth/auth-login-service"; import { AuthMethod } from "../auth/auth-type"; +import { TOrgDALFactory } from "../org/org-dal"; 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 { TAdminSignUpDTO, TUpdateServerCfgDTO } from "./super-admin-types"; type TSuperAdminServiceFactoryDep = { serverCfgDAL: TSuperAdminDALFactory; + orgDAL: Pick; userDAL: TUserDALFactory; authService: Pick; orgService: Pick; @@ -25,7 +28,7 @@ type TSuperAdminServiceFactoryDep = { export type TSuperAdminServiceFactory = ReturnType; // eslint-disable-next-line -export let getServerCfg: () => Promise; +export let getServerCfg: () => Promise; const ADMIN_CONFIG_KEY = "infisical-admin-cfg"; const ADMIN_CONFIG_KEY_EXP = 60; // 60s @@ -33,6 +36,7 @@ const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000"; export const superAdminServiceFactory = ({ serverCfgDAL, + orgDAL, userDAL, authService, orgService, @@ -42,16 +46,18 @@ export const superAdminServiceFactory = ({ // TODO(akhilmhdh): bad pattern time less change this later to me itself getServerCfg = async () => { const config = await keyStore.getItem(ADMIN_CONFIG_KEY); + // missing in keystore means fetch from db if (!config) { const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); + if (serverCfg) { await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(serverCfg)); // insert it back to keystore } return serverCfg; } - const keyStoreServerCfg = JSON.parse(config) as TSuperAdmin; + const keyStoreServerCfg = JSON.parse(config) as TSuperAdmin & { defaultOrgSlug: string | null }; return { ...keyStoreServerCfg, // this is to allow admin router to work @@ -70,10 +76,33 @@ export const superAdminServiceFactory = ({ return newCfg; }; - const updateServerCfg = async (data: TSuperAdminUpdate) => { - const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, data); - await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg)); - return updatedServerCfg; + const updateServerCfg = async (data: TUpdateServerCfgDTO) => { + let organization: TOrganizations | undefined; + if (data.defaultOrgSlug) { + organization = await orgDAL.findOne({ + slug: data.defaultOrgSlug + }); + + if (!organization) { + throw new BadRequestError({ + name: "Update server config", + message: "Failed to find default organization" + }); + } + } + + const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, { + ...omit(data, ["defaultOrgSlug"]), + defaultOrgId: organization?.id || null + }); + + const result = { + ...updatedServerCfg, + defaultOrgSlug: organization?.slug || null + }; + + await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(result)); + return result; }; const adminSignUp = async ({ From 45da56346546a338adb185de60dffc715205dc0c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:18:08 +0200 Subject: [PATCH 07/19] Convert navigate function to hook --- frontend/src/views/Login/Login.tsx | 9 +++-- .../components/InitialStep/InitialStep.tsx | 36 ++++++++++++------- .../Login/components/MFAStep/MFAStep.tsx | 9 ++--- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/frontend/src/views/Login/Login.tsx b/frontend/src/views/Login/Login.tsx index cad207aa8..36cd355f5 100644 --- a/frontend/src/views/Login/Login.tsx +++ b/frontend/src/views/Login/Login.tsx @@ -1,16 +1,15 @@ import { useEffect, useState } from "react"; -import { useRouter } from "next/router"; import { isLoggedIn } from "@app/reactQuery"; import { InitialStep, MFAStep, SSOStep } from "./components"; -import { navigateUserToSelectOrg } from "./Login.utils"; +import { useNavigateToSelectOrganization } from "./Login.utils"; export const Login = () => { - const router = useRouter(); const [step, setStep] = useState(0); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); + const { navigateToSelectOrganization } = useNavigateToSelectOrganization(); const queryParams = new URLSearchParams(window.location.search); @@ -21,10 +20,10 @@ export const Login = () => { const callbackPort = queryParams?.get("callback_port"); // case: a callback port is set, meaning it's a cli login request: redirect to select org with callback port if (callbackPort) { - navigateUserToSelectOrg(router, callbackPort); + navigateToSelectOrganization(callbackPort); } else { // case: no callback port, meaning it's a regular login request: redirect to select org - navigateUserToSelectOrg(router); + navigateToSelectOrganization(); } } catch (error) { console.log("Error - Not logged in yet"); diff --git a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx index 82bee7eb4..bce018af0 100644 --- a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx +++ b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx @@ -1,4 +1,4 @@ -import { FormEvent, useEffect, useRef, useState } from "react"; +import { FormEvent, useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -16,7 +16,7 @@ import { Button, Input } from "@app/components/v2"; import { useServerConfig } from "@app/context"; import { useFetchServerStatus } from "@app/hooks/api"; -import { navigateUserToSelectOrg } from "../../Login.utils"; +import { useNavigateToSelectOrganization } from "../../Login.utils"; type Props = { setStep: (step: number) => void; @@ -39,16 +39,28 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: const captchaRef = useRef(null); const { data: serverDetails } = useFetchServerStatus(); + const { navigateToSelectOrganization } = useNavigateToSelectOrganization(); + + const redirectToSaml = (orgSlug: string) => { + const callbackPort = queryParams.get("callback_port"); + const redirectUrl = `/api/v1/sso/redirect/saml2/organizations/${orgSlug}${ + callbackPort ? `?callback_port=${callbackPort}` : "" + }`; + router.push(redirectUrl); + }; + useEffect(() => { - if (serverDetails?.samlDefaultOrgSlug) { - const callbackPort = queryParams.get("callback_port"); - const redirectUrl = `/api/v1/sso/redirect/saml2/organizations/${ - serverDetails?.samlDefaultOrgSlug - }${callbackPort ? `?callback_port=${callbackPort}` : ""}`; - router.push(redirectUrl); - } + if (serverDetails?.samlDefaultOrgSlug) redirectToSaml(serverDetails.samlDefaultOrgSlug); }, [serverDetails?.samlDefaultOrgSlug]); + const handleSaml = useCallback((step: number) => { + if (config.defaultOrgSlug) { + redirectToSaml(config.defaultOrgSlug); + } else { + setStep(step); + } + }, []); + const handleLogin = async (e: FormEvent) => { e.preventDefault(); try { @@ -75,7 +87,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: return; } - navigateUserToSelectOrg(router, callbackPort!); + navigateToSelectOrganization(callbackPort!); } else { setLoginError(true); createNotification({ @@ -100,7 +112,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: return; } - navigateUserToSelectOrg(router); + navigateToSelectOrganization(); // case: login does not require MFA step createNotification({ @@ -211,7 +223,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: colorSchema="primary" variant="outline_bg" onClick={() => { - setStep(2); + handleSaml(2); }} leftIcon={} className="mx-0 h-10 w-full" diff --git a/frontend/src/views/Login/components/MFAStep/MFAStep.tsx b/frontend/src/views/Login/components/MFAStep/MFAStep.tsx index 5e613008d..5f04454bc 100644 --- a/frontend/src/views/Login/components/MFAStep/MFAStep.tsx +++ b/frontend/src/views/Login/components/MFAStep/MFAStep.tsx @@ -16,7 +16,7 @@ import { useSelectOrganization, verifyMfaToken } from "@app/hooks/api/auth/queri import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchMyPrivateKey } from "@app/hooks/api/users/queries"; -import { navigateUserToOrg, navigateUserToSelectOrg } from "../../Login.utils"; +import { navigateUserToOrg, useNavigateToSelectOrganization } from "../../Login.utils"; // The style for the verification code input const props = { @@ -50,6 +50,7 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => { const [isLoading, setIsLoading] = useState(false); const [isLoadingResend, setIsLoadingResend] = useState(false); const [mfaCode, setMfaCode] = useState(""); + const { navigateToSelectOrganization } = useNavigateToSelectOrganization(); const [triesLeft, setTriesLeft] = useState(undefined); const { t } = useTranslation(); @@ -93,7 +94,7 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => { // case: user has orgs, so we navigate the user to select an org if (userOrgs.length > 0) { - navigateUserToSelectOrg(router, callbackPort); + navigateToSelectOrganization(callbackPort); } // case: no orgs found, so we navigate the user to create an org // cli login will fail in this case @@ -166,7 +167,7 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => { // case: user has orgs, so we navigate the user to select an org if (userOrgs.length > 0) { - navigateUserToSelectOrg(router, callbackPort); + navigateToSelectOrganization(callbackPort); } // case: no orgs found, so we navigate the user to create an org // cli login will fail in this case @@ -195,7 +196,7 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => { if (organizationId) { await navigateUserToOrg(router, organizationId); } else { - navigateUserToSelectOrg(router); + navigateToSelectOrganization(); } } else { createNotification({ From 56bc25025a6e7925532fe67491e2670f04f00799 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:18:19 +0200 Subject: [PATCH 08/19] Update Login.utils.tsx --- frontend/src/views/Login/Login.utils.tsx | 33 ++++++++++++++++++------ 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/frontend/src/views/Login/Login.utils.tsx b/frontend/src/views/Login/Login.utils.tsx index b6e3c1a10..337a4dc10 100644 --- a/frontend/src/views/Login/Login.utils.tsx +++ b/frontend/src/views/Login/Login.utils.tsx @@ -1,5 +1,7 @@ -import { NextRouter } from "next/router"; +import { NextRouter, useRouter } from "next/router"; +import { useServerConfig } from "@app/context"; +import { useSelectOrganization } from "@app/hooks/api"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { userKeys } from "@app/hooks/api/users/queries"; import { queryClient } from "@app/reactQuery"; @@ -27,14 +29,29 @@ export const navigateUserToOrg = async (router: NextRouter, organizationId?: str } }; -export const navigateUserToSelectOrg = (router: NextRouter, cliCallbackPort?: string) => { - queryClient.invalidateQueries(userKeys.getUser); +export const useNavigateToSelectOrganization = () => { + const { config } = useServerConfig(); + const selectOrganization = useSelectOrganization(); + const router = useRouter(); - let redirectTo = "/login/select-organization"; + const navigate = async (cliCallbackPort?: string) => { + if (config.defaultOrgId) { + await selectOrganization.mutateAsync({ + organizationId: config.defaultOrgId + }); - if (cliCallbackPort) { - redirectTo += `?callback_port=${cliCallbackPort}`; - } + await navigateUserToOrg(router, config.defaultOrgId); + } - router.push(redirectTo, undefined, { shallow: true }); + queryClient.invalidateQueries(userKeys.getUser); + let redirectTo = "/login/select-organization"; + + if (cliCallbackPort) { + redirectTo += `?callback_port=${cliCallbackPort}`; + } + + router.push(redirectTo, undefined, { shallow: true }); + }; + + return { navigateToSelectOrganization: navigate }; }; From 4905ad1f4898681d917c7d90f664c60e6cd4a442 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:18:36 +0200 Subject: [PATCH 09/19] Feat: Default SAML/LDAP organization slug --- .../components/PasswordStep/PasswordStep.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx index 06438a2f8..10be08f4c 100644 --- a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx +++ b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef,useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -16,7 +16,7 @@ import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchMyPrivateKey } from "@app/hooks/api/users/queries"; -import { navigateUserToOrg, navigateUserToSelectOrg } from "../../Login.utils"; +import { navigateUserToOrg, useNavigateToSelectOrganization } from "../../Login.utils"; type Props = { providerAuthToken: string; @@ -39,8 +39,11 @@ export const PasswordStep = ({ const { mutateAsync: selectOrganization } = useSelectOrganization(); const { mutateAsync: oauthTokenExchange } = useOauthTokenExchange(); - const { callbackPort, organizationId, hasExchangedPrivateKey } = - jwt_decode(providerAuthToken) as any; + const { navigateToSelectOrganization } = useNavigateToSelectOrganization(); + + const { callbackPort, organizationId, hasExchangedPrivateKey } = jwt_decode( + providerAuthToken + ) as any; const handleExchange = async () => { try { @@ -92,7 +95,7 @@ export const PasswordStep = ({ // case: user has orgs, so we navigate the user to select an org if (userOrgs.length > 0) { - navigateUserToSelectOrg(router, callbackPort); + navigateToSelectOrganization(callbackPort); } // case: no orgs found, so we navigate the user to create an org else { @@ -176,7 +179,7 @@ export const PasswordStep = ({ // case: user has orgs, so we navigate the user to select an org if (userOrgs.length > 0) { - navigateUserToSelectOrg(router, callbackPort); + navigateToSelectOrganization(callbackPort); } // case: no orgs found, so we navigate the user to create an org else { @@ -220,7 +223,7 @@ export const PasswordStep = ({ const userOrgs = await fetchOrganizations(); if (userOrgs.length > 0) { - navigateUserToSelectOrg(router); + navigateToSelectOrganization(); } else { await navigateUserToOrg(router); } @@ -270,7 +273,7 @@ export const PasswordStep = ({

- What's your Infisical password? + What's your Infisical password?

From e1a4185f76f1645bffbd573e98ed638b75b0a3d7 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:18:54 +0200 Subject: [PATCH 10/19] Hide org slug input when default slug is set --- frontend/src/views/Login/LoginLDAP.tsx | 36 +++++++++++++++----------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/frontend/src/views/Login/LoginLDAP.tsx b/frontend/src/views/Login/LoginLDAP.tsx index 021ac7334..278439808 100644 --- a/frontend/src/views/Login/LoginLDAP.tsx +++ b/frontend/src/views/Login/LoginLDAP.tsx @@ -4,15 +4,19 @@ import { useRouter } from "next/router"; import { createNotification } from "@app/components/notifications"; import { Button, Input } from "@app/components/v2"; +import { useServerConfig } from "@app/context"; import { loginLDAPRedirect } from "@app/hooks/api/auth/queries"; export const LoginLDAP = () => { const router = useRouter(); + const { config } = useServerConfig(); const queryParams = new URLSearchParams(window.location.search); const passedOrgSlug = queryParams.get("organizationSlug"); const passedUsername = queryParams.get("username"); - const [organizationSlug, setOrganizationSlug] = useState(passedOrgSlug || ""); + const [organizationSlug, setOrganizationSlug] = useState( + config.defaultOrgSlug || passedOrgSlug || "" + ); const [username, setUsername] = useState(passedUsername || ""); const [password, setPassword] = useState(""); @@ -63,21 +67,23 @@ export const LoginLDAP = () => { What's your LDAP Login?

-
-
- setOrganizationSlug(e.target.value)} - type="text" - placeholder="Enter your organization slug..." - isRequired - autoComplete="email" - id="email" - className="h-12" - isDisabled={passedOrgSlug !== null} - /> + {!config.defaultOrgSlug && ( +
+
+ setOrganizationSlug(e.target.value)} + type="text" + placeholder="Enter your organization slug..." + isRequired + autoComplete="email" + id="email" + className="h-12" + isDisabled={passedOrgSlug !== null} + /> +
-
+ )}
Date: Thu, 20 Jun 2024 18:19:00 +0200 Subject: [PATCH 11/19] Feat: Default SAML/LDAP organization slug --- .../admin/DashboardPage/DashboardPage.tsx | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx index f16c236b4..b841e61e2 100644 --- a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx @@ -40,7 +40,8 @@ const formSchema = z.object({ allowedSignUpDomain: z.string().optional().nullable(), trustSamlEmails: z.boolean(), trustLdapEmails: z.boolean(), - trustOidcEmails: z.boolean() + trustOidcEmails: z.boolean(), + defaultOrgSlug: z.string().optional().nullable() }); type TDashboardForm = z.infer; @@ -62,7 +63,8 @@ export const AdminDashboardPage = () => { allowedSignUpDomain: config.allowedSignUpDomain, trustSamlEmails: config.trustSamlEmails, trustLdapEmails: config.trustLdapEmails, - trustOidcEmails: config.trustOidcEmails + trustOidcEmails: config.trustOidcEmails, + defaultOrgSlug: config.defaultOrgSlug } }); @@ -86,10 +88,17 @@ export const AdminDashboardPage = () => { const onFormSubmit = async (formData: TDashboardForm) => { try { - const { signUpMode, allowedSignUpDomain, trustSamlEmails, trustLdapEmails, trustOidcEmails } = - formData; + const { + signUpMode, + allowedSignUpDomain, + trustSamlEmails, + trustLdapEmails, + trustOidcEmails, + defaultOrgSlug + } = formData; await updateServerConfig({ + defaultOrgSlug, allowSignUp: signUpMode !== SignUpModes.Disabled, allowedSignUpDomain: signUpMode === SignUpModes.Anyone ? allowedSignUpDomain : null, trustSamlEmails, @@ -130,7 +139,7 @@ export const AdminDashboardPage = () => {
@@ -165,7 +174,7 @@ export const AdminDashboardPage = () => { />
{signupMode === "anyone" && ( -
+
Restrict signup by email domain(s)
@@ -191,7 +200,33 @@ export const AdminDashboardPage = () => { />
)} -
+ +
+
+ Default organization slug +
+
+ Select the slug of the organization you want to set as default for SAML/LDAP + logins. +
+ ( + + + + )} + /> +
+ +
Trust emails
Select if you want Infisical to trust external emails from SAML/LDAP/OIDC From b0f3476e4a7fc0762d65b9525990b1f75fa2374f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:36:11 +0200 Subject: [PATCH 12/19] Fix: Completely hide org slug input field when org slug is passed or default slug is provided --- frontend/src/views/Login/LoginLDAP.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/views/Login/LoginLDAP.tsx b/frontend/src/views/Login/LoginLDAP.tsx index 278439808..a8dc6c738 100644 --- a/frontend/src/views/Login/LoginLDAP.tsx +++ b/frontend/src/views/Login/LoginLDAP.tsx @@ -67,7 +67,7 @@ export const LoginLDAP = () => { What's your LDAP Login?

- {!config.defaultOrgSlug && ( + {!config.defaultOrgSlug && !passedOrgSlug && (
{ autoComplete="email" id="email" className="h-12" - isDisabled={passedOrgSlug !== null} />
From 91bdd7ea6a7d858ed697195e6c0ff5bdf9348cd2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:52:35 +0200 Subject: [PATCH 13/19] Fix: UI descriptions --- .../admin/DashboardPage/DashboardPage.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx index b841e61e2..917bd1c54 100644 --- a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx @@ -41,7 +41,8 @@ const formSchema = z.object({ trustSamlEmails: z.boolean(), trustLdapEmails: z.boolean(), trustOidcEmails: z.boolean(), - defaultOrgSlug: z.string().optional().nullable() + defaultOrgSlug: z.string().optional().nullable(), + defaultAuthOrgSlug: z.string().optional().nullable() }); type TDashboardForm = z.infer; @@ -64,7 +65,8 @@ export const AdminDashboardPage = () => { trustSamlEmails: config.trustSamlEmails, trustLdapEmails: config.trustLdapEmails, trustOidcEmails: config.trustOidcEmails, - defaultOrgSlug: config.defaultOrgSlug + defaultOrgSlug: config.defaultOrgSlug, + defaultAuthOrgSlug: config.defaultAuthOrgSlug } }); @@ -94,11 +96,12 @@ export const AdminDashboardPage = () => { trustSamlEmails, trustLdapEmails, trustOidcEmails, - defaultOrgSlug + defaultOrgSlug, + defaultAuthOrgSlug } = formData; await updateServerConfig({ - defaultOrgSlug, + defaultAuthOrgSlug, allowSignUp: signUpMode !== SignUpModes.Disabled, allowedSignUpDomain: signUpMode === SignUpModes.Anyone ? allowedSignUpDomain : null, trustSamlEmails, @@ -207,15 +210,16 @@ export const AdminDashboardPage = () => {
Select the slug of the organization you want to set as default for SAML/LDAP - logins. + logins. When this field is set, users will also skip the organization selection + page and automatically be logged into the default organization.
( Date: Thu, 20 Jun 2024 18:53:07 +0200 Subject: [PATCH 14/19] Updated "defaultOrgId" and "defaultOrgSlug" to "defaultAuthOrgId" and "defaultAuthOrgSlug" --- .../20240620142418_default-saml-ldap-org.ts | 14 +++++++------- backend/src/db/schemas/super-admin.ts | 2 +- backend/src/server/routes/v1/admin-router.ts | 6 +++--- .../src/services/super-admin/super-admin-dal.ts | 8 ++++---- .../services/super-admin/super-admin-service.ts | 14 +++++++------- .../src/services/super-admin/super-admin-types.ts | 4 ++-- frontend/src/hooks/api/admin/types.ts | 4 ++-- frontend/src/views/Login/Login.utils.tsx | 6 +++--- frontend/src/views/Login/LoginLDAP.tsx | 4 ++-- .../Login/components/InitialStep/InitialStep.tsx | 4 ++-- .../views/admin/DashboardPage/DashboardPage.tsx | 3 --- 11 files changed, 33 insertions(+), 36 deletions(-) diff --git a/backend/src/db/migrations/20240620142418_default-saml-ldap-org.ts b/backend/src/db/migrations/20240620142418_default-saml-ldap-org.ts index 67753b679..fec132df4 100644 --- a/backend/src/db/migrations/20240620142418_default-saml-ldap-org.ts +++ b/backend/src/db/migrations/20240620142418_default-saml-ldap-org.ts @@ -2,26 +2,26 @@ import { Knex } from "knex"; import { TableName } from "../schemas"; -const DEFAULT_ORG_ID_FIELD = "defaultOrgId"; +const DEFAULT_AUTH_ORG_ID_FIELD = "defaultAuthOrgId"; export async function up(knex: Knex): Promise { - const hasDefaultOrgColumn = await knex.schema.hasColumn(TableName.SuperAdmin, DEFAULT_ORG_ID_FIELD); + const hasDefaultOrgColumn = await knex.schema.hasColumn(TableName.SuperAdmin, DEFAULT_AUTH_ORG_ID_FIELD); await knex.schema.alterTable(TableName.SuperAdmin, (t) => { if (!hasDefaultOrgColumn) { - t.uuid(DEFAULT_ORG_ID_FIELD).nullable(); - t.foreign(DEFAULT_ORG_ID_FIELD).references("id").inTable(TableName.Organization).onDelete("SET NULL"); + t.uuid(DEFAULT_AUTH_ORG_ID_FIELD).nullable(); + t.foreign(DEFAULT_AUTH_ORG_ID_FIELD).references("id").inTable(TableName.Organization).onDelete("SET NULL"); } }); } export async function down(knex: Knex): Promise { - const hasDefaultOrgColumn = await knex.schema.hasColumn(TableName.SuperAdmin, DEFAULT_ORG_ID_FIELD); + const hasDefaultOrgColumn = await knex.schema.hasColumn(TableName.SuperAdmin, DEFAULT_AUTH_ORG_ID_FIELD); await knex.schema.alterTable(TableName.SuperAdmin, (t) => { if (hasDefaultOrgColumn) { - t.dropForeign([DEFAULT_ORG_ID_FIELD]); - t.dropColumn(DEFAULT_ORG_ID_FIELD); + t.dropForeign([DEFAULT_AUTH_ORG_ID_FIELD]); + t.dropColumn(DEFAULT_AUTH_ORG_ID_FIELD); } }); } diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts index 1f2b6820b..29e41c78e 100644 --- a/backend/src/db/schemas/super-admin.ts +++ b/backend/src/db/schemas/super-admin.ts @@ -18,7 +18,7 @@ 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(), - defaultOrgId: z.string().uuid().nullable().optional() + defaultAuthOrgId: z.string().uuid().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 59a9f064e..943161b89 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -22,7 +22,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { 200: z.object({ config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }).extend({ isMigrationModeOn: z.boolean(), - defaultOrgSlug: z.string().nullable(), + defaultAuthOrgSlug: z.string().nullable(), isSecretScanningDisabled: z.boolean() }) }) @@ -54,12 +54,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { trustSamlEmails: z.boolean().optional(), trustLdapEmails: z.boolean().optional(), trustOidcEmails: z.boolean().optional(), - defaultOrgSlug: z.string().optional().nullable() + defaultAuthOrgSlug: z.string().optional().nullable() }), response: { 200: z.object({ config: SuperAdminSchema.extend({ - defaultOrgSlug: z.string().nullable() + defaultAuthOrgSlug: z.string().nullable() }) }) } diff --git a/backend/src/services/super-admin/super-admin-dal.ts b/backend/src/services/super-admin/super-admin-dal.ts index 90f1004da..9fd47b9ac 100644 --- a/backend/src/services/super-admin/super-admin-dal.ts +++ b/backend/src/services/super-admin/super-admin-dal.ts @@ -10,17 +10,17 @@ export const superAdminDALFactory = (db: TDbClient) => { const findById = async (id: string) => { const config = await db(TableName.SuperAdmin) .where(`${TableName.SuperAdmin}.id`, id) - .leftJoin(TableName.Organization, `${TableName.SuperAdmin}.defaultOrgId`, `${TableName.Organization}.id`) + .leftJoin(TableName.Organization, `${TableName.SuperAdmin}.defaultAuthOrgId`, `${TableName.Organization}.id`) .select( db.ref("*").withSchema(TableName.SuperAdmin) as unknown as keyof TSuperAdmin, - db.ref("slug").withSchema(TableName.Organization).as("defaultOrgSlug") + db.ref("slug").withSchema(TableName.Organization).as("defaultAuthOrgSlug") ) .first(); return { - defaultOrgSlug: config?.defaultOrgSlug || null, + defaultAuthOrgSlug: config?.defaultAuthOrgSlug || null, ...config - } as TSuperAdmin & { defaultOrgSlug: string | null }; + } as TSuperAdmin & { defaultAuthOrgSlug: string | null }; }; return { diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index b06757984..61c7fdf21 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -28,7 +28,7 @@ type TSuperAdminServiceFactoryDep = { export type TSuperAdminServiceFactory = ReturnType; // eslint-disable-next-line -export let getServerCfg: () => Promise; +export let getServerCfg: () => Promise; const ADMIN_CONFIG_KEY = "infisical-admin-cfg"; const ADMIN_CONFIG_KEY_EXP = 60; // 60s @@ -57,7 +57,7 @@ export const superAdminServiceFactory = ({ return serverCfg; } - const keyStoreServerCfg = JSON.parse(config) as TSuperAdmin & { defaultOrgSlug: string | null }; + const keyStoreServerCfg = JSON.parse(config) as TSuperAdmin & { defaultAuthOrgSlug: string | null }; return { ...keyStoreServerCfg, // this is to allow admin router to work @@ -78,9 +78,9 @@ export const superAdminServiceFactory = ({ const updateServerCfg = async (data: TUpdateServerCfgDTO) => { let organization: TOrganizations | undefined; - if (data.defaultOrgSlug) { + if (data.defaultAuthOrgSlug) { organization = await orgDAL.findOne({ - slug: data.defaultOrgSlug + slug: data.defaultAuthOrgSlug }); if (!organization) { @@ -92,13 +92,13 @@ export const superAdminServiceFactory = ({ } const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, { - ...omit(data, ["defaultOrgSlug"]), - defaultOrgId: organization?.id || null + ...omit(data, ["defaultAuthOrgSlug"]), + defaultAuthOrgId: organization?.id || null }); const result = { ...updatedServerCfg, - defaultOrgSlug: organization?.slug || null + defaultAuthOrgSlug: organization?.slug || null }; await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(result)); diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index 0c3789a37..94742751c 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -18,6 +18,6 @@ export type TAdminSignUpDTO = { userAgent: string; }; -export type TUpdateServerCfgDTO = Omit & { - defaultOrgSlug?: string | null; +export type TUpdateServerCfgDTO = Omit & { + defaultAuthOrgSlug?: string | null; }; diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index f1abf1a7a..524bc6ace 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -7,8 +7,8 @@ export type TServerConfig = { trustLdapEmails: boolean; trustOidcEmails: boolean; isSecretScanningDisabled: boolean; - defaultOrgSlug: string | null; - defaultOrgId: string | null; + defaultAuthOrgSlug: string | null; + defaultAuthOrgId: string | null; }; export type TCreateAdminUserDTO = { diff --git a/frontend/src/views/Login/Login.utils.tsx b/frontend/src/views/Login/Login.utils.tsx index 337a4dc10..00f6037f1 100644 --- a/frontend/src/views/Login/Login.utils.tsx +++ b/frontend/src/views/Login/Login.utils.tsx @@ -35,12 +35,12 @@ export const useNavigateToSelectOrganization = () => { const router = useRouter(); const navigate = async (cliCallbackPort?: string) => { - if (config.defaultOrgId) { + if (config.defaultAuthOrgId) { await selectOrganization.mutateAsync({ - organizationId: config.defaultOrgId + organizationId: config.defaultAuthOrgId }); - await navigateUserToOrg(router, config.defaultOrgId); + await navigateUserToOrg(router, config.defaultAuthOrgId); } queryClient.invalidateQueries(userKeys.getUser); diff --git a/frontend/src/views/Login/LoginLDAP.tsx b/frontend/src/views/Login/LoginLDAP.tsx index a8dc6c738..1e99c611d 100644 --- a/frontend/src/views/Login/LoginLDAP.tsx +++ b/frontend/src/views/Login/LoginLDAP.tsx @@ -15,7 +15,7 @@ export const LoginLDAP = () => { const passedUsername = queryParams.get("username"); const [organizationSlug, setOrganizationSlug] = useState( - config.defaultOrgSlug || passedOrgSlug || "" + config.defaultAuthOrgSlug || passedOrgSlug || "" ); const [username, setUsername] = useState(passedUsername || ""); const [password, setPassword] = useState(""); @@ -67,7 +67,7 @@ export const LoginLDAP = () => { What's your LDAP Login?

- {!config.defaultOrgSlug && !passedOrgSlug && ( + {!config.defaultAuthOrgSlug && !passedOrgSlug && (
{ - if (config.defaultOrgSlug) { - redirectToSaml(config.defaultOrgSlug); + if (config.defaultAuthOrgSlug) { + redirectToSaml(config.defaultAuthOrgSlug); } else { setStep(step); } diff --git a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx index 917bd1c54..d863e6765 100644 --- a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx @@ -41,7 +41,6 @@ const formSchema = z.object({ trustSamlEmails: z.boolean(), trustLdapEmails: z.boolean(), trustOidcEmails: z.boolean(), - defaultOrgSlug: z.string().optional().nullable(), defaultAuthOrgSlug: z.string().optional().nullable() }); @@ -65,7 +64,6 @@ export const AdminDashboardPage = () => { trustSamlEmails: config.trustSamlEmails, trustLdapEmails: config.trustLdapEmails, trustOidcEmails: config.trustOidcEmails, - defaultOrgSlug: config.defaultOrgSlug, defaultAuthOrgSlug: config.defaultAuthOrgSlug } }); @@ -96,7 +94,6 @@ export const AdminDashboardPage = () => { trustSamlEmails, trustLdapEmails, trustOidcEmails, - defaultOrgSlug, defaultAuthOrgSlug } = formData; From c735beea32e0b7c48c5e97fbb4d08874416e56d5 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 21 Jun 2024 01:02:33 +0200 Subject: [PATCH 15/19] Fix: Requested changes --- backend/src/server/routes/index.ts | 1 - backend/src/server/routes/v1/admin-router.ts | 2 +- .../services/super-admin/super-admin-dal.ts | 26 +++++++--- .../super-admin/super-admin-service.ts | 38 +++----------- .../services/super-admin/super-admin-types.ts | 6 --- .../admin/DashboardPage/DashboardPage.tsx | 51 +++++++++++++------ 6 files changed, 62 insertions(+), 62 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 21e867da4..265fc18fd 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -458,7 +458,6 @@ export const registerRoutes = async ( userDAL, authService: loginService, serverCfgDAL: superAdminDAL, - orgDAL, orgService, keyStore }); diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 943161b89..24c7e2a6e 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -54,7 +54,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { trustSamlEmails: z.boolean().optional(), trustLdapEmails: z.boolean().optional(), trustOidcEmails: z.boolean().optional(), - defaultAuthOrgSlug: z.string().optional().nullable() + defaultAuthOrgId: z.string().optional().nullable() }), response: { 200: z.object({ diff --git a/backend/src/services/super-admin/super-admin-dal.ts b/backend/src/services/super-admin/super-admin-dal.ts index 9fd47b9ac..eae42cbe1 100644 --- a/backend/src/services/super-admin/super-admin-dal.ts +++ b/backend/src/services/super-admin/super-admin-dal.ts @@ -1,14 +1,16 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; -import { TableName, TSuperAdmin } from "@app/db/schemas"; +import { TableName, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; import { ormify } from "@app/lib/knex"; export type TSuperAdminDALFactory = ReturnType; export const superAdminDALFactory = (db: TDbClient) => { - const orm = ormify(db, TableName.SuperAdmin); + const superAdminOrm = ormify(db, TableName.SuperAdmin); - const findById = async (id: string) => { - const config = await db(TableName.SuperAdmin) + const findById = async (id: string, tx?: Knex) => { + const config = await (tx || db)(TableName.SuperAdmin) .where(`${TableName.SuperAdmin}.id`, id) .leftJoin(TableName.Organization, `${TableName.SuperAdmin}.defaultAuthOrgId`, `${TableName.Organization}.id`) .select( @@ -23,8 +25,20 @@ export const superAdminDALFactory = (db: TDbClient) => { } as TSuperAdmin & { defaultAuthOrgSlug: string | null }; }; + const updateById = async (id: string, data: TSuperAdminUpdate, tx?: Knex) => { + const updatedConfig = await superAdminOrm.transaction(async (trx) => { + await superAdminOrm.updateById(id, data, tx || trx); + const config = await findById(id, tx || trx); + + return config; + }); + + return updatedConfig; + }; + return { - ...orm, - findById + ...superAdminOrm, + findById, + updateById }; }; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 61c7fdf21..706e5c5f4 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -1,24 +1,21 @@ import bcrypt from "bcrypt"; -import { TOrganizations, TSuperAdmin } from "@app/db/schemas"; +import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError } from "@app/lib/errors"; -import { omit } from "@app/lib/fn"; import { TAuthLoginFactory } from "../auth/auth-login-service"; import { AuthMethod } from "../auth/auth-type"; -import { TOrgDALFactory } from "../org/org-dal"; import { TOrgServiceFactory } from "../org/org-service"; import { TUserDALFactory } from "../user/user-dal"; import { TSuperAdminDALFactory } from "./super-admin-dal"; -import { TAdminSignUpDTO, TUpdateServerCfgDTO } from "./super-admin-types"; +import { TAdminSignUpDTO } from "./super-admin-types"; type TSuperAdminServiceFactoryDep = { serverCfgDAL: TSuperAdminDALFactory; - orgDAL: Pick; userDAL: TUserDALFactory; authService: Pick; orgService: Pick; @@ -36,7 +33,6 @@ const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000"; export const superAdminServiceFactory = ({ serverCfgDAL, - orgDAL, userDAL, authService, orgService, @@ -76,33 +72,11 @@ export const superAdminServiceFactory = ({ return newCfg; }; - const updateServerCfg = async (data: TUpdateServerCfgDTO) => { - let organization: TOrganizations | undefined; - if (data.defaultAuthOrgSlug) { - organization = await orgDAL.findOne({ - slug: data.defaultAuthOrgSlug - }); + const updateServerCfg = async (data: TSuperAdminUpdate) => { + const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, data); - if (!organization) { - throw new BadRequestError({ - name: "Update server config", - message: "Failed to find default organization" - }); - } - } - - const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, { - ...omit(data, ["defaultAuthOrgSlug"]), - defaultAuthOrgId: organization?.id || null - }); - - const result = { - ...updatedServerCfg, - defaultAuthOrgSlug: organization?.slug || null - }; - - await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(result)); - return result; + await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg)); + return updatedServerCfg; }; const adminSignUp = async ({ diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index 94742751c..e444c8843 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -1,5 +1,3 @@ -import { TSuperAdminUpdate } from "@app/db/schemas"; - export type TAdminSignUpDTO = { email: string; password: string; @@ -17,7 +15,3 @@ export type TAdminSignUpDTO = { ip: string; userAgent: string; }; - -export type TUpdateServerCfgDTO = Omit & { - defaultAuthOrgSlug?: string | null; -}; diff --git a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx index d863e6765..1c6ba8b8c 100644 --- a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx @@ -21,7 +21,7 @@ import { Tabs } from "@app/components/v2"; import { useOrganization, useServerConfig, useUser } from "@app/context"; -import { useUpdateServerConfig } from "@app/hooks/api"; +import { useGetOrganizations, useUpdateServerConfig } from "@app/hooks/api"; import { RateLimitPanel } from "./RateLimitPanel"; @@ -41,7 +41,7 @@ const formSchema = z.object({ trustSamlEmails: z.boolean(), trustLdapEmails: z.boolean(), trustOidcEmails: z.boolean(), - defaultAuthOrgSlug: z.string().optional().nullable() + defaultAuthOrgId: z.string() }); type TDashboardForm = z.infer; @@ -64,7 +64,7 @@ export const AdminDashboardPage = () => { trustSamlEmails: config.trustSamlEmails, trustLdapEmails: config.trustLdapEmails, trustOidcEmails: config.trustOidcEmails, - defaultAuthOrgSlug: config.defaultAuthOrgSlug + defaultAuthOrgId: config.defaultAuthOrgId ?? "" } }); @@ -74,6 +74,8 @@ export const AdminDashboardPage = () => { const { orgs } = useOrganization(); const { mutateAsync: updateServerConfig } = useUpdateServerConfig(); + const organizations = useGetOrganizations(); + const isNotAllowed = !user?.superAdmin; // TODO(akhilmhdh): on nextjs 14 roadmap this will be properly addressed with context split @@ -94,11 +96,11 @@ export const AdminDashboardPage = () => { trustSamlEmails, trustLdapEmails, trustOidcEmails, - defaultAuthOrgSlug + defaultAuthOrgId } = formData; await updateServerConfig({ - defaultAuthOrgSlug, + defaultAuthOrgId: defaultAuthOrgId || null, allowSignUp: signUpMode !== SignUpModes.Disabled, allowedSignUpDomain: signUpMode === SignUpModes.Anyone ? allowedSignUpDomain : null, trustSamlEmails, @@ -155,13 +157,13 @@ export const AdminDashboardPage = () => { name="signUpMode" render={({ field: { onChange, ...field }, fieldState: { error } }) => ( + )} /> From 7ab5c020001920f2b55fa7f5eb8bd5e976b464c1 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 21 Jun 2024 18:17:16 +0200 Subject: [PATCH 16/19] Requested changes --- backend/src/lib/fn/object.ts | 16 ---------------- .../src/services/super-admin/super-admin-dal.ts | 8 +++----- .../views/admin/DashboardPage/DashboardPage.tsx | 10 +++++----- 3 files changed, 8 insertions(+), 26 deletions(-) diff --git a/backend/src/lib/fn/object.ts b/backend/src/lib/fn/object.ts index 8e9ede8db..87db80343 100644 --- a/backend/src/lib/fn/object.ts +++ b/backend/src/lib/fn/object.ts @@ -13,22 +13,6 @@ export const pick = (obj: T, keys: TKey ); }; -/** - * Omit a list of properties from an object - * into a new object - */ -export const omit = (obj: T, keys: TKeys[]): Omit => { - if (!obj) return {} as Omit; - return (Object.keys(obj) as TKeys[]).reduce( - (acc, key) => { - if (!keys.includes(key)) { - (acc as T)[key] = obj[key]; - } - return acc; - }, - {} as Omit - ); -}; /** * Removes (shakes out) undefined entries from an * object. Optional second argument shakes out values diff --git a/backend/src/services/super-admin/super-admin-dal.ts b/backend/src/services/super-admin/super-admin-dal.ts index eae42cbe1..f95040aec 100644 --- a/backend/src/services/super-admin/super-admin-dal.ts +++ b/backend/src/services/super-admin/super-admin-dal.ts @@ -26,11 +26,9 @@ export const superAdminDALFactory = (db: TDbClient) => { }; const updateById = async (id: string, data: TSuperAdminUpdate, tx?: Knex) => { - const updatedConfig = await superAdminOrm.transaction(async (trx) => { - await superAdminOrm.updateById(id, data, tx || trx); - const config = await findById(id, tx || trx); - - return config; + const updatedConfig = await (superAdminOrm || tx).transaction(async (trx: Knex) => { + await superAdminOrm.updateById(id, data, trx); + return findById(id, trx); }); return updatedConfig; diff --git a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx index 1c6ba8b8c..6b20b2084 100644 --- a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx @@ -226,15 +226,15 @@ export const AdminDashboardPage = () => { dropdownContainerClassName="bg-mineshaft-800" defaultValue={field.value ?? " "} onValueChange={(e) => { - if (e === " ") { - onChange(null); - } else { - onChange(e); + if (e === "EMPTY") { + onChange(""); + return; } + onChange(e); }} {...field} > - Select organization... + Select organization... {organizations.data?.map((org) => ( {org.name} From 1aaca12781ee0c8feb83e0ed4e01224e91412691 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 21 Jun 2024 19:06:10 +0200 Subject: [PATCH 17/19] Update super-admin-dal.ts --- backend/src/services/super-admin/super-admin-dal.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/src/services/super-admin/super-admin-dal.ts b/backend/src/services/super-admin/super-admin-dal.ts index f95040aec..04ec8b791 100644 --- a/backend/src/services/super-admin/super-admin-dal.ts +++ b/backend/src/services/super-admin/super-admin-dal.ts @@ -19,9 +19,13 @@ export const superAdminDALFactory = (db: TDbClient) => { ) .first(); + if (!config) { + return null; + } + return { - defaultAuthOrgSlug: config?.defaultAuthOrgSlug || null, - ...config + ...config, + defaultAuthOrgSlug: config?.defaultAuthOrgSlug || null } as TSuperAdmin & { defaultAuthOrgSlug: string | null }; }; From a6865585f362c4f6e1f2f0c09481211de1796fc8 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 21 Jun 2024 19:33:42 +0200 Subject: [PATCH 18/19] Fix: Failing to create admin config on first run --- .../src/services/super-admin/super-admin-dal.ts | 13 ++++++++++++- .../services/super-admin/super-admin-service.ts | 16 ++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/backend/src/services/super-admin/super-admin-dal.ts b/backend/src/services/super-admin/super-admin-dal.ts index 04ec8b791..7e707e6fa 100644 --- a/backend/src/services/super-admin/super-admin-dal.ts +++ b/backend/src/services/super-admin/super-admin-dal.ts @@ -2,6 +2,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { TableName, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; export type TSuperAdminDALFactory = ReturnType; @@ -32,7 +33,17 @@ export const superAdminDALFactory = (db: TDbClient) => { const updateById = async (id: string, data: TSuperAdminUpdate, tx?: Knex) => { const updatedConfig = await (superAdminOrm || tx).transaction(async (trx: Knex) => { await superAdminOrm.updateById(id, data, trx); - return findById(id, trx); + const config = await findById(id, trx); + + if (!config) { + throw new DatabaseError({ + error: "Failed to find updated super admin config", + message: "Failed to update super admin config", + name: "UpdateById" + }); + } + + return config; }); return updatedConfig; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 706e5c5f4..41b97efa4 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -47,9 +47,11 @@ export const superAdminServiceFactory = ({ if (!config) { const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); - if (serverCfg) { - await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(serverCfg)); // insert it back to keystore + if (!serverCfg) { + throw new BadRequestError({ name: "Admin config", message: "Admin config not found" }); } + + await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(serverCfg)); // insert it back to keystore return serverCfg; } @@ -67,8 +69,13 @@ export const superAdminServiceFactory = ({ const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); if (serverCfg) return; - // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition - const newCfg = await serverCfgDAL.create({ initialized: false, allowSignUp: true, id: ADMIN_CONFIG_DB_UUID }); + const newCfg = await serverCfgDAL.create({ + // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition + id: ADMIN_CONFIG_DB_UUID, + initialized: false, + allowSignUp: true, + defaultAuthOrgId: null + }); return newCfg; }; @@ -76,6 +83,7 @@ export const superAdminServiceFactory = ({ const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, data); await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg)); + return updatedServerCfg; }; From 5a957514df7fcb23cac9ca8ce5c8e9ddacc06c13 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 21 Jun 2024 21:06:11 +0200 Subject: [PATCH 19/19] Feat: Clear select option --- frontend/src/components/v2/Select/Select.tsx | 156 ++++++++++++------ frontend/src/components/v2/Select/index.tsx | 2 +- .../admin/DashboardPage/DashboardPage.tsx | 34 ++-- 3 files changed, 122 insertions(+), 70 deletions(-) diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx index 29dba23c7..015b16f98 100644 --- a/frontend/src/components/v2/Select/Select.tsx +++ b/frontend/src/components/v2/Select/Select.tsx @@ -36,61 +36,73 @@ export const Select = forwardRef( ref ): JSX.Element => { return ( - - - - {props.icon ? : placeholder} - +
+ { + if (!props.onValueChange) return; - - - - - - + - -
- -
-
- - {isLoading ? ( -
- - Loading... -
- ) : ( - children +
+ {props.icon && } + +
+ + + + +
+ + - -
- -
-
-
-
-
+ position={position} + style={{ width: "var(--radix-select-trigger-width)" }} + > + +
+ +
+
+ + {isLoading ? ( +
+ + Loading... +
+ ) : ( + children + )} +
+ +
+ +
+
+ + + +
); } ); @@ -114,7 +126,7 @@ export const SelectItem = forwardRef( outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`, isSelected && "bg-primary", isDisabled && - "cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600", + "cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600", className )} ref={forwardedRef} @@ -129,3 +141,45 @@ export const SelectItem = forwardRef( ); SelectItem.displayName = "SelectItem"; + +export type SelectClearProps = Omit & { + onClear: () => void; + selectValue: string; +}; + +export const SelectClear = forwardRef( + ( + { children, className, isSelected, isDisabled, onClear, selectValue, ...props }, + forwardedRef + ) => { + return ( + onClear()} + onClick={() => onClear()} + className={twMerge( + `relative mb-0.5 flex + cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm + outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`, + isSelected && "bg-primary", + isDisabled && + "cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600", + className + )} + ref={forwardedRef} + > +
+ +
+ {children} +
+ ); + } +); +SelectClear.displayName = "SelectClear"; diff --git a/frontend/src/components/v2/Select/index.tsx b/frontend/src/components/v2/Select/index.tsx index 6a783605a..3765851d5 100644 --- a/frontend/src/components/v2/Select/index.tsx +++ b/frontend/src/components/v2/Select/index.tsx @@ -1,2 +1,2 @@ export type { SelectItemProps, SelectProps } from "./Select"; -export { Select, SelectItem } from "./Select"; +export { Select, SelectClear, SelectItem } from "./Select"; diff --git a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx index 6b20b2084..ef4ff099d 100644 --- a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx @@ -13,6 +13,7 @@ import { FormControl, Input, Select, + SelectClear, SelectItem, Switch, Tab, @@ -68,7 +69,8 @@ export const AdminDashboardPage = () => { } }); - const signupMode = watch("signUpMode"); + const signUpMode = watch("signUpMode"); + const defaultAuthOrgId = watch("defaultAuthOrgId"); const { user, isLoading: isUserLoading } = useUser(); const { orgs } = useOrganization(); @@ -90,14 +92,7 @@ export const AdminDashboardPage = () => { const onFormSubmit = async (formData: TDashboardForm) => { try { - const { - signUpMode, - allowedSignUpDomain, - trustSamlEmails, - trustLdapEmails, - trustOidcEmails, - defaultAuthOrgId - } = formData; + const { allowedSignUpDomain, trustSamlEmails, trustLdapEmails, trustOidcEmails } = formData; await updateServerConfig({ defaultAuthOrgId: defaultAuthOrgId || null, @@ -175,7 +170,7 @@ export const AdminDashboardPage = () => { )} />
- {signupMode === "anyone" && ( + {signUpMode === "anyone" && (
Restrict signup by email domain(s) @@ -222,19 +217,22 @@ export const AdminDashboardPage = () => { isError={Boolean(error)} >