ldap.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. import ldapjs from 'ldapjs';
  2. import util from 'util';
  3. import Bunyan from 'bunyan';
  4. import { log_debug, log_info, log_warn, log_error } from './logger';
  5. export default class LDAP {
  6. constructor() {
  7. this.ldapjs = ldapjs;
  8. this.connected = false;
  9. this.options = {
  10. host: this.constructor.settings_get('LDAP_HOST'),
  11. port: this.constructor.settings_get('LDAP_PORT'),
  12. Reconnect: this.constructor.settings_get('LDAP_RECONNECT'),
  13. timeout: this.constructor.settings_get('LDAP_TIMEOUT'),
  14. connect_timeout: this.constructor.settings_get('LDAP_CONNECT_TIMEOUT'),
  15. idle_timeout: this.constructor.settings_get('LDAP_IDLE_TIMEOUT'),
  16. encryption: this.constructor.settings_get('LDAP_ENCRYPTION'),
  17. ca_cert: this.constructor.settings_get('LDAP_CA_CERT'),
  18. reject_unauthorized:
  19. this.constructor.settings_get('LDAP_REJECT_UNAUTHORIZED') || false,
  20. Authentication: this.constructor.settings_get('LDAP_AUTHENTIFICATION'),
  21. Authentication_UserDN: this.constructor.settings_get(
  22. 'LDAP_AUTHENTIFICATION_USERDN',
  23. ),
  24. Authentication_Password: this.constructor.settings_get(
  25. 'LDAP_AUTHENTIFICATION_PASSWORD',
  26. ),
  27. Authentication_Fallback: this.constructor.settings_get(
  28. 'LDAP_LOGIN_FALLBACK',
  29. ),
  30. BaseDN: this.constructor.settings_get('LDAP_BASEDN'),
  31. Internal_Log_Level: this.constructor.settings_get('INTERNAL_LOG_LEVEL'),
  32. User_Authentication: this.constructor.settings_get(
  33. 'LDAP_USER_AUTHENTICATION',
  34. ),
  35. User_Authentication_Field: this.constructor.settings_get(
  36. 'LDAP_USER_AUTHENTICATION_FIELD',
  37. ),
  38. User_Attributes: this.constructor.settings_get('LDAP_USER_ATTRIBUTES'),
  39. User_Search_Filter: this.constructor.settings_get(
  40. 'LDAP_USER_SEARCH_FILTER',
  41. ),
  42. User_Search_Scope: this.constructor.settings_get(
  43. 'LDAP_USER_SEARCH_SCOPE',
  44. ),
  45. User_Search_Field: this.constructor.settings_get(
  46. 'LDAP_USER_SEARCH_FIELD',
  47. ),
  48. Search_Page_Size: this.constructor.settings_get('LDAP_SEARCH_PAGE_SIZE'),
  49. Search_Size_Limit: this.constructor.settings_get(
  50. 'LDAP_SEARCH_SIZE_LIMIT',
  51. ),
  52. group_filter_enabled: this.constructor.settings_get(
  53. 'LDAP_GROUP_FILTER_ENABLE',
  54. ),
  55. group_filter_object_class: this.constructor.settings_get(
  56. 'LDAP_GROUP_FILTER_OBJECTCLASS',
  57. ),
  58. group_filter_group_id_attribute: this.constructor.settings_get(
  59. 'LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE',
  60. ),
  61. group_filter_group_member_attribute: this.constructor.settings_get(
  62. 'LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE',
  63. ),
  64. group_filter_group_member_format: this.constructor.settings_get(
  65. 'LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT',
  66. ),
  67. group_filter_group_name: this.constructor.settings_get(
  68. 'LDAP_GROUP_FILTER_GROUP_NAME',
  69. ),
  70. };
  71. }
  72. static settings_get(name, ...args) {
  73. let value = process.env[name];
  74. if (value !== undefined) {
  75. if (value === 'true' || value === 'false') {
  76. value = JSON.parse(value);
  77. } else if (value !== '' && !isNaN(value)) {
  78. value = Number(value);
  79. }
  80. return value;
  81. } else {
  82. log_warn(`Lookup for unset variable: ${name}`);
  83. }
  84. }
  85. connectSync(...args) {
  86. if (!this._connectSync) {
  87. this._connectSync = Meteor.wrapAsync(this.connectAsync, this);
  88. }
  89. return this._connectSync(...args);
  90. }
  91. searchAllSync(...args) {
  92. if (!this._searchAllSync) {
  93. this._searchAllSync = Meteor.wrapAsync(this.searchAllAsync, this);
  94. }
  95. return this._searchAllSync(...args);
  96. }
  97. connectAsync(callback) {
  98. log_info('Init setup');
  99. let replied = false;
  100. const connectionOptions = {
  101. url: `${this.options.host}:${this.options.port}`,
  102. timeout: this.options.timeout,
  103. connectTimeout: this.options.connect_timeout,
  104. idleTimeout: this.options.idle_timeout,
  105. reconnect: this.options.Reconnect,
  106. };
  107. if (this.options.Internal_Log_Level !== 'disabled') {
  108. connectionOptions.log = new Bunyan({
  109. name: 'ldapjs',
  110. component: 'client',
  111. stream: process.stderr,
  112. level: this.options.Internal_Log_Level,
  113. });
  114. }
  115. const tlsOptions = {
  116. rejectUnauthorized: this.options.reject_unauthorized,
  117. };
  118. if (this.options.ca_cert && this.options.ca_cert !== '') {
  119. // Split CA cert into array of strings
  120. const chainLines = this.constructor
  121. .settings_get('LDAP_CA_CERT')
  122. .split('\n');
  123. let cert = [];
  124. const ca = [];
  125. chainLines.forEach(line => {
  126. cert.push(line);
  127. if (line.match(/-END CERTIFICATE-/)) {
  128. ca.push(cert.join('\n'));
  129. cert = [];
  130. }
  131. });
  132. tlsOptions.ca = ca;
  133. }
  134. if (this.options.encryption === 'ssl') {
  135. connectionOptions.url = `ldaps://${connectionOptions.url}`;
  136. connectionOptions.tlsOptions = tlsOptions;
  137. } else {
  138. connectionOptions.url = `ldap://${connectionOptions.url}`;
  139. }
  140. log_info('Connecting', connectionOptions.url);
  141. log_debug(`connectionOptions${util.inspect(connectionOptions)}`);
  142. this.client = ldapjs.createClient(connectionOptions);
  143. this.bindSync = Meteor.wrapAsync(this.client.bind, this.client);
  144. this.client.on('error', error => {
  145. log_error('connection', error);
  146. if (replied === false) {
  147. replied = true;
  148. callback(error, null);
  149. }
  150. });
  151. this.client.on('idle', () => {
  152. log_info('Idle');
  153. this.disconnect();
  154. });
  155. this.client.on('close', () => {
  156. log_info('Closed');
  157. });
  158. if (this.options.encryption === 'tls') {
  159. // 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).
  160. // https://github.com/RocketChat/Rocket.Chat/issues/2035
  161. // https://github.com/mcavage/node-ldapjs/issues/349
  162. tlsOptions.host = this.options.host;
  163. log_info('Starting TLS');
  164. log_debug('tlsOptions', tlsOptions);
  165. this.client.starttls(tlsOptions, null, (error, response) => {
  166. if (error) {
  167. log_error('TLS connection', error);
  168. if (replied === false) {
  169. replied = true;
  170. callback(error, null);
  171. }
  172. return;
  173. }
  174. log_info('TLS connected');
  175. this.connected = true;
  176. if (replied === false) {
  177. replied = true;
  178. callback(null, response);
  179. }
  180. });
  181. } else {
  182. this.client.on('connect', response => {
  183. log_info('LDAP connected');
  184. this.connected = true;
  185. if (replied === false) {
  186. replied = true;
  187. callback(null, response);
  188. }
  189. });
  190. }
  191. setTimeout(() => {
  192. if (replied === false) {
  193. log_error('connection time out', connectionOptions.connectTimeout);
  194. replied = true;
  195. callback(new Error('Timeout'));
  196. }
  197. }, connectionOptions.connectTimeout);
  198. }
  199. getUserFilter(username) {
  200. const filter = [];
  201. if (this.options.User_Search_Filter !== '') {
  202. if (this.options.User_Search_Filter[0] === '(') {
  203. filter.push(`${this.options.User_Search_Filter}`);
  204. } else {
  205. filter.push(`(${this.options.User_Search_Filter})`);
  206. }
  207. }
  208. const usernameFilter = this.options.User_Search_Field.split(',').map(
  209. item => `(${item}=${username})`,
  210. );
  211. if (usernameFilter.length === 0) {
  212. log_error('LDAP_LDAP_User_Search_Field not defined');
  213. } else if (usernameFilter.length === 1) {
  214. filter.push(`${usernameFilter[0]}`);
  215. } else {
  216. filter.push(`(|${usernameFilter.join('')})`);
  217. }
  218. return `(&${filter.join('')})`;
  219. }
  220. bindUserIfNecessary(username, password) {
  221. if (this.domainBinded === true) {
  222. return;
  223. }
  224. if (!this.options.User_Authentication) {
  225. return;
  226. }
  227. if (!this.options.BaseDN) throw new Error('BaseDN is not provided');
  228. const userDn = `${this.options.User_Authentication_Field}=${username},${this.options.BaseDN}`;
  229. this.bindSync(userDn, password);
  230. this.domainBinded = true;
  231. }
  232. bindIfNecessary() {
  233. if (this.domainBinded === true) {
  234. return;
  235. }
  236. if (this.options.Authentication !== true) {
  237. return;
  238. }
  239. log_info('Binding UserDN', this.options.Authentication_UserDN);
  240. this.bindSync(
  241. this.options.Authentication_UserDN,
  242. this.options.Authentication_Password,
  243. );
  244. this.domainBinded = true;
  245. }
  246. searchUsersSync(username, page) {
  247. this.bindIfNecessary();
  248. const searchOptions = {
  249. filter: this.getUserFilter(username),
  250. scope: this.options.User_Search_Scope || 'sub',
  251. sizeLimit: this.options.Search_Size_Limit,
  252. };
  253. if (!!this.options.User_Attributes)
  254. searchOptions.attributes = this.options.User_Attributes.split(',');
  255. if (this.options.Search_Page_Size > 0) {
  256. searchOptions.paged = {
  257. pageSize: this.options.Search_Page_Size,
  258. pagePause: !!page,
  259. };
  260. }
  261. log_info('Searching user', username);
  262. log_debug('searchOptions', searchOptions);
  263. log_debug('BaseDN', this.options.BaseDN);
  264. if (page) {
  265. return this.searchAllPaged(this.options.BaseDN, searchOptions, page);
  266. }
  267. return this.searchAllSync(this.options.BaseDN, searchOptions);
  268. }
  269. getUserByIdSync(id, attribute) {
  270. this.bindIfNecessary();
  271. const Unique_Identifier_Field = this.constructor
  272. .settings_get('LDAP_UNIQUE_IDENTIFIER_FIELD')
  273. .split(',');
  274. let filter;
  275. if (attribute) {
  276. filter = new this.ldapjs.filters.EqualityFilter({
  277. attribute,
  278. value: new Buffer(id, 'hex'),
  279. });
  280. } else {
  281. const filters = [];
  282. Unique_Identifier_Field.forEach(item => {
  283. filters.push(
  284. new this.ldapjs.filters.EqualityFilter({
  285. attribute: item,
  286. value: new Buffer(id, 'hex'),
  287. }),
  288. );
  289. });
  290. filter = new this.ldapjs.filters.OrFilter({ filters });
  291. }
  292. const searchOptions = {
  293. filter,
  294. scope: 'sub',
  295. };
  296. log_info('Searching by id', id);
  297. log_debug('search filter', searchOptions.filter.toString());
  298. log_debug('BaseDN', this.options.BaseDN);
  299. const result = this.searchAllSync(this.options.BaseDN, searchOptions);
  300. if (!Array.isArray(result) || result.length === 0) {
  301. return;
  302. }
  303. if (result.length > 1) {
  304. log_error('Search by id', id, 'returned', result.length, 'records');
  305. }
  306. return result[0];
  307. }
  308. getUserByUsernameSync(username) {
  309. this.bindIfNecessary();
  310. const searchOptions = {
  311. filter: this.getUserFilter(username),
  312. scope: this.options.User_Search_Scope || 'sub',
  313. };
  314. log_info('Searching user', username);
  315. log_debug('searchOptions', searchOptions);
  316. log_debug('BaseDN', this.options.BaseDN);
  317. const result = this.searchAllSync(this.options.BaseDN, searchOptions);
  318. if (!Array.isArray(result) || result.length === 0) {
  319. return;
  320. }
  321. if (result.length > 1) {
  322. log_error(
  323. 'Search by username',
  324. username,
  325. 'returned',
  326. result.length,
  327. 'records',
  328. );
  329. }
  330. return result[0];
  331. }
  332. getUserGroups(username, ldapUser) {
  333. if (!this.options.group_filter_enabled) {
  334. return true;
  335. }
  336. const filter = ['(&'];
  337. if (this.options.group_filter_object_class !== '') {
  338. filter.push(`(objectclass=${this.options.group_filter_object_class})`);
  339. }
  340. if (this.options.group_filter_group_member_attribute !== '') {
  341. const format_value =
  342. ldapUser[this.options.group_filter_group_member_format];
  343. if (format_value) {
  344. filter.push(
  345. `(${this.options.group_filter_group_member_attribute}=${format_value})`,
  346. );
  347. }
  348. }
  349. filter.push(')');
  350. const searchOptions = {
  351. filter: filter.join('').replace(/#{username}/g, username),
  352. scope: 'sub',
  353. };
  354. log_debug('Group list filter LDAP:', searchOptions.filter);
  355. const result = this.searchAllSync(this.options.BaseDN, searchOptions);
  356. if (!Array.isArray(result) || result.length === 0) {
  357. return [];
  358. }
  359. const grp_identifier = this.options.group_filter_group_id_attribute || 'cn';
  360. const groups = [];
  361. result.map(item => {
  362. groups.push(item[grp_identifier]);
  363. });
  364. log_debug(`Groups: ${groups.join(', ')}`);
  365. return groups;
  366. }
  367. isUserInGroup(username, ldapUser) {
  368. if (!this.options.group_filter_enabled) {
  369. return true;
  370. }
  371. const grps = this.getUserGroups(username, ldapUser);
  372. const filter = ['(&'];
  373. if (this.options.group_filter_object_class !== '') {
  374. filter.push(`(objectclass=${this.options.group_filter_object_class})`);
  375. }
  376. if (this.options.group_filter_group_member_attribute !== '') {
  377. const format_value =
  378. ldapUser[this.options.group_filter_group_member_format];
  379. if (format_value) {
  380. filter.push(
  381. `(${this.options.group_filter_group_member_attribute}=${format_value})`,
  382. );
  383. }
  384. }
  385. if (this.options.group_filter_group_id_attribute !== '') {
  386. filter.push(
  387. `(${this.options.group_filter_group_id_attribute}=${this.options.group_filter_group_name})`,
  388. );
  389. }
  390. filter.push(')');
  391. const searchOptions = {
  392. filter: filter.join('').replace(/#{username}/g, username),
  393. scope: 'sub',
  394. };
  395. log_debug('Group filter LDAP:', searchOptions.filter);
  396. const result = this.searchAllSync(this.options.BaseDN, searchOptions);
  397. if (!Array.isArray(result) || result.length === 0) {
  398. return false;
  399. }
  400. return true;
  401. }
  402. extractLdapEntryData(entry) {
  403. const values = {
  404. _raw: entry.raw,
  405. };
  406. Object.keys(values._raw).forEach(key => {
  407. const value = values._raw[key];
  408. if (!['thumbnailPhoto', 'jpegPhoto'].includes(key)) {
  409. if (value instanceof Buffer) {
  410. values[key] = value.toString();
  411. } else {
  412. values[key] = value;
  413. }
  414. }
  415. });
  416. return values;
  417. }
  418. searchAllPaged(BaseDN, options, page) {
  419. this.bindIfNecessary();
  420. const processPage = ({ entries, title, end, next }) => {
  421. log_info(title);
  422. // Force LDAP idle to wait the record processing
  423. this.client._updateIdle(true);
  424. page(null, entries, {
  425. end,
  426. next: () => {
  427. // Reset idle timer
  428. this.client._updateIdle();
  429. next && next();
  430. },
  431. });
  432. };
  433. this.client.search(BaseDN, options, (error, res) => {
  434. if (error) {
  435. log_error(error);
  436. page(error);
  437. return;
  438. }
  439. res.on('error', error => {
  440. log_error(error);
  441. page(error);
  442. return;
  443. });
  444. let entries = [];
  445. const internalPageSize =
  446. options.paged && options.paged.pageSize > 0
  447. ? options.paged.pageSize * 2
  448. : 500;
  449. res.on('searchEntry', entry => {
  450. entries.push(this.extractLdapEntryData(entry));
  451. if (entries.length >= internalPageSize) {
  452. processPage({
  453. entries,
  454. title: 'Internal Page',
  455. end: false,
  456. });
  457. entries = [];
  458. }
  459. });
  460. res.on('page', (result, next) => {
  461. if (!next) {
  462. this.client._updateIdle(true);
  463. processPage({
  464. entries,
  465. title: 'Final Page',
  466. end: true,
  467. });
  468. } else if (entries.length) {
  469. log_info('Page');
  470. processPage({
  471. entries,
  472. title: 'Page',
  473. end: false,
  474. next,
  475. });
  476. entries = [];
  477. }
  478. });
  479. res.on('end', () => {
  480. if (entries.length) {
  481. processPage({
  482. entries,
  483. title: 'Final Page',
  484. end: true,
  485. });
  486. entries = [];
  487. }
  488. });
  489. });
  490. }
  491. searchAllAsync(BaseDN, options, callback) {
  492. this.bindIfNecessary();
  493. this.client.search(BaseDN, options, (error, res) => {
  494. if (error) {
  495. log_error(error);
  496. callback(error);
  497. return;
  498. }
  499. res.on('error', error => {
  500. log_error(error);
  501. callback(error);
  502. return;
  503. });
  504. const entries = [];
  505. res.on('searchEntry', entry => {
  506. entries.push(this.extractLdapEntryData(entry));
  507. });
  508. res.on('end', () => {
  509. log_info('Search result count', entries.length);
  510. callback(null, entries);
  511. });
  512. });
  513. }
  514. authSync(dn, password) {
  515. log_info('Authenticating', dn);
  516. try {
  517. if (password === '') {
  518. throw new Error('Password is not provided');
  519. }
  520. this.bindSync(dn, password);
  521. log_info('Authenticated', dn);
  522. return true;
  523. } catch (error) {
  524. log_info('Not authenticated', dn);
  525. log_debug('error', error);
  526. return false;
  527. }
  528. }
  529. disconnect() {
  530. this.connected = false;
  531. this.domainBinded = false;
  532. log_info('Disconecting');
  533. this.client.unbind();
  534. }
  535. }