ldap.js 17 KB

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