dn.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. 'use strict'
  2. const warning = require('./deprecations')
  3. const RDN = require('./rdn')
  4. const parseString = require('./utils/parse-string')
  5. /**
  6. * Implements distinguished name strings as described in
  7. * https://www.rfc-editor.org/rfc/rfc4514 as an object.
  8. * This is the primary implementation for parsing and generating DN strings.
  9. *
  10. * @example
  11. * const dn = new DN({rdns: [{cn: 'jdoe', givenName: 'John'}] })
  12. * dn.toString() // 'cn=jdoe+givenName=John'
  13. */
  14. class DN {
  15. #rdns = []
  16. /**
  17. * @param {object} input
  18. * @param {RDN[]} [input.rdns=[]] A set of RDN objects that define the DN.
  19. * Remember that DNs are in reverse domain order. Thus, the target RDN must
  20. * be the first item and the top-level RDN the last item.
  21. *
  22. * @throws When the provided `rdns` array is invalid.
  23. */
  24. constructor ({ rdns = [] } = {}) {
  25. if (Array.isArray(rdns) === false) {
  26. throw Error('rdns must be an array')
  27. }
  28. const hasNonRdn = rdns.some(
  29. r => RDN.isRdn(r) === false
  30. )
  31. if (hasNonRdn === true) {
  32. throw Error('rdns must be an array of RDN objects')
  33. }
  34. Array.prototype.push.apply(
  35. this.#rdns,
  36. rdns.map(r => {
  37. if (Object.prototype.toString.call(r) === '[object LdapRdn]') {
  38. return r
  39. }
  40. return new RDN(r)
  41. })
  42. )
  43. }
  44. get [Symbol.toStringTag] () {
  45. return 'LdapDn'
  46. }
  47. /**
  48. * The number of RDNs that make up the DN.
  49. *
  50. * @returns {number}
  51. */
  52. get length () {
  53. return this.#rdns.length
  54. }
  55. /**
  56. * Determine if the current instance is the child of another DN instance or
  57. * DN string.
  58. *
  59. * @param {DN|string} dn
  60. *
  61. * @returns {boolean}
  62. */
  63. childOf (dn) {
  64. if (typeof dn === 'string') {
  65. const parsedDn = DN.fromString(dn)
  66. return parsedDn.parentOf(this)
  67. }
  68. return dn.parentOf(this)
  69. }
  70. /**
  71. * Get a new instance that is a replica of the current instance.
  72. *
  73. * @returns {DN}
  74. */
  75. clone () {
  76. return new DN({ rdns: this.#rdns })
  77. }
  78. /**
  79. * Determine if the instance is equal to another DN.
  80. *
  81. * @param {DN|string} dn
  82. *
  83. * @returns {boolean}
  84. */
  85. equals (dn) {
  86. if (typeof dn === 'string') {
  87. const parsedDn = DN.fromString(dn)
  88. return parsedDn.equals(this)
  89. }
  90. if (this.length !== dn.length) return false
  91. for (let i = 0; i < this.length; i += 1) {
  92. if (this.#rdns[i].equals(dn.rdnAt(i)) === false) {
  93. return false
  94. }
  95. }
  96. return true
  97. }
  98. /**
  99. * @deprecated Use .toString() instead.
  100. *
  101. * @returns {string}
  102. */
  103. format () {
  104. warning.emit('LDAP_DN_DEP_002')
  105. return this.toString()
  106. }
  107. /**
  108. * Determine if the instance has any RDNs defined.
  109. *
  110. * @returns {boolean}
  111. */
  112. isEmpty () {
  113. return this.#rdns.length === 0
  114. }
  115. /**
  116. * Get a DN representation of the parent of this instance.
  117. *
  118. * @returns {DN|undefined}
  119. */
  120. parent () {
  121. if (this.length === 0) return undefined
  122. const save = this.shift()
  123. const dn = new DN({ rdns: this.#rdns })
  124. this.unshift(save)
  125. return dn
  126. }
  127. /**
  128. * Determine if the instance is the parent of a given DN instance or DN
  129. * string.
  130. *
  131. * @param {DN|string} dn
  132. *
  133. * @returns {boolean}
  134. */
  135. parentOf (dn) {
  136. if (typeof dn === 'string') {
  137. const parsedDn = DN.fromString(dn)
  138. return this.parentOf(parsedDn)
  139. }
  140. if (this.length >= dn.length) {
  141. // If we have more RDNs in our set then we must be a descendent at least.
  142. return false
  143. }
  144. const numberOfElementsDifferent = dn.length - this.length
  145. for (let i = this.length - 1; i >= 0; i -= 1) {
  146. const myRdn = this.#rdns[i]
  147. const theirRdn = dn.rdnAt(i + numberOfElementsDifferent)
  148. if (myRdn.equals(theirRdn) === false) {
  149. return false
  150. }
  151. }
  152. return true
  153. }
  154. /**
  155. * Removes the last RDN from the list and returns it. This alters the
  156. * instance.
  157. *
  158. * @returns {RDN}
  159. */
  160. pop () {
  161. return this.#rdns.pop()
  162. }
  163. /**
  164. * Adds a new RDN to the end of the list (i.e. the "top most" RDN in the
  165. * directory path) and returns the new RDN count.
  166. *
  167. * @param {RDN} rdn
  168. *
  169. * @returns {number}
  170. *
  171. * @throws When the input is not a valid RDN.
  172. */
  173. push (rdn) {
  174. if (Object.prototype.toString.call(rdn) !== '[object LdapRdn]') {
  175. throw Error('rdn must be a RDN instance')
  176. }
  177. return this.#rdns.push(rdn)
  178. }
  179. /**
  180. * Return the RDN at the provided index in the list of RDNs associated with
  181. * this instance.
  182. *
  183. * @param {number} index
  184. *
  185. * @returns {RDN}
  186. */
  187. rdnAt (index) {
  188. return this.#rdns[index]
  189. }
  190. /**
  191. * Reverse the RDNs list such that the first element becomes the last, and
  192. * the last becomes the first. This is useful when the RDNs were added in the
  193. * opposite order of how they should have been.
  194. *
  195. * This is an in-place operation. The instance is changed as a result of
  196. * this operation.
  197. *
  198. * @returns {DN} The current instance (i.e. this method is chainable).
  199. */
  200. reverse () {
  201. this.#rdns.reverse()
  202. return this
  203. }
  204. /**
  205. * @deprecated Formatting options are not supported.
  206. */
  207. setFormat () {
  208. warning.emit('LDAP_DN_DEP_004')
  209. }
  210. /**
  211. * Remove the first RDN from the set of RDNs and return it.
  212. *
  213. * @returns {RDN}
  214. */
  215. shift () {
  216. return this.#rdns.shift()
  217. }
  218. /**
  219. * Render the DN instance as a spec compliant DN string.
  220. *
  221. * @returns {string}
  222. */
  223. toString () {
  224. let result = ''
  225. for (const rdn of this.#rdns) {
  226. const rdnString = rdn.toString()
  227. result += `,${rdnString}`
  228. }
  229. return result.substring(1)
  230. }
  231. /**
  232. * Adds an RDN to the beginning of the RDN list and returns the new length.
  233. *
  234. * @param {RDN} rdn
  235. *
  236. * @returns {number}
  237. *
  238. * @throws When the RDN is invalid.
  239. */
  240. unshift (rdn) {
  241. if (Object.prototype.toString.call(rdn) !== '[object LdapRdn]') {
  242. throw Error('rdn must be a RDN instance')
  243. }
  244. return this.#rdns.unshift(rdn)
  245. }
  246. /**
  247. * Determine if an object is an instance of {@link DN} or is at least
  248. * a DN-like object. It is safer to perform a `toString` check.
  249. *
  250. * @example Valid Instance
  251. * const dn = new DN()
  252. * DN.isDn(dn) // true
  253. *
  254. * @example DN-like Instance
  255. * let dn = { rdns: [{name: 'cn', value: 'foo'}] }
  256. * DN.isDn(dn) // true
  257. *
  258. * dn = { rdns: [{cn: 'foo', sn: 'bar'}, {dc: 'example'}, {dc: 'com'}]}
  259. * DN.isDn(dn) // true
  260. *
  261. * @example Preferred Check
  262. * let dn = new DN()
  263. * Object.prototype.toString.call(dn) === '[object LdapDn]' // true
  264. *
  265. * dn = { rdns: [{name: 'cn', value: 'foo'}] }
  266. * Object.prototype.toString.call(dn) === '[object LdapDn]' // false
  267. *
  268. * @param {object} dn
  269. * @returns {boolean}
  270. */
  271. static isDn (dn) {
  272. if (Object.prototype.toString.call(dn) === '[object LdapDn]') {
  273. return true
  274. }
  275. if (
  276. Object.prototype.toString.call(dn) !== '[object Object]' ||
  277. Array.isArray(dn.rdns) === false
  278. ) {
  279. return false
  280. }
  281. if (dn.rdns.some(dn => RDN.isRdn(dn) === false) === true) {
  282. return false
  283. }
  284. return true
  285. }
  286. /**
  287. * Parses a DN string and returns a new {@link DN} instance.
  288. *
  289. * @example
  290. * const dn = DN.fromString('cn=foo,dc=example,dc=com')
  291. * DN.isDn(dn) // true
  292. *
  293. * @param {string} dnString
  294. *
  295. * @returns {DN}
  296. *
  297. * @throws If the string is not parseable.
  298. */
  299. static fromString (dnString) {
  300. const rdns = parseString(dnString)
  301. return new DN({ rdns })
  302. }
  303. }
  304. module.exports = DN