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..fec132df4
--- /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_AUTH_ORG_ID_FIELD = "defaultAuthOrgId";
+
+export async function up(knex: Knex): Promise {
+ 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_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_AUTH_ORG_ID_FIELD);
+
+ await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
+ if (hasDefaultOrgColumn) {
+ 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 87ba35c83..29e41c78e 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(),
+ 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 ea701c828..24c7e2a6e 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(),
+ defaultAuthOrgSlug: 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(),
+ defaultAuthOrgId: z.string().optional().nullable()
}),
response: {
200: z.object({
- config: SuperAdminSchema
+ config: SuperAdminSchema.extend({
+ 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 64133ed7e..7e707e6fa 100644
--- a/backend/src/services/super-admin/super-admin-dal.ts
+++ b/backend/src/services/super-admin/super-admin-dal.ts
@@ -1,7 +1,57 @@
+import { Knex } from "knex";
+
import { TDbClient } from "@app/db";
-import { TableName } from "@app/db/schemas";
+import { TableName, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
+import { DatabaseError } from "@app/lib/errors";
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 superAdminOrm = ormify(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(
+ db.ref("*").withSchema(TableName.SuperAdmin) as unknown as keyof TSuperAdmin,
+ db.ref("slug").withSchema(TableName.Organization).as("defaultAuthOrgSlug")
+ )
+ .first();
+
+ if (!config) {
+ return null;
+ }
+
+ return {
+ ...config,
+ defaultAuthOrgSlug: config?.defaultAuthOrgSlug || null
+ } as TSuperAdmin & { defaultAuthOrgSlug: string | null };
+ };
+
+ const updateById = async (id: string, data: TSuperAdminUpdate, tx?: Knex) => {
+ const updatedConfig = await (superAdminOrm || tx).transaction(async (trx: Knex) => {
+ await superAdminOrm.updateById(id, data, 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;
+ };
+
+ return {
+ ...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 27e198d85..41b97efa4 100644
--- a/backend/src/services/super-admin/super-admin-service.ts
+++ b/backend/src/services/super-admin/super-admin-service.ts
@@ -25,7 +25,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
@@ -42,16 +42,20 @@ 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
+
+ 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;
}
- const keyStoreServerCfg = JSON.parse(config) as TSuperAdmin;
+ const keyStoreServerCfg = JSON.parse(config) as TSuperAdmin & { defaultAuthOrgSlug: string | null };
return {
...keyStoreServerCfg,
// this is to allow admin router to work
@@ -65,14 +69,21 @@ 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;
};
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;
};
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/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts
index 0d06a2aa1..524bc6ace 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;
+ defaultAuthOrgSlug: string | null;
+ defaultAuthOrgId: 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;
};
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/Login.utils.tsx b/frontend/src/views/Login/Login.utils.tsx
index b6e3c1a10..00f6037f1 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.defaultAuthOrgId) {
+ await selectOrganization.mutateAsync({
+ organizationId: config.defaultAuthOrgId
+ });
- if (cliCallbackPort) {
- redirectTo += `?callback_port=${cliCallbackPort}`;
- }
+ await navigateUserToOrg(router, config.defaultAuthOrgId);
+ }
- 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 };
};
diff --git a/frontend/src/views/Login/LoginLDAP.tsx b/frontend/src/views/Login/LoginLDAP.tsx
index 021ac7334..1e99c611d 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.defaultAuthOrgSlug || passedOrgSlug || ""
+ );
const [username, setUsername] = useState(passedUsername || "");
const [password, setPassword] = useState("");
@@ -63,21 +67,22 @@ export const LoginLDAP = () => {
What's your LDAP Login?