ldap.js 16 KB

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