Skip to content
This repository has been archived by the owner on May 14, 2024. It is now read-only.

Add PasswordPolicyControl #4

Merged
merged 1 commit into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -50,6 +51,11 @@ module.exports = {
break
}

case PasswordPolicyControl.OID: {
control = new PasswordPolicyControl(opts)
break
}

case PersistentSearchControl.OID: {
control = new PersistentSearchControl(opts)
break
Expand Down Expand Up @@ -88,6 +94,7 @@ module.exports = {
Control,
EntryChangeNotificationControl,
PagedResultsControl,
PasswordPolicyControl,
PersistentSearchControl,
ServerSideSortingRequestControl,
ServerSideSortingResponseControl,
Expand Down
21 changes: 21 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
118 changes: 118 additions & 0 deletions lib/controls/password-policy-control.js
Original file line number Diff line number Diff line change
@@ -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
113 changes: 113 additions & 0 deletions lib/controls/password-policy-control.test.js
Original file line number Diff line number Diff line change
@@ -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()
})