337 lines
7.4 KiB
JavaScript
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
|