ldap.js 17 KB

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