321 lines
7.9 KiB
JavaScript
321 lines
7.9 KiB
JavaScript
'use strict'
|
|
|
|
const { BerReader, BerWriter } = require('@ldapjs/asn1')
|
|
const Attribute = require('@ldapjs/attribute')
|
|
|
|
/**
|
|
* Implements an LDAP CHANGE sequence as described in
|
|
* https://www.rfc-editor.org/rfc/rfc4511.html#section-4.6.
|
|
*/
|
|
class Change {
|
|
#operation
|
|
#modification
|
|
|
|
/**
|
|
* @typedef {object} ChangeParameters
|
|
* @property {string | number} operation One of `add` (0), `delete` (1), or
|
|
* `replace` (2). Default: `add`.
|
|
* @property {object | import('@ldapjs/attribute')} modification An attribute
|
|
* instance or an object that is shaped like an attribute.
|
|
*/
|
|
|
|
/**
|
|
* @param {ChangeParameters} input
|
|
*
|
|
* @throws When the `modification` parameter is invalid.
|
|
*/
|
|
constructor ({ operation = 'add', modification }) {
|
|
this.operation = operation
|
|
this.modification = modification
|
|
}
|
|
|
|
get [Symbol.toStringTag] () {
|
|
return 'LdapChange'
|
|
}
|
|
|
|
/**
|
|
* The attribute that will be modified by the {@link Change}.
|
|
*
|
|
* @returns {import('@ldapjs/attribute')}
|
|
*/
|
|
get modification () {
|
|
return this.#modification
|
|
}
|
|
|
|
/**
|
|
* Define the attribute to be modified by the {@link Change}.
|
|
*
|
|
* @param {object|import('@ldapjs/attribute')} mod
|
|
*
|
|
* @throws When `mod` is not an instance of `Attribute` or is not an
|
|
* `Attribute` shaped object.
|
|
*/
|
|
set modification (mod) {
|
|
if (Attribute.isAttribute(mod) === false) {
|
|
throw Error('modification must be an Attribute')
|
|
}
|
|
if (Object.prototype.toString.call(mod) !== '[object LdapAttribute]') {
|
|
mod = new Attribute(mod)
|
|
}
|
|
this.#modification = mod
|
|
}
|
|
|
|
/**
|
|
* Get a plain JavaScript object representation of the change.
|
|
*
|
|
* @returns {object}
|
|
*/
|
|
get pojo () {
|
|
return {
|
|
operation: this.operation,
|
|
modification: this.modification.pojo
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The string name of the operation that will be performed.
|
|
*
|
|
* @returns {string} One of `add`, `delete`, or `replace`.
|
|
*/
|
|
get operation () {
|
|
switch (this.#operation) {
|
|
case 0x00: {
|
|
return 'add'
|
|
}
|
|
|
|
case 0x01: {
|
|
return 'delete'
|
|
}
|
|
|
|
case 0x02: {
|
|
return 'replace'
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Define the operation that the {@link Change} represents.
|
|
*
|
|
* @param {string|number} op May be one of `add` (0), `delete` (1),
|
|
* or `replace` (2).
|
|
*
|
|
* @throws When the `op` is not recognized.
|
|
*/
|
|
set operation (op) {
|
|
if (typeof op === 'string') {
|
|
op = op.toLowerCase()
|
|
}
|
|
|
|
switch (op) {
|
|
case 0x00:
|
|
case 'add': {
|
|
this.#operation = 0x00
|
|
break
|
|
}
|
|
|
|
case 0x01:
|
|
case 'delete': {
|
|
this.#operation = 0x01
|
|
break
|
|
}
|
|
|
|
case 0x02:
|
|
case 'replace': {
|
|
this.#operation = 0x02
|
|
break
|
|
}
|
|
|
|
default: {
|
|
const type = Number.isInteger(op)
|
|
? '0x' + Number(op).toString(16)
|
|
: op
|
|
throw Error(`invalid operation type: ${type}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Serialize the instance to a BER.
|
|
*
|
|
* @returns {import('@ldapjs/asn1').BerReader}
|
|
*/
|
|
toBer () {
|
|
const writer = new BerWriter()
|
|
writer.startSequence()
|
|
writer.writeEnumeration(this.#operation)
|
|
|
|
const attrBer = this.#modification.toBer()
|
|
writer.appendBuffer(attrBer.buffer)
|
|
writer.endSequence()
|
|
|
|
return new BerReader(writer.buffer)
|
|
}
|
|
|
|
/**
|
|
* See {@link pojo}.
|
|
*
|
|
* @returns {object}
|
|
*/
|
|
toJSON () {
|
|
return this.pojo
|
|
}
|
|
|
|
/**
|
|
* Applies a {@link Change} to a `target` object.
|
|
*
|
|
* @example
|
|
* const change = new Change({
|
|
* operation: 'add',
|
|
* modification: {
|
|
* type: 'cn',
|
|
* values: ['new']
|
|
* }
|
|
* })
|
|
* const target = {
|
|
* cn: ['old']
|
|
* }
|
|
* Change.apply(change, target)
|
|
* // target = { cn: ['old', 'new'] }
|
|
*
|
|
* @param {Change} change The change to apply.
|
|
* @param {object} target The object to modify. This object will be mutated
|
|
* by the function. It should have properties that match the `modification`
|
|
* of the change.
|
|
* @param {boolean} scalar When `true`, will convert single-item arrays
|
|
* to scalar values. Default: `false`.
|
|
*
|
|
* @returns {object} The mutated `target`.
|
|
*
|
|
* @throws When the `change` is not an instance of {@link Change}.
|
|
*/
|
|
static apply (change, target, scalar = false) {
|
|
if (Change.isChange(change) === false) {
|
|
throw Error('change must be an instance of Change')
|
|
}
|
|
|
|
const type = change.modification.type
|
|
const values = change.modification.values
|
|
|
|
let data = target[type]
|
|
if (data === undefined) {
|
|
data = []
|
|
} else if (Array.isArray(data) === false) {
|
|
data = [data]
|
|
}
|
|
|
|
switch (change.operation) {
|
|
case 'add': {
|
|
// Add only new unique entries.
|
|
const newValues = values.filter(v => data.indexOf(v) === -1)
|
|
Array.prototype.push.apply(data, newValues)
|
|
break
|
|
}
|
|
|
|
case 'delete': {
|
|
data = data.filter(v => values.indexOf(v) === -1)
|
|
if (data.length === 0) {
|
|
// An empty list indicates the attribute should be removed
|
|
// completely.
|
|
delete target[type]
|
|
return target
|
|
}
|
|
break
|
|
}
|
|
|
|
case 'replace': {
|
|
if (values.length === 0) {
|
|
// A new value set that is empty is a delete.
|
|
delete target[type]
|
|
return target
|
|
}
|
|
data = values
|
|
break
|
|
}
|
|
}
|
|
|
|
if (scalar === true && data.length === 1) {
|
|
// Replace array value with a scalar value if the modified set is
|
|
// single valued and the operation calls for a scalar.
|
|
target[type] = data[0]
|
|
} else {
|
|
target[type] = data
|
|
}
|
|
|
|
return target
|
|
}
|
|
|
|
/**
|
|
* Determines if an object is an instance of {@link Change}, or at least
|
|
* resembles the shape of a {@link Change} object. A plain object will match
|
|
* if it has a `modification` property that matches an `Attribute`,
|
|
* an `operation` property that is a string or number, and has a `toBer`
|
|
* method. An object that resembles a {@link Change} does not guarantee
|
|
* compatibility. A `toString` check is much more accurate.
|
|
*
|
|
* @param {Change|object} change
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
static isChange (change) {
|
|
if (Object.prototype.toString.call(change) === '[object LdapChange]') {
|
|
return true
|
|
}
|
|
if (Object.prototype.toString.call(change) !== '[object Object]') {
|
|
return false
|
|
}
|
|
if (
|
|
Attribute.isAttribute(change.modification) === true &&
|
|
(typeof change.operation === 'string' || typeof change.operation === 'number')
|
|
) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Compares two {@link Change} instance to determine the priority of the
|
|
* changes relative to each other.
|
|
*
|
|
* @param {Change} change1
|
|
* @param {Change} change2
|
|
*
|
|
* @returns {number} -1 for lower priority, 1 for higher priority, and 0
|
|
* for equal priority in relation to `change1`, e.g. -1 would mean `change`
|
|
* has lower priority than `change2`.
|
|
*
|
|
* @throws When neither parameter resembles a {@link Change} object.
|
|
*/
|
|
static compare (change1, change2) {
|
|
if (Change.isChange(change1) === false || Change.isChange(change2) === false) {
|
|
throw Error('can only compare Change instances')
|
|
}
|
|
if (change1.operation < change2.operation) {
|
|
return -1
|
|
}
|
|
if (change1.operation > change2.operation) {
|
|
return 1
|
|
}
|
|
return Attribute.compare(change1.modification, change2.modification)
|
|
}
|
|
|
|
/**
|
|
* Parse a BER into a new {@link Change} object.
|
|
*
|
|
* @param {import('@ldapjs/asn1').BerReader} ber The BER to process. It must
|
|
* be at an offset that starts a new change sequence. The reader will be
|
|
* advanced to the end of the change sequence by this method.
|
|
*
|
|
* @returns {Change}
|
|
*
|
|
* @throws When there is an error processing the BER.
|
|
*/
|
|
static fromBer (ber) {
|
|
ber.readSequence()
|
|
const operation = ber.readEnumeration()
|
|
const modification = Attribute.fromBer(ber)
|
|
return new Change({ operation, modification })
|
|
}
|
|
}
|
|
|
|
module.exports = Change
|