Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@ export class UserConverter extends RecordConverter<IImportUserRecord, UserConver
return;
}

if (Boolean(userData.federated) !== Boolean(existingUser.federated)) {
throw new Error("Local and Federated users can't be converted to each other.");
}

userData._id = _id;

if (!userData.roles && !existingUser.roles) {
Expand Down Expand Up @@ -295,7 +299,7 @@ export class UserConverter extends RecordConverter<IImportUserRecord, UserConver
await Users.setUtcOffset(_id, userData.utcOffset);
}

if (userData.name || userData.username) {
if (userData.name || (userData.username && !userData.federated)) {
await saveUserIdentity({ _id, name: userData.name, username: userData.username } as Parameters<typeof saveUserIdentity>[0]);
}

Expand Down Expand Up @@ -347,6 +351,7 @@ export class UserConverter extends RecordConverter<IImportUserRecord, UserConver
...(!!userData.customFields && { customFields: userData.customFields }),
...(userData.deleted !== undefined && { active: !userData.deleted }),
...(userData.voipExtension !== undefined && { freeSwitchExtension: userData.voipExtension }),
...(userData.federated !== undefined && { federated: userData.federated }),
};
}

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
93 changes: 55 additions & 38 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 @@ -41,6 +44,11 @@ export class LDAPManager {
return this.fallbackToDefaultLogin(username, password);
}

const homeServer = this.getFederationHomeServer(ldapUser);
if (homeServer) {
return this.fallbackToDefaultLogin(username, password);
}

const slugifiedUsername = this.slugifyUsername(ldapUser, username);
const user = await this.findExistingUser(ldapUser, slugifiedUsername);

Expand Down Expand Up @@ -78,6 +86,11 @@ export class LDAPManager {
return;
}

const homeServer = this.getFederationHomeServer(ldapUser);
if (homeServer) {
return;
}

const slugifiedUsername = this.slugifyUsername(ldapUser, username);
const user = await this.findExistingUser(ldapUser, slugifiedUsername);

Expand Down Expand Up @@ -165,6 +178,7 @@ export class LDAPManager {

const { attribute: idAttribute, value: id } = uniqueId;
const username = this.slugifyUsername(ldapUser, usedUsername || id || '') || undefined;
const homeServer = this.getFederationHomeServer(ldapUser);
const emails = this.getLdapEmails(ldapUser, username).map((email) => email.trim());
const name = this.getLdapName(ldapUser) || undefined;
const voipExtension = this.getLdapExtension(ldapUser);
Expand All @@ -182,6 +196,10 @@ export class LDAPManager {
id,
},
},
...(homeServer && {
username: `${username}:${homeServer}`,
federated: true,
}),
};

this.onMapUserData(ldapUser, userData);
Expand Down Expand Up @@ -401,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 @@ -446,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 @@ -497,7 +481,40 @@ 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 {
if (!settings.get<boolean>('Federation_Matrix_enabled')) {
return;
}

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

if (!homeServer) {
return;
}

logger.debug({ msg: 'User has a federation home server', homeServer });

const localServer = settings.get<string>('Federation_Matrix_homeserver_domain');
if (localServer === homeServer) {
return;
}

return homeServer;
}

protected static getFederatedUsername(ldapUser: ILDAPEntry, requestUsername: string): string {
const username = this.slugifyUsername(ldapUser, requestUsername);
const homeServer = this.getFederationHomeServer(ldapUser);

if (homeServer) {
return `${username}:${homeServer}`;
}

return username;
}

// This method will find existing users by LDAP id or by username.
Expand Down
30 changes: 30 additions & 0 deletions apps/meteor/server/lib/ldap/getLdapDynamicValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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)) {
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);
}
}
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 {
return ldapUser[key.trim()];
}
6 changes: 6 additions & 0 deletions apps/meteor/server/lib/ldap/ldapKeyExists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { ILDAPEntry } from '@rocket.chat/core-typings';
import _ from 'underscore';

export function ldapKeyExists(ldapUser: ILDAPEntry, key: string): boolean {
return !_.isEmpty(ldapUser[key.trim()]);
}
31 changes: 31 additions & 0 deletions apps/meteor/server/lib/ldap/operations/executeOperation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { ILDAPEntry } from '@rocket.chat/core-typings';

import { executeFallback, type LDAPVariableFallback } from './fallback';
import { executeMatch, type LDAPVariableMatch } from './match';
import { executeReplace, type LDAPVariableReplace } from './replace';
import { executeSplit, type LDAPVariableSplit } from './split';
import { executeSubstring, type LDAPVariableSubString } from './substring';

export type LDAPVariableOperation =
| LDAPVariableReplace
| LDAPVariableMatch
| LDAPVariableSubString
| LDAPVariableFallback
| LDAPVariableSplit;

export function executeOperation(ldapUser: ILDAPEntry, input: string, operation?: LDAPVariableOperation): string | undefined {
switch (operation?.operation) {
case 'replace':
return executeReplace(input, operation);
case 'match':
return executeMatch(input, operation);
case 'substring':
return executeSubstring(input, operation);
case 'fallback':
return executeFallback(ldapUser, input, operation);
case 'split':
return executeSplit(input, operation);
}

return input;
}
24 changes: 24 additions & 0 deletions apps/meteor/server/lib/ldap/operations/fallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { ILDAPEntry } from '@rocket.chat/core-typings';

import { getLdapDynamicValue } from '../getLdapDynamicValue';

export type LDAPVariableFallback = {
operation: 'fallback';
fallback: string;

minLength?: number;
};

export function executeFallback(ldapUser: ILDAPEntry, input: string, operation: LDAPVariableFallback): string | undefined {
let valid = Boolean(input);

if (valid && typeof operation.minLength === 'number') {
valid = input.length >= operation.minLength;
}

if (valid) {
return input;
}

return getLdapDynamicValue(ldapUser, operation.fallback);
}
28 changes: 28 additions & 0 deletions apps/meteor/server/lib/ldap/operations/match.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export type LDAPVariableMatch = {
operation: 'match';
pattern: string;
regex?: boolean;
flags?: string;
indexToUse?: number;
valueIfTrue?: string;
valueIfFalse?: string;
};

export function executeMatch(input: string, operation: LDAPVariableMatch): string | undefined {
if (!operation.pattern || (typeof operation.valueIfTrue !== 'string' && typeof operation.indexToUse !== 'number')) {
throw new Error('Invalid MATCH operation.');
}

const pattern = operation.regex ? new RegExp(operation.pattern, operation.flags) : operation.pattern;

const result = input.match(pattern);
if (!result) {
return operation.valueIfFalse;
}

if (typeof operation.indexToUse === 'number' && result.length > operation.indexToUse) {
return result[operation.indexToUse];
}

return operation.valueIfTrue;
}
23 changes: 23 additions & 0 deletions apps/meteor/server/lib/ldap/operations/replace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export type LDAPVariableReplace = {
operation: 'replace';
pattern: string;
regex?: boolean;
flags?: string;
all?: boolean;
replacement: string;
};

export function executeReplace(input: string, operation: LDAPVariableReplace): string {
if (!operation.pattern || typeof operation.replacement !== 'string') {
throw new Error('Invalid REPLACE operation.');
}

const flags = operation.regex && operation.all ? `${operation.flags || ''}${operation.flags?.includes('g') ? '' : 'g'}` : operation.flags;
const pattern = operation.regex ? new RegExp(operation.pattern, flags) : operation.pattern;

if (operation.all) {
return input.replaceAll(pattern, operation.replacement);
}

return input.replace(pattern, operation.replacement);
}
Loading
Loading