diff --git a/backend/src/db/migrations/20240424235842_user-search-filter.ts b/backend/src/db/migrations/20240424235842_user-search-filter.ts new file mode 100644 index 000000000..c078acf84 --- /dev/null +++ b/backend/src/db/migrations/20240424235842_user-search-filter.ts @@ -0,0 +1,15 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable(TableName.LdapConfig, (t) => { + t.string("searchFilter").notNullable().defaultTo(""); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable(TableName.LdapConfig, (t) => { + t.dropColumn("searchFilter"); + }); +} diff --git a/backend/src/db/schemas/ldap-configs.ts b/backend/src/db/schemas/ldap-configs.ts index 70394a65c..86fd6acb6 100644 --- a/backend/src/db/schemas/ldap-configs.ts +++ b/backend/src/db/schemas/ldap-configs.ts @@ -25,7 +25,8 @@ export const LdapConfigsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), groupSearchBase: z.string().default(""), - groupSearchFilter: z.string().default("") + groupSearchFilter: z.string().default(""), + searchFilter: z.string().default("") }); export type TLdapConfigs = z.infer; diff --git a/backend/src/ee/routes/v1/ldap-router.ts b/backend/src/ee/routes/v1/ldap-router.ts index 09819dde8..6730e9101 100644 --- a/backend/src/ee/routes/v1/ldap-router.ts +++ b/backend/src/ee/routes/v1/ldap-router.ts @@ -16,7 +16,7 @@ import { z } from "zod"; import { LdapConfigsSchema, LdapGroupMapsSchema } from "@app/db/schemas"; import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types"; -import { searchGroups } from "@app/ee/services/ldap-config/ldap-fns"; +import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -54,12 +54,19 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { try { const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as TLDAPConfig; - const groupFilter = "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))"; - const searchFilter = - ldapConfig.groupSearchFilter || - groupFilter.replace("{{.Username}}", user.uid).replace("{{.UserDN}}", user.dn); + let groups: { dn: string; cn: string }[] | undefined; + if (ldapConfig.groupSearchBase) { + const groupFilter = "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))"; + const groupSearchFilter = (ldapConfig.groupSearchFilter || groupFilter) + .replace(/{{\.Username}}/g, user.uid) + .replace(/{{\.UserDN}}/g, user.dn); - const shouldProcessGroups = ldapConfig.groupSearchFilter && ldapConfig.groupSearchBase; + if (!isValidLdapFilter(groupSearchFilter)) { + throw new Error("Generated LDAP search filter is invalid."); + } + + groups = await searchGroups(ldapConfig, groupSearchFilter, ldapConfig.groupSearchBase); + } const { isUserCompleted, providerAuthToken } = await server.services.ldap.ldapLogin({ ldapConfigId: ldapConfig.id, @@ -68,9 +75,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { firstName: user.givenName ?? user.cn ?? "", lastName: user.sn ?? "", emails: user.mail ? [user.mail] : [], - groups: shouldProcessGroups - ? await searchGroups(ldapConfig, searchFilter, ldapConfig.groupSearchBase) - : undefined, + groups, relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState, orgId: (req as unknown as FastifyRequest).ldapConfig.organization }); @@ -132,6 +137,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { bindDN: z.string(), bindPass: z.string(), searchBase: z.string(), + searchFilter: z.string(), groupSearchBase: z.string(), groupSearchFilter: z.string(), caCert: z.string() @@ -165,8 +171,12 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { bindDN: z.string().trim(), bindPass: z.string().trim(), searchBase: z.string().trim(), - groupSearchBase: z.string().trim().default(""), - groupSearchFilter: z.string().trim().default(""), + searchFilter: z.string().trim().default("(uid={{username}})"), + groupSearchBase: z.string().trim(), + groupSearchFilter: z + .string() + .trim() + .default("(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))"), caCert: z.string().trim().default("") }), response: { @@ -202,6 +212,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { bindDN: z.string().trim(), bindPass: z.string().trim(), searchBase: z.string().trim(), + searchFilter: z.string().trim(), groupSearchBase: z.string().trim(), groupSearchFilter: z.string().trim(), caCert: z.string().trim() @@ -327,4 +338,32 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { return ldapGroupMap; } }); + + server.route({ + method: "POST", + url: "/config/:configId/test-connection", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + configId: z.string().trim() + }), + response: { + 200: z.boolean() + } + }, + handler: async (req) => { + const result = await server.services.ldap.testLDAPConnection({ + actor: req.permission.type, + actorId: req.permission.id, + orgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ldapConfigId: req.params.configId + }); + return result; + } + }); }; 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 90c641109..85c537684 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -37,8 +37,10 @@ import { TGetLdapCfgDTO, TGetLdapGroupMapsDTO, TLdapLoginDTO, + TTestLdapConnectionDTO, TUpdateLdapCfgDTO } from "./ldap-config-types"; +import { testLDAPConfig } from "./ldap-fns"; import { TLdapGroupMapDALFactory } from "./ldap-group-map-dal"; type TLdapConfigServiceFactoryDep = { @@ -96,6 +98,7 @@ export const ldapConfigServiceFactory = ({ bindDN, bindPass, searchBase, + searchFilter, groupSearchBase, groupSearchFilter, caCert @@ -173,6 +176,7 @@ export const ldapConfigServiceFactory = ({ bindPassIV, bindPassTag, searchBase, + searchFilter, groupSearchBase, groupSearchFilter, encryptedCACert, @@ -194,6 +198,7 @@ export const ldapConfigServiceFactory = ({ bindDN, bindPass, searchBase, + searchFilter, groupSearchBase, groupSearchFilter, caCert @@ -212,6 +217,7 @@ export const ldapConfigServiceFactory = ({ isActive, url, searchBase, + searchFilter, groupSearchBase, groupSearchFilter }; @@ -315,6 +321,7 @@ export const ldapConfigServiceFactory = ({ bindDN, bindPass, searchBase: ldapConfig.searchBase, + searchFilter: ldapConfig.searchFilter, groupSearchBase: ldapConfig.groupSearchBase, groupSearchFilter: ldapConfig.groupSearchFilter, caCert @@ -350,7 +357,7 @@ export const ldapConfigServiceFactory = ({ bindDN: ldapConfig.bindDN, bindCredentials: ldapConfig.bindPass, searchBase: ldapConfig.searchBase, - searchFilter: "(uid={{username}})", + searchFilter: ldapConfig.searchFilter || "(uid={{username}})", // searchAttributes: ["uid", "uidNumber", "givenName", "sn", "mail"], ...(ldapConfig.caCert !== "" ? { @@ -650,6 +657,23 @@ export const ldapConfigServiceFactory = ({ return deletedGroupMap; }; + const testLDAPConnection = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TTestLdapConnectionDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap); + + const plan = await licenseService.getPlan(orgId); + if (!plan.ldap) + throw new BadRequestError({ + message: "Failed to test LDAP connection due to plan restriction. Upgrade plan to test the LDAP connection." + }); + + const ldapConfig = await getLdapCfg({ + orgId + }); + + return testLDAPConfig(ldapConfig); + }; + return { createLdapCfg, updateLdapCfg, @@ -660,6 +684,7 @@ export const ldapConfigServiceFactory = ({ bootLdap, getLdapGroupMaps, createLdapGroupMap, - deleteLdapGroupMap + deleteLdapGroupMap, + testLDAPConnection }; }; diff --git a/backend/src/ee/services/ldap-config/ldap-config-types.ts b/backend/src/ee/services/ldap-config/ldap-config-types.ts index 03254b92f..b7e9feb7b 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-types.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-types.ts @@ -20,6 +20,7 @@ export type TCreateLdapCfgDTO = { bindDN: string; bindPass: string; searchBase: string; + searchFilter: string; groupSearchBase: string; groupSearchFilter: string; caCert: string; @@ -33,6 +34,7 @@ export type TUpdateLdapCfgDTO = { bindDN: string; bindPass: string; searchBase: string; + searchFilter: string; groupSearchBase: string; groupSearchFilter: string; caCert: string; @@ -72,3 +74,7 @@ export type TDeleteLdapGroupMapDTO = { ldapConfigId: string; ldapGroupMapId: string; } & TOrgPermission; + +export type TTestLdapConnectionDTO = { + ldapConfigId: string; +} & TOrgPermission; diff --git a/backend/src/ee/services/ldap-config/ldap-fns.ts b/backend/src/ee/services/ldap-config/ldap-fns.ts index 922fcb3f5..66d799583 100644 --- a/backend/src/ee/services/ldap-config/ldap-fns.ts +++ b/backend/src/ee/services/ldap-config/ldap-fns.ts @@ -4,6 +4,65 @@ import { logger } from "@app/lib/logger"; import { TLDAPConfig } from "./ldap-config-types"; +export const isValidLdapFilter = (filter: string) => { + try { + ldapjs.parseFilter(filter); + return true; + } catch (error) { + logger.error("Invalid LDAP filter"); + logger.error(error); + return false; + } +}; + +/** + * Test the LDAP configuration by attempting to bind to the LDAP server + * @param ldapConfig - The LDAP configuration to test + * @returns {Boolean} isConnected - Whether or not the connection was successful + */ +export const testLDAPConfig = async (ldapConfig: TLDAPConfig): Promise => { + return new Promise((resolve) => { + const ldapClient = ldapjs.createClient({ + url: ldapConfig.url, + bindDN: ldapConfig.bindDN, + bindCredentials: ldapConfig.bindPass, + ...(ldapConfig.caCert !== "" + ? { + tlsOptions: { + ca: [ldapConfig.caCert] + } + } + : {}) + }); + + ldapClient.on("error", (err) => { + logger.error("LDAP client error:", err); + logger.error(err); + resolve(false); + }); + + ldapClient.bind(ldapConfig.bindDN, ldapConfig.bindPass, (err) => { + if (err) { + logger.error("Error binding to LDAP"); + logger.error(err); + ldapClient.unbind(); + resolve(false); + } else { + logger.info("Successfully connected and bound to LDAP."); + ldapClient.unbind(); + resolve(true); + } + }); + }); +}; + +/** + * Search for groups in the LDAP server + * @param ldapConfig - The LDAP configuration to use + * @param filter - The filter to use when searching for groups + * @param base - The base to search from + * @returns + */ export const searchGroups = async ( ldapConfig: TLDAPConfig, filter: string, @@ -31,11 +90,7 @@ export const searchGroups = async ( }, (err, res) => { if (err) { - ldapClient.unbind((unbindError) => { - if (unbindError) { - logger.error("Error unbinding LDAP client:", unbindError); - } - }); + ldapClient.unbind(); return reject(err); } @@ -51,19 +106,11 @@ export const searchGroups = async ( groups.push({ dn, cn }); }); res.on("error", (error) => { - ldapClient.unbind((unbindError) => { - if (unbindError) { - logger.error("Error unbinding LDAP client:", unbindError); - } - }); + ldapClient.unbind(); reject(error); }); res.on("end", () => { - ldapClient.unbind((unbindError) => { - if (unbindError) { - logger.error("Error unbinding LDAP client:", unbindError); - } - }); + ldapClient.unbind(); resolve(groups); }); } diff --git a/docs/documentation/platform/ldap/general.mdx b/docs/documentation/platform/ldap/general.mdx index 535cce734..aa4841625 100644 --- a/docs/documentation/platform/ldap/general.mdx +++ b/docs/documentation/platform/ldap/general.mdx @@ -25,9 +25,10 @@ You can configure your organization in Infisical to have members authenticate wi - URL: The LDAP server to connect to such as `ldap://ldap.your-org.com`, `ldaps://ldap.myorg.com:636` (for connection over SSL/TLS), etc. - Bind DN: The distinguished name of object to bind when performing the user search such as `cn=infisical,ou=Users,dc=acme,dc=com`. - Bind Pass: The password to use along with `Bind DN` when performing the user search. - - Search Base / User DN: Base DN under which to perform user search such as `ou=Users,dc=acme,dc=com` + - User Search Base / User DN: Base DN under which to perform user search such as `ou=Users,dc=acme,dc=com`. + - User Search Filter (optional): Template used to construct the LDAP user search filter such as `(uid={{username}})`; use literal `{{username}}` to have the given username used in the search. The default is `(uid={{username}})` which is compatible with several common directory schemas. - Group Search Base / Group DN (optional): LDAP search base to use for group membership search such as `ou=Groups,dc=acme,dc=com`. - - Group Filter (optional): Template used when constructing the group membership query such as `(objectClass=posixGroup)`. The template can access the following context variables: [`UserDN`, `UserUID`, `UserName`]. The default is `(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))` which is compatible with several common directory schemas. + - Group Filter (optional): Template used when constructing the group membership query such as `(&(objectClass=posixGroup)(memberUid={{.Username}}))`. The template can access the following context variables: [`UserDN`, `UserName`]. The default is `(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))` which is compatible with several common directory schemas. - CA Certificate: The CA certificate to use when verifying the LDAP server certificate. @@ -35,6 +36,14 @@ You can configure your organization in Infisical to have members authenticate wi + + Once you've filled out the LDAP configuration, you can test that part of the configuration is correct by pressing the **Test Connection** button. + + Infisical will attempt to bind to the LDAP server using the provided **URL**, **Bind DN**, and **Bind Pass**. If the operation is successful, then Infisical will display a success message; if not, then Infisical will display an error message and provide a fuller error in the server logs. + + ![LDAP test connection](/images/platform/ldap/ldap-test-connection.png) + + In order to sync LDAP groups to Infisical, head to the **LDAP Group Mappings** section to define mappings from LDAP groups to groups in Infisical. diff --git a/docs/documentation/platform/ldap/jumpcloud.mdx b/docs/documentation/platform/ldap/jumpcloud.mdx index 32b253eb7..0b40d8b3a 100644 --- a/docs/documentation/platform/ldap/jumpcloud.mdx +++ b/docs/documentation/platform/ldap/jumpcloud.mdx @@ -21,7 +21,6 @@ description: "Learn how to configure JumpCloud LDAP for authenticating into Infi Next, under User Security Settings and Permissions > Permission Settings, check the box next to **Enable as LDAP Bind DN**. ![LDAP JumpCloud](/images/platform/ldap/jumpcloud/ldap-jumpcloud-enable-bind-dn.png) - In Infisical, head to your Organization Settings > Security > LDAP and select **Manage**. @@ -35,9 +34,10 @@ description: "Learn how to configure JumpCloud LDAP for authenticating into Infi - URL: The LDAP server to connect to (`ldaps://ldap.jumpcloud.com:636`). - Bind DN: The distinguished name of object to bind when performing the user search (`uid=,ou=Users,o=,dc=jumpcloud,dc=com`). - Bind Pass: The password to use along with `Bind DN` when performing the user search. - - Search Base / User DN: Base DN under which to perform user search (`ou=Users,o=,dc=jumpcloud,dc=com`). + - User Search Base / User DN: Base DN under which to perform user search (`ou=Users,o=,dc=jumpcloud,dc=com`). + - User Search Filter (optional): Template used to construct the LDAP user search filter (`(uid={{username}})`). - Group Search Base / Group DN (optional): LDAP search base to use for group membership search (`ou=Users,o=,dc=jumpcloud,dc=com`). - - Group Filter (optional): Template used when constructing the group membership query (`(objectClass=groupOfNames)`). + - Group Filter (optional): Template used when constructing the group membership query (`(&(objectClass=groupOfNames)(member=uid={{.Username}},ou=Users,o=,dc=jumpcloud,dc=com))`) - CA Certificate: The CA certificate to use when verifying the LDAP server certificate (instructions to obtain the certificate for JumpCloud [here](https://jumpcloud.com/support/connect-to-ldap-with-tls-ssl)). @@ -47,6 +47,13 @@ description: "Learn how to configure JumpCloud LDAP for authenticating into Infi in your LDAP instance **ORG DN**. + + Once you've filled out the LDAP configuration, you can test that part of the configuration is correct by pressing the **Test Connection** button. + + Infisical will attempt to bind to the LDAP server using the provided **URL**, **Bind DN**, and **Bind Pass**. If the operation is successful, then Infisical will display a success message; if not, then Infisical will display an error message and provide a fuller error in the server logs. + + ![LDAP test connection](/images/platform/ldap/ldap-test-connection.png) + In order to sync LDAP groups to Infisical, head to the **LDAP Group Mappings** section to define mappings from LDAP groups to groups in Infisical. diff --git a/docs/images/platform/ldap/ldap-config.png b/docs/images/platform/ldap/ldap-config.png index 499b942b0..2cd711dd1 100644 Binary files a/docs/images/platform/ldap/ldap-config.png and b/docs/images/platform/ldap/ldap-config.png differ diff --git a/docs/images/platform/ldap/ldap-test-connection.png b/docs/images/platform/ldap/ldap-test-connection.png new file mode 100644 index 000000000..9f1a3896c Binary files /dev/null and b/docs/images/platform/ldap/ldap-test-connection.png differ diff --git a/frontend/src/hooks/api/ldapConfig/index.tsx b/frontend/src/hooks/api/ldapConfig/index.tsx index 26af75089..20ba1469a 100644 --- a/frontend/src/hooks/api/ldapConfig/index.tsx +++ b/frontend/src/hooks/api/ldapConfig/index.tsx @@ -2,5 +2,6 @@ export { useCreateLDAPConfig, useCreateLDAPGroupMapping, useDeleteLDAPGroupMapping, + useTestLDAPConnection, useUpdateLDAPConfig} from "./mutations"; export { useGetLDAPConfig, useGetLDAPGroupMaps } from "./queries"; diff --git a/frontend/src/hooks/api/ldapConfig/mutations.tsx b/frontend/src/hooks/api/ldapConfig/mutations.tsx index 50651b367..a93286cdb 100644 --- a/frontend/src/hooks/api/ldapConfig/mutations.tsx +++ b/frontend/src/hooks/api/ldapConfig/mutations.tsx @@ -14,6 +14,7 @@ export const useCreateLDAPConfig = () => { bindDN, bindPass, searchBase, + searchFilter, groupSearchBase, groupSearchFilter, caCert @@ -24,6 +25,7 @@ export const useCreateLDAPConfig = () => { bindDN: string; bindPass: string; searchBase: string; + searchFilter: string; groupSearchBase: string; groupSearchFilter: string; caCert?: string; @@ -35,6 +37,7 @@ export const useCreateLDAPConfig = () => { bindDN, bindPass, searchBase, + searchFilter, groupSearchBase, groupSearchFilter, caCert @@ -58,6 +61,7 @@ export const useUpdateLDAPConfig = () => { bindDN, bindPass, searchBase, + searchFilter, groupSearchBase, groupSearchFilter, caCert @@ -68,6 +72,7 @@ export const useUpdateLDAPConfig = () => { bindDN?: string; bindPass?: string; searchBase?: string; + searchFilter?: string; groupSearchBase?: string; groupSearchFilter?: string; caCert?: string; @@ -79,6 +84,7 @@ export const useUpdateLDAPConfig = () => { bindDN, bindPass, searchBase, + searchFilter, groupSearchBase, groupSearchFilter, caCert @@ -136,3 +142,14 @@ export const useDeleteLDAPGroupMapping = () => { } }); }; + +export const useTestLDAPConnection = () => { + return useMutation({ + mutationFn: async (ldapConfigId: string) => { + const { data } = await apiRequest.post( + `/api/v1/ldap/config/${ldapConfigId}/test-connection` + ); + return data; + } + }); +}; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/LDAPModal.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/LDAPModal.tsx index 773cba3f4..8a8a9e467 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/LDAPModal.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/LDAPModal.tsx @@ -6,7 +6,12 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Modal, ModalContent, TextArea } from "@app/components/v2"; import { useOrganization } from "@app/context"; -import { useCreateLDAPConfig, useGetLDAPConfig, useUpdateLDAPConfig } from "@app/hooks/api"; +import { + useCreateLDAPConfig, + useGetLDAPConfig, + useTestLDAPConnection, + useUpdateLDAPConfig +} from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; const LDAPFormSchema = z.object({ @@ -14,6 +19,7 @@ const LDAPFormSchema = z.object({ bindDN: z.string().default(""), bindPass: z.string().default(""), searchBase: z.string().default(""), + searchFilter: z.string().default(""), groupSearchBase: z.string().default(""), groupSearchFilter: z.string().default(""), caCert: z.string().optional() @@ -32,12 +38,22 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateLDAPConfig(); const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateLDAPConfig(); + const { mutateAsync: testLDAPConnection } = useTestLDAPConnection(); const { data } = useGetLDAPConfig(currentOrg?.id ?? ""); - const { control, handleSubmit, reset } = useForm({ + const { control, handleSubmit, reset, watch } = useForm({ resolver: zodResolver(LDAPFormSchema) }); + const watchUrl = watch("url"); + const watchBindDN = watch("bindDN"); + const watchBindPass = watch("bindPass"); + const watchSearchBase = watch("searchBase"); + const watchSearchFilter = watch("searchFilter"); + const watchGroupSearchBase = watch("groupSearchBase"); + const watchGroupSearchFilter = watch("groupSearchFilter"); + const watchCaCert = watch("caCert"); + useEffect(() => { if (data) { reset({ @@ -45,6 +61,7 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) bindDN: data?.bindDN ?? "", bindPass: data?.bindPass ?? "", searchBase: data?.searchBase ?? "", + searchFilter: data?.searchFilter ?? "", groupSearchBase: data?.groupSearchBase ?? "", groupSearchFilter: data?.groupSearchFilter ?? "", caCert: data?.caCert ?? "" @@ -57,10 +74,12 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) bindDN, bindPass, searchBase, + searchFilter, groupSearchBase, groupSearchFilter, - caCert - }: TLDAPFormData) => { + caCert, + shouldCloseModal = true + }: TLDAPFormData & { shouldCloseModal?: boolean }) => { try { if (!currentOrg) return; @@ -72,6 +91,7 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) bindDN, bindPass, searchBase, + searchFilter, groupSearchBase, groupSearchFilter, caCert @@ -84,13 +104,16 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) bindDN, bindPass, searchBase, + searchFilter, groupSearchBase, groupSearchFilter, caCert }); } - handlePopUpClose("addLDAP"); + if (shouldCloseModal) { + handlePopUpClose("addLDAP"); + } createNotification({ text: `Successfully ${!data ? "added" : "updated"} LDAP configuration`, @@ -105,6 +128,45 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) } }; + const handleTestLDAPConnection = async () => { + try { + await onSSOModalSubmit({ + url: watchUrl, + bindDN: watchBindDN, + bindPass: watchBindPass, + searchBase: watchSearchBase, + searchFilter: watchSearchFilter, + groupSearchBase: watchGroupSearchBase, + groupSearchFilter: watchGroupSearchFilter, + caCert: watchCaCert, + shouldCloseModal: false + }); + + if (!data) return; + + const result = await testLDAPConnection(data.id); + + if (!result) { + createNotification({ + text: "Failed to test the LDAP connection: Bind operation was unsuccessful", + type: "error" + }); + return; + } + + createNotification({ + text: "Successfully tested the LDAP connection: Bind operation was successful", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to test the LDAP connection", + type: "error" + }); + } + }; + return ( ( @@ -155,6 +217,19 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) )} /> + ( + + + + )} + /> - + )} /> @@ -203,12 +281,8 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) > {!data ? "Add" : "Update"} - diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgLDAPSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgLDAPSection.tsx index 4c2d4009e..87d0f28af 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgLDAPSection.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgLDAPSection.tsx @@ -68,6 +68,7 @@ export const OrgLDAPSection = (): JSX.Element => { bindDN: "", bindPass: "", searchBase: "", + searchFilter: "", groupSearchBase: "", groupSearchFilter: "" }); @@ -97,13 +98,15 @@ export const OrgLDAPSection = (): JSX.Element => {

LDAP

- - {(isAllowed) => ( - - )} - +
+ + {(isAllowed) => ( + + )} + +

Manage LDAP authentication configuration