commit
bdce4e0c1c
@ -0,0 +1,33 @@ |
||||
import { hasRole } from '../../../authorization/server'; |
||||
import { settings } from '../../../settings/server'; |
||||
import { API } from '../api'; |
||||
import { SystemLogger } from '../../../../server/lib/logger/system'; |
||||
import { LDAP } from '../../../../server/sdk'; |
||||
|
||||
|
||||
API.v1.addRoute('ldap.testConnection', { authRequired: true }, { |
||||
post() { |
||||
if (!this.userId) { |
||||
throw new Error('error-invalid-user'); |
||||
} |
||||
|
||||
if (!hasRole(this.userId, 'admin')) { |
||||
throw new Error('error-not-authorized'); |
||||
} |
||||
|
||||
if (settings.get('LDAP_Enable') !== true) { |
||||
throw new Error('LDAP_disabled'); |
||||
} |
||||
|
||||
try { |
||||
Promise.await(LDAP.testConnection()); |
||||
} catch (error) { |
||||
SystemLogger.error(error); |
||||
throw new Error('Connection_failed'); |
||||
} |
||||
|
||||
return API.v1.success({ |
||||
message: 'Connection_success', |
||||
}); |
||||
}, |
||||
}); |
||||
@ -0,0 +1,149 @@ |
||||
import { Random } from 'meteor/random'; |
||||
|
||||
import type { IImportUserRecord, IImportChannelRecord, IImportMessageRecord, IImportRecord, IImportRecordType, IImportData } from '../../../../definition/IImportRecord'; |
||||
import { IImportChannel } from '../../../../definition/IImportChannel'; |
||||
import { ImportDataConverter } from './ImportDataConverter'; |
||||
import type { IConverterOptions } from './ImportDataConverter'; |
||||
|
||||
export class VirtualDataConverter extends ImportDataConverter { |
||||
protected _userRecords: Array<IImportUserRecord>; |
||||
|
||||
protected _channelRecords: Array<IImportChannelRecord>; |
||||
|
||||
protected _messageRecords: Array<IImportMessageRecord>; |
||||
|
||||
protected useVirtual: boolean; |
||||
|
||||
constructor(virtual = true, options?: IConverterOptions) { |
||||
super(options); |
||||
|
||||
this.useVirtual = virtual; |
||||
if (virtual) { |
||||
this.clearVirtualData(); |
||||
} |
||||
} |
||||
|
||||
public clearImportData(): void { |
||||
if (!this.useVirtual) { |
||||
return super.clearImportData(); |
||||
} |
||||
|
||||
this.clearVirtualData(); |
||||
} |
||||
|
||||
public clearSuccessfullyImportedData(): void { |
||||
if (!this.useVirtual) { |
||||
return super.clearSuccessfullyImportedData(); |
||||
} |
||||
|
||||
this.clearVirtualData(); |
||||
} |
||||
|
||||
public findDMForImportedUsers(...users: Array<string>): IImportChannel | undefined { |
||||
if (!this.useVirtual) { |
||||
return super.findDMForImportedUsers(...users); |
||||
} |
||||
|
||||
// The original method is only used by the hipchat importer so we probably don't need to implement this on the virtual converter.
|
||||
return undefined; |
||||
} |
||||
|
||||
protected addObject(type: IImportRecordType, data: IImportData, options: Record<string, any> = {}): void { |
||||
if (!this.useVirtual) { |
||||
return super.addObject(type, data, options); |
||||
} |
||||
|
||||
const list = this.getObjectList(type); |
||||
|
||||
list.push({ |
||||
_id: Random.id(), |
||||
data, |
||||
dataType: type, |
||||
...options, |
||||
}); |
||||
} |
||||
|
||||
protected async getUsersToImport(): Promise<Array<IImportUserRecord>> { |
||||
if (!this.useVirtual) { |
||||
return super.getUsersToImport(); |
||||
} |
||||
|
||||
return this._userRecords; |
||||
} |
||||
|
||||
protected saveError(importId: string, error: Error): void { |
||||
if (!this.useVirtual) { |
||||
return super.saveError(importId, error); |
||||
} |
||||
|
||||
const record = this.getVirtualRecordById(importId); |
||||
|
||||
if (!record) { |
||||
return; |
||||
} |
||||
|
||||
if (!record.errors) { |
||||
record.errors = []; |
||||
} |
||||
|
||||
record.errors.push({ |
||||
message: error.message, |
||||
stack: error.stack, |
||||
}); |
||||
} |
||||
|
||||
protected skipRecord(_id: string): void { |
||||
if (!this.useVirtual) { |
||||
return super.skipRecord(_id); |
||||
} |
||||
|
||||
const record = this.getVirtualRecordById(_id); |
||||
|
||||
if (record) { |
||||
record.skipped = true; |
||||
} |
||||
} |
||||
|
||||
protected async getMessagesToImport(): Promise<IImportMessageRecord[]> { |
||||
if (!this.useVirtual) { |
||||
return super.getMessagesToImport(); |
||||
} |
||||
|
||||
return this._messageRecords; |
||||
} |
||||
|
||||
protected async getChannelsToImport(): Promise<IImportChannelRecord[]> { |
||||
if (!this.useVirtual) { |
||||
return super.getChannelsToImport(); |
||||
} |
||||
|
||||
return this._channelRecords; |
||||
} |
||||
|
||||
private clearVirtualData(): void { |
||||
this._userRecords = []; |
||||
this._channelRecords = []; |
||||
this._messageRecords = []; |
||||
} |
||||
|
||||
private getObjectList(type: IImportRecordType): Array<IImportRecord> { |
||||
switch (type) { |
||||
case 'user': |
||||
return this._userRecords; |
||||
case 'channel': |
||||
return this._channelRecords; |
||||
case 'message': |
||||
return this._messageRecords; |
||||
} |
||||
} |
||||
|
||||
private getVirtualRecordById(id: string): IImportRecord | undefined { |
||||
for (const store of [this._userRecords, this._channelRecords, this._messageRecords]) { |
||||
for (const record of store) { |
||||
if (record._id === id) { |
||||
return record; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,11 @@ |
||||
import { IImportUser } from '../../../../definition/IImportUser'; |
||||
import { IImportMessage } from '../../../../definition/IImportMessage'; |
||||
import { IImportChannel } from '../../../../definition/IImportChannel'; |
||||
|
||||
export type ImporterBeforeImportCallback = {(data: IImportUser | IImportChannel | IImportMessage, type: string): boolean} |
||||
export type ImporterAfterImportCallback = {(data: IImportUser | IImportChannel | IImportMessage, type: string, isNewRecord: boolean): void}; |
||||
|
||||
export interface IConversionCallbacks { |
||||
beforeImportFn?: ImporterBeforeImportCallback; |
||||
afterImportFn?: ImporterAfterImportCallback; |
||||
} |
||||
@ -1 +0,0 @@ |
||||
import './loginHelper'; |
||||
@ -1,34 +0,0 @@ |
||||
// Pass in username, password as normal
|
||||
// customLdapOptions should be passed in if you want to override LDAP_DEFAULTS
|
||||
// on any particular call (if you have multiple ldap servers you'd like to connect to)
|
||||
// You'll likely want to set the dn value here {dn: "..."}
|
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Accounts } from 'meteor/accounts-base'; |
||||
|
||||
Meteor.loginWithLDAP = function(...args) { |
||||
// Pull username and password
|
||||
const username = args.shift(); |
||||
const password = args.shift(); |
||||
|
||||
// Check if last argument is a function
|
||||
// if it is, pop it off and set callback to it
|
||||
const callback = typeof args[args.length - 1] === 'function' ? args.pop() : null; |
||||
|
||||
// if args still holds options item, grab it
|
||||
const customLdapOptions = args.length > 0 ? args.shift() : {}; |
||||
|
||||
// Set up loginRequest object
|
||||
const loginRequest = { |
||||
ldap: true, |
||||
username, |
||||
ldapPass: password, |
||||
ldapOptions: customLdapOptions, |
||||
}; |
||||
|
||||
Accounts.callLoginMethod({ |
||||
// Call login method with ldap = true
|
||||
// This will hook into our login handler for ldap
|
||||
methodArguments: [loginRequest], |
||||
userCallback: callback, |
||||
}); |
||||
}; |
||||
@ -1,5 +0,0 @@ |
||||
import './loginHandler'; |
||||
import './settings'; |
||||
import './testConnection'; |
||||
import './syncUsers'; |
||||
import './sync'; |
||||
@ -1,520 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import ldapjs from 'ldapjs'; |
||||
import Bunyan from 'bunyan'; |
||||
|
||||
import { callbacks } from '../../callbacks/server'; |
||||
import { settings } from '../../settings'; |
||||
import { Logger } from '../../logger'; |
||||
|
||||
const logger = new Logger('LDAP'); |
||||
|
||||
export const connLogger = logger.section('Connection'); |
||||
export const bindLogger = logger.section('Bind'); |
||||
export const searchLogger = logger.section('Search'); |
||||
export const authLogger = logger.section('Auth'); |
||||
|
||||
export default class LDAP { |
||||
constructor() { |
||||
this.ldapjs = ldapjs; |
||||
|
||||
this.connected = false; |
||||
|
||||
this.options = { |
||||
host: settings.get('LDAP_Host'), |
||||
port: settings.get('LDAP_Port'), |
||||
Reconnect: settings.get('LDAP_Reconnect'), |
||||
Internal_Log_Level: settings.get('LDAP_Internal_Log_Level'), |
||||
timeout: settings.get('LDAP_Timeout'), |
||||
connect_timeout: settings.get('LDAP_Connect_Timeout'), |
||||
idle_timeout: settings.get('LDAP_Idle_Timeout'), |
||||
encryption: settings.get('LDAP_Encryption'), |
||||
ca_cert: settings.get('LDAP_CA_Cert'), |
||||
reject_unauthorized: settings.get('LDAP_Reject_Unauthorized') || false, |
||||
Authentication: settings.get('LDAP_Authentication'), |
||||
Authentication_UserDN: settings.get('LDAP_Authentication_UserDN'), |
||||
Authentication_Password: settings.get('LDAP_Authentication_Password'), |
||||
BaseDN: settings.get('LDAP_BaseDN'), |
||||
User_Search_Filter: settings.get('LDAP_User_Search_Filter'), |
||||
User_Search_Scope: settings.get('LDAP_User_Search_Scope'), |
||||
User_Search_Field: settings.get('LDAP_User_Search_Field'), |
||||
Search_Page_Size: settings.get('LDAP_Search_Page_Size'), |
||||
Search_Size_Limit: settings.get('LDAP_Search_Size_Limit'), |
||||
group_filter_enabled: settings.get('LDAP_Group_Filter_Enable'), |
||||
group_filter_object_class: settings.get('LDAP_Group_Filter_ObjectClass'), |
||||
group_filter_group_id_attribute: settings.get('LDAP_Group_Filter_Group_Id_Attribute'), |
||||
group_filter_group_member_attribute: settings.get('LDAP_Group_Filter_Group_Member_Attribute'), |
||||
group_filter_group_member_format: settings.get('LDAP_Group_Filter_Group_Member_Format'), |
||||
group_filter_group_name: settings.get('LDAP_Group_Filter_Group_Name'), |
||||
find_user_after_login: settings.get('LDAP_Find_User_After_Login'), |
||||
}; |
||||
} |
||||
|
||||
connectSync(...args) { |
||||
if (!this._connectSync) { |
||||
this._connectSync = Meteor.wrapAsync(this.connectAsync, this); |
||||
} |
||||
return this._connectSync(...args); |
||||
} |
||||
|
||||
searchAllSync(...args) { |
||||
if (!this._searchAllSync) { |
||||
this._searchAllSync = Meteor.wrapAsync(this.searchAllAsync, this); |
||||
} |
||||
return this._searchAllSync(...args); |
||||
} |
||||
|
||||
connectAsync(callback) { |
||||
connLogger.info('Init setup'); |
||||
|
||||
let replied = false; |
||||
|
||||
const connectionOptions = { |
||||
url: `${ this.options.host }:${ this.options.port }`, |
||||
timeout: this.options.timeout, |
||||
connectTimeout: this.options.connect_timeout, |
||||
idleTimeout: this.options.idle_timeout, |
||||
reconnect: this.options.Reconnect, |
||||
}; |
||||
|
||||
if (this.options.Internal_Log_Level !== 'disabled') { |
||||
connectionOptions.log = new Bunyan({ |
||||
name: 'ldapjs', |
||||
component: 'client', |
||||
stream: process.stderr, |
||||
level: this.options.Internal_Log_Level, |
||||
}); |
||||
} |
||||
|
||||
const tlsOptions = { |
||||
rejectUnauthorized: this.options.reject_unauthorized, |
||||
}; |
||||
|
||||
if (this.options.ca_cert && this.options.ca_cert !== '') { |
||||
// Split CA cert into array of strings
|
||||
const chainLines = settings.get('LDAP_CA_Cert').split('\n'); |
||||
let cert = []; |
||||
const ca = []; |
||||
chainLines.forEach((line) => { |
||||
cert.push(line); |
||||
if (line.match(/-END CERTIFICATE-/)) { |
||||
ca.push(cert.join('\n')); |
||||
cert = []; |
||||
} |
||||
}); |
||||
tlsOptions.ca = ca; |
||||
} |
||||
|
||||
if (this.options.encryption === 'ssl') { |
||||
connectionOptions.url = `ldaps://${ connectionOptions.url }`; |
||||
connectionOptions.tlsOptions = tlsOptions; |
||||
} else { |
||||
connectionOptions.url = `ldap://${ connectionOptions.url }`; |
||||
} |
||||
|
||||
connLogger.info({ msg: 'Connecting', url: connectionOptions.url }); |
||||
connLogger.debug({ msg: 'connectionOptions', connectionOptions }); |
||||
|
||||
this.client = ldapjs.createClient(connectionOptions); |
||||
|
||||
this.bindSync = Meteor.wrapAsync(this.client.bind, this.client); |
||||
|
||||
this.client.on('error', (error) => { |
||||
connLogger.error({ msg: 'connection', err: error }); |
||||
if (replied === false) { |
||||
replied = true; |
||||
callback(error, null); |
||||
} |
||||
}); |
||||
|
||||
this.client.on('idle', () => { |
||||
searchLogger.info('Idle'); |
||||
this.disconnect(); |
||||
}); |
||||
|
||||
this.client.on('close', () => { |
||||
searchLogger.info('Closed'); |
||||
}); |
||||
|
||||
if (this.options.encryption === 'tls') { |
||||
// Set host parameter for tls.connect which is used by ldapjs starttls. This shouldn't be needed in newer nodejs versions (e.g v5.6.0).
|
||||
// https://github.com/RocketChat/Rocket.Chat/issues/2035
|
||||
// https://github.com/mcavage/node-ldapjs/issues/349
|
||||
tlsOptions.host = this.options.host; |
||||
|
||||
connLogger.info('Starting TLS'); |
||||
connLogger.debug({ tlsOptions }); |
||||
|
||||
this.client.starttls(tlsOptions, null, (error, response) => { |
||||
if (error) { |
||||
connLogger.error({ msg: 'TLS connection', err: error }); |
||||
if (replied === false) { |
||||
replied = true; |
||||
callback(error, null); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
connLogger.info('TLS connected'); |
||||
this.connected = true; |
||||
if (replied === false) { |
||||
replied = true; |
||||
callback(null, response); |
||||
} |
||||
}); |
||||
} else { |
||||
this.client.on('connect', (response) => { |
||||
connLogger.info('LDAP connected'); |
||||
this.connected = true; |
||||
if (replied === false) { |
||||
replied = true; |
||||
callback(null, response); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
setTimeout(() => { |
||||
if (replied === false) { |
||||
connLogger.error({ msg: 'connection time out', connectTimeout: connectionOptions.connectTimeout }); |
||||
replied = true; |
||||
callback(new Error('Timeout')); |
||||
} |
||||
}, connectionOptions.connectTimeout); |
||||
} |
||||
|
||||
getUserFilter(username) { |
||||
const filter = []; |
||||
|
||||
if (this.options.User_Search_Filter !== '') { |
||||
if (this.options.User_Search_Filter[0] === '(') { |
||||
filter.push(`${ this.options.User_Search_Filter }`); |
||||
} else { |
||||
filter.push(`(${ this.options.User_Search_Filter })`); |
||||
} |
||||
} |
||||
|
||||
const usernameFilter = this.options.User_Search_Field.split(',').map((item) => `(${ item }=${ username })`); |
||||
|
||||
if (usernameFilter.length === 0) { |
||||
logger.error('LDAP_LDAP_User_Search_Field not defined'); |
||||
} else if (usernameFilter.length === 1) { |
||||
filter.push(`${ usernameFilter[0] }`); |
||||
} else { |
||||
filter.push(`(|${ usernameFilter.join('') })`); |
||||
} |
||||
|
||||
return `(&${ filter.join('') })`; |
||||
} |
||||
|
||||
bindIfNecessary() { |
||||
if (this.domainBinded === true) { |
||||
return; |
||||
} |
||||
|
||||
if (this.options.Authentication !== true) { |
||||
return; |
||||
} |
||||
|
||||
bindLogger.info({ msg: 'Binding UserDN', userDN: this.options.Authentication_UserDN }); |
||||
this.bindSync(this.options.Authentication_UserDN, this.options.Authentication_Password); |
||||
this.domainBinded = true; |
||||
} |
||||
|
||||
searchUsersSync(username, page) { |
||||
this.bindIfNecessary(); |
||||
|
||||
const searchOptions = { |
||||
filter: this.getUserFilter(username), |
||||
scope: this.options.User_Search_Scope || 'sub', |
||||
sizeLimit: this.options.Search_Size_Limit, |
||||
}; |
||||
|
||||
if (this.options.Search_Page_Size > 0) { |
||||
searchOptions.paged = { |
||||
pageSize: this.options.Search_Page_Size, |
||||
pagePause: !!page, |
||||
}; |
||||
} |
||||
|
||||
searchLogger.info({ msg: 'Searching user', username }); |
||||
searchLogger.debug({ searchOptions, BaseDN: this.options.BaseDN }); |
||||
|
||||
if (page) { |
||||
return this.searchAllPaged(this.options.BaseDN, searchOptions, page); |
||||
} |
||||
|
||||
return this.searchAllSync(this.options.BaseDN, searchOptions); |
||||
} |
||||
|
||||
getUserByIdSync(id, attribute) { |
||||
this.bindIfNecessary(); |
||||
|
||||
const Unique_Identifier_Field = settings.get('LDAP_Unique_Identifier_Field').split(','); |
||||
|
||||
let filter; |
||||
|
||||
if (attribute) { |
||||
filter = new this.ldapjs.filters.EqualityFilter({ |
||||
attribute, |
||||
value: Buffer.from(id, 'hex'), |
||||
}); |
||||
} else { |
||||
const filters = []; |
||||
Unique_Identifier_Field.forEach((item) => { |
||||
filters.push(new this.ldapjs.filters.EqualityFilter({ |
||||
attribute: item, |
||||
value: Buffer.from(id, 'hex'), |
||||
})); |
||||
}); |
||||
|
||||
filter = new this.ldapjs.filters.OrFilter({ filters }); |
||||
} |
||||
|
||||
const searchOptions = { |
||||
filter, |
||||
scope: 'sub', |
||||
attributes: ['*', '+'], |
||||
}; |
||||
|
||||
searchLogger.info({ msg: 'Searching by id', id }); |
||||
searchLogger.debug({ msg: 'search filter', filter: searchOptions.filter, BaseDN: this.options.BaseDN }); |
||||
|
||||
const result = this.searchAllSync(this.options.BaseDN, searchOptions); |
||||
|
||||
if (!Array.isArray(result) || result.length === 0) { |
||||
return; |
||||
} |
||||
|
||||
if (result.length > 1) { |
||||
searchLogger.error(`Search by id ${ id } returned ${ result.length } records`); |
||||
} |
||||
|
||||
return result[0]; |
||||
} |
||||
|
||||
getUserByUsernameSync(username) { |
||||
this.bindIfNecessary(); |
||||
|
||||
const searchOptions = { |
||||
filter: this.getUserFilter(username), |
||||
scope: this.options.User_Search_Scope || 'sub', |
||||
}; |
||||
|
||||
searchLogger.info({ msg: 'Searching user', username }); |
||||
searchLogger.debug({ searchOptions, BaseDN: this.options.BaseDN }); |
||||
|
||||
const result = this.searchAllSync(this.options.BaseDN, searchOptions); |
||||
|
||||
if (!Array.isArray(result) || result.length === 0) { |
||||
return; |
||||
} |
||||
|
||||
if (result.length > 1) { |
||||
searchLogger.error(`Search by username ${ username } returned ${ result.length } records`); |
||||
} |
||||
|
||||
return result[0]; |
||||
} |
||||
|
||||
isUserInGroup(username, userdn) { |
||||
if (!this.options.group_filter_enabled) { |
||||
return true; |
||||
} |
||||
|
||||
const filter = ['(&']; |
||||
|
||||
if (this.options.group_filter_object_class !== '') { |
||||
filter.push(`(objectclass=${ this.options.group_filter_object_class })`); |
||||
} |
||||
|
||||
if (this.options.group_filter_group_member_attribute !== '') { |
||||
filter.push(`(${ this.options.group_filter_group_member_attribute }=${ this.options.group_filter_group_member_format })`); |
||||
} |
||||
|
||||
if (this.options.group_filter_group_id_attribute !== '') { |
||||
filter.push(`(${ this.options.group_filter_group_id_attribute }=${ this.options.group_filter_group_name })`); |
||||
} |
||||
filter.push(')'); |
||||
|
||||
const searchOptions = { |
||||
filter: filter.join('').replace(/#{username}/g, username).replace(/#{userdn}/g, userdn), |
||||
scope: 'sub', |
||||
}; |
||||
|
||||
searchLogger.debug({ msg: 'Group filter LDAP:', filter: searchOptions.filter }); |
||||
|
||||
const result = this.searchAllSync(this.options.BaseDN, searchOptions); |
||||
|
||||
if (!Array.isArray(result) || result.length === 0) { |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
extractLdapEntryData(entry) { |
||||
const values = { |
||||
_raw: entry.raw, |
||||
}; |
||||
|
||||
Object.keys(values._raw).forEach((key) => { |
||||
const value = values._raw[key]; |
||||
|
||||
if (!['thumbnailPhoto', 'jpegPhoto'].includes(key)) { |
||||
if (value instanceof Buffer) { |
||||
values[key] = value.toString(); |
||||
} else { |
||||
values[key] = value; |
||||
} |
||||
} |
||||
|
||||
if (key === 'ou' && Array.isArray(value)) { |
||||
value.forEach((item, index) => { |
||||
if (item instanceof Buffer) { |
||||
value[index] = item.toString(); |
||||
} |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
return values; |
||||
} |
||||
|
||||
searchAllPaged(BaseDN, options, page) { |
||||
this.bindIfNecessary(); |
||||
|
||||
({ BaseDN, options } = callbacks.run('ldap.beforeSearchAll', { BaseDN, options })); |
||||
|
||||
const processPage = ({ entries, title, end, next }) => { |
||||
searchLogger.info(title); |
||||
// Force LDAP idle to wait the record processing
|
||||
this.client._updateIdle(true); |
||||
page(null, entries, { end, |
||||
next: () => { |
||||
// Reset idle timer
|
||||
this.client._updateIdle(); |
||||
next && next(); |
||||
} }); |
||||
}; |
||||
|
||||
this.client.search(BaseDN, options, (error, res) => { |
||||
if (error) { |
||||
searchLogger.error(error); |
||||
page(error); |
||||
return; |
||||
} |
||||
|
||||
res.on('error', (error) => { |
||||
searchLogger.error(error); |
||||
page(error); |
||||
}); |
||||
|
||||
let entries = []; |
||||
|
||||
const internalPageSize = options.paged && options.paged.pageSize > 0 ? options.paged.pageSize * 2 : 500; |
||||
|
||||
res.on('searchEntry', (entry) => { |
||||
entries.push(this.extractLdapEntryData(entry)); |
||||
|
||||
if (entries.length >= internalPageSize) { |
||||
processPage({ |
||||
entries, |
||||
title: 'Internal Page', |
||||
end: false, |
||||
}); |
||||
entries = []; |
||||
} |
||||
}); |
||||
|
||||
res.on('page', (result, next) => { |
||||
if (!next) { |
||||
this.client._updateIdle(true); |
||||
processPage({ |
||||
entries, |
||||
title: 'Final Page', |
||||
end: true, |
||||
}); |
||||
entries = []; |
||||
} else if (entries.length) { |
||||
processPage({ |
||||
entries, |
||||
title: 'Page', |
||||
end: false, |
||||
next, |
||||
}); |
||||
entries = []; |
||||
} |
||||
}); |
||||
|
||||
res.on('end', () => { |
||||
if (entries.length) { |
||||
processPage({ |
||||
entries, |
||||
title: 'Final Page', |
||||
end: true, |
||||
}); |
||||
entries = []; |
||||
} |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
searchAllAsync(BaseDN, options, callback) { |
||||
this.bindIfNecessary(); |
||||
|
||||
({ BaseDN, options } = callbacks.run('ldap.beforeSearchAll', { BaseDN, options })); |
||||
|
||||
this.client.search(BaseDN, options, (error, res) => { |
||||
if (error) { |
||||
searchLogger.error(error); |
||||
callback(error); |
||||
return; |
||||
} |
||||
|
||||
res.on('error', (error) => { |
||||
searchLogger.error(error); |
||||
callback(error); |
||||
}); |
||||
|
||||
const entries = []; |
||||
|
||||
res.on('searchEntry', (entry) => { |
||||
entries.push(this.extractLdapEntryData(entry)); |
||||
}); |
||||
|
||||
res.on('end', () => { |
||||
searchLogger.info(`Search result count ${ entries.length }`); |
||||
callback(null, entries); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
authSync(dn, password) { |
||||
authLogger.info({ msg: 'Authenticating', dn }); |
||||
|
||||
try { |
||||
this.bindSync(dn, password); |
||||
if (this.options.find_user_after_login) { |
||||
const searchOptions = { |
||||
scope: this.options.User_Search_Scope || 'sub', |
||||
}; |
||||
const result = this.searchAllSync(dn, searchOptions); |
||||
if (result.length === 0) { |
||||
authLogger.info({ msg: 'Bind successful but user was not found via search', dn, searchOptions }); |
||||
return false; |
||||
} |
||||
} |
||||
authLogger.info({ msg: 'Authenticated', dn }); |
||||
return true; |
||||
} catch (error) { |
||||
authLogger.info({ msg: 'Not authenticated', dn }); |
||||
authLogger.debug(error); |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
disconnect() { |
||||
this.connected = false; |
||||
this.domainBinded = false; |
||||
connLogger.info('Disconecting'); |
||||
this.client.unbind(); |
||||
} |
||||
} |
||||
@ -1,184 +0,0 @@ |
||||
import { SHA256 } from 'meteor/sha'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Accounts } from 'meteor/accounts-base'; |
||||
import ldapEscape from 'ldap-escape'; |
||||
|
||||
import { slug, getLdapUsername, getLdapUserUniqueID, syncUserData, addLdapUser } from './sync'; |
||||
import LDAP from './ldap'; |
||||
import { settings } from '../../settings'; |
||||
import { callbacks } from '../../callbacks'; |
||||
import { Logger } from '../../logger'; |
||||
|
||||
|
||||
const logger = new Logger('LDAPHandler'); |
||||
|
||||
function fallbackDefaultAccountSystem(bind, username, password) { |
||||
if (typeof username === 'string') { |
||||
if (username.indexOf('@') === -1) { |
||||
username = { username }; |
||||
} else { |
||||
username = { email: username }; |
||||
} |
||||
} |
||||
|
||||
logger.info('Fallback to default account system', username); |
||||
|
||||
const loginRequest = { |
||||
user: username, |
||||
password: { |
||||
digest: SHA256(password), |
||||
algorithm: 'sha-256', |
||||
}, |
||||
}; |
||||
|
||||
return Accounts._runLoginHandlers(bind, loginRequest); |
||||
} |
||||
|
||||
Accounts.registerLoginHandler('ldap', function(loginRequest) { |
||||
if (!loginRequest.ldap || !loginRequest.ldapOptions) { |
||||
return undefined; |
||||
} |
||||
|
||||
logger.info('Init LDAP login', loginRequest.username); |
||||
|
||||
if (settings.get('LDAP_Enable') !== true) { |
||||
return fallbackDefaultAccountSystem(this, loginRequest.username, loginRequest.ldapPass); |
||||
} |
||||
|
||||
const self = this; |
||||
const ldap = new LDAP(); |
||||
let ldapUser; |
||||
|
||||
const escapedUsername = ldapEscape.filter`${ loginRequest.username }`; |
||||
|
||||
try { |
||||
ldap.connectSync(); |
||||
const users = ldap.searchUsersSync(escapedUsername); |
||||
|
||||
if (users.length !== 1) { |
||||
logger.info('Search returned', users.length, 'record(s) for', escapedUsername); |
||||
throw new Error('User not Found'); |
||||
} |
||||
|
||||
if (ldap.authSync(users[0].dn, loginRequest.ldapPass) === true) { |
||||
if (ldap.isUserInGroup(escapedUsername, users[0].dn)) { |
||||
ldapUser = users[0]; |
||||
} else { |
||||
throw new Error('User not in a valid group'); |
||||
} |
||||
} else { |
||||
logger.info('Wrong password for', escapedUsername); |
||||
} |
||||
} catch (error) { |
||||
logger.error(error); |
||||
} |
||||
|
||||
if (ldapUser === undefined) { |
||||
return fallbackDefaultAccountSystem(self, loginRequest.username, loginRequest.ldapPass); |
||||
} |
||||
|
||||
// Look to see if user already exists
|
||||
let userQuery; |
||||
|
||||
const Unique_Identifier_Field = getLdapUserUniqueID(ldapUser); |
||||
let user; |
||||
|
||||
if (Unique_Identifier_Field) { |
||||
userQuery = { |
||||
'services.ldap.id': Unique_Identifier_Field.value, |
||||
}; |
||||
|
||||
logger.info('Querying user'); |
||||
logger.debug('userQuery', userQuery); |
||||
|
||||
user = Meteor.users.findOne(userQuery); |
||||
} |
||||
|
||||
let username; |
||||
|
||||
if (settings.get('LDAP_Username_Field') !== '') { |
||||
username = slug(getLdapUsername(ldapUser)); |
||||
} else { |
||||
username = slug(loginRequest.username); |
||||
} |
||||
|
||||
if (!user) { |
||||
userQuery = { |
||||
username, |
||||
}; |
||||
|
||||
logger.debug('userQuery', userQuery); |
||||
|
||||
user = Meteor.users.findOne(userQuery); |
||||
} |
||||
|
||||
// Login user if they exist
|
||||
if (user) { |
||||
if (user.ldap !== true && settings.get('LDAP_Merge_Existing_Users') !== true) { |
||||
logger.info('User exists without "ldap: true"'); |
||||
throw new Meteor.Error('LDAP-login-error', `LDAP Authentication succeeded, but there's already an existing user with provided username [${ username }] in Mongo.`); |
||||
} |
||||
|
||||
logger.info('Logging user'); |
||||
|
||||
syncUserData(user, ldapUser, ldap); |
||||
|
||||
if (settings.get('LDAP_Login_Fallback') === true && typeof loginRequest.ldapPass === 'string' && loginRequest.ldapPass.trim() !== '') { |
||||
Accounts.setPassword(user._id, loginRequest.ldapPass, { logout: false }); |
||||
} |
||||
logger.info('running afterLDAPLogin'); |
||||
callbacks.run('afterLDAPLogin', { user, ldapUser, ldap }); |
||||
return { |
||||
userId: user._id, |
||||
}; |
||||
} |
||||
|
||||
logger.info('User does not exist, creating', username); |
||||
|
||||
if (settings.get('LDAP_Username_Field') === '') { |
||||
username = undefined; |
||||
} |
||||
|
||||
if (settings.get('LDAP_Login_Fallback') !== true) { |
||||
loginRequest.ldapPass = undefined; |
||||
} |
||||
|
||||
// Create new user
|
||||
const result = addLdapUser(ldapUser, username, loginRequest.ldapPass, ldap); |
||||
|
||||
if (result instanceof Error) { |
||||
throw result; |
||||
} |
||||
callbacks.run('afterLDAPLogin', { user: result, ldapUser, ldap }); |
||||
|
||||
return result; |
||||
}); |
||||
|
||||
let LDAP_Enable; |
||||
settings.get('LDAP_Enable', (key, value) => { |
||||
if (LDAP_Enable === value) { |
||||
return; |
||||
} |
||||
LDAP_Enable = value; |
||||
|
||||
if (!value) { |
||||
return callbacks.remove('beforeValidateLogin', 'validateLdapLoginFallback'); |
||||
} |
||||
|
||||
callbacks.add('beforeValidateLogin', (login) => { |
||||
if (!login.allowed) { |
||||
return login; |
||||
} |
||||
|
||||
// The fallback setting should only block password logins, so users that have other login services can continue using them
|
||||
if (login.type !== 'password') { |
||||
return login; |
||||
} |
||||
|
||||
if (login.user.services && login.user.services.ldap && login.user.services.ldap.id) { |
||||
login.allowed = !!settings.get('LDAP_Login_Fallback'); |
||||
} |
||||
|
||||
return login; |
||||
}, callbacks.priority.MEDIUM, 'validateLdapLoginFallback'); |
||||
}); |
||||
@ -1,134 +0,0 @@ |
||||
import { settings } from '../../settings'; |
||||
|
||||
settings.addGroup('LDAP', function() { |
||||
const enableQuery = { _id: 'LDAP_Enable', value: true }; |
||||
const enableAuthentication = [ |
||||
enableQuery, |
||||
{ _id: 'LDAP_Authentication', value: true }, |
||||
]; |
||||
const enableTLSQuery = [ |
||||
enableQuery, |
||||
{ _id: 'LDAP_Encryption', value: { $in: ['tls', 'ssl'] } }, |
||||
]; |
||||
const syncDataQuery = [ |
||||
enableQuery, |
||||
{ _id: 'LDAP_Sync_User_Data', value: true }, |
||||
]; |
||||
const syncGroupsQuery = [ |
||||
enableQuery, |
||||
{ _id: 'LDAP_Sync_User_Data_Groups', value: true }, |
||||
]; |
||||
const syncGroupsChannelsQuery = [ |
||||
enableQuery, |
||||
{ _id: 'LDAP_Sync_User_Data_Groups', value: true }, |
||||
{ _id: 'LDAP_Sync_User_Data_Groups_AutoChannels', value: true }, |
||||
]; |
||||
const groupFilterQuery = [ |
||||
enableQuery, |
||||
{ _id: 'LDAP_Group_Filter_Enable', value: true }, |
||||
]; |
||||
const backgroundSyncQuery = [ |
||||
enableQuery, |
||||
{ _id: 'LDAP_Background_Sync', value: true }, |
||||
]; |
||||
|
||||
this.add('LDAP_Enable', false, { type: 'boolean', public: true }); |
||||
this.add('LDAP_Login_Fallback', false, { type: 'boolean', enableQuery: null }); |
||||
this.add('LDAP_Find_User_After_Login', true, { type: 'boolean', enableQuery }); |
||||
this.add('LDAP_Host', '', { type: 'string', enableQuery }); |
||||
this.add('LDAP_Port', '389', { type: 'int', enableQuery }); |
||||
this.add('LDAP_Reconnect', false, { type: 'boolean', enableQuery }); |
||||
this.add('LDAP_Encryption', 'plain', { type: 'select', values: [{ key: 'plain', i18nLabel: 'No_Encryption' }, { key: 'tls', i18nLabel: 'StartTLS' }, { key: 'ssl', i18nLabel: 'SSL/LDAPS' }], enableQuery }); |
||||
this.add('LDAP_CA_Cert', '', { type: 'string', multiline: true, enableQuery: enableTLSQuery, secret: true }); |
||||
this.add('LDAP_Reject_Unauthorized', true, { type: 'boolean', enableQuery: enableTLSQuery }); |
||||
this.add('LDAP_BaseDN', '', { type: 'string', enableQuery }); |
||||
this.add('LDAP_Internal_Log_Level', 'disabled', { |
||||
type: 'select', |
||||
values: [ |
||||
{ key: 'disabled', i18nLabel: 'Disabled' }, |
||||
{ key: 'error', i18nLabel: 'Error' }, |
||||
{ key: 'warn', i18nLabel: 'Warn' }, |
||||
{ key: 'info', i18nLabel: 'Info' }, |
||||
{ key: 'debug', i18nLabel: 'Debug' }, |
||||
{ key: 'trace', i18nLabel: 'Trace' }, |
||||
], |
||||
enableQuery, |
||||
}); |
||||
this.add('LDAP_Test_Connection', 'ldap_test_connection', { type: 'action', actionText: 'Test_Connection' }); |
||||
|
||||
this.section('Authentication', function() { |
||||
this.add('LDAP_Authentication', false, { type: 'boolean', enableQuery }); |
||||
this.add('LDAP_Authentication_UserDN', '', { type: 'string', enableQuery: enableAuthentication, secret: true }); |
||||
this.add('LDAP_Authentication_Password', '', { type: 'password', enableQuery: enableAuthentication, secret: true }); |
||||
}); |
||||
|
||||
this.section('Timeouts', function() { |
||||
this.add('LDAP_Timeout', 60000, { type: 'int', enableQuery }); |
||||
this.add('LDAP_Connect_Timeout', 1000, { type: 'int', enableQuery }); |
||||
this.add('LDAP_Idle_Timeout', 1000, { type: 'int', enableQuery }); |
||||
}); |
||||
|
||||
this.section('User Search', function() { |
||||
this.add('LDAP_User_Search_Filter', '(objectclass=*)', { type: 'string', enableQuery }); |
||||
this.add('LDAP_User_Search_Scope', 'sub', { type: 'string', enableQuery }); |
||||
this.add('LDAP_User_Search_Field', 'sAMAccountName', { type: 'string', enableQuery }); |
||||
this.add('LDAP_Search_Page_Size', 250, { type: 'int', enableQuery }); |
||||
this.add('LDAP_Search_Size_Limit', 1000, { type: 'int', enableQuery }); |
||||
}); |
||||
|
||||
this.section('User Search (Group Validation)', function() { |
||||
this.add('LDAP_Group_Filter_Enable', false, { type: 'boolean', enableQuery }); |
||||
this.add('LDAP_Group_Filter_ObjectClass', 'groupOfUniqueNames', { type: 'string', enableQuery: groupFilterQuery }); |
||||
this.add('LDAP_Group_Filter_Group_Id_Attribute', 'cn', { type: 'string', enableQuery: groupFilterQuery }); |
||||
this.add('LDAP_Group_Filter_Group_Member_Attribute', 'uniqueMember', { type: 'string', enableQuery: groupFilterQuery }); |
||||
this.add('LDAP_Group_Filter_Group_Member_Format', 'uniqueMember', { type: 'string', enableQuery: groupFilterQuery }); |
||||
this.add('LDAP_Group_Filter_Group_Name', 'ROCKET_CHAT', { type: 'string', enableQuery: groupFilterQuery }); |
||||
}); |
||||
|
||||
this.section('Sync / Import', function() { |
||||
this.add('LDAP_Username_Field', 'sAMAccountName', { |
||||
type: 'string', |
||||
enableQuery, |
||||
// public so that it's visible to AccountProfilePage:
|
||||
public: true, |
||||
}); |
||||
this.add('LDAP_Unique_Identifier_Field', 'objectGUID,ibm-entryUUID,GUID,dominoUNID,nsuniqueId,uidNumber', { type: 'string', enableQuery }); |
||||
this.add('LDAP_Default_Domain', '', { type: 'string', enableQuery }); |
||||
this.add('LDAP_Merge_Existing_Users', false, { type: 'boolean', enableQuery }); |
||||
|
||||
this.add('LDAP_Sync_User_Data', false, { type: 'boolean', enableQuery }); |
||||
this.add('LDAP_Sync_User_Data_FieldMap', '{"cn":"name", "mail":"email"}', { type: 'string', enableQuery: syncDataQuery }); |
||||
|
||||
this.add('LDAP_Sync_User_Data_Groups', false, { type: 'boolean', enableQuery }); |
||||
this.add('LDAP_Sync_User_Data_Groups_AutoRemove', false, { type: 'boolean', enableQuery: syncGroupsQuery }); |
||||
this.add('LDAP_Sync_User_Data_Groups_Filter', '(&(cn=#{groupName})(memberUid=#{username}))', { type: 'string', enableQuery: syncGroupsQuery }); |
||||
this.add('LDAP_Sync_User_Data_Groups_BaseDN', '', { type: 'string', enableQuery: syncGroupsQuery }); |
||||
this.add('LDAP_Sync_User_Data_GroupsMap', '{\n\t"rocket-admin": "admin",\n\t"tech-support": "support"\n}', { |
||||
type: 'code', |
||||
multiline: true, |
||||
public: false, |
||||
code: 'application/json', |
||||
enableQuery: syncGroupsQuery, |
||||
}); |
||||
this.add('LDAP_Sync_User_Data_Groups_AutoChannels', false, { type: 'boolean', enableQuery: syncGroupsQuery }); |
||||
this.add('LDAP_Sync_User_Data_Groups_AutoChannels_Admin', 'rocket.cat', { type: 'string', enableQuery: syncGroupsChannelsQuery }); |
||||
this.add('LDAP_Sync_User_Data_Groups_AutoChannelsMap', '{\n\t"employee": "general",\n\t"techsupport": [\n\t\t"helpdesk",\n\t\t"support"\n\t]\n}', { |
||||
type: 'code', |
||||
multiline: true, |
||||
public: false, |
||||
code: 'application/json', |
||||
enableQuery: syncGroupsChannelsQuery, |
||||
}); |
||||
this.add('LDAP_Sync_User_Data_Groups_Enforce_AutoChannels', false, { type: 'boolean', enableQuery: syncGroupsChannelsQuery }); |
||||
|
||||
this.add('LDAP_Sync_User_Avatar', true, { type: 'boolean', enableQuery }); |
||||
this.add('LDAP_Avatar_Field', '', { type: 'string', enableQuery }); |
||||
|
||||
this.add('LDAP_Background_Sync', false, { type: 'boolean', enableQuery }); |
||||
this.add('LDAP_Background_Sync_Interval', 'Every 24 hours', { type: 'string', enableQuery: backgroundSyncQuery }); |
||||
this.add('LDAP_Background_Sync_Import_New_Users', true, { type: 'boolean', enableQuery: backgroundSyncQuery }); |
||||
this.add('LDAP_Background_Sync_Keep_Existant_Users_Updated', true, { type: 'boolean', enableQuery: backgroundSyncQuery }); |
||||
|
||||
this.add('LDAP_Sync_Now', 'ldap_sync_now', { type: 'action', actionText: 'Execute_Synchronization_Now' }); |
||||
}); |
||||
}); |
||||
@ -1,629 +0,0 @@ |
||||
import limax from 'limax'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Accounts } from 'meteor/accounts-base'; |
||||
import { SyncedCron } from 'meteor/littledata:synced-cron'; |
||||
import _ from 'underscore'; |
||||
|
||||
import LDAP from './ldap'; |
||||
import { callbacks } from '../../callbacks/server'; |
||||
import { RocketChatFile } from '../../file'; |
||||
import { settings } from '../../settings'; |
||||
import { Users, Roles, Rooms, Subscriptions } from '../../models'; |
||||
import { Logger } from '../../logger'; |
||||
import { _setRealName } from '../../lib'; |
||||
import { templateVarHandler } from '../../utils'; |
||||
import { FileUpload } from '../../file-upload'; |
||||
import { addUserToRoom, removeUserFromRoom, createRoom, saveUserIdentity } from '../../lib/server/functions'; |
||||
import { api } from '../../../server/sdk/api'; |
||||
|
||||
export const logger = new Logger('LDAPSync'); |
||||
|
||||
export function isUserInLDAPGroup(ldap, ldapUser, user, ldapGroup) { |
||||
const syncUserRolesFilter = settings.get('LDAP_Sync_User_Data_Groups_Filter').trim(); |
||||
const syncUserRolesBaseDN = settings.get('LDAP_Sync_User_Data_Groups_BaseDN').trim(); |
||||
|
||||
if (!syncUserRolesFilter || !syncUserRolesBaseDN) { |
||||
logger.error('Please setup LDAP Group Filter and LDAP Group BaseDN in LDAP Settings.'); |
||||
return false; |
||||
} |
||||
const searchOptions = { |
||||
filter: syncUserRolesFilter.replace(/#{username}/g, user.username).replace(/#{groupName}/g, ldapGroup).replace(/#{userdn}/g, ldapUser.dn), |
||||
scope: 'sub', |
||||
}; |
||||
|
||||
const result = ldap.searchAllSync(syncUserRolesBaseDN, searchOptions); |
||||
if (!Array.isArray(result) || result.length === 0) { |
||||
logger.debug(`${ user.username } is not in ${ ldapGroup } group!!!`); |
||||
} else { |
||||
logger.debug(`${ user.username } is in ${ ldapGroup } group.`); |
||||
return true; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
export function slug(text) { |
||||
if (settings.get('UTF8_Names_Slugify') !== true) { |
||||
return text; |
||||
} |
||||
text = limax(text, { replacement: '.' }); |
||||
return text.replace(/[^0-9a-z-_.]/g, ''); |
||||
} |
||||
|
||||
|
||||
export function getPropertyValue(obj, key) { |
||||
try { |
||||
return _.reduce(key.split('.'), (acc, el) => acc[el], obj); |
||||
} catch (err) { |
||||
return undefined; |
||||
} |
||||
} |
||||
|
||||
|
||||
export function getLdapUsername(ldapUser) { |
||||
const usernameField = settings.get('LDAP_Username_Field'); |
||||
|
||||
if (usernameField.indexOf('#{') > -1) { |
||||
return usernameField.replace(/#{(.+?)}/g, function(match, field) { |
||||
return ldapUser[field]; |
||||
}); |
||||
} |
||||
|
||||
return ldapUser[usernameField]; |
||||
} |
||||
|
||||
|
||||
export function getLdapUserUniqueID(ldapUser) { |
||||
let Unique_Identifier_Field = settings.get('LDAP_Unique_Identifier_Field'); |
||||
|
||||
if (Unique_Identifier_Field !== '') { |
||||
Unique_Identifier_Field = Unique_Identifier_Field.replace(/\s/g, '').split(','); |
||||
} else { |
||||
Unique_Identifier_Field = []; |
||||
} |
||||
|
||||
let User_Search_Field = settings.get('LDAP_User_Search_Field'); |
||||
|
||||
if (User_Search_Field !== '') { |
||||
User_Search_Field = User_Search_Field.replace(/\s/g, '').split(','); |
||||
} else { |
||||
User_Search_Field = []; |
||||
} |
||||
|
||||
Unique_Identifier_Field = Unique_Identifier_Field.concat(User_Search_Field); |
||||
|
||||
if (Unique_Identifier_Field.length > 0) { |
||||
Unique_Identifier_Field = Unique_Identifier_Field.find((field) => !_.isEmpty(ldapUser._raw[field])); |
||||
if (Unique_Identifier_Field) { |
||||
Unique_Identifier_Field = { |
||||
attribute: Unique_Identifier_Field, |
||||
value: ldapUser._raw[Unique_Identifier_Field].toString('hex'), |
||||
}; |
||||
} |
||||
return Unique_Identifier_Field; |
||||
} |
||||
} |
||||
|
||||
export function getDataToSyncUserData(ldapUser, user) { |
||||
const syncUserData = settings.get('LDAP_Sync_User_Data'); |
||||
const syncUserDataFieldMap = settings.get('LDAP_Sync_User_Data_FieldMap').trim(); |
||||
|
||||
const userData = {}; |
||||
|
||||
if (syncUserData && syncUserDataFieldMap) { |
||||
const whitelistedUserFields = ['email', 'name', 'customFields']; |
||||
const fieldMap = JSON.parse(syncUserDataFieldMap); |
||||
const emailList = []; |
||||
_.map(fieldMap, function(userField, ldapField) { |
||||
switch (userField) { |
||||
case 'email': |
||||
if (!ldapUser.hasOwnProperty(ldapField)) { |
||||
logger.debug(`user does not have attribute: ${ ldapField }`); |
||||
return; |
||||
} |
||||
|
||||
const verified = settings.get('Accounts_Verify_Email_For_External_Accounts'); |
||||
|
||||
if (_.isObject(ldapUser[ldapField])) { |
||||
_.map(ldapUser[ldapField], function(item) { |
||||
emailList.push({ address: item, verified }); |
||||
}); |
||||
} else { |
||||
emailList.push({ address: ldapUser[ldapField], verified }); |
||||
} |
||||
break; |
||||
|
||||
default: |
||||
const [outerKey, innerKeys] = userField.split(/\.(.+)/); |
||||
|
||||
if (!_.find(whitelistedUserFields, (el) => el === outerKey)) { |
||||
logger.debug(`user attribute not whitelisted: ${ userField }`); |
||||
return; |
||||
} |
||||
|
||||
if (outerKey === 'customFields') { |
||||
let customFieldsMeta; |
||||
|
||||
try { |
||||
customFieldsMeta = JSON.parse(settings.get('Accounts_CustomFields')); |
||||
} catch (e) { |
||||
logger.debug('Invalid JSON for Custom Fields'); |
||||
return; |
||||
} |
||||
|
||||
if (!getPropertyValue(customFieldsMeta, innerKeys)) { |
||||
logger.debug(`user attribute does not exist: ${ userField }`); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
const tmpUserField = getPropertyValue(user, userField); |
||||
const tmpLdapField = templateVarHandler(ldapField, ldapUser); |
||||
|
||||
if (tmpLdapField && tmpUserField !== tmpLdapField) { |
||||
// creates the object structure instead of just assigning 'tmpLdapField' to
|
||||
// 'userData[userField]' in order to avoid the "cannot use the part (...)
|
||||
// to traverse the element" (MongoDB) error that can happen. Do not handle
|
||||
// arrays.
|
||||
// TODO: Find a better solution.
|
||||
const dKeys = userField.split('.'); |
||||
const lastKey = _.last(dKeys); |
||||
_.reduce(dKeys, (obj, currKey) => { |
||||
if (currKey === lastKey) { |
||||
obj[currKey] = tmpLdapField; |
||||
} else { |
||||
obj[currKey] = obj[currKey] || {}; |
||||
} |
||||
return obj[currKey]; |
||||
}, userData); |
||||
logger.debug(`user.${ userField } changed to: ${ tmpLdapField }`); |
||||
} |
||||
} |
||||
}); |
||||
|
||||
if (emailList.length > 0) { |
||||
if (JSON.stringify(user.emails) !== JSON.stringify(emailList)) { |
||||
userData.emails = emailList; |
||||
} |
||||
} |
||||
} |
||||
|
||||
const uniqueId = getLdapUserUniqueID(ldapUser); |
||||
|
||||
if (uniqueId && (!user.services || !user.services.ldap || user.services.ldap.id !== uniqueId.value || user.services.ldap.idAttribute !== uniqueId.attribute)) { |
||||
userData['services.ldap.id'] = uniqueId.value; |
||||
userData['services.ldap.idAttribute'] = uniqueId.attribute; |
||||
} |
||||
|
||||
if (user.ldap !== true) { |
||||
userData.ldap = true; |
||||
} |
||||
|
||||
if (_.size(userData)) { |
||||
return userData; |
||||
} |
||||
} |
||||
export function mapLdapGroupsToUserRoles(ldap, ldapUser, user) { |
||||
const syncUserRoles = settings.get('LDAP_Sync_User_Data_Groups'); |
||||
const syncUserRolesAutoRemove = settings.get('LDAP_Sync_User_Data_Groups_AutoRemove'); |
||||
const syncUserRolesFieldMap = settings.get('LDAP_Sync_User_Data_GroupsMap').trim(); |
||||
|
||||
if (!syncUserRoles || !syncUserRolesFieldMap) { |
||||
logger.debug('not syncing user roles'); |
||||
return []; |
||||
} |
||||
|
||||
const roles = Roles.find({}, { |
||||
fields: { |
||||
_updatedAt: 0, |
||||
}, |
||||
}).fetch(); |
||||
|
||||
if (!roles) { |
||||
return []; |
||||
} |
||||
|
||||
let fieldMap; |
||||
|
||||
try { |
||||
fieldMap = JSON.parse(syncUserRolesFieldMap); |
||||
} catch (err) { |
||||
logger.error(`Unexpected error : ${ err.message }`); |
||||
return []; |
||||
} |
||||
if (!fieldMap) { |
||||
return []; |
||||
} |
||||
|
||||
const userRoles = []; |
||||
|
||||
for (const ldapField in fieldMap) { |
||||
if (!fieldMap.hasOwnProperty(ldapField)) { |
||||
continue; |
||||
} |
||||
|
||||
const userField = fieldMap[ldapField]; |
||||
|
||||
const [roleName] = userField.split(/\.(.+)/); |
||||
if (!_.find(roles, (el) => el._id === roleName)) { |
||||
logger.debug(`User Role doesn't exist: ${ roleName }`); |
||||
continue; |
||||
} |
||||
|
||||
logger.debug(`User role exists for mapping ${ ldapField } -> ${ roleName }`); |
||||
|
||||
if (isUserInLDAPGroup(ldap, ldapUser, user, ldapField)) { |
||||
userRoles.push(roleName); |
||||
continue; |
||||
} |
||||
|
||||
if (!syncUserRolesAutoRemove) { |
||||
continue; |
||||
} |
||||
|
||||
const del = Roles.removeUserRoles(user._id, roleName); |
||||
if (settings.get('UI_DisplayRoles') && del) { |
||||
api.broadcast('user.roleUpdate', { |
||||
type: 'removed', |
||||
_id: roleName, |
||||
u: { |
||||
_id: user._id, |
||||
username: user.username, |
||||
}, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
return userRoles; |
||||
} |
||||
export function createRoomForSync(channel) { |
||||
logger.info(`Channel '${ channel }' doesn't exist, creating it.`); |
||||
|
||||
const room = createRoom('c', channel, settings.get('LDAP_Sync_User_Data_Groups_AutoChannels_Admin'), [], false, { customFields: { ldap: true } }); |
||||
if (!room || !room.rid) { |
||||
logger.error(`Unable to auto-create channel '${ channel }' during ldap sync.`); |
||||
return; |
||||
} |
||||
room._id = room.rid; |
||||
return room; |
||||
} |
||||
|
||||
export function mapLDAPGroupsToChannels(ldap, ldapUser, user) { |
||||
const syncUserRoles = settings.get('LDAP_Sync_User_Data_Groups'); |
||||
const syncUserRolesAutoChannels = settings.get('LDAP_Sync_User_Data_Groups_AutoChannels'); |
||||
const syncUserRolesEnforceAutoChannels = settings.get('LDAP_Sync_User_Data_Groups_Enforce_AutoChannels'); |
||||
const syncUserRolesChannelFieldMap = settings.get('LDAP_Sync_User_Data_Groups_AutoChannelsMap').trim(); |
||||
|
||||
const userChannels = []; |
||||
if (!syncUserRoles || !syncUserRolesAutoChannels || !syncUserRolesChannelFieldMap) { |
||||
logger.debug('not syncing groups to channels'); |
||||
return []; |
||||
} |
||||
|
||||
let fieldMap; |
||||
try { |
||||
fieldMap = JSON.parse(syncUserRolesChannelFieldMap); |
||||
} catch (err) { |
||||
logger.error(`Unexpected error : ${ err.message }`); |
||||
return []; |
||||
} |
||||
|
||||
if (!fieldMap) { |
||||
return []; |
||||
} |
||||
|
||||
_.map(fieldMap, function(channels, ldapField) { |
||||
if (!Array.isArray(channels)) { |
||||
channels = [channels]; |
||||
} |
||||
|
||||
for (const channel of channels) { |
||||
let room = Rooms.findOneByNonValidatedName(channel); |
||||
|
||||
if (!room) { |
||||
room = createRoomForSync(channel); |
||||
} |
||||
if (isUserInLDAPGroup(ldap, ldapUser, user, ldapField)) { |
||||
if (room.teamMain) { |
||||
logger.error(`Can't add user to channel ${ channel } because it is a team.`); |
||||
} else { |
||||
userChannels.push(room._id); |
||||
} |
||||
} else if (syncUserRolesEnforceAutoChannels && !room.teamMain) { |
||||
const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, user._id); |
||||
if (subscription) { |
||||
removeUserFromRoom(room._id, user); |
||||
} |
||||
} |
||||
} |
||||
}); |
||||
|
||||
return userChannels; |
||||
} |
||||
|
||||
function syncUserAvatar(user, ldapUser) { |
||||
if (!user?._id || settings.get('LDAP_Sync_User_Avatar') !== true) { |
||||
return; |
||||
} |
||||
|
||||
const avatarField = (settings.get('LDAP_Avatar_Field') || 'thumbnailPhoto').trim(); |
||||
const avatar = ldapUser._raw[avatarField] || ldapUser._raw.thumbnailPhoto || ldapUser._raw.jpegPhoto; |
||||
if (!avatar) { |
||||
return; |
||||
} |
||||
|
||||
logger.info('Syncing user avatar'); |
||||
|
||||
Meteor.defer(() => { |
||||
const rs = RocketChatFile.bufferToStream(avatar); |
||||
const fileStore = FileUpload.getStore('Avatars'); |
||||
fileStore.deleteByName(user.username); |
||||
|
||||
const file = { |
||||
userId: user._id, |
||||
type: 'image/jpeg', |
||||
size: avatar.length, |
||||
}; |
||||
|
||||
Meteor.runAsUser(user._id, () => { |
||||
fileStore.insert(file, rs, (err, result) => { |
||||
Meteor.setTimeout(function() { |
||||
Users.setAvatarData(user._id, 'ldap', result.etag); |
||||
api.broadcast('user.avatarUpdate', { username: user.username, avatarETag: result.etag }); |
||||
}, 500); |
||||
}); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
export function syncUserData(user, ldapUser, ldap) { |
||||
logger.info('Syncing user data'); |
||||
logger.debug('user', { email: user.email, _id: user._id }); |
||||
logger.debug('ldapUser', ldapUser.object); |
||||
|
||||
const userData = getDataToSyncUserData(ldapUser, user); |
||||
|
||||
// Returns a list of Rocket.Chat Groups a user should belong
|
||||
// to if their LDAP group matches the LDAP_Sync_User_Data_GroupsMap
|
||||
const userRoles = mapLdapGroupsToUserRoles(ldap, ldapUser, user); |
||||
|
||||
// Returns a list of Rocket.Chat Channels a user should belong
|
||||
// to if their LDAP group matches the LDAP_Sync_User_Data_Groups_AutoChannelsMap
|
||||
const userChannels = mapLDAPGroupsToChannels(ldap, ldapUser, user); |
||||
|
||||
if (user && user._id && userData) { |
||||
logger.debug({ msg: 'setting', userData }); |
||||
if (userData.name) { |
||||
_setRealName(user._id, userData.name); |
||||
delete userData.name; |
||||
} |
||||
userData.customFields = { |
||||
...user.customFields, ...userData.customFields, |
||||
}; |
||||
Meteor.users.update(user._id, { $set: userData }); |
||||
user = Meteor.users.findOne({ _id: user._id }); |
||||
} |
||||
|
||||
if (settings.get('LDAP_Username_Field') !== '') { |
||||
const username = slug(getLdapUsername(ldapUser)); |
||||
if (user && user._id && username !== user.username) { |
||||
logger.info('Syncing user username', user.username, '->', username); |
||||
saveUserIdentity({ _id: user._id, username }); |
||||
} |
||||
} |
||||
|
||||
if (settings.get('LDAP_Sync_User_Data_Groups') === true) { |
||||
for (const roleName of userRoles) { |
||||
const add = Roles.addUserRoles(user._id, roleName); |
||||
if (settings.get('UI_DisplayRoles') && add) { |
||||
api.broadcast('user.roleUpdate', { |
||||
type: 'added', |
||||
_id: roleName, |
||||
u: { |
||||
_id: user._id, |
||||
username: user.username, |
||||
}, |
||||
}); |
||||
} |
||||
logger.info('Synced user group', roleName, 'from LDAP for', user.username); |
||||
} |
||||
} |
||||
|
||||
if (settings.get('LDAP_Sync_User_Data_Groups_AutoChannels') === true) { |
||||
for (const userChannel of userChannels) { |
||||
addUserToRoom(userChannel, user); |
||||
logger.info('Synced user channel', userChannel, 'from LDAP for', user.username); |
||||
} |
||||
} |
||||
|
||||
syncUserAvatar(user, ldapUser); |
||||
} |
||||
|
||||
export function addLdapUser(ldapUser, username, password, ldap) { |
||||
const uniqueId = getLdapUserUniqueID(ldapUser); |
||||
|
||||
const userObject = {}; |
||||
|
||||
if (username) { |
||||
userObject.username = username; |
||||
} |
||||
|
||||
const userData = getDataToSyncUserData(ldapUser, {}); |
||||
|
||||
if (userData && userData.emails && userData.emails[0] && userData.emails[0].address) { |
||||
if (Array.isArray(userData.emails[0].address)) { |
||||
userObject.email = userData.emails[0].address[0]; |
||||
} else { |
||||
userObject.email = userData.emails[0].address; |
||||
} |
||||
} else if (ldapUser.mail && ldapUser.mail.indexOf('@') > -1) { |
||||
userObject.email = ldapUser.mail; |
||||
} else if (settings.get('LDAP_Default_Domain') !== '') { |
||||
userObject.email = `${ username || uniqueId.value }@${ settings.get('LDAP_Default_Domain') }`; |
||||
} else { |
||||
const error = new Meteor.Error('LDAP-login-error', 'LDAP Authentication succeeded, there is no email to create an account. Have you tried setting your Default Domain in LDAP Settings?'); |
||||
logger.error(error); |
||||
throw error; |
||||
} |
||||
|
||||
logger.debug('New user data', userObject); |
||||
|
||||
if (password) { |
||||
userObject.password = password; |
||||
} |
||||
|
||||
try { |
||||
userObject._id = Accounts.createUser(userObject); |
||||
} catch (error) { |
||||
logger.error('Error creating user', error); |
||||
return error; |
||||
} |
||||
|
||||
syncUserData(userObject, ldapUser, ldap); |
||||
|
||||
return { |
||||
userId: userObject._id, |
||||
}; |
||||
} |
||||
|
||||
export function importNewUsers(ldap) { |
||||
if (settings.get('LDAP_Enable') !== true) { |
||||
logger.error('Can\'t run LDAP Import, LDAP is disabled'); |
||||
return; |
||||
} |
||||
|
||||
if (!ldap) { |
||||
ldap = new LDAP(); |
||||
} |
||||
|
||||
if (!ldap.connected) { |
||||
ldap.connectSync(); |
||||
} |
||||
|
||||
let count = 0; |
||||
ldap.searchUsersSync('*', Meteor.bindEnvironment((error, ldapUsers, { next, end } = {}) => { |
||||
if (error) { |
||||
throw error; |
||||
} |
||||
|
||||
ldapUsers.forEach((ldapUser) => { |
||||
count++; |
||||
|
||||
const uniqueId = getLdapUserUniqueID(ldapUser); |
||||
// Look to see if user already exists
|
||||
const userQuery = { |
||||
'services.ldap.id': uniqueId.value, |
||||
}; |
||||
|
||||
logger.debug('userQuery', userQuery); |
||||
|
||||
let username; |
||||
if (settings.get('LDAP_Username_Field') !== '') { |
||||
username = slug(getLdapUsername(ldapUser)); |
||||
} |
||||
|
||||
// Add user if it was not added before
|
||||
let user = Meteor.users.findOne(userQuery); |
||||
|
||||
if (!user && username && settings.get('LDAP_Merge_Existing_Users') === true) { |
||||
const userQuery = { |
||||
username, |
||||
}; |
||||
|
||||
logger.debug('userQuery merge', userQuery); |
||||
|
||||
user = Meteor.users.findOne(userQuery); |
||||
if (user) { |
||||
syncUserData(user, ldapUser, ldap); |
||||
} |
||||
} |
||||
|
||||
if (!user) { |
||||
addLdapUser(ldapUser, username, undefined, ldap); |
||||
} |
||||
|
||||
if (count % 100 === 0) { |
||||
logger.info('Import running. Users imported until now:', count); |
||||
} |
||||
}); |
||||
|
||||
if (end) { |
||||
logger.info('Import finished. Users imported:', count); |
||||
} |
||||
|
||||
next(count); |
||||
})); |
||||
} |
||||
|
||||
export function sync() { |
||||
if (settings.get('LDAP_Enable') !== true) { |
||||
return; |
||||
} |
||||
|
||||
const ldap = new LDAP(); |
||||
|
||||
try { |
||||
ldap.connectSync(); |
||||
|
||||
let users; |
||||
if (settings.get('LDAP_Background_Sync_Keep_Existant_Users_Updated') === true) { |
||||
users = Users.findLDAPUsers(); |
||||
} |
||||
|
||||
if (settings.get('LDAP_Background_Sync_Import_New_Users') === true) { |
||||
importNewUsers(ldap); |
||||
} |
||||
|
||||
if (settings.get('LDAP_Background_Sync_Keep_Existant_Users_Updated') === true) { |
||||
users.forEach(function(user) { |
||||
let ldapUser; |
||||
|
||||
if (user.services && user.services.ldap && user.services.ldap.id) { |
||||
ldapUser = ldap.getUserByIdSync(user.services.ldap.id, user.services.ldap.idAttribute); |
||||
} else { |
||||
ldapUser = ldap.getUserByUsernameSync(user.username); |
||||
} |
||||
|
||||
if (ldapUser) { |
||||
syncUserData(user, ldapUser, ldap); |
||||
} |
||||
|
||||
callbacks.run('ldap.afterSyncExistentUser', { ldapUser, user }); |
||||
}); |
||||
} |
||||
} catch (error) { |
||||
logger.error(error); |
||||
return error; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
const jobName = 'LDAP_Sync'; |
||||
|
||||
const addCronJob = _.debounce(Meteor.bindEnvironment(function addCronJobDebounced() { |
||||
if (settings.get('LDAP_Background_Sync') !== true) { |
||||
logger.info('Disabling LDAP Background Sync'); |
||||
if (SyncedCron.nextScheduledAtDate(jobName)) { |
||||
SyncedCron.remove(jobName); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
if (settings.get('LDAP_Background_Sync_Interval')) { |
||||
logger.info('Enabling LDAP Background Sync'); |
||||
SyncedCron.add({ |
||||
name: jobName, |
||||
schedule: (parser) => parser.text(settings.get('LDAP_Background_Sync_Interval')), |
||||
job() { |
||||
sync(); |
||||
}, |
||||
}); |
||||
} |
||||
}), 500); |
||||
|
||||
Meteor.startup(() => { |
||||
Meteor.defer(() => { |
||||
settings.get('LDAP_Background_Sync', addCronJob); |
||||
settings.get('LDAP_Background_Sync_Interval', addCronJob); |
||||
}); |
||||
}); |
||||
@ -1,31 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { sync } from './sync'; |
||||
import { hasRole } from '../../authorization'; |
||||
import { settings } from '../../settings'; |
||||
|
||||
Meteor.methods({ |
||||
ldap_sync_now() { |
||||
const user = Meteor.user(); |
||||
if (!user) { |
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'ldap_sync_users' }); |
||||
} |
||||
|
||||
if (!hasRole(user._id, 'admin')) { |
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'ldap_sync_users' }); |
||||
} |
||||
|
||||
if (settings.get('LDAP_Enable') !== true) { |
||||
throw new Meteor.Error('LDAP_disabled'); |
||||
} |
||||
|
||||
Meteor.defer(() => { |
||||
sync(); |
||||
}); |
||||
|
||||
return { |
||||
message: 'Sync_in_progress', |
||||
params: [], |
||||
}; |
||||
}, |
||||
}); |
||||
@ -1,43 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import LDAP from './ldap'; |
||||
import { hasRole } from '../../authorization/server'; |
||||
import { settings } from '../../settings/server'; |
||||
import { SystemLogger } from '../../../server/lib/logger/system'; |
||||
|
||||
Meteor.methods({ |
||||
ldap_test_connection() { |
||||
const user = Meteor.user(); |
||||
if (!user) { |
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'ldap_test_connection' }); |
||||
} |
||||
|
||||
if (!hasRole(user._id, 'admin')) { |
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'ldap_test_connection' }); |
||||
} |
||||
|
||||
if (settings.get('LDAP_Enable') !== true) { |
||||
throw new Meteor.Error('LDAP_disabled'); |
||||
} |
||||
|
||||
let ldap; |
||||
try { |
||||
ldap = new LDAP(); |
||||
ldap.connectSync(); |
||||
} catch (error) { |
||||
SystemLogger.error(error); |
||||
throw new Meteor.Error(error.message); |
||||
} |
||||
|
||||
try { |
||||
ldap.bindIfNecessary(); |
||||
} catch (error) { |
||||
throw new Meteor.Error(error.name || error.message); |
||||
} |
||||
|
||||
return { |
||||
message: 'Connection_success', |
||||
params: [], |
||||
}; |
||||
}, |
||||
}); |
||||
@ -0,0 +1,18 @@ |
||||
import { Cursor } from 'mongodb'; |
||||
|
||||
import { BaseRaw } from './BaseRaw'; |
||||
import { IImportRecord, IImportUserRecord, IImportMessageRecord, IImportChannelRecord } from '../../../../definition/IImportRecord'; |
||||
|
||||
export class ImportDataRaw extends BaseRaw<IImportRecord> { |
||||
getAllUsers(): Cursor<IImportUserRecord> { |
||||
return this.find({ dataType: 'user' }) as Cursor<IImportUserRecord>; |
||||
} |
||||
|
||||
getAllMessages(): Cursor<IImportMessageRecord> { |
||||
return this.find({ dataType: 'message' }) as Cursor<IImportMessageRecord>; |
||||
} |
||||
|
||||
getAllChannels(): Cursor<IImportChannelRecord> { |
||||
return this.find({ dataType: 'channel' }) as Cursor<IImportChannelRecord>; |
||||
} |
||||
} |
||||
@ -0,0 +1,14 @@ |
||||
import type { TranslationKey } from '../../../TranslationContext'; |
||||
|
||||
export type LDAPEndpoints = { |
||||
'ldap.testConnection': { |
||||
POST: () => { |
||||
message: TranslationKey; |
||||
}; |
||||
}; |
||||
'ldap.syncNow': { |
||||
POST: () => { |
||||
message: TranslationKey; |
||||
}; |
||||
}; |
||||
}; |
||||
@ -0,0 +1,20 @@ |
||||
import { Accounts } from 'meteor/accounts-base'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
(Meteor as any).loginWithLDAP = function ( |
||||
username: string, |
||||
password: string, |
||||
callback?: (err?: any) => void, |
||||
): void { |
||||
Accounts.callLoginMethod({ |
||||
methodArguments: [ |
||||
{ |
||||
ldap: true, |
||||
username, |
||||
ldapPass: password, |
||||
ldapOptions: {}, |
||||
}, |
||||
], |
||||
userCallback: callback, |
||||
}); |
||||
}; |
||||
@ -0,0 +1,103 @@ |
||||
import { Button } from '@rocket.chat/fuselage'; |
||||
import React, { memo, useMemo } from 'react'; |
||||
|
||||
import type { ISetting } from '../../../../../definition/ISetting'; |
||||
import { useEditableSettings } from '../../../../contexts/EditableSettingsContext'; |
||||
import { useModal } from '../../../../contexts/ModalContext'; |
||||
import { useEndpoint } from '../../../../contexts/ServerContext'; |
||||
import { useSetting } from '../../../../contexts/SettingsContext'; |
||||
import { useToastMessageDispatch } from '../../../../contexts/ToastMessagesContext'; |
||||
import { useTranslation } from '../../../../contexts/TranslationContext'; |
||||
import TabbedGroupPage from './TabbedGroupPage'; |
||||
|
||||
function LDAPGroupPage({ _id, ...group }: ISetting): JSX.Element { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
const testConnection = useEndpoint('POST', 'ldap.testConnection'); |
||||
const syncNow = useEndpoint('POST', 'ldap.syncNow'); |
||||
const ldapEnabled = useSetting('LDAP_Enable'); |
||||
const ldapSyncEnabled = useSetting('LDAP_Background_Sync') && ldapEnabled; |
||||
const modal = useModal(); |
||||
|
||||
const editableSettings = useEditableSettings( |
||||
useMemo( |
||||
() => ({ |
||||
group: _id, |
||||
}), |
||||
[_id], |
||||
), |
||||
); |
||||
|
||||
const changed = useMemo( |
||||
() => editableSettings.some(({ changed }) => changed), |
||||
[editableSettings], |
||||
); |
||||
|
||||
const handleTestConnectionButtonClick = async (): Promise<void> => { |
||||
try { |
||||
const { message } = await testConnection(undefined); |
||||
dispatchToastMessage({ type: 'success', message: t(message) }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}; |
||||
|
||||
const handleSyncNowButtonClick = async (): Promise<void> => { |
||||
try { |
||||
await testConnection(undefined); |
||||
// #ToDo: Switch to modal.setModal
|
||||
modal.open( |
||||
{ |
||||
title: t('Execute_Synchronization_Now'), |
||||
text: t('LDAP_Sync_Now_Description'), |
||||
confirmButtonText: t('Sync'), |
||||
showCancelButton: true, |
||||
closeOnConfirm: true, |
||||
closeOnCancel: true, |
||||
}, |
||||
async (isConfirm: boolean): Promise<void> => { |
||||
if (!isConfirm) { |
||||
return; |
||||
} |
||||
|
||||
try { |
||||
const { message } = await syncNow(undefined); |
||||
dispatchToastMessage({ type: 'success', message: t(message) }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}, |
||||
); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}; |
||||
|
||||
return ( |
||||
<TabbedGroupPage |
||||
_id={_id} |
||||
{...group} |
||||
headerButtons={ |
||||
<> |
||||
<Button |
||||
children={t('Test_Connection')} |
||||
disabled={!ldapEnabled || changed} |
||||
onClick={handleTestConnectionButtonClick} |
||||
/> |
||||
{ldapSyncEnabled && ( |
||||
<Button |
||||
children={t('LDAP_Sync_Now')} |
||||
disabled={!ldapSyncEnabled || changed} |
||||
onClick={handleSyncNowButtonClick} |
||||
/> |
||||
)} |
||||
<Button is='a' href='https://go.rocket.chat/i/ldap-doc' target='_blank'> |
||||
{t('LDAP_Documentation')} |
||||
</Button> |
||||
</> |
||||
} |
||||
/> |
||||
); |
||||
} |
||||
|
||||
export default memo(LDAPGroupPage); |
||||
@ -0,0 +1,66 @@ |
||||
import { Tabs } from '@rocket.chat/fuselage'; |
||||
import React, { ReactNode, memo, useState, useMemo } from 'react'; |
||||
|
||||
import { |
||||
useEditableSettingsGroupSections, |
||||
useEditableSettingsGroupTabs, |
||||
} from '../../../../contexts/EditableSettingsContext'; |
||||
import { useTranslation, TranslationKey } from '../../../../contexts/TranslationContext'; |
||||
import GroupPage from '../GroupPage'; |
||||
import Section from '../Section'; |
||||
import GenericGroupPage from './GenericGroupPage'; |
||||
|
||||
function TabbedGroupPage({ |
||||
_id, |
||||
...group |
||||
}: { |
||||
children?: ReactNode; |
||||
headerButtons?: ReactNode; |
||||
_id: string; |
||||
i18nLabel: string; |
||||
i18nDescription?: string; |
||||
tabs?: ReactNode; |
||||
}): JSX.Element { |
||||
const t = useTranslation(); |
||||
const tabs = useEditableSettingsGroupTabs(_id); |
||||
|
||||
const [tab, setTab] = useState(tabs[0]); |
||||
const handleTabClick = useMemo(() => (tab: string) => (): void => setTab(tab), [setTab]); |
||||
const sections = useEditableSettingsGroupSections(_id, tab); |
||||
|
||||
const solo = sections.length === 1; |
||||
|
||||
if (!tabs.length || (tabs.length === 1 && !tabs[0])) { |
||||
return <GenericGroupPage _id={_id} {...group} />; |
||||
} |
||||
|
||||
if (!tab && tabs[0]) { |
||||
setTab(tabs[0]); |
||||
} |
||||
|
||||
const tabsComponent = ( |
||||
<Tabs> |
||||
{tabs.map((tabName) => ( |
||||
<Tabs.Item key={tabName || ''} selected={tab === tabName} onClick={handleTabClick(tabName)}> |
||||
{tabName ? t(tabName as TranslationKey) : t(_id as TranslationKey)} |
||||
</Tabs.Item> |
||||
))} |
||||
</Tabs> |
||||
); |
||||
|
||||
return ( |
||||
<GroupPage _id={_id} {...group} tabs={tabsComponent}> |
||||
{sections.map((sectionName) => ( |
||||
<Section |
||||
key={sectionName || ''} |
||||
groupId={_id} |
||||
sectionName={sectionName} |
||||
tabName={tab} |
||||
solo={solo} |
||||
/> |
||||
))} |
||||
</GroupPage> |
||||
); |
||||
} |
||||
|
||||
export default memo(TabbedGroupPage); |
||||
@ -0,0 +1,4 @@ |
||||
declare module 'ldap-escape' { |
||||
export function filter(strings: TemplateStringsArray, ...values: string[]): string; |
||||
export function dn(strings: TemplateStringsArray, ...values: string[]): string; |
||||
} |
||||
@ -0,0 +1,12 @@ |
||||
export interface ILDAPCallback { |
||||
(error?: Error | null, result?: any): void; |
||||
} |
||||
|
||||
export interface ILDAPPageData { |
||||
end: boolean; |
||||
next: Function | undefined; |
||||
} |
||||
|
||||
export interface ILDAPPageCallback { |
||||
(error?: Error | null, result?: any, page?: ILDAPPageData): void; |
||||
} |
||||
@ -0,0 +1,3 @@ |
||||
export interface ILDAPEntry extends Record<string, any> { |
||||
_raw: Record<string, any>; |
||||
} |
||||
@ -0,0 +1,6 @@ |
||||
export interface ILDAPLoginRequest { |
||||
ldap?: boolean; |
||||
ldapOptions?: Record<string, any>; |
||||
username: string; |
||||
ldapPass: string; |
||||
} |
||||
@ -0,0 +1,5 @@ |
||||
export interface ILDAPLoginResult extends Record<string, any> { |
||||
userId?: string; |
||||
} |
||||
|
||||
export type LDAPLoginResult = ILDAPLoginResult | undefined; |
||||
@ -0,0 +1,27 @@ |
||||
export type LDAPEncryptionType = 'plain' | 'tls' | 'ssl'; |
||||
export type LDAPSearchScope = 'base' | 'one' | 'sub'; |
||||
|
||||
export interface ILDAPConnectionOptions { |
||||
host?: string; |
||||
port: number; |
||||
reconnect: boolean; |
||||
timeout: number; |
||||
connectionTimeout: number; |
||||
idleTimeout: number; |
||||
encryption: LDAPEncryptionType; |
||||
caCert?: string; |
||||
rejectUnauthorized: boolean; |
||||
baseDN: string; |
||||
userSearchFilter: string; |
||||
userSearchScope: LDAPSearchScope; |
||||
userSearchField: string; |
||||
searchPageSize: number; |
||||
searchSizeLimit: number; |
||||
uniqueIdentifierField?: string; |
||||
groupFilterEnabled: boolean; |
||||
groupFilterObjectClass?: string; |
||||
groupFilterGroupIdAttribute?: string; |
||||
groupFilterGroupMemberAttribute?: string; |
||||
groupFilterGroupMemberFormat?: string; |
||||
groupFilterGroupName?: string; |
||||
} |
||||
@ -0,0 +1,4 @@ |
||||
export type ILDAPUniqueIdentifierField = { |
||||
attribute: string; |
||||
value: string; |
||||
} |
||||
@ -0,0 +1,3 @@ |
||||
declare module 'meteor/sha' { |
||||
function SHA256(input: string): string; |
||||
} |
||||
@ -1,17 +1,15 @@ |
||||
import { settings } from '../../../../app/settings'; |
||||
|
||||
export const createSettings = () => { |
||||
settings.addGroup('Canned_Responses', function() { |
||||
this.section('Canned_Responses', function() { |
||||
this.add('Canned_Responses_Enable', false, { |
||||
type: 'boolean', |
||||
public: true, |
||||
enterprise: true, |
||||
invalidValue: false, |
||||
modules: [ |
||||
'canned-responses', |
||||
], |
||||
}); |
||||
}); |
||||
settings.add('Canned_Responses_Enable', true, { |
||||
group: 'Omnichannel', |
||||
section: 'Canned_Responses', |
||||
type: 'boolean', |
||||
public: true, |
||||
enterprise: true, |
||||
invalidValue: false, |
||||
modules: [ |
||||
'canned-responses', |
||||
], |
||||
}); |
||||
}; |
||||
|
||||
@ -1,22 +0,0 @@ |
||||
# LDAP-Enterprise |
||||
|
||||
This package enables the administrator option to map the roles used on your LDAP server with the Rocket.Chat server roles. |
||||
With the correspondent license for this product, a new "Roles" section will be enabled in the admin panel in the LDAP group, |
||||
where the administrator can map LDAP roles to Rocket.Chat roles`(Admin panel -> LDAP -> Roles)`, following the pattern described below: |
||||
|
||||
``` |
||||
{ |
||||
"ldapRole": "["admin", "guest"], //must be an array of valid Rocket.Chat Users Roles |
||||
"anotherLdapRole": "["anonymous"] |
||||
} |
||||
|
||||
``` |
||||
**Note:** If some mapping error occurs, be aware of the server log, that the error will be shown. |
||||
<br/> |
||||
<br/> |
||||
In addition to the options described above, still in the same section, there are other options such as: |
||||
* `LDAP_Enable_LDAP_Roles_To_RC_Roles`: Enable or disable this feature; |
||||
* `LDAP_Validate_Roles_For_Each_Login`: If the validation should occurs for each login (**Be careful with this setting because it will overwrite |
||||
the user roles in each login, otherwise this will be validated only at the moment of user creation**); |
||||
* `LDAP_Default_Role_To_User`: The default Rocket.Chat role to be defined, if any LDAP role that the user has, is not mapped; |
||||
* `LDAP_Query_To_Get_User_Groups`: The LDAP query to get the LDAP groups that the user is part of; |
||||
@ -1,11 +0,0 @@ |
||||
export const beforeSearchAll = (searchParams) => { |
||||
const { options } = searchParams; |
||||
|
||||
if (!Array.isArray(options.attributes)) { |
||||
options.attributes = options.attributes ? [options.attributes] : ['*']; |
||||
} |
||||
|
||||
options.attributes.push('pwdAccountLockedTime'); |
||||
|
||||
return searchParams; |
||||
}; |
||||
@ -1,19 +0,0 @@ |
||||
import { logger } from '../../../../../app/ldap/server/sync'; |
||||
import { setUserActiveStatus } from '../../../../../app/lib/server/functions/setUserActiveStatus'; |
||||
import { settings } from '../../../../../app/settings'; |
||||
|
||||
export const syncExistentUser = ({ ldapUser, user }) => { |
||||
const activate = !!ldapUser && !ldapUser.pwdAccountLockedTime; |
||||
|
||||
if (activate === user.active) { |
||||
return; |
||||
} |
||||
|
||||
const syncUserState = settings.get('LDAP_Sync_User_Active_State'); |
||||
if (syncUserState === 'none' || (syncUserState === 'disable' && activate)) { |
||||
return; |
||||
} |
||||
|
||||
setUserActiveStatus(user._id, activate); |
||||
logger.info(`${ activate ? 'Activating' : 'Deactivating' } user ${ user.name } (${ user._id })`); |
||||
}; |
||||
@ -1,69 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
|
||||
import { syncExistentUser } from './hooks/syncExistentUser'; |
||||
import { beforeSearchAll } from './hooks/beforeSearchAll'; |
||||
import { callbacks } from '../../../../app/callbacks/server'; |
||||
import { settings } from '../../../../app/settings'; |
||||
import { onLicense } from '../../license/server'; |
||||
|
||||
onLicense('ldap-enterprise', () => { |
||||
const { createSettings } = require('./settings'); |
||||
const { validateLDAPRolesMappingChanges, validateLDAPTeamsMappingChanges } = require('./ldapEnterprise'); |
||||
const { onLdapLogin } = require('./listener'); |
||||
|
||||
Meteor.startup(function() { |
||||
createSettings(); |
||||
validateLDAPRolesMappingChanges(); |
||||
validateLDAPTeamsMappingChanges(); |
||||
|
||||
let LDAP_Enable_LDAP_Roles_To_RC_Roles; |
||||
let LDAP_Enable_LDAP_Groups_To_RC_Teams; |
||||
let callbackEnabled = false; |
||||
let LDAP_Sync_User_Active_State; |
||||
|
||||
const updateCallbackState = () => { |
||||
if (callbackEnabled) { |
||||
if (!LDAP_Enable_LDAP_Roles_To_RC_Roles && !LDAP_Enable_LDAP_Groups_To_RC_Teams) { |
||||
callbacks.remove('afterLDAPLogin', 'checkRoleMapping'); |
||||
callbackEnabled = false; |
||||
} |
||||
|
||||
return; |
||||
} |
||||
|
||||
if (LDAP_Enable_LDAP_Roles_To_RC_Roles || LDAP_Enable_LDAP_Groups_To_RC_Teams) { |
||||
callbackEnabled = true; |
||||
callbacks.add('afterLDAPLogin', onLdapLogin, callbacks.priority.MEDIUM, 'checkRoleMapping'); |
||||
} |
||||
}; |
||||
|
||||
settings.get('LDAP_Enable_LDAP_Roles_To_RC_Roles', (key, value) => { |
||||
LDAP_Enable_LDAP_Roles_To_RC_Roles = value; |
||||
updateCallbackState(); |
||||
}); |
||||
|
||||
settings.get('LDAP_Enable_LDAP_Groups_To_RC_Teams', (key, value) => { |
||||
LDAP_Enable_LDAP_Groups_To_RC_Teams = value; |
||||
updateCallbackState(); |
||||
}); |
||||
|
||||
settings.get('LDAP_Sync_User_Active_State', (key, value) => { |
||||
if (LDAP_Sync_User_Active_State === value) { |
||||
return; |
||||
} |
||||
|
||||
if (value === 'none') { |
||||
// If it changed to 'none', disable
|
||||
callbacks.remove('ldap.afterSyncExistentUser', 'ldap-sync-user-active-state'); |
||||
} else if (LDAP_Sync_User_Active_State === 'none' || !LDAP_Sync_User_Active_State) { |
||||
// If it changed from 'none' to something else, enable
|
||||
callbacks.add('ldap.afterSyncExistentUser', syncExistentUser, callbacks.priority.MEDIUM, 'ldap-sync-user-active-state'); |
||||
} |
||||
|
||||
LDAP_Sync_User_Active_State = value; |
||||
}); |
||||
|
||||
callbacks.add('ldap.beforeSearchAll', beforeSearchAll, callbacks.priority.MEDIUM, 'ldap-return-attribute-AccountLockedTime'); |
||||
}); |
||||
}); |
||||
@ -1,144 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { Roles } from '../../../../app/models'; |
||||
import { Logger } from '../../../../app/logger'; |
||||
import { settings } from '../../../../app/settings'; |
||||
import { Team } from '../../../../server/sdk'; |
||||
|
||||
const logger = new Logger('ldapEnterprise'); |
||||
|
||||
const mustBeAnArrayOfStrings = (array) => Array.isArray(array) && array.length && array.every((item) => typeof item === 'string'); |
||||
|
||||
const validateRoleMapping = (mappedRoles) => { |
||||
const allRocketChatUserRoles = Roles.find({ scope: 'Users' }).fetch().map((role) => role._id); |
||||
const mappedRocketChatRoles = Object.values(mappedRoles); |
||||
const validRolesMapping = mappedRocketChatRoles.every((roles) => roles.every((role) => allRocketChatUserRoles.includes(role))); |
||||
if (!validRolesMapping) { |
||||
throw new Error('Please verify your mapping for LDAP X RocketChat Roles. There is some invalid Rocket Chat Role.'); |
||||
} |
||||
}; |
||||
|
||||
const validateLDAPRolesMappingStructure = (mappedRoles) => { |
||||
const mappedRocketChatRoles = Object.values(mappedRoles); |
||||
const validStructureMapping = mappedRocketChatRoles.every(mustBeAnArrayOfStrings); |
||||
if (!validStructureMapping) { |
||||
throw new Error('Please verify your mapping for LDAP X RocketChat Roles. The structure is invalid, the structure should be an object like: {key: LdapRole, value: [An array of rocket.chat roles]}'); |
||||
} |
||||
}; |
||||
|
||||
const validateLDAPTeamsMappingStructure = (mappedTeams) => { |
||||
const mappedRocketChatTeams = Object.values(mappedTeams); |
||||
const validStructureMapping = mappedRocketChatTeams.every(mustBeAnArrayOfStrings); |
||||
if (!validStructureMapping) { |
||||
throw new Error('Please verify your mapping for LDAP X RocketChat Teams. The structure is invalid, the structure should be an object like: {key: LdapTeam, value: [An array of rocket.chat teams]}'); |
||||
} |
||||
}; |
||||
|
||||
export const getLdapRolesByUsername = (username, ldap) => { |
||||
const searchOptions = { |
||||
filter: settings.get('LDAP_Query_To_Get_User_Groups').replace(/#{username}/g, username), |
||||
scope: ldap.options.User_Search_Scope || 'sub', |
||||
sizeLimit: ldap.options.Search_Size_Limit, |
||||
}; |
||||
const getLdapRoles = (ldapUserGroups) => ldapUserGroups.filter((field) => field && field.ou).map((field) => field.ou); |
||||
const ldapUserGroups = ldap.searchAllSync(ldap.options.BaseDN, searchOptions); |
||||
return Array.isArray(ldapUserGroups) ? getLdapRoles(ldapUserGroups) : []; |
||||
}; |
||||
|
||||
export const getLdapTeamsByUsername = (username, ldap) => { |
||||
const searchOptions = { |
||||
filter: settings.get('LDAP_Query_To_Get_User_Teams').replace(/#{username}/g, username), |
||||
scope: ldap.options.User_Search_Scope || 'sub', |
||||
sizeLimit: ldap.options.Search_Size_Limit, |
||||
}; |
||||
const ldapUserGroups = ldap.searchAllSync(ldap.options.BaseDN, searchOptions); |
||||
|
||||
if (!Array.isArray(ldapUserGroups)) { |
||||
return []; |
||||
} |
||||
|
||||
return ldapUserGroups.filter((field) => field && field.ou).map((field) => field.ou).flat(); |
||||
}; |
||||
|
||||
export const getRocketChatRolesByLdapRoles = (mappedRoles, ldapUserRoles) => { |
||||
const mappedLdapRoles = Object.keys(mappedRoles); |
||||
if (!ldapUserRoles.length) { |
||||
logger.error('The LDAP user has no role, so we set the default role value'); |
||||
return [settings.get('LDAP_Default_Role_To_User')]; |
||||
} |
||||
const unmappedLdapRoles = ldapUserRoles.filter((ldapRole) => !mappedLdapRoles.includes(ldapRole)); |
||||
const getRocketChatMappedRoles = (acc, role) => acc.concat(mappedRoles[role]); |
||||
const removeRepeatedRoles = (acc, role) => (acc.includes(role) ? acc : acc.concat(role)); |
||||
if (unmappedLdapRoles.length) { |
||||
logger.error(`The following LDAP roles is/are not mapped in Rocket.Chat: "${ unmappedLdapRoles.join(', ') }". Because it, we set the default LDAP role.`); |
||||
return [settings.get('LDAP_Default_Role_To_User')]; |
||||
} |
||||
return ldapUserRoles |
||||
.reduce(getRocketChatMappedRoles, []) |
||||
.reduce(removeRepeatedRoles, []); |
||||
}; |
||||
|
||||
export const getRocketChatTeamsByLdapTeams = (mappedTeams, ldapUserTeams) => { |
||||
const mappedLdapTeams = Object.keys(mappedTeams); |
||||
const filteredTeams = ldapUserTeams.filter((ldapTeam) => mappedLdapTeams.includes(ldapTeam)); |
||||
|
||||
if (filteredTeams.length < ldapUserTeams.length) { |
||||
const unmappedLdapTeams = ldapUserTeams.filter((ldapRole) => !mappedLdapTeams.includes(ldapRole)); |
||||
logger.error(`The following LDAP teams are not mapped in Rocket.Chat: "${ unmappedLdapTeams.join(', ') }".`); |
||||
} |
||||
|
||||
if (!filteredTeams.length) { |
||||
return []; |
||||
} |
||||
|
||||
return [...new Set(filteredTeams.map((ldapTeam) => mappedTeams[ldapTeam]).flat())]; |
||||
}; |
||||
|
||||
export const updateUserUsingMappedLdapRoles = (userId, roles) => { |
||||
Meteor.users.update({ _id: userId }, { $set: { roles } }); |
||||
}; |
||||
|
||||
async function updateUserUsingMappedLdapTeamsAsync(userId, teamNames, map) { |
||||
const allTeamNames = [...new Set(Object.values(map).flat())]; |
||||
const allTeams = await Team.listByNames(allTeamNames, { projection: { _id: 1, name: 1 } }); |
||||
|
||||
const inTeamIds = allTeams.filter(({ name }) => teamNames.includes(name)).map(({ _id }) => _id); |
||||
const notInTeamIds = allTeams.filter(({ name }) => !teamNames.includes(name)).map(({ _id }) => _id); |
||||
|
||||
const currentTeams = await Team.listTeamsBySubscriberUserId(userId, { projection: { teamId: 1 } }); |
||||
const currentTeamIds = await currentTeams.map(({ teamId }) => teamId); |
||||
const teamsToRemove = currentTeamIds.filter((teamId) => notInTeamIds.includes(teamId)); |
||||
const teamsToAdd = inTeamIds.filter((teamId) => !currentTeamIds.includes(teamId)); |
||||
|
||||
await Team.insertMemberOnTeams(userId, teamsToAdd); |
||||
await Team.removeMemberFromTeams(userId, teamsToRemove); |
||||
} |
||||
|
||||
export const updateUserUsingMappedLdapTeams = (userId, teamNames, map) => Promise.await(updateUserUsingMappedLdapTeamsAsync(userId, teamNames, map)); |
||||
|
||||
export const validateLDAPRolesMappingChanges = () => { |
||||
settings.get('LDAP_Roles_To_Rocket_Chat_Roles', (key, value) => { |
||||
try { |
||||
if (value) { |
||||
const mappedRoles = JSON.parse(value); |
||||
validateLDAPRolesMappingStructure(mappedRoles); |
||||
validateRoleMapping(mappedRoles); |
||||
} |
||||
} catch (error) { |
||||
logger.error(error); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
export const validateLDAPTeamsMappingChanges = () => { |
||||
settings.get('LDAP_Groups_To_Rocket_Chat_Teams', (key, value) => { |
||||
try { |
||||
if (value) { |
||||
const mappedTeams = JSON.parse(value); |
||||
validateLDAPTeamsMappingStructure(mappedTeams); |
||||
} |
||||
} catch (error) { |
||||
logger.error(error); |
||||
} |
||||
}); |
||||
}; |
||||
@ -1,30 +0,0 @@ |
||||
import { settings } from '../../../../app/settings'; |
||||
import { |
||||
getLdapRolesByUsername, |
||||
getRocketChatRolesByLdapRoles, |
||||
updateUserUsingMappedLdapRoles, |
||||
getLdapTeamsByUsername, |
||||
getRocketChatTeamsByLdapTeams, |
||||
updateUserUsingMappedLdapTeams, |
||||
} from './ldapEnterprise'; |
||||
|
||||
export const onLdapLogin = ({ user, ldapUser, ldap }) => { |
||||
const userExists = user._id; |
||||
const userId = userExists ? user._id : user.userId; |
||||
|
||||
const mapRoles = settings.get('LDAP_Enable_LDAP_Roles_To_RC_Roles') && (!userExists || settings.get('LDAP_Validate_Roles_For_Each_Login')); |
||||
const mapTeams = settings.get('LDAP_Enable_LDAP_Groups_To_RC_Teams') && (!userExists || settings.get('LDAP_Validate_Teams_For_Each_Login')); |
||||
|
||||
if (mapRoles) { |
||||
const ldapUserRoles = getLdapRolesByUsername(ldapUser.uid, ldap); |
||||
const roles = getRocketChatRolesByLdapRoles(JSON.parse(settings.get('LDAP_Roles_To_Rocket_Chat_Roles')), ldapUserRoles); |
||||
updateUserUsingMappedLdapRoles(userId, roles); |
||||
} |
||||
|
||||
if (mapTeams) { |
||||
const ldapUserTeams = getLdapTeamsByUsername(ldapUser.uid, ldap); |
||||
const map = JSON.parse(settings.get('LDAP_Groups_To_Rocket_Chat_Teams')); |
||||
const teams = getRocketChatTeamsByLdapTeams(map, ldapUserTeams); |
||||
updateUserUsingMappedLdapTeams(userId, teams, map); |
||||
} |
||||
}; |
||||
@ -1,112 +0,0 @@ |
||||
import { settings } from '../../../../app/settings'; |
||||
import { Roles } from '../../../../app/models'; |
||||
|
||||
export const createSettings = () => { |
||||
settings.addGroup('LDAP', function() { |
||||
this.section('Role_Mapping', function() { |
||||
this.add('LDAP_Enable_LDAP_Roles_To_RC_Roles', false, { |
||||
type: 'boolean', |
||||
enableQuery: { _id: 'LDAP_Enable', value: true }, |
||||
enterprise: true, |
||||
invalidValue: false, |
||||
modules: [ |
||||
'ldap-enterprise', |
||||
], |
||||
}); |
||||
this.add('LDAP_Roles_To_Rocket_Chat_Roles', '{}', { |
||||
type: 'code', |
||||
enableQuery: { _id: 'LDAP_Enable_LDAP_Roles_To_RC_Roles', value: true }, |
||||
enterprise: true, |
||||
invalidValue: '{}', |
||||
modules: [ |
||||
'ldap-enterprise', |
||||
], |
||||
}); |
||||
this.add('LDAP_Validate_Roles_For_Each_Login', false, { |
||||
type: 'boolean', |
||||
enableQuery: { _id: 'LDAP_Enable_LDAP_Roles_To_RC_Roles', value: true }, |
||||
enterprise: true, |
||||
invalidValue: false, |
||||
modules: [ |
||||
'ldap-enterprise', |
||||
], |
||||
}); |
||||
this.add('LDAP_Default_Role_To_User', 'user', { |
||||
type: 'select', |
||||
values: Roles.find({ scope: 'Users' }).fetch().map((role) => ({ key: role._id, i18nLabel: role._id })), |
||||
enableQuery: { _id: 'LDAP_Enable_LDAP_Roles_To_RC_Roles', value: true }, |
||||
enterprise: true, |
||||
invalidValue: 'user', |
||||
modules: [ |
||||
'ldap-enterprise', |
||||
], |
||||
}); |
||||
this.add('LDAP_Query_To_Get_User_Groups', '(&(ou=*)(uniqueMember=uid=#{username},dc=example,dc=com))', { |
||||
type: 'string', |
||||
enableQuery: { _id: 'LDAP_Enable_LDAP_Roles_To_RC_Roles', value: true }, |
||||
enterprise: true, |
||||
invalidValue: '(&(ou=*)(uniqueMember=uid=#{username},dc=example,dc=com))', |
||||
modules: [ |
||||
'ldap-enterprise', |
||||
], |
||||
}); |
||||
}); |
||||
|
||||
this.section('Team_Mapping', function() { |
||||
this.add('LDAP_Enable_LDAP_Groups_To_RC_Teams', false, { |
||||
type: 'boolean', |
||||
enableQuery: { _id: 'LDAP_Enable', value: true }, |
||||
enterprise: true, |
||||
invalidValue: false, |
||||
modules: [ |
||||
'ldap-enterprise', |
||||
], |
||||
}); |
||||
this.add('LDAP_Groups_To_Rocket_Chat_Teams', '{}', { |
||||
type: 'code', |
||||
enableQuery: { _id: 'LDAP_Enable_LDAP_Groups_To_RC_Teams', value: true }, |
||||
enterprise: true, |
||||
invalidValue: '{}', |
||||
modules: [ |
||||
'ldap-enterprise', |
||||
], |
||||
}); |
||||
this.add('LDAP_Validate_Teams_For_Each_Login', false, { |
||||
type: 'boolean', |
||||
enableQuery: { _id: 'LDAP_Enable_LDAP_Groups_To_RC_Teams', value: true }, |
||||
enterprise: true, |
||||
invalidValue: false, |
||||
modules: [ |
||||
'ldap-enterprise', |
||||
], |
||||
}); |
||||
this.add('LDAP_Query_To_Get_User_Teams', '(&(ou=*)(uniqueMember=uid=#{username},dc=example,dc=com))', { |
||||
type: 'string', |
||||
enableQuery: { _id: 'LDAP_Enable_LDAP_Groups_To_RC_Teams', value: true }, |
||||
enterprise: true, |
||||
invalidValue: '(&(ou=*)(uniqueMember=uid=#{username},dc=example,dc=com))', |
||||
modules: [ |
||||
'ldap-enterprise', |
||||
], |
||||
}); |
||||
}); |
||||
|
||||
this.section('LDAP_Advanced_Sync', function() { |
||||
this.add('LDAP_Sync_User_Active_State', 'disable', { |
||||
type: 'select', |
||||
values: [ |
||||
{ key: 'none', i18nLabel: 'LDAP_Sync_User_Active_State_Nothing' }, |
||||
{ key: 'disable', i18nLabel: 'LDAP_Sync_User_Active_State_Disable' }, |
||||
{ key: 'both', i18nLabel: 'LDAP_Sync_User_Active_State_Both' }, |
||||
], |
||||
i18nDescription: 'LDAP_Sync_User_Active_State_Description', |
||||
enableQuery: { _id: 'LDAP_Enable', value: true }, |
||||
enterprise: true, |
||||
invalidValue: 'none', |
||||
modules: [ |
||||
'ldap-enterprise', |
||||
], |
||||
}); |
||||
}); |
||||
}); |
||||
}; |
||||
@ -1,32 +1,64 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { callbacks } from '../../../../../app/callbacks'; |
||||
import { callbacks } from '../../../../../app/callbacks/server'; |
||||
import { Users } from '../../../../../app/models/server/raw'; |
||||
import { settings } from '../../../../../app/settings'; |
||||
import { settings } from '../../../../../app/settings/server'; |
||||
import { getMaxNumberSimultaneousChat } from '../lib/Helper'; |
||||
import { allowAgentSkipQueue } from '../../../../../app/livechat/server/lib/Helper'; |
||||
import { cbLogger } from '../lib/logger'; |
||||
import { Livechat } from '../../../../../app/livechat/server'; |
||||
|
||||
callbacks.add('livechat.checkAgentBeforeTakeInquiry', async ({ agent, inquiry, options }) => { |
||||
if (!settings.get('Livechat_waiting_queue')) { |
||||
cbLogger.debug('Skipping callback. Disabled by setting'); |
||||
return agent; |
||||
callbacks.add('livechat.checkAgentBeforeTakeInquiry', async ({ |
||||
agent, |
||||
inquiry, |
||||
options, |
||||
}: { |
||||
agent: { |
||||
agentId: string; |
||||
}; |
||||
inquiry: { |
||||
_id: string; |
||||
department: string; |
||||
}; |
||||
options: { |
||||
forwardingToDepartment? : { |
||||
oldDepartmentId: string; |
||||
transferData: any; |
||||
}; |
||||
clientAction? : boolean; |
||||
}; |
||||
}) => { |
||||
if (!inquiry?._id || !agent?.agentId) { |
||||
cbLogger.debug('Callback with error. No inquiry or agent provided'); |
||||
return null; |
||||
} |
||||
const { |
||||
agentId, |
||||
} = agent; |
||||
|
||||
if (!inquiry || !agent) { |
||||
cbLogger.debug('Callback with error. No inquiry or agent provided'); |
||||
if (!Livechat.checkOnlineAgents(null, agent)) { |
||||
cbLogger.debug('Callback with error. provided agent is not online'); |
||||
return null; |
||||
} |
||||
|
||||
if (!settings.get('Livechat_waiting_queue')) { |
||||
cbLogger.debug('Skipping callback. Disabled by setting'); |
||||
return agent; |
||||
} |
||||
|
||||
if (allowAgentSkipQueue(agent)) { |
||||
cbLogger.debug(`Callback success. Agent ${ agent.agentId } can skip queue`); |
||||
return agent; |
||||
} |
||||
|
||||
const { department: departmentId } = inquiry; |
||||
const { agentId } = agent; |
||||
const { |
||||
department: departmentId, |
||||
} = inquiry; |
||||
|
||||
const maxNumberSimultaneousChat = getMaxNumberSimultaneousChat({ agentId, departmentId }); |
||||
const maxNumberSimultaneousChat = getMaxNumberSimultaneousChat({ |
||||
agentId, |
||||
departmentId, |
||||
}); |
||||
if (maxNumberSimultaneousChat === 0) { |
||||
cbLogger.debug(`Callback success. Agent ${ agentId } max number simultaneous chats on range`); |
||||
return agent; |
||||
@ -0,0 +1,5 @@ |
||||
export interface ILDAPEEConnectionOptions { |
||||
authentication: boolean; |
||||
authenticationUserDN: string; |
||||
authenticationPassword: string; |
||||
} |
||||
@ -1 +1,2 @@ |
||||
import './ldap'; |
||||
import './licenses'; |
||||
|
||||
@ -0,0 +1,31 @@ |
||||
import { hasRole } from '../../../app/authorization/server'; |
||||
import { settings } from '../../../app/settings/server'; |
||||
import { API } from '../../../app/api/server/api'; |
||||
import { LDAPEE } from '../sdk'; |
||||
import { hasLicense } from '../../app/license/server/license'; |
||||
|
||||
API.v1.addRoute('ldap.syncNow', { authRequired: true }, { |
||||
post() { |
||||
if (!this.userId) { |
||||
throw new Error('error-invalid-user'); |
||||
} |
||||
|
||||
if (!hasRole(this.userId, 'admin')) { |
||||
throw new Error('error-not-authorized'); |
||||
} |
||||
|
||||
if (!hasLicense('ldap-enterprise')) { |
||||
throw new Error('error-not-authorized'); |
||||
} |
||||
|
||||
if (settings.get('LDAP_Enable') !== true) { |
||||
throw new Error('LDAP_disabled'); |
||||
} |
||||
|
||||
LDAPEE.sync(); |
||||
|
||||
return API.v1.success({ |
||||
message: 'Sync_in_progress', |
||||
}); |
||||
}, |
||||
}); |
||||
@ -0,0 +1,57 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Promise } from 'meteor/promise'; |
||||
import _ from 'underscore'; |
||||
|
||||
import { LDAPEE } from '../sdk'; |
||||
import { settings } from '../../../app/settings/server'; |
||||
import { logger } from '../../../server/lib/ldap/Logger'; |
||||
import { cronJobs } from '../../../app/utils/server/lib/cron/Cronjobs'; |
||||
import { LDAPEEConnection } from '../lib/ldap/Connection'; |
||||
import { LDAPEEManager } from '../lib/ldap/Manager'; |
||||
import { callbacks } from '../../../app/callbacks/server'; |
||||
import type { LDAPConnection } from '../../../server/lib/ldap/Connection'; |
||||
import type { IImportUser } from '../../../definition/IImportUser'; |
||||
import type { ILDAPEntry } from '../../../definition/ldap/ILDAPEntry'; |
||||
import { onLicense } from '../../app/license/server'; |
||||
|
||||
onLicense('ldap-enterprise', () => { |
||||
// Configure background sync cronjob
|
||||
const jobName = 'LDAP_Sync'; |
||||
const addCronJob = _.debounce(Meteor.bindEnvironment(function addCronJobDebounced() { |
||||
if (settings.get('LDAP_Background_Sync') !== true) { |
||||
logger.info('Disabling LDAP Background Sync'); |
||||
if (cronJobs.nextScheduledAtDate(jobName)) { |
||||
cronJobs.remove(jobName); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
const schedule = settings.get<string>('LDAP_Background_Sync_Interval'); |
||||
if (schedule) { |
||||
logger.info('Enabling LDAP Background Sync'); |
||||
cronJobs.add(jobName, schedule, () => Promise.await(LDAPEE.sync()), 'text'); |
||||
} |
||||
}), 500); |
||||
|
||||
Meteor.defer(() => { |
||||
settings.get('LDAP_Background_Sync', addCronJob); |
||||
settings.get('LDAP_Background_Sync_Interval', addCronJob); |
||||
|
||||
settings.get('LDAP_Groups_To_Rocket_Chat_Teams', (_key, value) => { |
||||
try { |
||||
LDAPEEManager.validateLDAPTeamsMappingChanges(value as string); |
||||
} catch (error) { |
||||
logger.error(error); |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
callbacks.add('getLDAPConnectionClass', function(): typeof LDAPConnection { |
||||
return LDAPEEConnection; |
||||
}, callbacks.priority.HIGH, 'replaceLDAPConnectionClass'); |
||||
|
||||
callbacks.add('mapLDAPUserData', (userData: IImportUser, ldapUser: ILDAPEntry) => { |
||||
LDAPEEManager.copyCustomFields(ldapUser, userData); |
||||
LDAPEEManager.copyActiveState(ldapUser, userData); |
||||
}, callbacks.priority.MEDIUM, 'mapLDAPCustomFields'); |
||||
}); |
||||
@ -0,0 +1,65 @@ |
||||
import ldapjs from 'ldapjs'; |
||||
|
||||
import { LDAPConnection } from '../../../../server/lib/ldap/Connection'; |
||||
import { logger, bindLogger } from '../../../../server/lib/ldap/Logger'; |
||||
import { settings } from '../../../../app/settings/server'; |
||||
import type { ILDAPEEConnectionOptions } from '../../../definition/ldap/ILDAPEEOptions'; |
||||
|
||||
export class LDAPEEConnection extends LDAPConnection { |
||||
public eeOptions: ILDAPEEConnectionOptions; |
||||
|
||||
private usingAuthentication: boolean; |
||||
|
||||
constructor() { |
||||
super(); |
||||
|
||||
this.eeOptions = { |
||||
authentication: settings.get<boolean>('LDAP_Authentication') ?? false, |
||||
authenticationUserDN: settings.get<string>('LDAP_Authentication_UserDN') ?? '', |
||||
authenticationPassword: settings.get<string>('LDAP_Authentication_Password') ?? '', |
||||
}; |
||||
} |
||||
|
||||
/* |
||||
Bind UserDN and Password if specified and not yet bound |
||||
*/ |
||||
public async maybeBindDN(): Promise<void> { |
||||
if (this.usingAuthentication) { |
||||
return; |
||||
} |
||||
|
||||
if (!this.eeOptions.authentication) { |
||||
return; |
||||
} |
||||
|
||||
if (!this.eeOptions.authenticationUserDN) { |
||||
logger.error('Invalid UserDN for authentication'); |
||||
return; |
||||
} |
||||
|
||||
bindLogger.info({ msg: 'Binding UserDN', userDN: this.eeOptions.authenticationUserDN }); |
||||
await this.bindDN(this.eeOptions.authenticationUserDN, this.eeOptions.authenticationPassword); |
||||
this.usingAuthentication = true; |
||||
} |
||||
|
||||
public disconnect(): void { |
||||
this.usingAuthentication = false; |
||||
super.disconnect(); |
||||
} |
||||
|
||||
public async testConnection(): Promise<void> { |
||||
await super.testConnection(); |
||||
|
||||
await this.maybeBindDN(); |
||||
} |
||||
|
||||
protected async runBeforeSearch(searchOptions: ldapjs.SearchOptions): Promise<void> { |
||||
await this.maybeBindDN(); |
||||
|
||||
if (!Array.isArray(searchOptions.attributes)) { |
||||
searchOptions.attributes = searchOptions.attributes ? [searchOptions.attributes] : ['*']; |
||||
} |
||||
searchOptions.attributes.push('pwdAccountLockedTime'); |
||||
super.runBeforeSearch(searchOptions); |
||||
} |
||||
} |
||||
@ -0,0 +1,501 @@ |
||||
import _ from 'underscore'; |
||||
import type ldapjs from 'ldapjs'; |
||||
|
||||
import { ILDAPEntry } from '../../../../definition/ldap/ILDAPEntry'; |
||||
import type { IUser } from '../../../../definition/IUser'; |
||||
import type { IRoom, ICreatedRoom } from '../../../../definition/IRoom'; |
||||
import type { IRole } from '../../../../definition/IRole'; |
||||
import { IImportUser } from '../../../../definition/IImportUser'; |
||||
import { ImporterAfterImportCallback } from '../../../../app/importer/server/definitions/IConversionCallbacks'; |
||||
import { settings } from '../../../../app/settings/server'; |
||||
import { Roles, Rooms } from '../../../../app/models/server'; |
||||
import { |
||||
Users as UsersRaw, |
||||
Roles as RolesRaw, |
||||
Subscriptions as SubscriptionsRaw, |
||||
} from '../../../../app/models/server/raw'; |
||||
import { LDAPDataConverter } from '../../../../server/lib/ldap/DataConverter'; |
||||
import type { LDAPConnection } from '../../../../server/lib/ldap/Connection'; |
||||
import { LDAPManager } from '../../../../server/lib/ldap/Manager'; |
||||
import { logger } from '../../../../server/lib/ldap/Logger'; |
||||
import { templateVarHandler } from '../../../../app/utils/lib/templateVarHandler'; |
||||
import { LDAPEEConnection } from './Connection'; |
||||
import { api } from '../../../../server/sdk/api'; |
||||
import { addUserToRoom, removeUserFromRoom, createRoom } from '../../../../app/lib/server/functions'; |
||||
import { Team } from '../../../../server/sdk'; |
||||
|
||||
export class LDAPEEManager extends LDAPManager { |
||||
public static async sync(): Promise<void> { |
||||
if (settings.get('LDAP_Enable') !== true) { |
||||
return; |
||||
} |
||||
|
||||
const options = this.getConverterOptions(); |
||||
const ldap = new LDAPEEConnection(); |
||||
const converter = new LDAPDataConverter(true, options); |
||||
|
||||
try { |
||||
await ldap.connect(); |
||||
|
||||
try { |
||||
const createNewUsers = settings.get<boolean>('LDAP_Background_Sync_Import_New_Users') ?? true; |
||||
const updateExistingUsers = settings.get<boolean>('LDAP_Background_Sync_Keep_Existant_Users_Updated') ?? true; |
||||
|
||||
if (createNewUsers) { |
||||
await this.importNewUsers(ldap, converter, updateExistingUsers); |
||||
} else if (updateExistingUsers) { |
||||
await this.updateExistingUsers(ldap, converter); |
||||
} |
||||
} finally { |
||||
ldap.disconnect(); |
||||
} |
||||
|
||||
converter.convertUsers({ |
||||
afterImportFn: ((data: IImportUser, _type: string, isNewRecord: boolean): void => Promise.await(this.advancedSync(ldap, data, converter, isNewRecord))) as ImporterAfterImportCallback, |
||||
}); |
||||
} catch (error) { |
||||
logger.error(error); |
||||
} |
||||
} |
||||
|
||||
public static validateLDAPTeamsMappingChanges(json: string): void { |
||||
if (!json) { |
||||
return; |
||||
} |
||||
|
||||
const mustBeAnArrayOfStrings = (array: Array<string>): boolean => Boolean(Array.isArray(array) && array.length && array.every((item) => typeof item === 'string')); |
||||
const mappedTeams = this.parseJson(json); |
||||
if (!mappedTeams) { |
||||
return; |
||||
} |
||||
|
||||
const mappedRocketChatTeams = Object.values(mappedTeams); |
||||
const validStructureMapping = mappedRocketChatTeams.every(mustBeAnArrayOfStrings); |
||||
if (!validStructureMapping) { |
||||
throw new Error('Please verify your mapping for LDAP X RocketChat Teams. The structure is invalid, the structure should be an object like: {key: LdapTeam, value: [An array of rocket.chat teams]}'); |
||||
} |
||||
} |
||||
|
||||
private static async advancedSync(ldap: LDAPConnection, importUser: IImportUser, converter: LDAPDataConverter, isNewRecord: boolean): Promise<void> { |
||||
const user = converter.findExistingUser(importUser); |
||||
if (!user || user.username) { |
||||
return; |
||||
} |
||||
|
||||
const dn = importUser.importIds[0]; |
||||
await this.syncUserRoles(ldap, user, dn); |
||||
await this.syncUserChannels(ldap, user, dn); |
||||
await this.syncUserTeams(ldap, user, isNewRecord); |
||||
} |
||||
|
||||
private static async isUserInGroup(ldap: LDAPConnection, baseDN: string, filter: string, { dn, username }: { dn: string; username: string }, groupName: string): Promise<boolean> { |
||||
if (!filter || !baseDN) { |
||||
logger.error('Please setup LDAP Group Filter and LDAP Group BaseDN in LDAP Settings.'); |
||||
return false; |
||||
} |
||||
const searchOptions: ldapjs.SearchOptions = { |
||||
filter: filter.replace(/#{username}/g, username).replace(/#{groupName}/g, groupName).replace(/#{userdn}/g, dn), |
||||
scope: 'sub', |
||||
}; |
||||
|
||||
const result = await ldap.searchRaw(baseDN, searchOptions); |
||||
if (!Array.isArray(result) || result.length === 0) { |
||||
logger.debug(`${ username } is not in ${ groupName } group!!!`); |
||||
} else { |
||||
logger.debug(`${ username } is in ${ groupName } group.`); |
||||
return true; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
private static parseJson(json: string): Record<string, any> | undefined { |
||||
try { |
||||
return JSON.parse(json); |
||||
} catch (err) { |
||||
logger.error(`Unexpected error : ${ err.message }`); |
||||
} |
||||
} |
||||
|
||||
private static broadcastRoleChange(type: string, _id: string, uid: string, username: string): void { |
||||
// #ToDo: would be better to broadcast this only once for all users and roles, or at least once by user.
|
||||
if (!settings.get('UI_DisplayRoles')) { |
||||
return; |
||||
} |
||||
|
||||
api.broadcast('user.roleUpdate', { |
||||
type, |
||||
_id, |
||||
u: { |
||||
_id: uid, |
||||
username, |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
private static async syncUserRoles(ldap: LDAPConnection, user: IUser, dn: string): Promise<void> { |
||||
const { username } = user; |
||||
if (!username) { |
||||
logger.debug('User has no username'); |
||||
return; |
||||
} |
||||
|
||||
const syncUserRoles = settings.get<boolean>('LDAP_Sync_User_Data_Roles') ?? false; |
||||
const syncUserRolesAutoRemove = settings.get<boolean>('LDAP_Sync_User_Data_Roles_AutoRemove') ?? false; |
||||
const syncUserRolesFieldMap = (settings.get<string>('LDAP_Sync_User_Data_RolesMap') ?? '').trim(); |
||||
const syncUserRolesFilter = (settings.get<string>('LDAP_Sync_User_Data_Roles_Filter') ?? '').trim(); |
||||
const syncUserRolesBaseDN = (settings.get<string>('LDAP_Sync_User_Data_Roles_BaseDN') ?? '').trim(); |
||||
|
||||
if (!syncUserRoles || !syncUserRolesFieldMap) { |
||||
logger.debug('not syncing user roles'); |
||||
return; |
||||
} |
||||
|
||||
const roles = await RolesRaw.find({}, { |
||||
fields: { |
||||
_updatedAt: 0, |
||||
}, |
||||
}).toArray() as Array<IRole>; |
||||
|
||||
if (!roles) { |
||||
return; |
||||
} |
||||
|
||||
const fieldMap = this.parseJson(syncUserRolesFieldMap); |
||||
if (!fieldMap) { |
||||
return; |
||||
} |
||||
|
||||
Object.keys(fieldMap).forEach(async (ldapField) => { |
||||
if (!fieldMap.hasOwnProperty(ldapField)) { |
||||
return; |
||||
} |
||||
|
||||
const userField = fieldMap[ldapField]; |
||||
|
||||
const [roleName] = userField.split(/\.(.+)/); |
||||
if (!_.find<IRole>(roles, (el) => el._id === roleName)) { |
||||
logger.debug(`User Role doesn't exist: ${ roleName }`); |
||||
return; |
||||
} |
||||
|
||||
logger.debug(`User role exists for mapping ${ ldapField } -> ${ roleName }`); |
||||
|
||||
if (await this.isUserInGroup(ldap, syncUserRolesBaseDN, syncUserRolesFilter, { dn, username }, ldapField)) { |
||||
if (Roles.addUserRoles(user._id, roleName)) { |
||||
this.broadcastRoleChange('added', roleName, user._id, username); |
||||
} |
||||
logger.debug(`Synced user group ${ roleName } from LDAP for ${ user.username }`); |
||||
return; |
||||
} |
||||
|
||||
if (!syncUserRolesAutoRemove) { |
||||
return; |
||||
} |
||||
|
||||
if (Roles.removeUserRoles(user._id, roleName)) { |
||||
this.broadcastRoleChange('removed', roleName, user._id, username); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
private static createRoomForSync(channel: string): IRoom | undefined { |
||||
logger.debug(`Channel '${ channel }' doesn't exist, creating it.`); |
||||
|
||||
const roomOwner = settings.get('LDAP_Sync_User_Data_Channels_Admin') || ''; |
||||
// #ToDo: Remove typecastings when createRoom is converted to ts.
|
||||
const room = createRoom('c', channel, roomOwner, [], false, { customFields: { ldap: true } } as any) as unknown as ICreatedRoom | undefined; |
||||
if (!room?.rid) { |
||||
logger.error(`Unable to auto-create channel '${ channel }' during ldap sync.`); |
||||
return; |
||||
} |
||||
|
||||
room._id = room.rid; |
||||
return room; |
||||
} |
||||
|
||||
private static async syncUserChannels(ldap: LDAPConnection, user: IUser, dn: string): Promise<void> { |
||||
const syncUserChannels = settings.get<boolean>('LDAP_Sync_User_Data_Channels') ?? false; |
||||
const syncUserChannelsRemove = settings.get<boolean>('LDAP_Sync_User_Data_Channels_Enforce_AutoChannels') ?? false; |
||||
const syncUserChannelsFieldMap = (settings.get<string>('LDAP_Sync_User_Data_ChannelsMap') ?? '').trim(); |
||||
const syncUserChannelsFilter = (settings.get<string>('LDAP_Sync_User_Data_Channels_Filter') ?? '').trim(); |
||||
const syncUserChannelsBaseDN = (settings.get<string>('LDAP_Sync_User_Data_Channels_BaseDN') ?? '').trim(); |
||||
|
||||
if (!syncUserChannels || !syncUserChannelsFieldMap) { |
||||
logger.debug('not syncing groups to channels'); |
||||
return; |
||||
} |
||||
|
||||
const fieldMap = this.parseJson(syncUserChannelsFieldMap); |
||||
if (!fieldMap) { |
||||
return; |
||||
} |
||||
|
||||
const username = user.username as string; |
||||
_.map(fieldMap, (channels, ldapField) => { |
||||
if (!Array.isArray(channels)) { |
||||
channels = [channels]; |
||||
} |
||||
|
||||
channels.forEach(async (channel: string) => { |
||||
const room: IRoom | undefined = Rooms.findOneByNonValidatedName(channel) || this.createRoomForSync(channel); |
||||
if (!room) { |
||||
return; |
||||
} |
||||
|
||||
if (await this.isUserInGroup(ldap, syncUserChannelsBaseDN, syncUserChannelsFilter, { dn, username }, ldapField)) { |
||||
if (room.teamMain) { |
||||
logger.error(`Can't add user to channel ${ channel } because it is a team.`); |
||||
} else { |
||||
addUserToRoom(room._id, user); |
||||
logger.debug(`Synced user channel ${ room._id } from LDAP for ${ username }`); |
||||
} |
||||
} else if (syncUserChannelsRemove && !room.teamMain) { |
||||
const subscription = await SubscriptionsRaw.findOneByRoomIdAndUserId(room._id, user._id); |
||||
if (subscription) { |
||||
removeUserFromRoom(room._id, user); |
||||
} |
||||
} |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
private static async syncUserTeams(ldap: LDAPConnection, user: IUser, isNewRecord: boolean): Promise<void> { |
||||
if (!user.username) { |
||||
return; |
||||
} |
||||
|
||||
const mapTeams = settings.get<boolean>('LDAP_Enable_LDAP_Groups_To_RC_Teams') && (isNewRecord || settings.get<boolean>('LDAP_Validate_Teams_For_Each_Login')); |
||||
if (!mapTeams) { |
||||
return; |
||||
} |
||||
|
||||
const ldapUserTeams = await this.getLdapTeamsByUsername(ldap, user.username); |
||||
const mapJson = settings.get<string>('LDAP_Groups_To_Rocket_Chat_Teams'); |
||||
if (!mapJson) { |
||||
return; |
||||
} |
||||
const map = this.parseJson(mapJson) as Record<string, string>; |
||||
if (!map) { |
||||
return; |
||||
} |
||||
|
||||
const teamNames = this.getRocketChatTeamsByLdapTeams(map, ldapUserTeams); |
||||
|
||||
const allTeamNames = [...new Set(Object.values(map).flat())]; |
||||
const allTeams = await Team.listByNames(allTeamNames, { projection: { _id: 1, name: 1 } }); |
||||
|
||||
const inTeamIds = allTeams.filter(({ name }) => teamNames.includes(name)).map(({ _id }) => _id); |
||||
const notInTeamIds = allTeams.filter(({ name }) => !teamNames.includes(name)).map(({ _id }) => _id); |
||||
|
||||
const currentTeams = await Team.listTeamsBySubscriberUserId(user._id, { projection: { teamId: 1 } }); |
||||
const currentTeamIds = currentTeams && currentTeams.map(({ teamId }) => teamId); |
||||
const teamsToRemove = currentTeamIds && currentTeamIds.filter((teamId) => notInTeamIds.includes(teamId)); |
||||
const teamsToAdd = inTeamIds.filter((teamId) => !currentTeamIds?.includes(teamId)); |
||||
|
||||
await Team.insertMemberOnTeams(user._id, teamsToAdd); |
||||
if (teamsToRemove) { |
||||
await Team.removeMemberFromTeams(user._id, teamsToRemove); |
||||
} |
||||
} |
||||
|
||||
private static getRocketChatTeamsByLdapTeams(mappedTeams: Record<string, string>, ldapUserTeams: Array<string>): Array<string> { |
||||
const mappedLdapTeams = Object.keys(mappedTeams); |
||||
const filteredTeams = ldapUserTeams.filter((ldapTeam) => mappedLdapTeams.includes(ldapTeam)); |
||||
|
||||
if (filteredTeams.length < ldapUserTeams.length) { |
||||
const unmappedLdapTeams = ldapUserTeams.filter((ldapTeam) => !mappedLdapTeams.includes(ldapTeam)); |
||||
logger.error(`The following LDAP teams are not mapped in Rocket.Chat: "${ unmappedLdapTeams.join(', ') }".`); |
||||
} |
||||
|
||||
if (!filteredTeams.length) { |
||||
return []; |
||||
} |
||||
|
||||
return [...new Set(filteredTeams.map((ldapTeam) => mappedTeams[ldapTeam]).flat())]; |
||||
} |
||||
|
||||
private static async getLdapTeamsByUsername(ldap: LDAPConnection, username: string): Promise<Array<string>> { |
||||
const query = settings.get<string>('LDAP_Query_To_Get_User_Teams'); |
||||
if (!query) { |
||||
return []; |
||||
} |
||||
|
||||
const searchOptions = { |
||||
filter: query.replace(/#{username}/g, username), |
||||
scope: ldap.options.userSearchScope || 'sub', |
||||
sizeLimit: ldap.options.searchSizeLimit, |
||||
}; |
||||
|
||||
const ldapUserGroups = await ldap.searchRaw(ldap.options.baseDN, searchOptions); |
||||
if (!Array.isArray(ldapUserGroups)) { |
||||
return []; |
||||
} |
||||
|
||||
return ldapUserGroups.filter((entry) => entry?.raw?.ou).map((entry) => (ldap.extractLdapAttribute(entry.raw.ou) as string)).flat(); |
||||
} |
||||
|
||||
public static copyActiveState(ldapUser: ILDAPEntry, userData: IImportUser): void { |
||||
if (!ldapUser) { |
||||
return; |
||||
} |
||||
|
||||
const syncUserState = settings.get('LDAP_Sync_User_Active_State'); |
||||
if (syncUserState === 'none') { |
||||
return; |
||||
} |
||||
|
||||
const deleted = Boolean(ldapUser.pwdAccountLockedTime); |
||||
if (deleted === userData.deleted || (userData.deleted === undefined && !deleted)) { |
||||
return; |
||||
} |
||||
|
||||
if (syncUserState === 'disable' && !deleted) { |
||||
return; |
||||
} |
||||
|
||||
userData.deleted = deleted; |
||||
logger.debug(`${ deleted ? 'Deactivating' : 'Activating' } user ${ userData.name } (${ userData.username })`); |
||||
} |
||||
|
||||
public static copyCustomFields(ldapUser: ILDAPEntry, userData: IImportUser): void { |
||||
if (!settings.get<boolean>('LDAP_Sync_Custom_Fields')) { |
||||
return; |
||||
} |
||||
|
||||
const customFieldsSettings = settings.get<string>('Accounts_CustomFields'); |
||||
const customFieldsMap = settings.get<string>('LDAP_CustomFieldMap'); |
||||
|
||||
if (!customFieldsMap || !customFieldsSettings) { |
||||
if (customFieldsMap) { |
||||
logger.debug('Skipping LDAP custom fields because there are no custom fields configured.'); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
let map: Record<string, string>; |
||||
try { |
||||
map = JSON.parse(customFieldsMap) as Record<string, string>; |
||||
} catch (error) { |
||||
logger.error('Failed to parse LDAP Custom Fields mapping'); |
||||
logger.error(error); |
||||
return; |
||||
} |
||||
|
||||
let customFields: Record<string, any>; |
||||
try { |
||||
customFields = JSON.parse(customFieldsSettings) as Record<string, any>; |
||||
} catch (error) { |
||||
logger.error('Failed to parse Custom Fields'); |
||||
logger.error(error); |
||||
return; |
||||
} |
||||
|
||||
_.map(map, (userField, ldapField) => { |
||||
if (!this.getCustomField(customFields, userField)) { |
||||
logger.debug(`User attribute does not exist: ${ userField }`); |
||||
return; |
||||
} |
||||
|
||||
if (!userData.customFields) { |
||||
userData.customFields = {}; |
||||
} |
||||
|
||||
const value = templateVarHandler(ldapField, ldapUser); |
||||
|
||||
if (value) { |
||||
let ref: Record<string, any> = userData.customFields; |
||||
const attributeNames = userField.split('.'); |
||||
let previousKey: string | undefined; |
||||
|
||||
for (const key of attributeNames) { |
||||
if (previousKey) { |
||||
if (ref[previousKey] === undefined) { |
||||
ref[previousKey] = {}; |
||||
} else if (typeof ref[previousKey] !== 'object') { |
||||
logger.error(`Failed to assign custom field: ${ userField }`); |
||||
return; |
||||
} |
||||
|
||||
ref = ref[previousKey]; |
||||
} |
||||
|
||||
previousKey = key; |
||||
} |
||||
|
||||
if (previousKey) { |
||||
ref[previousKey] = value; |
||||
logger.debug(`user.customFields.${ userField } changed to: ${ value }`); |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
private static async importNewUsers(ldap: LDAPConnection, converter: LDAPDataConverter, updateExistingUsers: boolean): Promise<void> { |
||||
return new Promise((resolve, reject) => { |
||||
let count = 0; |
||||
|
||||
ldap.searchAllUsers<IImportUser>({ |
||||
entryCallback: (entry: ldapjs.SearchEntry): IImportUser | undefined => { |
||||
const data = ldap.extractLdapEntryData(entry); |
||||
count++; |
||||
|
||||
if (!updateExistingUsers) { |
||||
const existingUser = Promise.await(this.findExistingLDAPUser(data)); |
||||
if (existingUser) { |
||||
return; |
||||
} |
||||
} |
||||
|
||||
const userData = this.mapUserData(data); |
||||
converter.addUser(userData); |
||||
return userData; |
||||
}, |
||||
endCallback: (error: any): void => { |
||||
if (error) { |
||||
logger.error(error); |
||||
reject(error); |
||||
return; |
||||
} |
||||
|
||||
logger.info('LDAP finished importing. New users imported:', count); |
||||
resolve(); |
||||
}, |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
private static async updateExistingUsers(ldap: LDAPConnection, converter: LDAPDataConverter): Promise<void> { |
||||
return new Promise(async (resolve, reject) => { |
||||
try { |
||||
const users = await UsersRaw.findLDAPUsers(); |
||||
for await (const user of users) { |
||||
let ldapUser: ILDAPEntry | undefined; |
||||
|
||||
if (user.services?.ldap?.id) { |
||||
ldapUser = await ldap.findOneById(user.services.ldap.id, user.services.ldap.idAttribute); |
||||
} else if (user.username) { |
||||
ldapUser = await ldap.findOneByUsername(user.username); |
||||
} |
||||
|
||||
if (ldapUser) { |
||||
const userData = this.mapUserData(ldapUser, user.username); |
||||
converter.addUser(userData); |
||||
} |
||||
} |
||||
|
||||
resolve(); |
||||
} catch (error) { |
||||
reject(error); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
private static getCustomField(customFields: Record<string, any>, property: string): any { |
||||
try { |
||||
return _.reduce(property.split('.'), (acc, el) => acc[el], customFields); |
||||
} catch { |
||||
// ignore errors
|
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,16 @@ |
||||
import '../../broker'; |
||||
|
||||
import { api } from '../../../../server/sdk/api'; |
||||
import { LDAPEEManager } from '../../lib/ldap/Manager'; |
||||
import { ILDAPEEService } from '../../sdk/types/ILDAPEEService'; |
||||
import { ServiceClass } from '../../../../server/sdk/types/ServiceClass'; |
||||
|
||||
export class LDAPEEService extends ServiceClass implements ILDAPEEService { |
||||
protected name = 'ldap-enterprise'; |
||||
|
||||
async sync(): Promise<void> { |
||||
return LDAPEEManager.sync(); |
||||
} |
||||
} |
||||
|
||||
api.registerService(new LDAPEEService()); |
||||
@ -0,0 +1,4 @@ |
||||
import { ILDAPEEService } from './types/ILDAPEEService'; |
||||
import { proxifyWithWait } from '../../../server/sdk/lib/proxify'; |
||||
|
||||
export const LDAPEE = proxifyWithWait<ILDAPEEService>('ldap-enterprise'); |
||||
@ -0,0 +1,3 @@ |
||||
export interface ILDAPEEService { |
||||
sync(): Promise<void>; |
||||
} |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue