Add user search filter field for LDAP and validation for search filters

This commit is contained in:
Tuan Dang
2024-04-24 20:58:16 -07:00
parent 99d59a38d5
commit 8d5ef5f4d9
12 changed files with 132 additions and 14 deletions
@@ -0,0 +1,15 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.LdapConfig, (t) => {
t.string("searchFilter").notNullable().defaultTo("");
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.LdapConfig, (t) => {
t.dropColumn("searchFilter");
});
}
+2 -1
View File
@@ -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<typeof LdapConfigsSchema>;
+15 -5
View File
@@ -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";
@@ -55,10 +55,14 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as TLDAPConfig;
const groupFilter = "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))";
const searchFilter = (ldapConfig.groupSearchFilter || groupFilter)
const groupSearchFilter = (ldapConfig.groupSearchFilter || groupFilter)
.replace(/{{\.Username}}/g, user.uid)
.replace(/{{\.UserDN}}/g, user.dn);
if (!isValidLdapFilter(groupSearchFilter)) {
throw new Error("Generated LDAP search filter is invalid.");
}
const { isUserCompleted, providerAuthToken } = await server.services.ldap.ldapLogin({
ldapConfigId: ldapConfig.id,
externalId: user.uidNumber,
@@ -67,7 +71,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
lastName: user.sn ?? "",
emails: user.mail ? [user.mail] : [],
groups: ldapConfig.groupSearchBase
? await searchGroups(ldapConfig, searchFilter, ldapConfig.groupSearchBase)
? await searchGroups(ldapConfig, groupSearchFilter, ldapConfig.groupSearchBase)
: undefined,
relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState,
orgId: (req as unknown as FastifyRequest).ldapConfig.organization
@@ -130,6 +134,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()
@@ -163,8 +168,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: {
@@ -200,6 +209,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()
@@ -40,7 +40,7 @@ import {
TTestLdapConnectionDTO,
TUpdateLdapCfgDTO
} from "./ldap-config-types";
import { testLDAPConfig } from "./ldap-fns";
import { isValidLdapFilter, testLDAPConfig } from "./ldap-fns";
import { TLdapGroupMapDALFactory } from "./ldap-group-map-dal";
type TLdapConfigServiceFactoryDep = {
@@ -98,6 +98,7 @@ export const ldapConfigServiceFactory = ({
bindDN,
bindPass,
searchBase,
searchFilter,
groupSearchBase,
groupSearchFilter,
caCert
@@ -112,6 +113,18 @@ export const ldapConfigServiceFactory = ({
"Failed to create LDAP configuration due to plan restriction. Upgrade plan to create LDAP configuration."
});
const isSearchFilterValid = isValidLdapFilter(searchFilter);
if (!isSearchFilterValid)
throw new BadRequestError({
message: "Failed to create LDAP configuration due to invalid search filter."
});
const isGroupSearchFilterValid = isValidLdapFilter(groupSearchFilter);
if (!isGroupSearchFilterValid)
throw new BadRequestError({
message: "Failed to create LDAP configuration due to invalid group search filter."
});
const orgBot = await orgBotDAL.transaction(async (tx) => {
const doc = await orgBotDAL.findOne({ orgId }, tx);
if (doc) return doc;
@@ -175,6 +188,7 @@ export const ldapConfigServiceFactory = ({
bindPassIV,
bindPassTag,
searchBase,
searchFilter,
groupSearchBase,
groupSearchFilter,
encryptedCACert,
@@ -196,6 +210,7 @@ export const ldapConfigServiceFactory = ({
bindDN,
bindPass,
searchBase,
searchFilter,
groupSearchBase,
groupSearchFilter,
caCert
@@ -210,10 +225,27 @@ export const ldapConfigServiceFactory = ({
"Failed to update LDAP configuration due to plan restriction. Upgrade plan to update LDAP configuration."
});
if (searchFilter) {
const isSearchFilterValid = isValidLdapFilter(searchFilter);
if (!isSearchFilterValid)
throw new BadRequestError({
message: "Failed to update LDAP configuration due to invalid search filter."
});
}
if (groupSearchFilter) {
const isGroupSearchFilterValid = isValidLdapFilter(groupSearchFilter);
if (!isGroupSearchFilterValid)
throw new BadRequestError({
message: "Failed to update LDAP configuration due to invalid group search filter."
});
}
const updateQuery: TLdapConfigsUpdate = {
isActive,
url,
searchBase,
searchFilter,
groupSearchBase,
groupSearchFilter
};
@@ -317,6 +349,7 @@ export const ldapConfigServiceFactory = ({
bindDN,
bindPass,
searchBase: ldapConfig.searchBase,
searchFilter: ldapConfig.searchFilter,
groupSearchBase: ldapConfig.groupSearchBase,
groupSearchFilter: ldapConfig.groupSearchFilter,
caCert
@@ -352,7 +385,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 !== ""
? {
@@ -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;
@@ -4,6 +4,17 @@ 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