CLI for testing.
This commit is contained in:
parent
641e13496e
commit
b8e2cfc47f
@ -19,7 +19,9 @@
|
|||||||
},
|
},
|
||||||
"preferGlobal": false,
|
"preferGlobal": false,
|
||||||
"main": "dist/",
|
"main": "dist/",
|
||||||
"bin": {},
|
"bin": {
|
||||||
|
"xo-server-auth-ldap": "dist/test-cli.js"
|
||||||
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist/"
|
"dist/"
|
||||||
],
|
],
|
||||||
@ -27,7 +29,9 @@
|
|||||||
"babel-runtime": "^6.3.19",
|
"babel-runtime": "^6.3.19",
|
||||||
"bluebird": "^3.1.1",
|
"bluebird": "^3.1.1",
|
||||||
"event-to-promise": "^0.6.0",
|
"event-to-promise": "^0.6.0",
|
||||||
|
"exec-promise": "^0.5.1",
|
||||||
"fs-promise": "^0.3.1",
|
"fs-promise": "^0.3.1",
|
||||||
|
"inquirer": "^0.11.0",
|
||||||
"ldapjs": "^1.0.0"
|
"ldapjs": "^1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -12,6 +12,10 @@ const bind = (fn, thisArg) => function () {
|
|||||||
return fn.apply(thisArg, arguments)
|
return fn.apply(thisArg, arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const noop = () => {}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
const VAR_RE = /\{\{([^}]+)\}\}/g
|
const VAR_RE = /\{\{([^}]+)\}\}/g
|
||||||
const evalFilter = (filter, vars) => filter.replace(VAR_RE, (_, name) => {
|
const evalFilter = (filter, vars) => filter.replace(VAR_RE, (_, name) => {
|
||||||
const value = vars[name]
|
const value = vars[name]
|
||||||
@ -144,8 +148,10 @@ class AuthLdap {
|
|||||||
this._xo.unregisterAuthenticationProvider(this._authenticate)
|
this._xo.unregisterAuthenticationProvider(this._authenticate)
|
||||||
}
|
}
|
||||||
|
|
||||||
async _authenticate ({ username, password }) {
|
async _authenticate ({ username, password }, logger = noop) {
|
||||||
if (username === undefined || password === undefined) {
|
if (username === undefined || password === undefined) {
|
||||||
|
logger('require `username` and `password` to authenticate!')
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,13 +166,16 @@ class AuthLdap {
|
|||||||
{
|
{
|
||||||
const {_credentials: credentials} = this
|
const {_credentials: credentials} = this
|
||||||
if (credentials) {
|
if (credentials) {
|
||||||
|
logger(`attempting to bind with as ${credentials.dn}...`)
|
||||||
await bind(credentials.dn, credentials.password)
|
await bind(credentials.dn, credentials.password)
|
||||||
|
logger(`successfully bound as ${credentials.dn}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search for the user.
|
// Search for the user.
|
||||||
const entries = []
|
const entries = []
|
||||||
{
|
{
|
||||||
|
logger('searching for entries...')
|
||||||
const response = await search(this._searchBase, {
|
const response = await search(this._searchBase, {
|
||||||
scope: 'sub',
|
scope: 'sub',
|
||||||
filter: evalFilter(this._searchFilter, {
|
filter: evalFilter(this._searchFilter, {
|
||||||
@ -175,6 +184,7 @@ class AuthLdap {
|
|||||||
})
|
})
|
||||||
|
|
||||||
response.on('searchEntry', entry => {
|
response.on('searchEntry', entry => {
|
||||||
|
logger('.')
|
||||||
entries.push(entry.json)
|
entries.push(entry.json)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -182,16 +192,21 @@ class AuthLdap {
|
|||||||
if (status) {
|
if (status) {
|
||||||
throw new Error('unexpected search response status: ' + 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.
|
// Try to find an entry which can be bind with the given password.
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
try {
|
try {
|
||||||
|
logger(`attempting to bind as ${entry.objectName}`)
|
||||||
await bind(entry.objectName, password)
|
await bind(entry.objectName, password)
|
||||||
|
logger(`successfully bound as ${entry.objectName} => ${username} authenticated`)
|
||||||
return { username }
|
return { username }
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger(`could not authenticate ${username}`)
|
||||||
return null
|
return null
|
||||||
} finally {
|
} finally {
|
||||||
client.unbind()
|
client.unbind()
|
||||||
|
220
packages/xo-server-auth-ldap/src/prompt-schema.js
Normal file
220
packages/xo-server-auth-ldap/src/prompt-schema.js
Normal 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)
|
||||||
|
}
|
||||||
|
|
47
packages/xo-server-auth-ldap/src/test-cli.js
Executable file
47
packages/xo-server-auth-ldap/src/test-cli.js
Executable 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)
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user