ldap.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import assert from 'assert';
  2. import ldap from 'ldapjs';
  3. import loggerFactory from '~/utils/logger';
  4. import { configManager } from './config-manager';
  5. const logger = loggerFactory('growi:service:ldap-service');
  6. // @types/ldapjs is outdated, and SearchResultEntry does not exist.
  7. // Declare it manually in the meantime.
  8. export interface SearchResultEntry extends Omit<ldap.SearchEntry, 'attributes'> {
  9. attributes: {
  10. type: string,
  11. values: string | string[]
  12. }[]
  13. }
  14. /**
  15. * Service to connect to LDAP server.
  16. * User auth using LDAP is done with PassportService, not here.
  17. */
  18. class LdapService {
  19. username?: string; // Necessary when bind type is user bind
  20. password?: string; // Necessary when bind type is user bind
  21. constructor(username?: string, password?: string) {
  22. this.username = username;
  23. this.password = password;
  24. }
  25. /**
  26. * Execute search on LDAP server and return result
  27. * @param {string} filter Search filter
  28. * @param {string} base Base DN to execute search on
  29. * @returns {SearchEntry[]} Search result. Default scope is set to 'sub'.
  30. */
  31. search(filter?: string, base?: string, scope: 'sub' | 'base' | 'one' = 'sub'): Promise<SearchResultEntry[]> {
  32. const isLdapEnabled = configManager?.getConfig('crowi', 'security:passport-ldap:isEnabled');
  33. if (!isLdapEnabled) {
  34. const notEnabledMessage = 'LDAP is not enabled';
  35. logger.error(notEnabledMessage);
  36. throw new Error(notEnabledMessage);
  37. }
  38. // get configurations
  39. const isUserBind = configManager?.getConfig('crowi', 'security:passport-ldap:isUserBind');
  40. const serverUrl = configManager?.getConfig('crowi', 'security:passport-ldap:serverUrl');
  41. const bindDN = configManager?.getConfig('crowi', 'security:passport-ldap:bindDN');
  42. const bindCredentials = configManager?.getConfig('crowi', 'security:passport-ldap:bindDNPassword');
  43. // parse serverUrl
  44. // see: https://regex101.com/r/0tuYBB/1
  45. const match = serverUrl.match(/(ldaps?:\/\/[^/]+)\/(.*)?/);
  46. if (match == null || match.length < 1) {
  47. const urlInvalidMessage = 'serverUrl is invalid';
  48. logger.error(urlInvalidMessage);
  49. throw new Error(urlInvalidMessage);
  50. }
  51. const url = match[1];
  52. const searchBase = match[2] || '';
  53. // user bind
  54. const fixedBindDN = (isUserBind)
  55. ? bindDN.replace(/{{username}}/, this.username)
  56. : bindDN;
  57. const fixedBindCredentials = (isUserBind) ? this.password : bindCredentials;
  58. const client = ldap.createClient({
  59. url,
  60. });
  61. client.bind(fixedBindDN, fixedBindCredentials, (err) => {
  62. assert.ifError(err);
  63. });
  64. const searchResults: SearchResultEntry[] = [];
  65. return new Promise((resolve, reject) => {
  66. client.search(base || searchBase, { scope, filter }, (err, res) => {
  67. if (err != null) {
  68. reject(err);
  69. }
  70. // @types/ldapjs is outdated, and pojo property (type SearchResultEntry) does not exist.
  71. // Typecast to manually declared SearchResultEntry in the meantime.
  72. res.on('searchEntry', (entry: any) => {
  73. const pojo = entry?.pojo as SearchResultEntry;
  74. searchResults.push(pojo);
  75. });
  76. res.on('error', (err) => {
  77. reject(err);
  78. });
  79. res.on('end', (result) => {
  80. if (result?.status === 0) {
  81. resolve(searchResults);
  82. }
  83. else {
  84. reject(new Error(`LDAP search failed: status code ${result?.status}`));
  85. }
  86. });
  87. });
  88. });
  89. }
  90. searchGroupDir(): Promise<SearchResultEntry[]> {
  91. const groupSearchBase = configManager?.getConfig('crowi', 'external-user-group:ldap:groupSearchBase')
  92. || configManager?.getConfig('crowi', 'security:passport-ldap:groupSearchBase');
  93. return this.search(undefined, groupSearchBase);
  94. }
  95. }
  96. export default LdapService;