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') !== undefined ? this.constructor.settings_get('LDAP_REJECT_UNAUTHORIZED') : true,
  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').replace(/\\n/g,'\n').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 SimpleAuth is configured, the BaseDN is not needed */
  195. if (!this.options.BaseDN && !this.options.AD_Simple_Auth) throw new Error('BaseDN is not provided');
  196. var userDn = "";
  197. if (this.options.AD_Simple_Auth === true || this.options.AD_Simple_Auth === 'true') {
  198. userDn = `${username}@${this.options.Default_Domain}`;
  199. } else {
  200. userDn = `${this.options.User_Authentication_Field}=${username},${this.options.BaseDN}`;
  201. }
  202. log_info('Binding with User', userDn);
  203. this.bindSync(userDn, password);
  204. this.domainBinded = true;
  205. }
  206. bindIfNecessary() {
  207. if (this.domainBinded === true) {
  208. return;
  209. }
  210. if (this.options.Authentication !== true) {
  211. return;
  212. }
  213. log_info('Binding UserDN', this.options.Authentication_UserDN);
  214. this.bindSync(this.options.Authentication_UserDN, this.options.Authentication_Password);
  215. this.domainBinded = true;
  216. }
  217. searchUsersSync(username, page) {
  218. this.bindIfNecessary();
  219. const searchOptions = {
  220. filter : this.getUserFilter(username),
  221. scope : this.options.User_Search_Scope || 'sub',
  222. sizeLimit: this.options.Search_Size_Limit,
  223. };
  224. if (!!this.options.User_Attributes) searchOptions.attributes = this.options.User_Attributes.split(',');
  225. if (this.options.Search_Page_Size > 0) {
  226. searchOptions.paged = {
  227. pageSize : this.options.Search_Page_Size,
  228. pagePause: !!page,
  229. };
  230. }
  231. log_info('Searching user', username);
  232. log_debug('searchOptions', searchOptions);
  233. log_debug('BaseDN', this.options.BaseDN);
  234. if (page) {
  235. return this.searchAllPaged(this.options.BaseDN, searchOptions, page);
  236. }
  237. return this.searchAllSync(this.options.BaseDN, searchOptions);
  238. }
  239. getUserByIdSync(id, attribute) {
  240. this.bindIfNecessary();
  241. const Unique_Identifier_Field = this.constructor.settings_get('LDAP_UNIQUE_IDENTIFIER_FIELD').split(',');
  242. let filter;
  243. if (attribute) {
  244. filter = new this.ldapjs.filters.EqualityFilter({
  245. attribute,
  246. value: Buffer.from(id, 'hex'),
  247. });
  248. } else {
  249. const filters = [];
  250. Unique_Identifier_Field.forEach((item) => {
  251. filters.push(new this.ldapjs.filters.EqualityFilter({
  252. attribute: item,
  253. value : Buffer.from(id, 'hex'),
  254. }));
  255. });
  256. filter = new this.ldapjs.filters.OrFilter({ filters });
  257. }
  258. const searchOptions = {
  259. filter,
  260. scope: 'sub',
  261. };
  262. log_info('Searching by id', id);
  263. log_debug('search filter', searchOptions.filter.toString());
  264. log_debug('BaseDN', this.options.BaseDN);
  265. const result = this.searchAllSync(this.options.BaseDN, searchOptions);
  266. if (!Array.isArray(result) || result.length === 0) {
  267. return;
  268. }
  269. if (result.length > 1) {
  270. log_error('Search by id', id, 'returned', result.length, 'records');
  271. }
  272. return result[0];
  273. }
  274. getUserByUsernameSync(username) {
  275. this.bindIfNecessary();
  276. const searchOptions = {
  277. filter: this.getUserFilter(username),
  278. scope : this.options.User_Search_Scope || 'sub',
  279. };
  280. log_info('Searching user', username);
  281. log_debug('searchOptions', searchOptions);
  282. log_debug('BaseDN', this.options.BaseDN);
  283. const result = this.searchAllSync(this.options.BaseDN, searchOptions);
  284. if (!Array.isArray(result) || result.length === 0) {
  285. return;
  286. }
  287. if (result.length > 1) {
  288. log_error('Search by username', username, 'returned', result.length, 'records');
  289. }
  290. return result[0];
  291. }
  292. getUserGroups(username, ldapUser) {
  293. if (!this.options.group_filter_enabled) {
  294. return true;
  295. }
  296. const filter = ['(&'];
  297. if (this.options.group_filter_object_class !== '') {
  298. filter.push(`(objectclass=${this.options.group_filter_object_class})`);
  299. }
  300. if (this.options.group_filter_group_member_attribute !== '') {
  301. const format_value = ldapUser[this.options.group_filter_group_member_format];
  302. if (format_value) {
  303. filter.push(`(${this.options.group_filter_group_member_attribute}=${format_value})`);
  304. }
  305. }
  306. filter.push(')');
  307. const searchOptions = {
  308. filter: filter.join('').replace(/#{username}/g, username),
  309. scope : 'sub',
  310. };
  311. log_debug('Group list filter LDAP:', searchOptions.filter);
  312. const result = this.searchAllSync(this.options.BaseDN, searchOptions);
  313. if (!Array.isArray(result) || result.length === 0) {
  314. return [];
  315. }
  316. const grp_identifier = this.options.group_filter_group_id_attribute || 'cn';
  317. const groups = [];
  318. result.map((item) => {
  319. groups.push(item[grp_identifier]);
  320. });
  321. log_debug(`Groups: ${groups.join(', ')}`);
  322. return groups;
  323. }
  324. isUserInGroup(username, ldapUser) {
  325. if (!this.options.group_filter_enabled) {
  326. return true;
  327. }
  328. const grps = this.getUserGroups(username, ldapUser);
  329. const filter = ['(&'];
  330. if (this.options.group_filter_object_class !== '') {
  331. filter.push(`(objectclass=${this.options.group_filter_object_class})`);
  332. }
  333. if (this.options.group_filter_group_member_attribute !== '') {
  334. const format_value = ldapUser[this.options.group_filter_group_member_format];
  335. if (format_value) {
  336. filter.push(`(${this.options.group_filter_group_member_attribute}=${format_value})`);
  337. }
  338. }
  339. if (this.options.group_filter_group_id_attribute !== '') {
  340. filter.push(`(${this.options.group_filter_group_id_attribute}=${this.options.group_filter_group_name})`);
  341. }
  342. filter.push(')');
  343. const searchOptions = {
  344. filter: filter.join('').replace(/#{username}/g, username),
  345. scope : 'sub',
  346. };
  347. log_debug('Group filter LDAP:', searchOptions.filter);
  348. const result = this.searchAllSync(this.options.BaseDN, searchOptions);
  349. if (!Array.isArray(result) || result.length === 0) {
  350. return false;
  351. }
  352. return true;
  353. }
  354. extractLdapEntryData(entry) {
  355. const values = {
  356. _raw: entry.raw,
  357. };
  358. Object.keys(values._raw).forEach((key) => {
  359. const value = values._raw[key];
  360. if (!['thumbnailPhoto', 'jpegPhoto'].includes(key)) {
  361. if (value instanceof Buffer) {
  362. values[key] = value.toString();
  363. } else {
  364. values[key] = value;
  365. }
  366. }
  367. });
  368. return values;
  369. }
  370. searchAllPaged(BaseDN, options, page) {
  371. this.bindIfNecessary();
  372. const processPage = ({ entries, title, end, next }) => {
  373. log_info(title);
  374. // Force LDAP idle to wait the record processing
  375. this.client._updateIdle(true);
  376. page(null, entries, {
  377. end, next: () => {
  378. // Reset idle timer
  379. this.client._updateIdle();
  380. next && next();
  381. }
  382. });
  383. };
  384. this.client.search(BaseDN, options, (error, res) => {
  385. if (error) {
  386. log_error(error);
  387. page(error);
  388. return;
  389. }
  390. res.on('error', (error) => {
  391. log_error(error);
  392. page(error);
  393. return;
  394. });
  395. let entries = [];
  396. const internalPageSize = options.paged && options.paged.pageSize > 0 ? options.paged.pageSize * 2 : 500;
  397. res.on('searchEntry', (entry) => {
  398. entries.push(this.extractLdapEntryData(entry));
  399. if (entries.length >= internalPageSize) {
  400. processPage({
  401. entries,
  402. title: 'Internal Page',
  403. end : false,
  404. });
  405. entries = [];
  406. }
  407. });
  408. res.on('page', (result, next) => {
  409. if (!next) {
  410. this.client._updateIdle(true);
  411. processPage({
  412. entries,
  413. title: 'Final Page',
  414. end : true,
  415. });
  416. } else if (entries.length) {
  417. log_info('Page');
  418. processPage({
  419. entries,
  420. title: 'Page',
  421. end : false,
  422. next,
  423. });
  424. entries = [];
  425. }
  426. });
  427. res.on('end', () => {
  428. if (entries.length) {
  429. processPage({
  430. entries,
  431. title: 'Final Page',
  432. end : true,
  433. });
  434. entries = [];
  435. }
  436. });
  437. });
  438. }
  439. searchAllAsync(BaseDN, options, callback) {
  440. this.bindIfNecessary();
  441. this.client.search(BaseDN, options, (error, res) => {
  442. if (error) {
  443. log_error(error);
  444. callback(error);
  445. return;
  446. }
  447. res.on('error', (error) => {
  448. log_error(error);
  449. callback(error);
  450. return;
  451. });
  452. const entries = [];
  453. res.on('searchEntry', (entry) => {
  454. entries.push(this.extractLdapEntryData(entry));
  455. });
  456. res.on('end', () => {
  457. log_info('Search result count', entries.length);
  458. callback(null, entries);
  459. });
  460. });
  461. }
  462. authSync(dn, password) {
  463. log_info('Authenticating', dn);
  464. try {
  465. if (password === '') {
  466. throw new Error('Password is not provided');
  467. }
  468. this.bindSync(dn, password);
  469. log_info('Authenticated', dn);
  470. return true;
  471. } catch (error) {
  472. log_info('Not authenticated', dn);
  473. log_debug('error', error);
  474. return false;
  475. }
  476. }
  477. disconnect() {
  478. this.connected = false;
  479. this.domainBinded = false;
  480. log_info('Disconecting');
  481. this.client.unbind();
  482. }
  483. }