Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/beige-days-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@rocket.chat/core-typings': minor
'@rocket.chat/i18n': minor
'@rocket.chat/meteor': minor
---

Adds new settings to allow configuring custom variables with string manipulation functions on the LDAP data mapper
2 changes: 2 additions & 0 deletions apps/meteor/.mocharc.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ module.exports = {
exit: true,
spec: [
'lib/callbacks.spec.ts',
'server/lib/ldap/*.spec.ts',
'server/lib/ldap/**/*.spec.ts',
'ee/server/lib/ldap/*.spec.ts',
'ee/tests/**/*.tests.ts',
'ee/tests/**/*.spec.ts',
Expand Down
15 changes: 14 additions & 1 deletion apps/meteor/server/lib/ldap/Connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import type {
ILDAPCallback,
ILDAPPageCallback,
} from '@rocket.chat/core-typings';
import { wrapExceptions } from '@rocket.chat/tools';
import ldapjs from 'ldapjs';

import { logger, connLogger, searchLogger, authLogger, bindLogger, mapLogger } from './Logger';
import { getLDAPConditionalSetting } from './getLDAPConditionalSetting';
import { processLdapVariables, type LDAPVariableMap } from './processLdapVariables';
import { settings } from '../../../app/settings/server';
import { ensureArray } from '../../../lib/utils/arrayUtils';

Expand Down Expand Up @@ -50,6 +52,8 @@ export class LDAPConnection {

private usingAuthentication: boolean;

private _variableMap: LDAPVariableMap;

constructor() {
this.ldapjs = ldapjs;

Expand Down Expand Up @@ -83,9 +87,18 @@ export class LDAPConnection {
authentication: settings.get<boolean>('LDAP_Authentication') ?? false,
authenticationUserDN: settings.get<string>('LDAP_Authentication_UserDN') ?? '',
authenticationPassword: settings.get<string>('LDAP_Authentication_Password') ?? '',
useVariables: settings.get<boolean>('LDAP_DataSync_UseVariables') ?? false,
variableMap: settings.get<string>('LDAP_DataSync_VariableMap') ?? '{}',
attributesToQuery: this.parseAttributeList(settings.get<string>('LDAP_User_Search_AttributesToQuery')),
};

this._variableMap =
(this.options.useVariables &&
wrapExceptions(() => JSON.parse(this.options.variableMap)).suppress(() => {
mapLogger.error({ msg: 'Failed to parse LDAP Variable Map', map: this.options.variableMap });
})) ||
{};

if (!this.options.host) {
logger.warn('LDAP Host is not configured.');
}
Expand Down Expand Up @@ -322,7 +335,7 @@ export class LDAPConnection {
mapLogger.debug({ msg: 'Extracted Attribute', key, type: dataType, value: values[key] });
});

return values;
return processLdapVariables(values, this._variableMap);
}

public async doCustomSearch<T>(baseDN: string, searchOptions: ldapjs.SearchOptions, entryCallback: ILDAPEntryCallback<T>): Promise<T[]> {
Expand Down
47 changes: 8 additions & 39 deletions apps/meteor/server/lib/ldap/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { LDAPConnection } from './Connection';
import { logger, authLogger, connLogger } from './Logger';
import { LDAPUserConverter } from './UserConverter';
import { getLDAPConditionalSetting } from './getLDAPConditionalSetting';
import { getLdapDynamicValue } from './getLdapDynamicValue';
import { getLdapString } from './getLdapString';
import { ldapKeyExists } from './ldapKeyExists';
import type { UserConverterOptions } from '../../../app/importer/server/classes/converters/UserConverter';
import { setUserAvatar } from '../../../app/lib/server/functions/setUserAvatar';
import { settings } from '../../../app/settings/server';
Expand Down Expand Up @@ -416,43 +419,9 @@ export class LDAPManager {
connLogger.debug(ldapUser);
}

private static ldapKeyExists(ldapUser: ILDAPEntry, key: string): boolean {
return !_.isEmpty(ldapUser[key.trim()]);
}

private static getLdapString(ldapUser: ILDAPEntry, key: string): string {
return ldapUser[key.trim()];
}

private static getLdapDynamicValue(ldapUser: ILDAPEntry, attributeSetting: string | undefined): string | undefined {
if (!attributeSetting) {
return;
}

// If the attribute setting is a template, then convert the variables in it
if (attributeSetting.includes('#{')) {
return attributeSetting.replace(/#{(.+?)}/g, (_match, field) => {
const key = field.trim();

if (this.ldapKeyExists(ldapUser, key)) {
return this.getLdapString(ldapUser, key);
}

return '';
});
}

// If it's not a template, then treat the setting as a CSV list of possible attribute names and return the first valid one.
const attributeList: string[] = attributeSetting.replace(/\s/g, '').split(',');
const key = attributeList.find((field) => this.ldapKeyExists(ldapUser, field));
if (key) {
return this.getLdapString(ldapUser, key);
}
}

private static getLdapName(ldapUser: ILDAPEntry): string | undefined {
const nameAttributes = getLDAPConditionalSetting<string | undefined>('LDAP_Name_Field');
return this.getLdapDynamicValue(ldapUser, nameAttributes);
return getLdapDynamicValue(ldapUser, nameAttributes);
}

private static getLdapExtension(ldapUser: ILDAPEntry): string | undefined {
Expand All @@ -461,14 +430,14 @@ export class LDAPManager {
return;
}

return this.getLdapString(ldapUser, extensionAttribute);
return getLdapString(ldapUser, extensionAttribute);
}

private static getLdapEmails(ldapUser: ILDAPEntry, username?: string): string[] {
const emailAttributes = getLDAPConditionalSetting<string>('LDAP_Email_Field');
if (emailAttributes) {
const attributeList: string[] = emailAttributes.replace(/\s/g, '').split(',');
const key = attributeList.find((field) => this.ldapKeyExists(ldapUser, field));
const key = attributeList.find((field) => ldapKeyExists(ldapUser, field));

const emails: string[] = [].concat(key ? ldapUser[key.trim()] : []);
const filteredEmails = emails.filter((email) => email.includes('@'));
Expand Down Expand Up @@ -512,7 +481,7 @@ export class LDAPManager {

protected static getLdapUsername(ldapUser: ILDAPEntry): string | undefined {
const usernameField = getLDAPConditionalSetting('LDAP_Username_Field') as string;
return this.getLdapDynamicValue(ldapUser, usernameField);
return getLdapDynamicValue(ldapUser, usernameField);
}

protected static getFederationHomeServer(ldapUser: ILDAPEntry): string | undefined {
Expand All @@ -521,7 +490,7 @@ export class LDAPManager {
}

const homeServerField = settings.get<string>('LDAP_FederationHomeServer_Field');
const homeServer = this.getLdapDynamicValue(ldapUser, homeServerField);
const homeServer = getLdapDynamicValue(ldapUser, homeServerField);

if (!homeServer) {
return;
Expand Down
65 changes: 65 additions & 0 deletions apps/meteor/server/lib/ldap/getLdapDynamicValue.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { ILDAPEntry } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import 'mocha';

import { getLdapDynamicValue } from './getLdapDynamicValue';

describe('getLdapDynamicValue', () => {
const ldapUser: ILDAPEntry = {
_raw: {},
displayName: 'John Doe',
email: '[email protected]',
uid: 'johndoe',
emptyField: '',
};

it('should return undefined if attributeSetting is undefined', () => {
const result = getLdapDynamicValue(ldapUser, undefined);
expect(result).to.be.undefined;
});

it('should return the correct value from a single valid attribute', () => {
const result = getLdapDynamicValue(ldapUser, 'displayName');
expect(result).to.equal('John Doe');
});

it('should return the correct value from a template attribute', () => {
const result = getLdapDynamicValue(ldapUser, 'Hello, #{displayName}!');
expect(result).to.equal('Hello, John Doe!');
});

it('should replace missing keys with an empty string in a template', () => {
const result = getLdapDynamicValue(ldapUser, 'Hello, #{nonExistentField}!');
expect(result).to.equal('Hello, !');
});

it('should return the first valid key from a CSV list of attributes', () => {
const result = getLdapDynamicValue(ldapUser, 'nonExistentField,email,uid');
expect(result).to.equal('[email protected]');
});

it('should return undefined if none of the keys in CSV list exist', () => {
const result = getLdapDynamicValue(ldapUser, 'nonExistentField,anotherNonExistentField');
expect(result).to.be.undefined;
});

it('should handle attribute keys with surrounding whitespace correctly', () => {
const result = getLdapDynamicValue(ldapUser, ' email ');
expect(result).to.equal('[email protected]');
});

it('should correctly resolve multiple variables in a template', () => {
const result = getLdapDynamicValue(ldapUser, 'User: #{displayName}, Email: #{email}, UID: #{uid}');
expect(result).to.equal('User: John Doe, Email: [email protected], UID: johndoe');
});

it('should return undefined if the attribute has an empty value', () => {
const result = getLdapDynamicValue(ldapUser, 'emptyField');
expect(result).to.be.undefined;
});

it('should return an empty string if using only a template attribute that has an empty value', () => {
const result = getLdapDynamicValue(ldapUser, '#{emptyField}');
expect(result).to.be.equal('');
});
});
31 changes: 31 additions & 0 deletions apps/meteor/server/lib/ldap/getLdapDynamicValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { ILDAPEntry } from '@rocket.chat/core-typings';

import { getLdapString } from './getLdapString';
import { ldapKeyExists } from './ldapKeyExists';

export function getLdapDynamicValue(ldapUser: ILDAPEntry, attributeSetting: string | undefined): string | undefined {
if (!attributeSetting) {
return;
}

// If the attribute setting is a template, then convert the variables in it
if (attributeSetting.includes('#{')) {
return attributeSetting.replace(/#{(.+?)}/g, (_match, field) => {
const key = field.trim();

if (ldapKeyExists(ldapUser, key)) {
// We've already validated so it won't ever return undefined, but add a fallback to ensure it doesn't break if something gets changed
return getLdapString(ldapUser, key) || '';
}

return '';
});
}

// If it's not a template, then treat the setting as a CSV list of possible attribute names and return the first valid one.
const attributeList: string[] = attributeSetting.replace(/\s/g, '').split(',');
const key = attributeList.find((field) => ldapKeyExists(ldapUser, field));
if (key) {
return getLdapString(ldapUser, key);
}
}
47 changes: 47 additions & 0 deletions apps/meteor/server/lib/ldap/getLdapString.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { ILDAPEntry } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import 'mocha';

import { getLdapString } from './getLdapString';

const ldapUser: ILDAPEntry = {
_raw: {},
username: 'john_doe',
email: '[email protected]',
phoneNumber: '123-456-7890',
memberOf: 'group1,group2',
};

describe('getLdapString', () => {
it('should return the correct value for a given key', () => {
expect(getLdapString(ldapUser, 'username')).to.equal('john_doe');
expect(getLdapString(ldapUser, 'email')).to.equal('[email protected]');
expect(getLdapString(ldapUser, 'phoneNumber')).to.equal('123-456-7890');
expect(getLdapString(ldapUser, 'memberOf')).to.equal('group1,group2');
});

it('should trim the key and return the correct value', () => {
expect(getLdapString(ldapUser, ' username ')).to.equal('john_doe');
expect(getLdapString(ldapUser, ' email ')).to.equal('[email protected]');
});

it('should return undefined for non-existing keys', () => {
expect(getLdapString(ldapUser, 'nonExistingKey')).to.be.undefined;
expect(getLdapString(ldapUser, 'foo')).to.be.undefined;
});

it('should handle empty keys and return an empty string', () => {
expect(getLdapString(ldapUser, '')).to.be.undefined;
expect(getLdapString(ldapUser, ' ')).to.be.undefined;
});

it('should handle keys with only whitespace', () => {
expect(getLdapString(ldapUser, ' ')).to.be.undefined;
expect(getLdapString(ldapUser, ' ')).to.be.undefined;
});

it('should handle case-sensitive keys accurately', () => {
expect(getLdapString(ldapUser, 'Username')).to.be.undefined;
expect(getLdapString(ldapUser, 'EMAIL')).to.be.undefined;
});
});
5 changes: 5 additions & 0 deletions apps/meteor/server/lib/ldap/getLdapString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { ILDAPEntry } from '@rocket.chat/core-typings';

export function getLdapString(ldapUser: ILDAPEntry, key: string): string | undefined {
return ldapUser[key.trim()];
}
Loading
Loading