| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598 | import ldapjs from 'ldapjs';import { Log } from 'meteor/logging';// copied from https://github.com/ldapjs/node-ldapjs/blob/a113953e0d91211eb945d2a3952c84b7af6de41c/lib/filters/index.js#L167function escapedToHex (str) {  if (str !== undefined) {    return str.replace(/\\([0-9a-f][^0-9a-f]|[0-9a-f]$|[^0-9a-f]|$)/gi, function (match, p1) {      if (!p1) {        return '\\5c';      }      const hexCode = p1.charCodeAt(0).toString(16);      const rest = p1.substring(1);      return '\\' + hexCode + rest;    });  } else {    return undefined;  }}export default class LDAP {  constructor() {    this.ldapjs = ldapjs;    this.connected = false;    this.options = {      host                               : this.constructor.settings_get('LDAP_HOST'),      port                               : this.constructor.settings_get('LDAP_PORT'),      Reconnect                          : this.constructor.settings_get('LDAP_RECONNECT'),      timeout                            : this.constructor.settings_get('LDAP_TIMEOUT'),      connect_timeout                    : this.constructor.settings_get('LDAP_CONNECT_TIMEOUT'),      idle_timeout                       : this.constructor.settings_get('LDAP_IDLE_TIMEOUT'),      encryption                         : this.constructor.settings_get('LDAP_ENCRYPTION'),      ca_cert                            : this.constructor.settings_get('LDAP_CA_CERT'),      reject_unauthorized                : this.constructor.settings_get('LDAP_REJECT_UNAUTHORIZED') !== undefined ? this.constructor.settings_get('LDAP_REJECT_UNAUTHORIZED') : true,      Authentication                     : this.constructor.settings_get('LDAP_AUTHENTIFICATION'),      Authentication_UserDN              : this.constructor.settings_get('LDAP_AUTHENTIFICATION_USERDN'),      Authentication_Password            : this.constructor.settings_get('LDAP_AUTHENTIFICATION_PASSWORD'),      Authentication_Fallback            : this.constructor.settings_get('LDAP_LOGIN_FALLBACK'),      BaseDN                             : this.constructor.settings_get('LDAP_BASEDN'),      Internal_Log_Level                 : this.constructor.settings_get('INTERNAL_LOG_LEVEL'), //this setting does not have any effect any more and should be deprecated      User_Authentication                : this.constructor.settings_get('LDAP_USER_AUTHENTICATION'),      User_Authentication_Field          : this.constructor.settings_get('LDAP_USER_AUTHENTICATION_FIELD'),      User_Attributes                    : this.constructor.settings_get('LDAP_USER_ATTRIBUTES'),      User_Search_Filter                 : escapedToHex(this.constructor.settings_get('LDAP_USER_SEARCH_FILTER')),      User_Search_Scope                  : this.constructor.settings_get('LDAP_USER_SEARCH_SCOPE'),      User_Search_Field                  : this.constructor.settings_get('LDAP_USER_SEARCH_FIELD'),      Search_Page_Size                   : this.constructor.settings_get('LDAP_SEARCH_PAGE_SIZE'),      Search_Size_Limit                  : this.constructor.settings_get('LDAP_SEARCH_SIZE_LIMIT'),      group_filter_enabled               : this.constructor.settings_get('LDAP_GROUP_FILTER_ENABLE'),      group_filter_object_class          : this.constructor.settings_get('LDAP_GROUP_FILTER_OBJECTCLASS'),      group_filter_group_id_attribute    : this.constructor.settings_get('LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE'),      group_filter_group_member_attribute: this.constructor.settings_get('LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE'),      group_filter_group_member_format   : this.constructor.settings_get('LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT'),      group_filter_group_name            : this.constructor.settings_get('LDAP_GROUP_FILTER_GROUP_NAME'),      AD_Simple_Auth                     : this.constructor.settings_get('LDAP_AD_SIMPLE_AUTH'),      Default_Domain                     : this.constructor.settings_get('LDAP_DEFAULT_DOMAIN'),    };  }  static settings_get(name, ...args) {    let value = process.env[name];    if (value !== undefined) {      if (value === 'true' || value === 'false') {        value = JSON.parse(value);      } else if (value !== '' && !isNaN(value)) {        value = Number(value);      }      return value;    } else {      //Log.warn(`Lookup for unset variable: ${name}`);    }  }  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) {    Log.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,    };    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 = this.constructor.settings_get('LDAP_CA_CERT').replace(/\\n/g,'\n').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}`;    }    Log.info(`Connecting ${connectionOptions.url}`);    Log.debug(`connectionOptions ${JSON.stringify(connectionOptions)}`);    this.client = ldapjs.createClient(connectionOptions);    this.bindSync = Meteor.wrapAsync(this.client.bind, this.client);    this.client.on('error', (error) => {      Log.error(`connection ${error}`);      if (replied === false) {        replied = true;        callback(error, null);      }    });    this.client.on('idle', () => {      Log.info('Idle');      this.disconnect();    });    this.client.on('close', () => {      Log.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;      Log.info('Starting TLS');      Log.debug(`tlsOptions ${JSON.stringify(tlsOptions)}`);      this.client.starttls(tlsOptions, null, (error, response) => {        if (error) {          Log.error(`TLS connection ${JSON.stringify(error)}`);          if (replied === false) {            replied = true;            callback(error, null);          }          return;        }        Log.info('TLS connected');        this.connected = true;        if (replied === false) {          replied = true;          callback(null, response);        }      });    } else {      this.client.on('connect', (response) => {        Log.info('LDAP connected');        this.connected = true;        if (replied === false) {          replied = true;          callback(null, response);        }      });    }    setTimeout(() => {      if (replied === false) {        Log.error(`connection time out ${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) {      Log.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('')})`;  }  bindUserIfNecessary(username, password) {    if (this.domainBinded === true) {      return;    }    if (!this.options.User_Authentication) {      return;    }    /* if SimpleAuth is configured, the BaseDN is not needed */    if (!this.options.BaseDN && !this.options.AD_Simple_Auth) throw new Error('BaseDN is not provided');    var userDn = "";    if (this.options.AD_Simple_Auth === true || this.options.AD_Simple_Auth === 'true') {      userDn = `${username}@${this.options.Default_Domain}`;    } else {      userDn = `${this.options.User_Authentication_Field}=${username},${this.options.BaseDN}`;    }    Log.info(`Binding with User ${userDn}`);    this.bindSync(userDn, password);    this.domainBinded = true;  }  bindIfNecessary() {    if (this.domainBinded === true) {      return;    }    if (this.options.Authentication !== true) {      return;    }    Log.info(`Binding 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.User_Attributes) searchOptions.attributes = this.options.User_Attributes.split(',');    if (this.options.Search_Page_Size > 0) {      searchOptions.paged = {        pageSize : this.options.Search_Page_Size,        pagePause: !!page,      };    }    Log.info(`Searching user ${username}`);    Log.debug(`searchOptions ${searchOptions}`);    Log.debug(`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 = this.constructor.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',    };    Log.info(`Searching by id ${id}`);    Log.debug(`search filter ${searchOptions.filter.toString()}`);    Log.debug(`BaseDN ${this.options.BaseDN}`);    const result = this.searchAllSync(this.options.BaseDN, searchOptions);    if (!Array.isArray(result) || result.length === 0) {      return;    }    if (result.length > 1) {      Log.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',    };    Log.info(`Searching user ${username}`);    Log.debug(`searchOptions ${searchOptions}`);    Log.debug(`BaseDN ${this.options.BaseDN}`);    const result = this.searchAllSync(this.options.BaseDN, searchOptions);    if (!Array.isArray(result) || result.length === 0) {      return;    }    if (result.length > 1) {      Log.error(`Search by username ${username} returned ${result.length} records`);    }    return result[0];  }  getUserGroups(username, ldapUser) {    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 !== '') {      const format_value = ldapUser[this.options.group_filter_group_member_format];      if (format_value) {        filter.push(`(${this.options.group_filter_group_member_attribute}=${format_value})`);      }    }    filter.push(')');    const searchOptions = {      filter: filter.join('').replace(/#{username}/g, username).replace("\\", "\\\\"),      scope : 'sub',    };    Log.debug(`Group list filter LDAP: ${searchOptions.filter}`);    const result = this.searchAllSync(this.options.BaseDN, searchOptions);    if (!Array.isArray(result) || result.length === 0) {      return [];    }    const grp_identifier = this.options.group_filter_group_id_attribute || 'cn';    const groups         = [];    result.map((item) => {      groups.push(item[grp_identifier]);    });    Log.debug(`Groups: ${groups.join(', ')}`);    return groups;  }  isUserInGroup(username, ldapUser) {    if (!this.options.group_filter_enabled) {      return true;    }    const grps = this.getUserGroups(username, ldapUser);    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 !== '') {      const format_value = ldapUser[this.options.group_filter_group_member_format];      if (format_value) {        filter.push(`(${this.options.group_filter_group_member_attribute}=${format_value})`);      }    }    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("\\", "\\\\"),      scope : 'sub',    };    Log.debug(`Group filter LDAP: ${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;        }      }    });    return values;  }  searchAllPaged(BaseDN, options, page) {    this.bindIfNecessary();    const processPage = ({ entries, title, end, next }) => {      Log.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) {        Log.error(error);        page(error);        return;      }      res.on('error', (error) => {        Log.error(error);        page(error);        return;      });      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,          });        } else if (entries.length) {          Log.info('Page');          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();    this.client.search(BaseDN, options, (error, res) => {      if (error) {        Log.error(error);        callback(error);        return;      }      res.on('error', (error) => {        Log.error(error);        callback(error);        return;      });      const entries = [];      res.on('searchEntry', (entry) => {        entries.push(this.extractLdapEntryData(entry));      });      res.on('end', () => {        Log.info(`Search result count ${entries.length}`);        callback(null, entries);      });    });  }  authSync(dn, password) {    Log.info(`Authenticating ${dn}`);    try {      if (password === '') {        throw new Error('Password is not provided');      }      this.bindSync(dn, password);      Log.info(`Authenticated ${dn}`);      return true;    } catch (error) {      Log.info(`Not authenticated ${dn}`);      Log.debug('error', error);      return false;    }  }  disconnect() {    this.connected    = false;    this.domainBinded = false;    Log.info('Disconecting');    this.client.unbind();  }}
 |