From 6a01170d8696322462c4065ce0cf4a637a058975 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Thu, 16 Jan 2020 06:15:10 +0200 Subject: Use Meteor 1.8.x on Snap. --- .snap-meteor-1.8/ldap.js | 640 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 640 insertions(+) create mode 100644 .snap-meteor-1.8/ldap.js (limited to '.snap-meteor-1.8/ldap.js') diff --git a/.snap-meteor-1.8/ldap.js b/.snap-meteor-1.8/ldap.js new file mode 100644 index 00000000..3b963823 --- /dev/null +++ b/.snap-meteor-1.8/ldap.js @@ -0,0 +1,640 @@ +import ldapjs from 'ldapjs'; +import util from 'util'; +import Bunyan from 'bunyan'; +import { log_debug, log_info, log_warn, log_error } from './logger'; + +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') || false, + 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'), + 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: 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', + ), + }; + } + + 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, + }; + + 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 = this.constructor + .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}`; + } + + log_info('Connecting', connectionOptions.url); + log_debug(`connectionOptions${util.inspect(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', tlsOptions); + + this.client.starttls(tlsOptions, null, (error, response) => { + if (error) { + log_error('TLS connection', 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 (!this.options.BaseDN) throw new Error('BaseDN is not provided'); + + const userDn = `${this.options.User_Authentication_Field}=${username},${this.options.BaseDN}`; + + 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: new Buffer(id, 'hex'), + }); + } else { + const filters = []; + Unique_Identifier_Field.forEach(item => { + filters.push( + new this.ldapjs.filters.EqualityFilter({ + attribute: item, + value: new Buffer(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), + 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), + 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(); + } +} -- cgit v1.2.3-1-g7c22