123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- 'use strict'
- const LdapMessage = require('../ldap-message')
- const { operations, search } = require('@ldapjs/protocol')
- const { DN } = require('@ldapjs/dn')
- const filter = require('@ldapjs/filter')
- const { BerReader, BerTypes } = require('@ldapjs/asn1')
- const warning = require('../deprecations')
- const recognizedScopes = new Map([
- ['base', [search.SCOPE_BASE_OBJECT, 'base']],
- ['single', [search.SCOPE_ONE_LEVEL, 'single', 'one']],
- ['subtree', [search.SCOPE_SUBTREE, 'subtree', 'sub']]
- ])
- const scopeAliasToScope = alias => {
- alias = typeof alias === 'string' ? alias.toLowerCase() : alias
- if (recognizedScopes.has(alias)) {
- return recognizedScopes.get(alias)[0]
- }
- for (const value of recognizedScopes.values()) {
- if (value.includes(alias)) {
- return value[0]
- }
- }
- return undefined
- }
- const isValidAttributeString = str => {
- // special filter strings
- if (['*', '1.1', '+'].includes(str) === true) {
- return true
- }
- // "@<object_clas>"
- if (/^@[a-zA-Z][\w\d.-]*$/.test(str) === true) {
- return true
- }
- // ascii attribute names per RFC 4512 §2.5
- if (/^[a-zA-Z][\w\d.;-]*$/.test(str) === true) {
- return true
- }
- // Matches the non-standard `range=<low>-<high>` ActiveDirectory
- // extension as described in §3.1.1.3.1.3.3 (revision 57.0) of
- // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/d2435927-0999-4c62-8c6d-13ba31a52e1a.
- if (/^[a-zA-Z][\w\d.-]*(;[\w\d.-]+)*;range=\d+-(\d+|\*)(;[\w\d.-]+)*$/.test(str) === true) {
- return true
- }
- return false
- }
- /**
- * Implements the add request message as described in
- * https://www.rfc-editor.org/rfc/rfc4511.html#section-4.5.1.
- *
- * Various constants for searching and options can be used from the `search`
- * object in the `@ldapjs/protocol` package. The same constants are exported
- * here as static properties for convenience.
- */
- class SearchRequest extends LdapMessage {
- /**
- * Limit searches to the specified {@link baseObject}.
- *
- * @type {number}
- */
- static SCOPE_BASE = search.SCOPE_BASE_OBJECT
- /**
- * Limit searches to the immediate children of the specified
- * {@link baseObject}.
- *
- * @type {number}
- */
- static SCOPE_SINGLE = search.SCOPE_ONE_LEVEL
- /**
- * Limit searches to the {@link baseObject} and all descendents of that
- * object.
- *
- * @type {number}
- */
- static SCOPE_SUBTREE = search.SCOPE_SUBTREE
- /**
- * Do not perform any dereferencing of aliases at all.
- *
- * @type {number}
- */
- static DEREF_ALIASES_NEVER = search.NEVER_DEREF_ALIASES
- /**
- * Dereference aliases in subordinate searches of the {@link baseObject}.
- *
- * @type {number}
- */
- static DEREF_IN_SEARCHING = search.DEREF_IN_SEARCHING
- /**
- * Dereference aliases when finding the base object only.
- *
- * @type {number}
- */
- static DEREF_BASE_OBJECT = search.DEREF_BASE_OBJECT
- /**
- * Dereference aliases when finding the base object and when searching
- * subordinates.
- *
- * @type {number}
- */
- static DEREF_ALWAYS = search.DEREF_ALWAYS
- #baseObject
- #scope
- #derefAliases
- #sizeLimit
- #timeLimit
- #typesOnly
- #filter
- #attributes = []
- /**
- * @typedef {LdapMessageOptions} SearchRequestOptions
- * @property {string | import('@ldapjs/dn').DN} baseObject The path to the
- * LDAP object that will serve as the basis of the search.
- * @property {number | string} scope The type of search to be performed.
- * May be one of {@link SCOPE_BASE}, {@link SCOPE_SINGLE},
- * {@link SCOPE_SUBTREE}, `'base'`, `'single'` (`'one'`), or `'subtree'`
- * (`'sub'`).
- * @property {number} derefAliases Indicates if aliases should be dereferenced
- * during searches. May be one of {@link DEREF_ALIASES_NEVER},
- * {@link DEREF_BASE_OBJECT}, {@link DEREF_IN_SEARCHING}, or
- * {@link DEREF_ALWAYS}.
- * @property {number} sizeLimit The number of search results the server should
- * limit the result set to. `0` indicates no desired limit.
- * @property {number} timeLimit The number of seconds the server should work
- * before aborting the search request. `0` indicates no desired limit.
- * @property {boolean} typesOnly Indicates if only attribute names should
- * be returned (`true`), or both names and values should be returned (`false`).
- * @property {string | import('@ldapjs/filter').FilterString} filter The
- * filter to apply when searching.
- * @property {string[]} attributes A set of attribute filtering strings
- * to apply. See the docs for the {@link attributes} setter.
- */
- /**
- * @param {SearchRequestOptions} options
- */
- constructor (options = {}) {
- options.protocolOp = operations.LDAP_REQ_SEARCH
- super(options)
- this.baseObject = options.baseObject ?? ''
- this.scope = options.scope ?? search.SCOPE_BASE_OBJECT
- this.derefAliases = options.derefAliases ?? search.NEVER_DEREF_ALIASES
- this.sizeLimit = options.sizeLimit ?? 0
- this.timeLimit = options.timeLimit ?? 0
- this.typesOnly = options.typesOnly ?? false
- this.filter = options.filter ?? new filter.PresenceFilter({ attribute: 'objectclass' })
- this.attributes = options.attributes ?? []
- }
- /**
- * Alias of {@link baseObject}.
- *
- * @type {import('@ldapjs/dn').DN}
- */
- get _dn () {
- return this.#baseObject
- }
- /**
- * The name of the request type.
- *
- * @type {string}
- */
- get type () {
- return 'SearchRequest'
- }
- /**
- * The list of attributes to match against.
- *
- * @returns {string[]}
- */
- get attributes () {
- return this.#attributes
- }
- /**
- * Set the list of attributes to match against. Overwrites any existing
- * attributes. The list is a set of spec defined strings. They are not
- * instances of `@ldapjs/attribute`.
- *
- * See:
- * + https://www.rfc-editor.org/rfc/rfc4511.html#section-4.5.1.8
- * + https://www.rfc-editor.org/rfc/rfc3673.html
- * + https://www.rfc-editor.org/rfc/rfc4529.html
- *
- * @param {string)[]} attrs
- */
- set attributes (attrs) {
- if (Array.isArray(attrs) === false) {
- throw Error('attributes must be an array of attribute strings')
- }
- const newAttrs = []
- for (const attr of attrs) {
- if (typeof attr === 'string' && isValidAttributeString(attr) === true) {
- newAttrs.push(attr)
- } else if (typeof attr === 'string' && attr === '') {
- // TODO: emit warning about spec violation via log and/or telemetry
- warning.emit('LDAP_ATTRIBUTE_SPEC_ERR_001')
- } else {
- throw Error('attribute must be a valid string')
- }
- }
- this.#attributes = newAttrs
- }
- /**
- * The base LDAP object that the search will start from.
- *
- * @returns {import('@ldapjs/dn').DN}
- */
- get baseObject () {
- return this.#baseObject
- }
- /**
- * Define the base LDAP object to start searches from.
- *
- * @param {string | import('@ldapjs/dn').DN} obj
- */
- set baseObject (obj) {
- if (typeof obj === 'string') {
- this.#baseObject = DN.fromString(obj)
- } else if (Object.prototype.toString.call(obj) === '[object LdapDn]') {
- this.#baseObject = obj
- } else {
- throw Error('baseObject must be a DN string or DN instance')
- }
- }
- /**
- * The alias dereferencing method that will be provided to the server.
- * May be one of {@link DEREF_ALIASES_NEVER}, {@link DEREF_IN_SEARCHING},
- * {@link DEREF_BASE_OBJECT},or {@link DEREF_ALWAYS}.
- *
- * @returns {number}
- */
- get derefAliases () {
- return this.#derefAliases
- }
- /**
- * Define the dereferencing method that will be provided to the server.
- * May be one of {@link DEREF_ALIASES_NEVER}, {@link DEREF_IN_SEARCHING},
- * {@link DEREF_BASE_OBJECT},or {@link DEREF_ALWAYS}.
- *
- * @param {number} value
- */
- set derefAliases (value) {
- if (Number.isInteger(value) === false) {
- throw Error('derefAliases must be set to an integer')
- }
- this.#derefAliases = value
- }
- /**
- * The filter that will be used in the search.
- *
- * @returns {import('@ldapjs/filter').FilterString}
- */
- get filter () {
- return this.#filter
- }
- /**
- * Define the filter to use in the search.
- *
- * @param {string | import('@ldapjs/filter').FilterString} value
- */
- set filter (value) {
- if (
- typeof value !== 'string' &&
- Object.prototype.toString.call(value) !== '[object FilterString]'
- ) {
- throw Error('filter must be a string or a FilterString instance')
- }
- if (typeof value === 'string') {
- this.#filter = filter.parseString(value)
- } else {
- this.#filter = value
- }
- }
- /**
- * The current search scope value. Can be matched against the exported
- * scope statics.
- *
- * @returns {number}
- *
- * @throws When the scope is set to an unrecognized scope constant.
- */
- get scope () {
- return this.#scope
- }
- /**
- * Define the scope of the search.
- *
- * @param {number|string} value Accepts one of {@link SCOPE_BASE},
- * {@link SCOPE_SINGLE}, or {@link SCOPE_SUBTREE}. Or, as a string, one of
- * "base", "single", "one", "subtree", or "sub".
- *
- * @throws When the provided scope does not resolve to a recognized scope.
- */
- set scope (value) {
- const resolvedScope = scopeAliasToScope(value)
- if (resolvedScope === undefined) {
- throw Error(value + ' is an invalid search scope')
- }
- this.#scope = resolvedScope
- }
- /**
- * The current search scope value as a string name.
- *
- * @returns {string} One of 'base', 'single', or 'subtree'.
- *
- * @throws When the scope is set to an unrecognized scope constant.
- */
- get scopeName () {
- switch (this.#scope) {
- case search.SCOPE_BASE_OBJECT:
- return 'base'
- case search.SCOPE_ONE_LEVEL:
- return 'single'
- case search.SCOPE_SUBTREE:
- return 'subtree'
- }
- }
- /**
- * The number of entries to limit search results to.
- *
- * @returns {number}
- */
- get sizeLimit () {
- return this.#sizeLimit
- }
- /**
- * Define the number of entries to limit search results to.
- *
- * @param {number} value `0` indicates no restriction.
- */
- set sizeLimit (value) {
- if (Number.isInteger(value) === false) {
- throw Error('sizeLimit must be an integer')
- }
- this.#sizeLimit = value
- }
- /**
- * The number of seconds that the search should be limited to for execution.
- * A value of `0` indicates a willingness to wait as long as the server is
- * willing to work.
- *
- * @returns {number}
- */
- get timeLimit () {
- return this.#timeLimit
- }
- /**
- * Define the number of seconds to wait for a search result before the server
- * should abort the search.
- *
- * @param {number} value `0` indicates no time limit restriction.
- */
- set timeLimit (value) {
- if (Number.isInteger(value) === false) {
- throw Error('timeLimit must be an integer')
- }
- this.#timeLimit = value
- }
- /**
- * Indicates if only attribute names (`true`) should be returned, or if both
- * attribute names and attribute values (`false`) should be returned.
- *
- * @returns {boolean}
- */
- get typesOnly () {
- return this.#typesOnly
- }
- /**
- * Define if the search results should include only the attributes names
- * or attribute names and attribute values.
- *
- * @param {boolean} value `false` for both names and values, `true` for
- * names only.
- */
- set typesOnly (value) {
- if (typeof value !== 'boolean') {
- throw Error('typesOnly must be set to a boolean value')
- }
- this.#typesOnly = value
- }
- /**
- * Internal use only.
- *
- * @param {import('@ldapjs/asn1').BerWriter} ber
- *
- * @returns {import('@ldapjs/asn1').BerWriter}
- */
- _toBer (ber) {
- ber.startSequence(operations.LDAP_REQ_SEARCH)
- ber.writeString(this.#baseObject.toString())
- ber.writeEnumeration(this.#scope)
- ber.writeEnumeration(this.#derefAliases)
- ber.writeInt(this.#sizeLimit)
- ber.writeInt(this.#timeLimit)
- ber.writeBoolean(this.#typesOnly)
- ber.appendBuffer(this.#filter.toBer().buffer)
- ber.startSequence(BerTypes.Sequence | BerTypes.Constructor)
- for (const attr of this.#attributes) {
- ber.writeString(attr)
- }
- ber.endSequence()
- ber.endSequence()
- return ber
- }
- /**
- * Internal use only.
- *
- * @param {object}
- *
- * @returns {object}
- */
- _pojo (obj = {}) {
- obj.baseObject = this.baseObject.toString()
- obj.scope = this.scopeName
- obj.derefAliases = this.derefAliases
- obj.sizeLimit = this.sizeLimit
- obj.timeLimit = this.timeLimit
- obj.typesOnly = this.typesOnly
- obj.filter = this.filter.toString()
- obj.attributes = []
- for (const attr of this.#attributes) {
- obj.attributes.push(attr)
- }
- return obj
- }
- /**
- * Implements the standardized `parseToPojo` method.
- *
- * @see LdapMessage.parseToPojo
- *
- * @param {import('@ldapjs/asn1').BerReader} ber
- *
- * @returns {object}
- */
- static parseToPojo (ber) {
- const protocolOp = ber.readSequence()
- if (protocolOp !== operations.LDAP_REQ_SEARCH) {
- const op = protocolOp.toString(16).padStart(2, '0')
- throw Error(`found wrong protocol operation: 0x${op}`)
- }
- const baseObject = ber.readString()
- const scope = ber.readEnumeration()
- const derefAliases = ber.readEnumeration()
- const sizeLimit = ber.readInt()
- const timeLimit = ber.readInt()
- const typesOnly = ber.readBoolean()
- const filterTag = ber.peek()
- const filterBuffer = ber.readRawBuffer(filterTag)
- const parsedFilter = filter.parseBer(new BerReader(filterBuffer))
- const attributes = []
- // Advance to the first attribute sequence in the set
- // of attribute sequences.
- ber.readSequence()
- const endOfAttributesPos = ber.offset + ber.length
- while (ber.offset < endOfAttributesPos) {
- const attribute = ber.readString()
- attributes.push(attribute)
- }
- return {
- protocolOp,
- baseObject,
- scope,
- derefAliases,
- sizeLimit,
- timeLimit,
- typesOnly,
- filter: parsedFilter.toString(),
- attributes
- }
- }
- }
- module.exports = SearchRequest
|