diff --git a/index.js b/index.js index 9f260b7..b535c1c 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ const { Ber } = require('@ldapjs/asn1') const Control = require('./lib/control') const EntryChangeNotificationControl = require('./lib/controls/entry-change-notification-control') const PagedResultsControl = require('./lib/controls/paged-results-control') +const PasswordPolicyControl = require('./lib/controls/password-policy-control') const PersistentSearchControl = require('./lib/controls/persistent-search-control') const ServerSideSortingRequestControl = require('./lib/controls/server-side-sorting-request-control') const ServerSideSortingResponseControl = require('./lib/controls/server-side-sorting-response-control') @@ -50,6 +51,11 @@ module.exports = { break } + case PasswordPolicyControl.OID: { + control = new PasswordPolicyControl(opts) + break + } + case PersistentSearchControl.OID: { control = new PersistentSearchControl(opts) break @@ -88,6 +94,7 @@ module.exports = { Control, EntryChangeNotificationControl, PagedResultsControl, + PasswordPolicyControl, PersistentSearchControl, ServerSideSortingRequestControl, ServerSideSortingResponseControl, diff --git a/index.test.js b/index.test.js index 6c6eae5..1762149 100644 --- a/index.test.js +++ b/index.test.js @@ -95,6 +95,27 @@ tap.test('#getControl', t => { t.equal(Buffer.compare(c.value.cookie, Buffer.alloc(0)), 0) }) + t.test('returns a PasswordPolicyControl', async t => { + const ppc = new controls.PasswordPolicyControl({ + type: controls.PasswordPolicyControl.OID, + criticality: true, + value: { + error: 1, + timeBeforeExpiration: 2 + } + }) + + const ber = new BerWriter() + ppc.toBer(ber) + + const c = controls.getControl(new BerReader(ber.buffer)) + t.ok(c) + t.equal(c.type, controls.PasswordPolicyControl.OID) + t.ok(c.criticality) + t.equal(c.value.error, 1) + t.equal(c.value.timeBeforeExpiration, 2) + }) + t.test('returns a PersistentSearchControl', async t => { const buf = Buffer.from([ 0x30, 0x26, 0x04, 0x17, 0x32, 0x2e, 0x31, 0x36, 0x2e, 0x38, 0x34, 0x30, diff --git a/lib/controls/password-policy-control.js b/lib/controls/password-policy-control.js new file mode 100644 index 0000000..b10a083 --- /dev/null +++ b/lib/controls/password-policy-control.js @@ -0,0 +1,118 @@ +'use strict' + +const { BerReader, BerWriter } = require('@ldapjs/asn1') +const isObject = require('../is-object') +const hasOwn = require('../has-own') +const Control = require('../control') + +/** + * @typedef {object} PasswordPolicyResponseControlValue + * @property {number} error One of 0 (passwordExpired), 1 (accountLocked), + * 2 (changeAfterReset), 3 (passwordModNotAllowed), 4 (mustSupplyOldPassword), + * 5 (insufficientPasswordQuality), 6 (passwordTooShort), 7 (passwordTooYoung), + * 8 (passwordInHistory), 9 (passwordTooYoung) + * @property {number} timeBeforeExpiration + * @property {number} graceAuthNsRemaining + */ + +/** + * Implements both request and response controls: + * https://datatracker.ietf.org/doc/html/draft-behera-ldap-password-policy-11#name-controls-used-for-password- + * + * @extends Control + */ +class PasswordPolicyControl extends Control { + static OID = '1.3.6.1.4.1.42.2.27.8.5.1' + + /** + * @typedef {ControlParams} PasswordPolicyResponseParams + * @property {PasswordPolicyResponseControlValue | Buffer} [value] + */ + + /** + * Creates a new password policy control. + * + * @param {PasswordPolicyResponseParams} [options] + */ + constructor (options = {}) { + options.type = PasswordPolicyControl.OID + super(options) + + this._value = {} + + if (hasOwn(options, 'value') === false) { + return + } + + if (Buffer.isBuffer(options.value)) { + this.#parse(options.value) + } else if (isObject(options.value)) { + if (hasOwn(options.value, 'timeBeforeExpiration') === true && hasOwn(options.value, 'graceAuthNsRemaining') === true) { + throw new Error('options.value must contain either timeBeforeExpiration or graceAuthNsRemaining, not both') + } + this._value = options.value + } else { + throw new TypeError('options.value must be a Buffer or Object') + } + } + + get value () { + return this._value + } + + set value (obj) { + this._value = Object.assign({}, this._value, obj) + } + + /** + * Given a BER buffer that represents a + * {@link PasswordPolicyResponseControlValue}, read that buffer into the + * current instance. + */ + #parse (buffer) { + const ber = new BerReader(buffer) + if (ber.readSequence()) { + this._value = {} + if (ber.peek() === 0xa0) { + ber.readSequence(0xa0) + if (ber.peek() === 0x80) { + this._value.timeBeforeExpiration = ber._readTag(0x80) + } else if (ber.peek() === 0x81) { + this._value.graceAuthNsRemaining = ber._readTag(0x81) + } + } + if (ber.peek() === 0x81) { + this._value.error = ber._readTag(0x81) + } + } + } + + _toBer (ber) { + if (!this._value || Object.keys(this._value).length === 0) { return } + + const writer = new BerWriter() + writer.startSequence() + if (hasOwn(this._value, 'timeBeforeExpiration')) { + writer.startSequence(0xa0) + writer.writeInt(this._value.timeBeforeExpiration, 0x80) + writer.endSequence() + } else if (hasOwn(this._value, 'graceAuthNsRemaining')) { + writer.startSequence(0xa0) + writer.writeInt(this._value.graceAuthNsRemaining, 0x81) + writer.endSequence() + } + if (hasOwn(this._value, 'error')) { + writer.writeInt(this._value.error, 0x81) + } + writer.endSequence() + + ber.writeBuffer(writer.buffer, 0x04) + return ber + } + + _updatePlainObject (obj) { + obj.controlValue = this.value + return obj + } +} +module.exports = PasswordPolicyControl diff --git a/lib/controls/password-policy-control.test.js b/lib/controls/password-policy-control.test.js new file mode 100644 index 0000000..a55ccfa --- /dev/null +++ b/lib/controls/password-policy-control.test.js @@ -0,0 +1,113 @@ +'use strict' + +const tap = require('tap') +const { BerWriter } = require('@ldapjs/asn1') +const PPC = require('./password-policy-control') +const Control = require('../control') + +tap.test('contructor', t => { + t.test('new no args', async t => { + const control = new PPC() + t.ok(control) + t.type(control, PPC) + t.type(control, Control) + t.equal(control.type, PPC.OID) + t.same(control.value, {}) + }) + + t.test('new with args', async t => { + const control = new PPC({ + type: '1.3.6.1.4.1.42.2.27.8.5.1', + criticality: true, + value: { + error: 1, + timeBeforeExpiration: 2 + } + }) + t.ok(control) + t.equal(control.type, '1.3.6.1.4.1.42.2.27.8.5.1') + t.ok(control.criticality) + t.same(control.value, { + error: 1, + timeBeforeExpiration: 2 + }) + }) + + t.test('with value buffer', async t => { + const value = new BerWriter() + value.startSequence() + value.writeInt(5, 0x81) + value.endSequence() + + const control = new PPC({ value: value.buffer }) + t.same(control.value, { + error: 5 + }) + }) + + t.test('throws for bad value', async t => { + t.throws(() => new PPC({ value: 42 })) + t.throws(() => new PPC({ value: { timeBeforeExpiration: 1, graceAuthNsRemaining: 2 } })) + }) + + t.end() +}) + +tap.test('pojo', t => { + t.test('adds control value', async t => { + const control = new PPC() + t.same(control.pojo, { + type: PPC.OID, + criticality: false, + value: {} + }) + }) + + t.end() +}) + +tap.test('toBer', t => { + t.test('converts empty instance to BER', async t => { + const target = new BerWriter() + target.startSequence() + target.writeString(PPC.OID) + target.writeBoolean(false) // Control.criticality + target.endSequence() + + const control = new PPC() + const ber = control.toBer() + + t.equal(Buffer.compare(ber.buffer, target.buffer), 0) + }) + + t.test('converts full instance to BER', async t => { + const target = new BerWriter() + target.startSequence() + target.writeString(PPC.OID) + target.writeBoolean(true) // Control.criticality + + const value = new BerWriter() + value.startSequence() + value.startSequence(0xa0) + value.writeInt(2, 0x81) + value.endSequence() + value.writeInt(1, 0x81) + value.endSequence() + + target.writeBuffer(value.buffer, 0x04) + target.endSequence() + + const control = new PPC({ + criticality: true, + value: { + error: 1, + graceAuthNsRemaining: 2 + } + }) + const ber = control.toBer() + + t.equal(Buffer.compare(ber.buffer, target.buffer), 0) + }) + + t.end() +})