mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 20:27:43 +00:00
Clean LDAP group search impl async/await
This commit is contained in:
@@ -11,11 +11,11 @@ import { IncomingMessage } from "node:http";
|
|||||||
import { Authenticator } from "@fastify/passport";
|
import { Authenticator } from "@fastify/passport";
|
||||||
import fastifySession from "@fastify/session";
|
import fastifySession from "@fastify/session";
|
||||||
import { FastifyRequest } from "fastify";
|
import { FastifyRequest } from "fastify";
|
||||||
import ldapjs from "ldapjs";
|
|
||||||
import LdapStrategy from "passport-ldapauth";
|
import LdapStrategy from "passport-ldapauth";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { LdapConfigsSchema, LdapGroupMapsSchema } from "@app/db/schemas";
|
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 { searchGroups } from "@app/ee/services/ldap-config/ldap-fns";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
@@ -46,91 +46,36 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
interface LDAPConfig {
|
|
||||||
id: string;
|
|
||||||
organization: string;
|
|
||||||
isActive: boolean;
|
|
||||||
url: string;
|
|
||||||
bindDN: string;
|
|
||||||
bindPass: string;
|
|
||||||
searchBase: string;
|
|
||||||
groupSearchBase: string;
|
|
||||||
groupSearchFilter: string;
|
|
||||||
caCert: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
passport.use(
|
passport.use(
|
||||||
new LdapStrategy(
|
new LdapStrategy(
|
||||||
getLdapPassportOpts as any,
|
getLdapPassportOpts as any,
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
async (req: IncomingMessage, user, cb) => {
|
async (req: IncomingMessage, user, cb) => {
|
||||||
try {
|
try {
|
||||||
const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as LDAPConfig;
|
const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as TLDAPConfig;
|
||||||
|
|
||||||
if (!ldapConfig.groupSearchFilter || !ldapConfig.groupSearchBase) {
|
|
||||||
// If group search values are not provided, proceed directly to LDAP login
|
|
||||||
return await server.services.ldap
|
|
||||||
.ldapLogin({
|
|
||||||
ldapConfigId: ldapConfig.id,
|
|
||||||
externalId: user.uidNumber,
|
|
||||||
username: user.uid,
|
|
||||||
firstName: user.givenName ?? user.cn ?? "",
|
|
||||||
lastName: user.sn ?? "",
|
|
||||||
emails: user.mail ? [user.mail] : [],
|
|
||||||
relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState,
|
|
||||||
orgId: (req as unknown as FastifyRequest).ldapConfig.organization
|
|
||||||
})
|
|
||||||
.then(({ isUserCompleted, providerAuthToken }) => {
|
|
||||||
cb(null, { isUserCompleted, providerAuthToken });
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.error(err);
|
|
||||||
cb(err, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// query for groups
|
|
||||||
const ldapClient = ldapjs.createClient({
|
|
||||||
url: ldapConfig.url,
|
|
||||||
bindDN: ldapConfig.bindDN,
|
|
||||||
bindCredentials: ldapConfig.bindPass,
|
|
||||||
...(ldapConfig.caCert !== ""
|
|
||||||
? {
|
|
||||||
tlsOptions: {
|
|
||||||
ca: [ldapConfig.caCert]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: {})
|
|
||||||
});
|
|
||||||
|
|
||||||
const groupFilter = "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))";
|
const groupFilter = "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))";
|
||||||
const searchFilter =
|
const searchFilter =
|
||||||
ldapConfig.groupSearchFilter ||
|
ldapConfig.groupSearchFilter ||
|
||||||
groupFilter.replace("{{.Username}}", user.uid).replace("{{.UserDN}}", user.dn);
|
groupFilter.replace("{{.Username}}", user.uid).replace("{{.UserDN}}", user.dn);
|
||||||
|
|
||||||
searchGroups(ldapClient, searchFilter, ldapConfig.groupSearchBase)
|
const shouldProcessGroups = ldapConfig.groupSearchFilter && ldapConfig.groupSearchBase;
|
||||||
.then((groups) => {
|
|
||||||
ldapClient.unbind();
|
const { isUserCompleted, providerAuthToken } = await server.services.ldap.ldapLogin({
|
||||||
return server.services.ldap.ldapLogin({
|
ldapConfigId: ldapConfig.id,
|
||||||
ldapConfigId: ldapConfig.id,
|
externalId: user.uidNumber,
|
||||||
externalId: user.uidNumber,
|
username: user.uid,
|
||||||
username: user.uid,
|
firstName: user.givenName ?? user.cn ?? "",
|
||||||
firstName: user.givenName ?? user.cn ?? "",
|
lastName: user.sn ?? "",
|
||||||
lastName: user.sn ?? "",
|
emails: user.mail ? [user.mail] : [],
|
||||||
emails: user.mail ? [user.mail] : [],
|
groups: shouldProcessGroups
|
||||||
groups,
|
? await searchGroups(ldapConfig, searchFilter, ldapConfig.groupSearchBase)
|
||||||
relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState,
|
: undefined,
|
||||||
orgId: (req as unknown as FastifyRequest).ldapConfig.organization
|
relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState,
|
||||||
});
|
orgId: (req as unknown as FastifyRequest).ldapConfig.organization
|
||||||
})
|
});
|
||||||
.then(({ isUserCompleted, providerAuthToken }) => {
|
|
||||||
cb(null, { isUserCompleted, providerAuthToken });
|
return cb(null, { isUserCompleted, providerAuthToken });
|
||||||
})
|
|
||||||
.catch((err2) => {
|
|
||||||
ldapClient.unbind();
|
|
||||||
logger.error(err2);
|
|
||||||
cb(err2, false);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
return cb(error, false);
|
return cb(error, false);
|
||||||
@@ -220,8 +165,8 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
|||||||
bindDN: z.string().trim(),
|
bindDN: z.string().trim(),
|
||||||
bindPass: z.string().trim(),
|
bindPass: z.string().trim(),
|
||||||
searchBase: z.string().trim(),
|
searchBase: z.string().trim(),
|
||||||
groupSearchBase: z.string().trim(),
|
groupSearchBase: z.string().trim().default(""),
|
||||||
groupSearchFilter: z.string().trim(),
|
groupSearchFilter: z.string().trim().default(""),
|
||||||
caCert: z.string().trim().default("")
|
caCert: z.string().trim().default("")
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
|||||||
@@ -450,79 +450,84 @@ export const ldapConfigServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await userDAL.findOne({ id: userAlias.userId });
|
const user = await userDAL.transaction(async (tx) => {
|
||||||
|
const newUser = await userDAL.findOne({ id: userAlias.userId }, tx);
|
||||||
|
if (groups) {
|
||||||
|
const ldapGroupIdsToBePartOf = (
|
||||||
|
await ldapGroupMapDAL.find({
|
||||||
|
ldapConfigId,
|
||||||
|
$in: {
|
||||||
|
ldapGroupCN: groups.map((group) => group.cn)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).map((groupMap) => groupMap.groupId);
|
||||||
|
|
||||||
if (groups) {
|
const groupsToBePartOf = await groupDAL.find({
|
||||||
const ldapGroupIdsToBePartOf = (
|
orgId,
|
||||||
await ldapGroupMapDAL.find({
|
|
||||||
ldapConfigId,
|
|
||||||
$in: {
|
$in: {
|
||||||
ldapGroupCN: groups.map((group) => group.cn)
|
id: ldapGroupIdsToBePartOf
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
).map((groupMap) => groupMap.groupId);
|
const toBePartOfGroupIdsSet = new Set(groupsToBePartOf.map((groupToBePartOf) => groupToBePartOf.id));
|
||||||
|
|
||||||
const groupsToBePartOf = await groupDAL.find({
|
const allLdapGroupMaps = await ldapGroupMapDAL.find({
|
||||||
orgId,
|
ldapConfigId
|
||||||
$in: {
|
});
|
||||||
id: ldapGroupIdsToBePartOf
|
|
||||||
|
const ldapGroupIdsCurrentlyPartOf = (
|
||||||
|
await userGroupMembershipDAL.find({
|
||||||
|
userId: newUser.id,
|
||||||
|
$in: {
|
||||||
|
groupId: allLdapGroupMaps.map((groupMap) => groupMap.groupId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).map((userGroupMembership) => userGroupMembership.groupId);
|
||||||
|
|
||||||
|
const userGroupMembershipGroupIdsSet = new Set(ldapGroupIdsCurrentlyPartOf);
|
||||||
|
|
||||||
|
for await (const group of groupsToBePartOf) {
|
||||||
|
if (!userGroupMembershipGroupIdsSet.has(group.id)) {
|
||||||
|
// add user to group that they should be part of
|
||||||
|
await addUsersToGroupByUserIds({
|
||||||
|
group,
|
||||||
|
userIds: [newUser.id],
|
||||||
|
userDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
|
orgDAL,
|
||||||
|
groupProjectDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
const toBePartOfGroupIdsSet = new Set(groupsToBePartOf.map((groupToBePartOf) => groupToBePartOf.id));
|
|
||||||
|
|
||||||
const allLdapGroupMaps = await ldapGroupMapDAL.find({
|
const groupsCurrentlyPartOf = await groupDAL.find({
|
||||||
ldapConfigId
|
orgId,
|
||||||
});
|
|
||||||
|
|
||||||
const ldapGroupIdsCurrentlyPartOf = (
|
|
||||||
await userGroupMembershipDAL.find({
|
|
||||||
userId: user.id,
|
|
||||||
$in: {
|
$in: {
|
||||||
groupId: allLdapGroupMaps.map((groupMap) => groupMap.groupId)
|
id: ldapGroupIdsCurrentlyPartOf
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
).map((userGroupMembership) => userGroupMembership.groupId);
|
|
||||||
|
|
||||||
const userGroupMembershipGroupIdsSet = new Set(ldapGroupIdsCurrentlyPartOf);
|
for await (const group of groupsCurrentlyPartOf) {
|
||||||
|
if (!toBePartOfGroupIdsSet.has(group.id)) {
|
||||||
for await (const group of groupsToBePartOf) {
|
// remove user from group that they should no longer be part of
|
||||||
if (!userGroupMembershipGroupIdsSet.has(group.id)) {
|
await removeUsersFromGroupByUserIds({
|
||||||
// add user to group that they should be part of
|
group,
|
||||||
await addUsersToGroupByUserIds({
|
userIds: [newUser.id],
|
||||||
group,
|
userDAL,
|
||||||
userIds: [user.id],
|
userGroupMembershipDAL,
|
||||||
userDAL,
|
groupProjectDAL,
|
||||||
userGroupMembershipDAL,
|
projectKeyDAL,
|
||||||
orgDAL,
|
tx
|
||||||
groupProjectDAL,
|
});
|
||||||
projectKeyDAL,
|
}
|
||||||
projectDAL,
|
|
||||||
projectBotDAL
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupsCurrentlyPartOf = await groupDAL.find({
|
return newUser;
|
||||||
orgId,
|
});
|
||||||
$in: {
|
|
||||||
id: ldapGroupIdsCurrentlyPartOf
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for await (const group of groupsCurrentlyPartOf) {
|
|
||||||
if (!toBePartOfGroupIdsSet.has(group.id)) {
|
|
||||||
// remove user from group that they should no longer be part of
|
|
||||||
await removeUsersFromGroupByUserIds({
|
|
||||||
group,
|
|
||||||
userIds: [user.id],
|
|
||||||
userDAL,
|
|
||||||
userGroupMembershipDAL,
|
|
||||||
groupProjectDAL,
|
|
||||||
projectKeyDAL
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUserCompleted = Boolean(user.isAccepted);
|
const isUserCompleted = Boolean(user.isAccepted);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
import { TOrgPermission } from "@app/lib/types";
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TLDAPConfig = {
|
||||||
|
id: string;
|
||||||
|
organization: string;
|
||||||
|
isActive: boolean;
|
||||||
|
url: string;
|
||||||
|
bindDN: string;
|
||||||
|
bindPass: string;
|
||||||
|
searchBase: string;
|
||||||
|
groupSearchBase: string;
|
||||||
|
groupSearchFilter: string;
|
||||||
|
caCert: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TCreateLdapCfgDTO = {
|
export type TCreateLdapCfgDTO = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
|||||||
@@ -1,11 +1,28 @@
|
|||||||
import ldap from "ldapjs";
|
import ldapjs from "ldapjs";
|
||||||
|
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { TLDAPConfig } from "./ldap-config-types";
|
||||||
|
|
||||||
export const searchGroups = async (
|
export const searchGroups = async (
|
||||||
ldapClient: ldap.Client,
|
ldapConfig: TLDAPConfig,
|
||||||
filter: string,
|
filter: string,
|
||||||
base: string
|
base: string
|
||||||
): Promise<{ dn: string; cn: string }[]> => {
|
): Promise<{ dn: string; cn: string }[]> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
const ldapClient = ldapjs.createClient({
|
||||||
|
url: ldapConfig.url,
|
||||||
|
bindDN: ldapConfig.bindDN,
|
||||||
|
bindCredentials: ldapConfig.bindPass,
|
||||||
|
...(ldapConfig.caCert !== ""
|
||||||
|
? {
|
||||||
|
tlsOptions: {
|
||||||
|
ca: [ldapConfig.caCert]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
});
|
||||||
|
|
||||||
ldapClient.search(
|
ldapClient.search(
|
||||||
base,
|
base,
|
||||||
{
|
{
|
||||||
@@ -14,6 +31,11 @@ export const searchGroups = async (
|
|||||||
},
|
},
|
||||||
(err, res) => {
|
(err, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
ldapClient.unbind((unbindError) => {
|
||||||
|
if (unbindError) {
|
||||||
|
logger.error("Error unbinding LDAP client:", unbindError);
|
||||||
|
}
|
||||||
|
});
|
||||||
return reject(err);
|
return reject(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,10 +51,19 @@ export const searchGroups = async (
|
|||||||
groups.push({ dn, cn });
|
groups.push({ dn, cn });
|
||||||
});
|
});
|
||||||
res.on("error", (error) => {
|
res.on("error", (error) => {
|
||||||
console.error(`error: ${error.message}`);
|
ldapClient.unbind((unbindError) => {
|
||||||
|
if (unbindError) {
|
||||||
|
logger.error("Error unbinding LDAP client:", unbindError);
|
||||||
|
}
|
||||||
|
});
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
res.on("end", () => {
|
res.on("end", () => {
|
||||||
|
ldapClient.unbind((unbindError) => {
|
||||||
|
if (unbindError) {
|
||||||
|
logger.error("Error unbinding LDAP client:", unbindError);
|
||||||
|
}
|
||||||
|
});
|
||||||
resolve(groups);
|
resolve(groups);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ export const ldapGroupMapDALFactory = (db: TDbClient) => {
|
|||||||
.select(selectAllTableCols(TableName.LdapGroupMap))
|
.select(selectAllTableCols(TableName.LdapGroupMap))
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.Groups).as("groupId"),
|
db.ref("id").withSchema(TableName.Groups).as("groupId"),
|
||||||
db.ref("name").withSchema(TableName.Groups).as("groupSlug"),
|
db.ref("name").withSchema(TableName.Groups).as("groupName"),
|
||||||
db.ref("slug").withSchema(TableName.Groups).as("groupName")
|
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||||
);
|
);
|
||||||
|
|
||||||
return docs.map((doc) => {
|
return docs.map((doc) => {
|
||||||
|
|||||||
+2
-2
@@ -201,11 +201,11 @@ export const LDAPGroupMapModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }:
|
|||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={3} innerKey="ldap-group-maps" />}
|
{isLoading && <TableSkeleton columns={3} innerKey="ldap-group-maps" />}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
groupMaps?.map(({ id, ldapGroupCN, group: { name } }) => {
|
groupMaps?.map(({ id, ldapGroupCN, group }) => {
|
||||||
return (
|
return (
|
||||||
<Tr className="h-10 items-center" key={`ldap-group-map-${id}`}>
|
<Tr className="h-10 items-center" key={`ldap-group-map-${id}`}>
|
||||||
<Td>{ldapGroupCN}</Td>
|
<Td>{ldapGroupCN}</Td>
|
||||||
<Td>{name}</Td>
|
<Td>{group.name}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user