Clean LDAP group search impl async/await

This commit is contained in:
Tuan Dang
2024-04-24 08:15:19 -07:00
parent 1c95b3abe7
commit 3ca596d4af
6 changed files with 138 additions and 144 deletions
+12 -67
View File
@@ -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, groups: shouldProcessGroups
? await searchGroups(ldapConfig, searchFilter, ldapConfig.groupSearchBase)
: undefined,
relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState, relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState,
orgId: (req as unknown as FastifyRequest).ldapConfig.organization orgId: (req as unknown as FastifyRequest).ldapConfig.organization
}); });
})
.then(({ isUserCompleted, providerAuthToken }) => { return cb(null, { isUserCompleted, providerAuthToken });
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,8 +450,8 @@ 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) { if (groups) {
const ldapGroupIdsToBePartOf = ( const ldapGroupIdsToBePartOf = (
await ldapGroupMapDAL.find({ await ldapGroupMapDAL.find({
@@ -476,7 +476,7 @@ export const ldapConfigServiceFactory = ({
const ldapGroupIdsCurrentlyPartOf = ( const ldapGroupIdsCurrentlyPartOf = (
await userGroupMembershipDAL.find({ await userGroupMembershipDAL.find({
userId: user.id, userId: newUser.id,
$in: { $in: {
groupId: allLdapGroupMaps.map((groupMap) => groupMap.groupId) groupId: allLdapGroupMaps.map((groupMap) => groupMap.groupId)
} }
@@ -490,14 +490,15 @@ export const ldapConfigServiceFactory = ({
// add user to group that they should be part of // add user to group that they should be part of
await addUsersToGroupByUserIds({ await addUsersToGroupByUserIds({
group, group,
userIds: [user.id], userIds: [newUser.id],
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
orgDAL, orgDAL,
groupProjectDAL, groupProjectDAL,
projectKeyDAL, projectKeyDAL,
projectDAL, projectDAL,
projectBotDAL projectBotDAL,
tx
}); });
} }
} }
@@ -514,16 +515,20 @@ export const ldapConfigServiceFactory = ({
// remove user from group that they should no longer be part of // remove user from group that they should no longer be part of
await removeUsersFromGroupByUserIds({ await removeUsersFromGroupByUserIds({
group, group,
userIds: [user.id], userIds: [newUser.id],
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
groupProjectDAL, groupProjectDAL,
projectKeyDAL projectKeyDAL,
tx
}); });
} }
} }
} }
return newUser;
});
const isUserCompleted = Boolean(user.isAccepted); const isUserCompleted = Boolean(user.isAccepted);
const providerAuthToken = jwt.sign( const providerAuthToken = jwt.sign(
@@ -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) => {
@@ -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={() => {