ldap.js 17 KB

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