diff --git a/backend/src/db/migrations/20240626053953_add-ldap-unique-user-attribute.ts b/backend/src/db/migrations/20240626053953_add-ldap-unique-user-attribute.ts new file mode 100644 index 000000000..dc87ff515 --- /dev/null +++ b/backend/src/db/migrations/20240626053953_add-ldap-unique-user-attribute.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.LdapConfig, "uniqueUserAttribute"))) { + await knex.schema.alterTable(TableName.LdapConfig, (tb) => { + tb.string("uniqueUserAttribute").notNullable().defaultTo(""); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.LdapConfig, "uniqueUserAttribute")) { + await knex.schema.alterTable(TableName.LdapConfig, (t) => { + t.dropColumn("uniqueUserAttribute"); + }); + } +} diff --git a/backend/src/db/schemas/ldap-configs.ts b/backend/src/db/schemas/ldap-configs.ts index 86fd6acb6..460c2cff6 100644 --- a/backend/src/db/schemas/ldap-configs.ts +++ b/backend/src/db/schemas/ldap-configs.ts @@ -26,7 +26,8 @@ export const LdapConfigsSchema = z.object({ updatedAt: z.date(), groupSearchBase: z.string().default(""), groupSearchFilter: z.string().default(""), - searchFilter: z.string().default("") + searchFilter: z.string().default(""), + uniqueUserAttribute: 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 8cccbaac6..735ba632c 100644 --- a/backend/src/ee/routes/v1/ldap-router.ts +++ b/backend/src/ee/routes/v1/ldap-router.ts @@ -70,10 +70,13 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { groups = await searchGroups(ldapConfig, groupSearchFilter, ldapConfig.groupSearchBase); } + const externalId = ldapConfig.uniqueUserAttribute ? user[ldapConfig.uniqueUserAttribute] : user.uidNumber; + const username = ldapConfig.uniqueUserAttribute ? externalId : user.uid; + const { isUserCompleted, providerAuthToken } = await server.services.ldap.ldapLogin({ + externalId, + username, ldapConfigId: ldapConfig.id, - externalId: user.uidNumber, - username: user.uid, firstName: user.givenName ?? user.cn ?? "", lastName: user.sn ?? "", email: user.mail, @@ -138,6 +141,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { url: z.string(), bindDN: z.string(), bindPass: z.string(), + uniqueUserAttribute: z.string(), searchBase: z.string(), searchFilter: z.string(), groupSearchBase: z.string(), @@ -172,6 +176,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { url: z.string().trim(), bindDN: z.string().trim(), bindPass: z.string().trim(), + uniqueUserAttribute: z.string().trim().default("uidNumber"), searchBase: z.string().trim(), searchFilter: z.string().trim().default("(uid={{username}})"), groupSearchBase: z.string().trim(), @@ -213,6 +218,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { url: z.string().trim(), bindDN: z.string().trim(), bindPass: z.string().trim(), + uniqueUserAttribute: z.string().trim(), searchBase: z.string().trim(), searchFilter: z.string().trim(), groupSearchBase: z.string().trim(), 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 63565d94c..62791d7a4 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -122,6 +122,7 @@ export const ldapConfigServiceFactory = ({ url, bindDN, bindPass, + uniqueUserAttribute, searchBase, searchFilter, groupSearchBase, @@ -200,6 +201,7 @@ export const ldapConfigServiceFactory = ({ encryptedBindPass, bindPassIV, bindPassTag, + uniqueUserAttribute, searchBase, searchFilter, groupSearchBase, @@ -222,6 +224,7 @@ export const ldapConfigServiceFactory = ({ url, bindDN, bindPass, + uniqueUserAttribute, searchBase, searchFilter, groupSearchBase, @@ -244,7 +247,8 @@ export const ldapConfigServiceFactory = ({ searchBase, searchFilter, groupSearchBase, - groupSearchFilter + groupSearchFilter, + uniqueUserAttribute }; const orgBot = await orgBotDAL.findOne({ orgId }); @@ -345,6 +349,7 @@ export const ldapConfigServiceFactory = ({ url: ldapConfig.url, bindDN, bindPass, + uniqueUserAttribute: ldapConfig.uniqueUserAttribute, searchBase: ldapConfig.searchBase, searchFilter: ldapConfig.searchFilter, groupSearchBase: ldapConfig.groupSearchBase, @@ -381,6 +386,7 @@ export const ldapConfigServiceFactory = ({ url: ldapConfig.url, bindDN: ldapConfig.bindDN, bindCredentials: ldapConfig.bindPass, + uniqueUserAttribute: ldapConfig.uniqueUserAttribute, searchBase: ldapConfig.searchBase, searchFilter: ldapConfig.searchFilter || "(uid={{username}})", // searchAttributes: ["uid", "uidNumber", "givenName", "sn", "mail"], 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 aa4aa8da7..86f4bf0d5 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-types.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-types.ts @@ -7,6 +7,7 @@ export type TLDAPConfig = { url: string; bindDN: string; bindPass: string; + uniqueUserAttribute: string; searchBase: string; groupSearchBase: string; groupSearchFilter: string; @@ -19,6 +20,7 @@ export type TCreateLdapCfgDTO = { url: string; bindDN: string; bindPass: string; + uniqueUserAttribute: string; searchBase: string; searchFilter: string; groupSearchBase: string; @@ -33,6 +35,7 @@ export type TUpdateLdapCfgDTO = { url: string; bindDN: string; bindPass: string; + uniqueUserAttribute: string; searchBase: string; searchFilter: string; groupSearchBase: string; diff --git a/docs/documentation/platform/ldap/general.mdx b/docs/documentation/platform/ldap/general.mdx index 5e4253a34..939eaa727 100644 --- a/docs/documentation/platform/ldap/general.mdx +++ b/docs/documentation/platform/ldap/general.mdx @@ -30,6 +30,7 @@ Prerequisites: - 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. - User Search Base / User DN: Base DN under which to perform user search such as `ou=Users,dc=acme,dc=com`. + - Unique User Attribute: The attribute to use as the unique identifier of LDAP users such as `sAMAccountName`, `cn`, `uid`, `objectGUID` ... If left blank, defaults to `uidNumber` - 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)(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. diff --git a/docs/documentation/platform/ldap/jumpcloud.mdx b/docs/documentation/platform/ldap/jumpcloud.mdx index b92b52bb9..39579b785 100644 --- a/docs/documentation/platform/ldap/jumpcloud.mdx +++ b/docs/documentation/platform/ldap/jumpcloud.mdx @@ -39,6 +39,7 @@ Prerequisites: - 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. - User Search Base / User DN: Base DN under which to perform user search (`ou=Users,o=,dc=jumpcloud,dc=com`). + - Unique User Attribute: The attribute to use as the unique identifier of LDAP users such as `sAMAccountName`, `cn`, `uid`, `objectGUID` ... If left blank, defaults to `uidNumber` - 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)(member=uid={{.Username}},ou=Users,o=,dc=jumpcloud,dc=com))`) diff --git a/docs/images/platform/ldap/ldap-config.png b/docs/images/platform/ldap/ldap-config.png index 2cd711dd1..0ba0b5772 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 index 9f1a3896c..7400aafd5 100644 Binary files a/docs/images/platform/ldap/ldap-test-connection.png and b/docs/images/platform/ldap/ldap-test-connection.png differ diff --git a/frontend/src/hooks/api/ldapConfig/mutations.tsx b/frontend/src/hooks/api/ldapConfig/mutations.tsx index a93286cdb..2014f52bd 100644 --- a/frontend/src/hooks/api/ldapConfig/mutations.tsx +++ b/frontend/src/hooks/api/ldapConfig/mutations.tsx @@ -13,6 +13,7 @@ export const useCreateLDAPConfig = () => { url, bindDN, bindPass, + uniqueUserAttribute, searchBase, searchFilter, groupSearchBase, @@ -24,6 +25,7 @@ export const useCreateLDAPConfig = () => { url: string; bindDN: string; bindPass: string; + uniqueUserAttribute: string; searchBase: string; searchFilter: string; groupSearchBase: string; @@ -36,6 +38,7 @@ export const useCreateLDAPConfig = () => { url, bindDN, bindPass, + uniqueUserAttribute, searchBase, searchFilter, groupSearchBase, @@ -60,6 +63,7 @@ export const useUpdateLDAPConfig = () => { url, bindDN, bindPass, + uniqueUserAttribute, searchBase, searchFilter, groupSearchBase, @@ -71,6 +75,7 @@ export const useUpdateLDAPConfig = () => { url?: string; bindDN?: string; bindPass?: string; + uniqueUserAttribute?: string; searchBase?: string; searchFilter?: string; groupSearchBase?: string; @@ -83,6 +88,7 @@ export const useUpdateLDAPConfig = () => { url, bindDN, bindPass, + uniqueUserAttribute, searchBase, searchFilter, groupSearchBase, diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/LDAPModal.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/LDAPModal.tsx index 8a8a9e467..e8afe9822 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/LDAPModal.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/LDAPModal.tsx @@ -20,6 +20,7 @@ const LDAPFormSchema = z.object({ bindPass: z.string().default(""), searchBase: z.string().default(""), searchFilter: z.string().default(""), + uniqueUserAttribute: z.string().default(""), groupSearchBase: z.string().default(""), groupSearchFilter: z.string().default(""), caCert: z.string().optional() @@ -53,6 +54,7 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) const watchGroupSearchBase = watch("groupSearchBase"); const watchGroupSearchFilter = watch("groupSearchFilter"); const watchCaCert = watch("caCert"); + const watchUniqueUserAttribute = watch("uniqueUserAttribute"); useEffect(() => { if (data) { @@ -64,7 +66,8 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) searchFilter: data?.searchFilter ?? "", groupSearchBase: data?.groupSearchBase ?? "", groupSearchFilter: data?.groupSearchFilter ?? "", - caCert: data?.caCert ?? "" + caCert: data?.caCert ?? "", + uniqueUserAttribute: data?.uniqueUserAttribute ?? "" }); } }, [data]); @@ -73,6 +76,7 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) url, bindDN, bindPass, + uniqueUserAttribute, searchBase, searchFilter, groupSearchBase, @@ -92,6 +96,7 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) bindPass, searchBase, searchFilter, + uniqueUserAttribute, groupSearchBase, groupSearchFilter, caCert @@ -105,6 +110,7 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) bindPass, searchBase, searchFilter, + uniqueUserAttribute, groupSearchBase, groupSearchFilter, caCert @@ -138,6 +144,7 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) searchFilter: watchSearchFilter, groupSearchBase: watchGroupSearchBase, groupSearchFilter: watchGroupSearchFilter, + uniqueUserAttribute: watchUniqueUserAttribute, caCert: watchCaCert, shouldCloseModal: false }); @@ -217,6 +224,19 @@ export const LDAPModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) )} /> + ( + + + + )} + /> { url: "", bindDN: "", bindPass: "", + uniqueUserAttribute: "", searchBase: "", searchFilter: "", groupSearchBase: "",