index.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. 'use strict'
  2. const { BerReader, BerWriter } = require('@ldapjs/asn1')
  3. const Attribute = require('@ldapjs/attribute')
  4. /**
  5. * Implements an LDAP CHANGE sequence as described in
  6. * https://www.rfc-editor.org/rfc/rfc4511.html#section-4.6.
  7. */
  8. class Change {
  9. #operation
  10. #modification
  11. /**
  12. * @typedef {object} ChangeParameters
  13. * @property {string | number} operation One of `add` (0), `delete` (1), or
  14. * `replace` (2). Default: `add`.
  15. * @property {object | import('@ldapjs/attribute')} modification An attribute
  16. * instance or an object that is shaped like an attribute.
  17. */
  18. /**
  19. * @param {ChangeParameters} input
  20. *
  21. * @throws When the `modification` parameter is invalid.
  22. */
  23. constructor ({ operation = 'add', modification }) {
  24. this.operation = operation
  25. this.modification = modification
  26. }
  27. get [Symbol.toStringTag] () {
  28. return 'LdapChange'
  29. }
  30. /**
  31. * The attribute that will be modified by the {@link Change}.
  32. *
  33. * @returns {import('@ldapjs/attribute')}
  34. */
  35. get modification () {
  36. return this.#modification
  37. }
  38. /**
  39. * Define the attribute to be modified by the {@link Change}.
  40. *
  41. * @param {object|import('@ldapjs/attribute')} mod
  42. *
  43. * @throws When `mod` is not an instance of `Attribute` or is not an
  44. * `Attribute` shaped object.
  45. */
  46. set modification (mod) {
  47. if (Attribute.isAttribute(mod) === false) {
  48. throw Error('modification must be an Attribute')
  49. }
  50. if (Object.prototype.toString.call(mod) !== '[object LdapAttribute]') {
  51. mod = new Attribute(mod)
  52. }
  53. this.#modification = mod
  54. }
  55. /**
  56. * Get a plain JavaScript object representation of the change.
  57. *
  58. * @returns {object}
  59. */
  60. get pojo () {
  61. return {
  62. operation: this.operation,
  63. modification: this.modification.pojo
  64. }
  65. }
  66. /**
  67. * The string name of the operation that will be performed.
  68. *
  69. * @returns {string} One of `add`, `delete`, or `replace`.
  70. */
  71. get operation () {
  72. switch (this.#operation) {
  73. case 0x00: {
  74. return 'add'
  75. }
  76. case 0x01: {
  77. return 'delete'
  78. }
  79. case 0x02: {
  80. return 'replace'
  81. }
  82. }
  83. }
  84. /**
  85. * Define the operation that the {@link Change} represents.
  86. *
  87. * @param {string|number} op May be one of `add` (0), `delete` (1),
  88. * or `replace` (2).
  89. *
  90. * @throws When the `op` is not recognized.
  91. */
  92. set operation (op) {
  93. if (typeof op === 'string') {
  94. op = op.toLowerCase()
  95. }
  96. switch (op) {
  97. case 0x00:
  98. case 'add': {
  99. this.#operation = 0x00
  100. break
  101. }
  102. case 0x01:
  103. case 'delete': {
  104. this.#operation = 0x01
  105. break
  106. }
  107. case 0x02:
  108. case 'replace': {
  109. this.#operation = 0x02
  110. break
  111. }
  112. default: {
  113. const type = Number.isInteger(op)
  114. ? '0x' + Number(op).toString(16)
  115. : op
  116. throw Error(`invalid operation type: ${type}`)
  117. }
  118. }
  119. }
  120. /**
  121. * Serialize the instance to a BER.
  122. *
  123. * @returns {import('@ldapjs/asn1').BerReader}
  124. */
  125. toBer () {
  126. const writer = new BerWriter()
  127. writer.startSequence()
  128. writer.writeEnumeration(this.#operation)
  129. const attrBer = this.#modification.toBer()
  130. writer.appendBuffer(attrBer.buffer)
  131. writer.endSequence()
  132. return new BerReader(writer.buffer)
  133. }
  134. /**
  135. * See {@link pojo}.
  136. *
  137. * @returns {object}
  138. */
  139. toJSON () {
  140. return this.pojo
  141. }
  142. /**
  143. * Applies a {@link Change} to a `target` object.
  144. *
  145. * @example
  146. * const change = new Change({
  147. * operation: 'add',
  148. * modification: {
  149. * type: 'cn',
  150. * values: ['new']
  151. * }
  152. * })
  153. * const target = {
  154. * cn: ['old']
  155. * }
  156. * Change.apply(change, target)
  157. * // target = { cn: ['old', 'new'] }
  158. *
  159. * @param {Change} change The change to apply.
  160. * @param {object} target The object to modify. This object will be mutated
  161. * by the function. It should have properties that match the `modification`
  162. * of the change.
  163. * @param {boolean} scalar When `true`, will convert single-item arrays
  164. * to scalar values. Default: `false`.
  165. *
  166. * @returns {object} The mutated `target`.
  167. *
  168. * @throws When the `change` is not an instance of {@link Change}.
  169. */
  170. static apply (change, target, scalar = false) {
  171. if (Change.isChange(change) === false) {
  172. throw Error('change must be an instance of Change')
  173. }
  174. const type = change.modification.type
  175. const values = change.modification.values
  176. let data = target[type]
  177. if (data === undefined) {
  178. data = []
  179. } else if (Array.isArray(data) === false) {
  180. data = [data]
  181. }
  182. switch (change.operation) {
  183. case 'add': {
  184. // Add only new unique entries.
  185. const newValues = values.filter(v => data.indexOf(v) === -1)
  186. Array.prototype.push.apply(data, newValues)
  187. break
  188. }
  189. case 'delete': {
  190. data = data.filter(v => values.indexOf(v) === -1)
  191. if (data.length === 0) {
  192. // An empty list indicates the attribute should be removed
  193. // completely.
  194. delete target[type]
  195. return target
  196. }
  197. break
  198. }
  199. case 'replace': {
  200. if (values.length === 0) {
  201. // A new value set that is empty is a delete.
  202. delete target[type]
  203. return target
  204. }
  205. data = values
  206. break
  207. }
  208. }
  209. if (scalar === true && data.length === 1) {
  210. // Replace array value with a scalar value if the modified set is
  211. // single valued and the operation calls for a scalar.
  212. target[type] = data[0]
  213. } else {
  214. target[type] = data
  215. }
  216. return target
  217. }
  218. /**
  219. * Determines if an object is an instance of {@link Change}, or at least
  220. * resembles the shape of a {@link Change} object. A plain object will match
  221. * if it has a `modification` property that matches an `Attribute`,
  222. * an `operation` property that is a string or number, and has a `toBer`
  223. * method. An object that resembles a {@link Change} does not guarantee
  224. * compatibility. A `toString` check is much more accurate.
  225. *
  226. * @param {Change|object} change
  227. *
  228. * @returns {boolean}
  229. */
  230. static isChange (change) {
  231. if (Object.prototype.toString.call(change) === '[object LdapChange]') {
  232. return true
  233. }
  234. if (Object.prototype.toString.call(change) !== '[object Object]') {
  235. return false
  236. }
  237. if (
  238. Attribute.isAttribute(change.modification) === true &&
  239. (typeof change.operation === 'string' || typeof change.operation === 'number')
  240. ) {
  241. return true
  242. }
  243. return false
  244. }
  245. /**
  246. * Compares two {@link Change} instance to determine the priority of the
  247. * changes relative to each other.
  248. *
  249. * @param {Change} change1
  250. * @param {Change} change2
  251. *
  252. * @returns {number} -1 for lower priority, 1 for higher priority, and 0
  253. * for equal priority in relation to `change1`, e.g. -1 would mean `change`
  254. * has lower priority than `change2`.
  255. *
  256. * @throws When neither parameter resembles a {@link Change} object.
  257. */
  258. static compare (change1, change2) {
  259. if (Change.isChange(change1) === false || Change.isChange(change2) === false) {
  260. throw Error('can only compare Change instances')
  261. }
  262. if (change1.operation < change2.operation) {
  263. return -1
  264. }
  265. if (change1.operation > change2.operation) {
  266. return 1
  267. }
  268. return Attribute.compare(change1.modification, change2.modification)
  269. }
  270. /**
  271. * Parse a BER into a new {@link Change} object.
  272. *
  273. * @param {import('@ldapjs/asn1').BerReader} ber The BER to process. It must
  274. * be at an offset that starts a new change sequence. The reader will be
  275. * advanced to the end of the change sequence by this method.
  276. *
  277. * @returns {Change}
  278. *
  279. * @throws When there is an error processing the BER.
  280. */
  281. static fromBer (ber) {
  282. ber.readSequence()
  283. const operation = ber.readEnumeration()
  284. const modification = Attribute.fromBer(ber)
  285. return new Change({ operation, modification })
  286. }
  287. }
  288. module.exports = Change