Files
ldap-to-oauth2/node_modules/@ldapjs/dn/lib/dn.js
2025-10-08 11:12:59 -04:00

337 lines
7.4 KiB
JavaScript

'use strict'
const warning = require('./deprecations')
const RDN = require('./rdn')
const parseString = require('./utils/parse-string')
/**
* Implements distinguished name strings as described in
* https://www.rfc-editor.org/rfc/rfc4514 as an object.
* This is the primary implementation for parsing and generating DN strings.
*
* @example
* const dn = new DN({rdns: [{cn: 'jdoe', givenName: 'John'}] })
* dn.toString() // 'cn=jdoe+givenName=John'
*/
class DN {
#rdns = []
/**
* @param {object} input
* @param {RDN[]} [input.rdns=[]] A set of RDN objects that define the DN.
* Remember that DNs are in reverse domain order. Thus, the target RDN must
* be the first item and the top-level RDN the last item.
*
* @throws When the provided `rdns` array is invalid.
*/
constructor ({ rdns = [] } = {}) {
if (Array.isArray(rdns) === false) {
throw Error('rdns must be an array')
}
const hasNonRdn = rdns.some(
r => RDN.isRdn(r) === false
)
if (hasNonRdn === true) {
throw Error('rdns must be an array of RDN objects')
}
Array.prototype.push.apply(
this.#rdns,
rdns.map(r => {
if (Object.prototype.toString.call(r) === '[object LdapRdn]') {
return r
}
return new RDN(r)
})
)
}
get [Symbol.toStringTag] () {
return 'LdapDn'
}
/**
* The number of RDNs that make up the DN.
*
* @returns {number}
*/
get length () {
return this.#rdns.length
}
/**
* Determine if the current instance is the child of another DN instance or
* DN string.
*
* @param {DN|string} dn
*
* @returns {boolean}
*/
childOf (dn) {
if (typeof dn === 'string') {
const parsedDn = DN.fromString(dn)
return parsedDn.parentOf(this)
}
return dn.parentOf(this)
}
/**
* Get a new instance that is a replica of the current instance.
*
* @returns {DN}
*/
clone () {
return new DN({ rdns: this.#rdns })
}
/**
* Determine if the instance is equal to another DN.
*
* @param {DN|string} dn
*
* @returns {boolean}
*/
equals (dn) {
if (typeof dn === 'string') {
const parsedDn = DN.fromString(dn)
return parsedDn.equals(this)
}
if (this.length !== dn.length) return false
for (let i = 0; i < this.length; i += 1) {
if (this.#rdns[i].equals(dn.rdnAt(i)) === false) {
return false
}
}
return true
}
/**
* @deprecated Use .toString() instead.
*
* @returns {string}
*/
format () {
warning.emit('LDAP_DN_DEP_002')
return this.toString()
}
/**
* Determine if the instance has any RDNs defined.
*
* @returns {boolean}
*/
isEmpty () {
return this.#rdns.length === 0
}
/**
* Get a DN representation of the parent of this instance.
*
* @returns {DN|undefined}
*/
parent () {
if (this.length === 0) return undefined
const save = this.shift()
const dn = new DN({ rdns: this.#rdns })
this.unshift(save)
return dn
}
/**
* Determine if the instance is the parent of a given DN instance or DN
* string.
*
* @param {DN|string} dn
*
* @returns {boolean}
*/
parentOf (dn) {
if (typeof dn === 'string') {
const parsedDn = DN.fromString(dn)
return this.parentOf(parsedDn)
}
if (this.length >= dn.length) {
// If we have more RDNs in our set then we must be a descendent at least.
return false
}
const numberOfElementsDifferent = dn.length - this.length
for (let i = this.length - 1; i >= 0; i -= 1) {
const myRdn = this.#rdns[i]
const theirRdn = dn.rdnAt(i + numberOfElementsDifferent)
if (myRdn.equals(theirRdn) === false) {
return false
}
}
return true
}
/**
* Removes the last RDN from the list and returns it. This alters the
* instance.
*
* @returns {RDN}
*/
pop () {
return this.#rdns.pop()
}
/**
* Adds a new RDN to the end of the list (i.e. the "top most" RDN in the
* directory path) and returns the new RDN count.
*
* @param {RDN} rdn
*
* @returns {number}
*
* @throws When the input is not a valid RDN.
*/
push (rdn) {
if (Object.prototype.toString.call(rdn) !== '[object LdapRdn]') {
throw Error('rdn must be a RDN instance')
}
return this.#rdns.push(rdn)
}
/**
* Return the RDN at the provided index in the list of RDNs associated with
* this instance.
*
* @param {number} index
*
* @returns {RDN}
*/
rdnAt (index) {
return this.#rdns[index]
}
/**
* Reverse the RDNs list such that the first element becomes the last, and
* the last becomes the first. This is useful when the RDNs were added in the
* opposite order of how they should have been.
*
* This is an in-place operation. The instance is changed as a result of
* this operation.
*
* @returns {DN} The current instance (i.e. this method is chainable).
*/
reverse () {
this.#rdns.reverse()
return this
}
/**
* @deprecated Formatting options are not supported.
*/
setFormat () {
warning.emit('LDAP_DN_DEP_004')
}
/**
* Remove the first RDN from the set of RDNs and return it.
*
* @returns {RDN}
*/
shift () {
return this.#rdns.shift()
}
/**
* Render the DN instance as a spec compliant DN string.
*
* @returns {string}
*/
toString () {
let result = ''
for (const rdn of this.#rdns) {
const rdnString = rdn.toString()
result += `,${rdnString}`
}
return result.substring(1)
}
/**
* Adds an RDN to the beginning of the RDN list and returns the new length.
*
* @param {RDN} rdn
*
* @returns {number}
*
* @throws When the RDN is invalid.
*/
unshift (rdn) {
if (Object.prototype.toString.call(rdn) !== '[object LdapRdn]') {
throw Error('rdn must be a RDN instance')
}
return this.#rdns.unshift(rdn)
}
/**
* Determine if an object is an instance of {@link DN} or is at least
* a DN-like object. It is safer to perform a `toString` check.
*
* @example Valid Instance
* const dn = new DN()
* DN.isDn(dn) // true
*
* @example DN-like Instance
* let dn = { rdns: [{name: 'cn', value: 'foo'}] }
* DN.isDn(dn) // true
*
* dn = { rdns: [{cn: 'foo', sn: 'bar'}, {dc: 'example'}, {dc: 'com'}]}
* DN.isDn(dn) // true
*
* @example Preferred Check
* let dn = new DN()
* Object.prototype.toString.call(dn) === '[object LdapDn]' // true
*
* dn = { rdns: [{name: 'cn', value: 'foo'}] }
* Object.prototype.toString.call(dn) === '[object LdapDn]' // false
*
* @param {object} dn
* @returns {boolean}
*/
static isDn (dn) {
if (Object.prototype.toString.call(dn) === '[object LdapDn]') {
return true
}
if (
Object.prototype.toString.call(dn) !== '[object Object]' ||
Array.isArray(dn.rdns) === false
) {
return false
}
if (dn.rdns.some(dn => RDN.isRdn(dn) === false) === true) {
return false
}
return true
}
/**
* Parses a DN string and returns a new {@link DN} instance.
*
* @example
* const dn = DN.fromString('cn=foo,dc=example,dc=com')
* DN.isDn(dn) // true
*
* @param {string} dnString
*
* @returns {DN}
*
* @throws If the string is not parseable.
*/
static fromString (dnString) {
const rdns = parseString(dnString)
return new DN({ rdns })
}
}
module.exports = DN