From b8e2cfc47ff6231becbba7d708ed6a93b7cd62af Mon Sep 17 00:00:00 2001 From: Julien Fontanet Date: Wed, 30 Dec 2015 17:39:00 +0100 Subject: [PATCH] CLI for testing. --- packages/xo-server-auth-ldap/package.json | 6 +- packages/xo-server-auth-ldap/src/index.js | 17 +- .../xo-server-auth-ldap/src/prompt-schema.js | 220 ++++++++++++++++++ packages/xo-server-auth-ldap/src/test-cli.js | 47 ++++ 4 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 packages/xo-server-auth-ldap/src/prompt-schema.js create mode 100755 packages/xo-server-auth-ldap/src/test-cli.js diff --git a/packages/xo-server-auth-ldap/package.json b/packages/xo-server-auth-ldap/package.json index ca9f17e0c..c6add5845 100644 --- a/packages/xo-server-auth-ldap/package.json +++ b/packages/xo-server-auth-ldap/package.json @@ -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": { diff --git a/packages/xo-server-auth-ldap/src/index.js b/packages/xo-server-auth-ldap/src/index.js index 1092eafc4..856a2be56 100644 --- a/packages/xo-server-auth-ldap/src/index.js +++ b/packages/xo-server-auth-ldap/src/index.js @@ -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() diff --git a/packages/xo-server-auth-ldap/src/prompt-schema.js b/packages/xo-server-auth-ldap/src/prompt-schema.js new file mode 100644 index 000000000..342bb79c4 --- /dev/null +++ b/packages/xo-server-auth-ldap/src/prompt-schema.js @@ -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) +} + diff --git a/packages/xo-server-auth-ldap/src/test-cli.js b/packages/xo-server-auth-ldap/src/test-cli.js new file mode 100755 index 000000000..0a602eefa --- /dev/null +++ b/packages/xo-server-auth-ldap/src/test-cli.js @@ -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) +})