CLI for testing.

This commit is contained in:
Julien Fontanet 2015-12-30 17:39:00 +01:00
parent 641e13496e
commit b8e2cfc47f
4 changed files with 288 additions and 2 deletions

View File

@ -19,7 +19,9 @@
},
"preferGlobal": false,
"main": "dist/",
"bin": {},
"bin": {
"xo-server-auth-ldap": "dist/test-cli.js"
},
"files": [
"dist/"
],
@ -27,7 +29,9 @@
"babel-runtime": "^6.3.19",
"bluebird": "^3.1.1",
"event-to-promise": "^0.6.0",
"exec-promise": "^0.5.1",
"fs-promise": "^0.3.1",
"inquirer": "^0.11.0",
"ldapjs": "^1.0.0"
},
"devDependencies": {

View File

@ -12,6 +12,10 @@ const bind = (fn, thisArg) => function () {
return fn.apply(thisArg, arguments)
}
const noop = () => {}
// -------------------------------------------------------------------
const VAR_RE = /\{\{([^}]+)\}\}/g
const evalFilter = (filter, vars) => filter.replace(VAR_RE, (_, name) => {
const value = vars[name]
@ -144,8 +148,10 @@ class AuthLdap {
this._xo.unregisterAuthenticationProvider(this._authenticate)
}
async _authenticate ({ username, password }) {
async _authenticate ({ username, password }, logger = noop) {
if (username === undefined || password === undefined) {
logger('require `username` and `password` to authenticate!')
return null
}
@ -160,13 +166,16 @@ class AuthLdap {
{
const {_credentials: credentials} = this
if (credentials) {
logger(`attempting to bind with as ${credentials.dn}...`)
await bind(credentials.dn, credentials.password)
logger(`successfully bound as ${credentials.dn}`)
}
}
// Search for the user.
const entries = []
{
logger('searching for entries...')
const response = await search(this._searchBase, {
scope: 'sub',
filter: evalFilter(this._searchFilter, {
@ -175,6 +184,7 @@ class AuthLdap {
})
response.on('searchEntry', entry => {
logger('.')
entries.push(entry.json)
})
@ -182,16 +192,21 @@ class AuthLdap {
if (status) {
throw new Error('unexpected search response status: ' + status)
}
logger(`${entries.length} entries found`)
}
// Try to find an entry which can be bind with the given password.
for (const entry of entries) {
try {
logger(`attempting to bind as ${entry.objectName}`)
await bind(entry.objectName, password)
logger(`successfully bound as ${entry.objectName} => ${username} authenticated`)
return { username }
} catch (_) {}
}
logger(`could not authenticate ${username}`)
return null
} finally {
client.unbind()

View File

@ -0,0 +1,220 @@
import { prompt } from 'inquirer'
// ===================================================================
const forArray = (array, iteratee) => {
for (let i = 0, n = array.length; i < n; ++i) {
iteratee(array[i], i, array)
}
}
const { hasOwnProperty } = Object.prototype
const forOwn = (object, iteratee) => {
for (const key in object) {
if (hasOwnProperty.call(object, key)) {
iteratee(object[key], key, object)
}
}
}
// -------------------------------------------------------------------
const _makeAsyncIterator = iterator => (promises, cb) => {
let mainPromise = Promise.resolve()
iterator(promises, (promise, key) => {
mainPromise = mainPromise
// Waits the current promise.
.then(() => promise)
// Executes the callback.
.then(value => cb(value, key))
})
return mainPromise
}
const forOwnAsync = _makeAsyncIterator(forOwn)
// -------------------------------------------------------------------
const _isNaN = (
Number.isNaN ||
(value => value !== value) // eslint-disable-line no-self-compare
)
const isNumber = value => !_isNaN(value) && typeof value === 'number'
const isInteger = (
Number.isInteger ||
(value => (
isNumber(value) &&
value > -Infinity && value < Infinity &&
Math.floor(value) === value
))
)
// ===================================================================
const EMPTY_OBJECT = Object.freeze({ __proto__: null })
const _extractValue = ({ value }) => value
export const confirm = (message, {
default: defaultValue = null
} = EMPTY_OBJECT) => new Promise(resolve => prompt({
default: defaultValue,
message,
name: 'value',
type: 'confirm'
}, resolve)).then(_extractValue)
export const input = (message, {
default: defaultValue = null,
filter = undefined,
validate = undefined
} = EMPTY_OBJECT) => new Promise(resolve => prompt({
default: defaultValue,
message,
name: 'value',
type: 'input',
validate
}, resolve)).then(_extractValue)
export const list = (message, choices, {
default: defaultValue = null
} = EMPTY_OBJECT) => new Promise(resolve => prompt({
default: defaultValue,
choices,
message,
name: 'value',
type: 'list'
}, resolve)).then(_extractValue)
export const password = (message, {
default: defaultValue = null,
filter = undefined,
validate = undefined
} = EMPTY_OBJECT) => new Promise(resolve => prompt({
default: defaultValue,
message,
name: 'value',
type: 'password',
validate
}, resolve)).then(_extractValue)
// ===================================================================
const promptByType = {
__proto__: null,
array: async (schema, defaultValue, path) => {
const items = []
if (defaultValue == null) {
defaultValue = items
}
let i = 0
const itemSchema = schema.items
const promptItem = async () => {
items[i] = await promptGeneric(
itemSchema,
defaultValue[i],
path
? `${path} [${i}]`
: `[${i}]`
)
++i
}
let n = schema.minItems || 0
while (i < n) {
await promptItem()
}
n = schema.maxItems || Infinity
while (
i < n &&
await confirm('additional item?', {
default: false
})
) {
await promptItem()
}
return items
},
boolean: (schema, defaultValue, path) => confirm(path, {
default: defaultValue || schema.default
}),
enum: (schema, defaultValue, path) => list(path, schema.enum, {
defaultValue: defaultValue || schema.defaultValue
}),
integer: (schema, defaultValue, path) => input(path, {
default: defaultValue || schema.default,
filter: input => +input,
validate: input => isInteger(+input)
}),
number: (schema, defaultValue, path) => input(path, {
default: defaultValue || schema.default,
filter: input => +input,
validate: input => isNumber(+input)
}),
object: async (schema, defaultValue, path) => {
const value = {}
const required = {}
schema.required && forArray(schema.required, name => {
required[name] = true
})
const promptProperty = async (schema, name) => {
const subpath = path
? `${path} > ${schema.title || name}`
: schema.title || name
if (
required[name] ||
await confirm(`fill optional ${subpath}?`, {
default: false
})
) {
value[name] = await promptGeneric(
schema,
defaultValue && defaultValue[name],
subpath
)
}
}
await forOwnAsync(schema.properties || {}, promptProperty)
return value
},
string: (schema, defaultValue, path) => input(path, {
default: defaultValue || schema.default
})
}
export default function promptGeneric (schema, defaultValue, path) {
const type = schema.enum
? 'enum'
: schema.type
const prompt = promptByType[type.toLowerCase()]
if (!prompt) {
throw new Error(`unsupported type: ${type}`)
}
return prompt(schema, defaultValue, path)
}

View File

@ -0,0 +1,47 @@
#!/usr/bin/env node
import execPromise from 'exec-promise'
import { readFile, writeFile } from 'fs-promise'
import promptSchema, {
input,
password
} from './prompt-schema'
import createPlugin, {
configurationSchema
} from './'
// ===================================================================
const CACHE_FILE = './ldap.cache.conf'
// -------------------------------------------------------------------
execPromise(async args => {
const config = await promptSchema(
configurationSchema,
await readFile(CACHE_FILE, 'utf-8').then(
JSON.parse,
() => ({})
)
)
await writeFile(CACHE_FILE, JSON.stringify(config, null, 2)).then(
() => {
console.log('configuration saved in %s', CACHE_FILE)
},
error => {
console.warn('failed to save configuration in %s', CACHE_FILE)
console.warn(error.message)
}
)
const plugin = createPlugin({})
await plugin.configure(config)
await plugin._authenticate({
username: await input('Username', {
validate: input => !!input.length
}),
password: await password('Password')
}, ::console.log)
})