search-request.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. 'use strict'
  2. const LdapMessage = require('../ldap-message')
  3. const { operations, search } = require('@ldapjs/protocol')
  4. const { DN } = require('@ldapjs/dn')
  5. const filter = require('@ldapjs/filter')
  6. const { BerReader, BerTypes } = require('@ldapjs/asn1')
  7. const warning = require('../deprecations')
  8. const recognizedScopes = new Map([
  9. ['base', [search.SCOPE_BASE_OBJECT, 'base']],
  10. ['single', [search.SCOPE_ONE_LEVEL, 'single', 'one']],
  11. ['subtree', [search.SCOPE_SUBTREE, 'subtree', 'sub']]
  12. ])
  13. const scopeAliasToScope = alias => {
  14. alias = typeof alias === 'string' ? alias.toLowerCase() : alias
  15. if (recognizedScopes.has(alias)) {
  16. return recognizedScopes.get(alias)[0]
  17. }
  18. for (const value of recognizedScopes.values()) {
  19. if (value.includes(alias)) {
  20. return value[0]
  21. }
  22. }
  23. return undefined
  24. }
  25. const isValidAttributeString = str => {
  26. // special filter strings
  27. if (['*', '1.1', '+'].includes(str) === true) {
  28. return true
  29. }
  30. // "@<object_clas>"
  31. if (/^@[a-zA-Z][\w\d.-]*$/.test(str) === true) {
  32. return true
  33. }
  34. // ascii attribute names per RFC 4512 §2.5
  35. if (/^[a-zA-Z][\w\d.;-]*$/.test(str) === true) {
  36. return true
  37. }
  38. // Matches the non-standard `range=<low>-<high>` ActiveDirectory
  39. // extension as described in §3.1.1.3.1.3.3 (revision 57.0) of
  40. // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/d2435927-0999-4c62-8c6d-13ba31a52e1a.
  41. if (/^[a-zA-Z][\w\d.-]*(;[\w\d.-]+)*;range=\d+-(\d+|\*)(;[\w\d.-]+)*$/.test(str) === true) {
  42. return true
  43. }
  44. return false
  45. }
  46. /**
  47. * Implements the add request message as described in
  48. * https://www.rfc-editor.org/rfc/rfc4511.html#section-4.5.1.
  49. *
  50. * Various constants for searching and options can be used from the `search`
  51. * object in the `@ldapjs/protocol` package. The same constants are exported
  52. * here as static properties for convenience.
  53. */
  54. class SearchRequest extends LdapMessage {
  55. /**
  56. * Limit searches to the specified {@link baseObject}.
  57. *
  58. * @type {number}
  59. */
  60. static SCOPE_BASE = search.SCOPE_BASE_OBJECT
  61. /**
  62. * Limit searches to the immediate children of the specified
  63. * {@link baseObject}.
  64. *
  65. * @type {number}
  66. */
  67. static SCOPE_SINGLE = search.SCOPE_ONE_LEVEL
  68. /**
  69. * Limit searches to the {@link baseObject} and all descendents of that
  70. * object.
  71. *
  72. * @type {number}
  73. */
  74. static SCOPE_SUBTREE = search.SCOPE_SUBTREE
  75. /**
  76. * Do not perform any dereferencing of aliases at all.
  77. *
  78. * @type {number}
  79. */
  80. static DEREF_ALIASES_NEVER = search.NEVER_DEREF_ALIASES
  81. /**
  82. * Dereference aliases in subordinate searches of the {@link baseObject}.
  83. *
  84. * @type {number}
  85. */
  86. static DEREF_IN_SEARCHING = search.DEREF_IN_SEARCHING
  87. /**
  88. * Dereference aliases when finding the base object only.
  89. *
  90. * @type {number}
  91. */
  92. static DEREF_BASE_OBJECT = search.DEREF_BASE_OBJECT
  93. /**
  94. * Dereference aliases when finding the base object and when searching
  95. * subordinates.
  96. *
  97. * @type {number}
  98. */
  99. static DEREF_ALWAYS = search.DEREF_ALWAYS
  100. #baseObject
  101. #scope
  102. #derefAliases
  103. #sizeLimit
  104. #timeLimit
  105. #typesOnly
  106. #filter
  107. #attributes = []
  108. /**
  109. * @typedef {LdapMessageOptions} SearchRequestOptions
  110. * @property {string | import('@ldapjs/dn').DN} baseObject The path to the
  111. * LDAP object that will serve as the basis of the search.
  112. * @property {number | string} scope The type of search to be performed.
  113. * May be one of {@link SCOPE_BASE}, {@link SCOPE_SINGLE},
  114. * {@link SCOPE_SUBTREE}, `'base'`, `'single'` (`'one'`), or `'subtree'`
  115. * (`'sub'`).
  116. * @property {number} derefAliases Indicates if aliases should be dereferenced
  117. * during searches. May be one of {@link DEREF_ALIASES_NEVER},
  118. * {@link DEREF_BASE_OBJECT}, {@link DEREF_IN_SEARCHING}, or
  119. * {@link DEREF_ALWAYS}.
  120. * @property {number} sizeLimit The number of search results the server should
  121. * limit the result set to. `0` indicates no desired limit.
  122. * @property {number} timeLimit The number of seconds the server should work
  123. * before aborting the search request. `0` indicates no desired limit.
  124. * @property {boolean} typesOnly Indicates if only attribute names should
  125. * be returned (`true`), or both names and values should be returned (`false`).
  126. * @property {string | import('@ldapjs/filter').FilterString} filter The
  127. * filter to apply when searching.
  128. * @property {string[]} attributes A set of attribute filtering strings
  129. * to apply. See the docs for the {@link attributes} setter.
  130. */
  131. /**
  132. * @param {SearchRequestOptions} options
  133. */
  134. constructor (options = {}) {
  135. options.protocolOp = operations.LDAP_REQ_SEARCH
  136. super(options)
  137. this.baseObject = options.baseObject ?? ''
  138. this.scope = options.scope ?? search.SCOPE_BASE_OBJECT
  139. this.derefAliases = options.derefAliases ?? search.NEVER_DEREF_ALIASES
  140. this.sizeLimit = options.sizeLimit ?? 0
  141. this.timeLimit = options.timeLimit ?? 0
  142. this.typesOnly = options.typesOnly ?? false
  143. this.filter = options.filter ?? new filter.PresenceFilter({ attribute: 'objectclass' })
  144. this.attributes = options.attributes ?? []
  145. }
  146. /**
  147. * Alias of {@link baseObject}.
  148. *
  149. * @type {import('@ldapjs/dn').DN}
  150. */
  151. get _dn () {
  152. return this.#baseObject
  153. }
  154. /**
  155. * The name of the request type.
  156. *
  157. * @type {string}
  158. */
  159. get type () {
  160. return 'SearchRequest'
  161. }
  162. /**
  163. * The list of attributes to match against.
  164. *
  165. * @returns {string[]}
  166. */
  167. get attributes () {
  168. return this.#attributes
  169. }
  170. /**
  171. * Set the list of attributes to match against. Overwrites any existing
  172. * attributes. The list is a set of spec defined strings. They are not
  173. * instances of `@ldapjs/attribute`.
  174. *
  175. * See:
  176. * + https://www.rfc-editor.org/rfc/rfc4511.html#section-4.5.1.8
  177. * + https://www.rfc-editor.org/rfc/rfc3673.html
  178. * + https://www.rfc-editor.org/rfc/rfc4529.html
  179. *
  180. * @param {string)[]} attrs
  181. */
  182. set attributes (attrs) {
  183. if (Array.isArray(attrs) === false) {
  184. throw Error('attributes must be an array of attribute strings')
  185. }
  186. const newAttrs = []
  187. for (const attr of attrs) {
  188. if (typeof attr === 'string' && isValidAttributeString(attr) === true) {
  189. newAttrs.push(attr)
  190. } else if (typeof attr === 'string' && attr === '') {
  191. // TODO: emit warning about spec violation via log and/or telemetry
  192. warning.emit('LDAP_ATTRIBUTE_SPEC_ERR_001')
  193. } else {
  194. throw Error('attribute must be a valid string')
  195. }
  196. }
  197. this.#attributes = newAttrs
  198. }
  199. /**
  200. * The base LDAP object that the search will start from.
  201. *
  202. * @returns {import('@ldapjs/dn').DN}
  203. */
  204. get baseObject () {
  205. return this.#baseObject
  206. }
  207. /**
  208. * Define the base LDAP object to start searches from.
  209. *
  210. * @param {string | import('@ldapjs/dn').DN} obj
  211. */
  212. set baseObject (obj) {
  213. if (typeof obj === 'string') {
  214. this.#baseObject = DN.fromString(obj)
  215. } else if (Object.prototype.toString.call(obj) === '[object LdapDn]') {
  216. this.#baseObject = obj
  217. } else {
  218. throw Error('baseObject must be a DN string or DN instance')
  219. }
  220. }
  221. /**
  222. * The alias dereferencing method that will be provided to the server.
  223. * May be one of {@link DEREF_ALIASES_NEVER}, {@link DEREF_IN_SEARCHING},
  224. * {@link DEREF_BASE_OBJECT},or {@link DEREF_ALWAYS}.
  225. *
  226. * @returns {number}
  227. */
  228. get derefAliases () {
  229. return this.#derefAliases
  230. }
  231. /**
  232. * Define the dereferencing method that will be provided to the server.
  233. * May be one of {@link DEREF_ALIASES_NEVER}, {@link DEREF_IN_SEARCHING},
  234. * {@link DEREF_BASE_OBJECT},or {@link DEREF_ALWAYS}.
  235. *
  236. * @param {number} value
  237. */
  238. set derefAliases (value) {
  239. if (Number.isInteger(value) === false) {
  240. throw Error('derefAliases must be set to an integer')
  241. }
  242. this.#derefAliases = value
  243. }
  244. /**
  245. * The filter that will be used in the search.
  246. *
  247. * @returns {import('@ldapjs/filter').FilterString}
  248. */
  249. get filter () {
  250. return this.#filter
  251. }
  252. /**
  253. * Define the filter to use in the search.
  254. *
  255. * @param {string | import('@ldapjs/filter').FilterString} value
  256. */
  257. set filter (value) {
  258. if (
  259. typeof value !== 'string' &&
  260. Object.prototype.toString.call(value) !== '[object FilterString]'
  261. ) {
  262. throw Error('filter must be a string or a FilterString instance')
  263. }
  264. if (typeof value === 'string') {
  265. this.#filter = filter.parseString(value)
  266. } else {
  267. this.#filter = value
  268. }
  269. }
  270. /**
  271. * The current search scope value. Can be matched against the exported
  272. * scope statics.
  273. *
  274. * @returns {number}
  275. *
  276. * @throws When the scope is set to an unrecognized scope constant.
  277. */
  278. get scope () {
  279. return this.#scope
  280. }
  281. /**
  282. * Define the scope of the search.
  283. *
  284. * @param {number|string} value Accepts one of {@link SCOPE_BASE},
  285. * {@link SCOPE_SINGLE}, or {@link SCOPE_SUBTREE}. Or, as a string, one of
  286. * "base", "single", "one", "subtree", or "sub".
  287. *
  288. * @throws When the provided scope does not resolve to a recognized scope.
  289. */
  290. set scope (value) {
  291. const resolvedScope = scopeAliasToScope(value)
  292. if (resolvedScope === undefined) {
  293. throw Error(value + ' is an invalid search scope')
  294. }
  295. this.#scope = resolvedScope
  296. }
  297. /**
  298. * The current search scope value as a string name.
  299. *
  300. * @returns {string} One of 'base', 'single', or 'subtree'.
  301. *
  302. * @throws When the scope is set to an unrecognized scope constant.
  303. */
  304. get scopeName () {
  305. switch (this.#scope) {
  306. case search.SCOPE_BASE_OBJECT:
  307. return 'base'
  308. case search.SCOPE_ONE_LEVEL:
  309. return 'single'
  310. case search.SCOPE_SUBTREE:
  311. return 'subtree'
  312. }
  313. }
  314. /**
  315. * The number of entries to limit search results to.
  316. *
  317. * @returns {number}
  318. */
  319. get sizeLimit () {
  320. return this.#sizeLimit
  321. }
  322. /**
  323. * Define the number of entries to limit search results to.
  324. *
  325. * @param {number} value `0` indicates no restriction.
  326. */
  327. set sizeLimit (value) {
  328. if (Number.isInteger(value) === false) {
  329. throw Error('sizeLimit must be an integer')
  330. }
  331. this.#sizeLimit = value
  332. }
  333. /**
  334. * The number of seconds that the search should be limited to for execution.
  335. * A value of `0` indicates a willingness to wait as long as the server is
  336. * willing to work.
  337. *
  338. * @returns {number}
  339. */
  340. get timeLimit () {
  341. return this.#timeLimit
  342. }
  343. /**
  344. * Define the number of seconds to wait for a search result before the server
  345. * should abort the search.
  346. *
  347. * @param {number} value `0` indicates no time limit restriction.
  348. */
  349. set timeLimit (value) {
  350. if (Number.isInteger(value) === false) {
  351. throw Error('timeLimit must be an integer')
  352. }
  353. this.#timeLimit = value
  354. }
  355. /**
  356. * Indicates if only attribute names (`true`) should be returned, or if both
  357. * attribute names and attribute values (`false`) should be returned.
  358. *
  359. * @returns {boolean}
  360. */
  361. get typesOnly () {
  362. return this.#typesOnly
  363. }
  364. /**
  365. * Define if the search results should include only the attributes names
  366. * or attribute names and attribute values.
  367. *
  368. * @param {boolean} value `false` for both names and values, `true` for
  369. * names only.
  370. */
  371. set typesOnly (value) {
  372. if (typeof value !== 'boolean') {
  373. throw Error('typesOnly must be set to a boolean value')
  374. }
  375. this.#typesOnly = value
  376. }
  377. /**
  378. * Internal use only.
  379. *
  380. * @param {import('@ldapjs/asn1').BerWriter} ber
  381. *
  382. * @returns {import('@ldapjs/asn1').BerWriter}
  383. */
  384. _toBer (ber) {
  385. ber.startSequence(operations.LDAP_REQ_SEARCH)
  386. ber.writeString(this.#baseObject.toString())
  387. ber.writeEnumeration(this.#scope)
  388. ber.writeEnumeration(this.#derefAliases)
  389. ber.writeInt(this.#sizeLimit)
  390. ber.writeInt(this.#timeLimit)
  391. ber.writeBoolean(this.#typesOnly)
  392. ber.appendBuffer(this.#filter.toBer().buffer)
  393. ber.startSequence(BerTypes.Sequence | BerTypes.Constructor)
  394. for (const attr of this.#attributes) {
  395. ber.writeString(attr)
  396. }
  397. ber.endSequence()
  398. ber.endSequence()
  399. return ber
  400. }
  401. /**
  402. * Internal use only.
  403. *
  404. * @param {object}
  405. *
  406. * @returns {object}
  407. */
  408. _pojo (obj = {}) {
  409. obj.baseObject = this.baseObject.toString()
  410. obj.scope = this.scopeName
  411. obj.derefAliases = this.derefAliases
  412. obj.sizeLimit = this.sizeLimit
  413. obj.timeLimit = this.timeLimit
  414. obj.typesOnly = this.typesOnly
  415. obj.filter = this.filter.toString()
  416. obj.attributes = []
  417. for (const attr of this.#attributes) {
  418. obj.attributes.push(attr)
  419. }
  420. return obj
  421. }
  422. /**
  423. * Implements the standardized `parseToPojo` method.
  424. *
  425. * @see LdapMessage.parseToPojo
  426. *
  427. * @param {import('@ldapjs/asn1').BerReader} ber
  428. *
  429. * @returns {object}
  430. */
  431. static parseToPojo (ber) {
  432. const protocolOp = ber.readSequence()
  433. if (protocolOp !== operations.LDAP_REQ_SEARCH) {
  434. const op = protocolOp.toString(16).padStart(2, '0')
  435. throw Error(`found wrong protocol operation: 0x${op}`)
  436. }
  437. const baseObject = ber.readString()
  438. const scope = ber.readEnumeration()
  439. const derefAliases = ber.readEnumeration()
  440. const sizeLimit = ber.readInt()
  441. const timeLimit = ber.readInt()
  442. const typesOnly = ber.readBoolean()
  443. const filterTag = ber.peek()
  444. const filterBuffer = ber.readRawBuffer(filterTag)
  445. const parsedFilter = filter.parseBer(new BerReader(filterBuffer))
  446. const attributes = []
  447. // Advance to the first attribute sequence in the set
  448. // of attribute sequences.
  449. ber.readSequence()
  450. const endOfAttributesPos = ber.offset + ber.length
  451. while (ber.offset < endOfAttributesPos) {
  452. const attribute = ber.readString()
  453. attributes.push(attribute)
  454. }
  455. return {
  456. protocolOp,
  457. baseObject,
  458. scope,
  459. derefAliases,
  460. sizeLimit,
  461. timeLimit,
  462. typesOnly,
  463. filter: parsedFilter.toString(),
  464. attributes
  465. }
  466. }
  467. }
  468. module.exports = SearchRequest