Merge remote-tracking branch 'xo-server-auth-ldap/master'
This commit is contained in:
commit
e08689ff0e
65
packages/xo-server-auth-ldap/.editorconfig
Normal file
65
packages/xo-server-auth-ldap/.editorconfig
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# http://EditorConfig.org
|
||||||
|
#
|
||||||
|
# Julien Fontanet's configuration
|
||||||
|
# https://gist.github.com/julien-f/8096213
|
||||||
|
|
||||||
|
# Top-most EditorConfig file.
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Common config.
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespaces = true
|
||||||
|
|
||||||
|
# CoffeeScript
|
||||||
|
#
|
||||||
|
# https://github.com/polarmobile/coffeescript-style-guide/blob/master/README.md
|
||||||
|
[*.{,lit}coffee]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
# Markdown
|
||||||
|
[*.{md,mdwn,mdown,markdown}]
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
# Package.json
|
||||||
|
#
|
||||||
|
# This indentation style is the one used by npm.
|
||||||
|
[/package.json]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
# Jade
|
||||||
|
[*.jade]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
# JavaScript
|
||||||
|
#
|
||||||
|
# Two spaces seems to be the standard most common style, at least in
|
||||||
|
# Node.js (http://nodeguide.com/style.html#tabs-vs-spaces).
|
||||||
|
[*.js]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
# Less
|
||||||
|
[*.less]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
# Sass
|
||||||
|
#
|
||||||
|
# Style used for http://libsass.com
|
||||||
|
[*.s[ac]ss]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
# YAML
|
||||||
|
#
|
||||||
|
# Only spaces are allowed.
|
||||||
|
[*.yaml]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
8
packages/xo-server-auth-ldap/.gitignore
vendored
Normal file
8
packages/xo-server-auth-ldap/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/dist/
|
||||||
|
/ldap.cache.conf
|
||||||
|
/node_modules/
|
||||||
|
|
||||||
|
npm-debug.log
|
||||||
|
npm-debug.log.*
|
||||||
|
pnpm-debug.log
|
||||||
|
pnpm-debug.log.*
|
10
packages/xo-server-auth-ldap/.npmignore
Normal file
10
packages/xo-server-auth-ldap/.npmignore
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/examples/
|
||||||
|
example.js
|
||||||
|
example.js.map
|
||||||
|
*.example.js
|
||||||
|
*.example.js.map
|
||||||
|
|
||||||
|
/test/
|
||||||
|
/tests/
|
||||||
|
*.spec.js
|
||||||
|
*.spec.js.map
|
9
packages/xo-server-auth-ldap/.travis.yml
Normal file
9
packages/xo-server-auth-ldap/.travis.yml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- 'stable'
|
||||||
|
- '6'
|
||||||
|
- '4'
|
||||||
|
|
||||||
|
# Use containers.
|
||||||
|
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||||
|
sudo: false
|
82
packages/xo-server-auth-ldap/README.md
Normal file
82
packages/xo-server-auth-ldap/README.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# xo-server-auth-ldap [](https://travis-ci.org/vatesfr/xo-server-auth-ldap)
|
||||||
|
|
||||||
|
> LDAP authentication plugin for XO-Server
|
||||||
|
|
||||||
|
This plugin allows LDAP users to authenticate to Xen-Orchestra.
|
||||||
|
|
||||||
|
The first time a user signs in, XO will create a new XO user with the
|
||||||
|
same identifier.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Installation of the [npm package](https://npmjs.org/package/xo-server-auth-ldap):
|
||||||
|
|
||||||
|
```
|
||||||
|
> npm install --global xo-server-auth-ldap
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Like all other xo-server plugins, it can be configured directly via
|
||||||
|
the web iterface, see [the plugin documentation](https://xen-orchestra.com/docs/plugins.html).
|
||||||
|
|
||||||
|
If you have issues, you can use the provided CLI to gather more
|
||||||
|
information:
|
||||||
|
|
||||||
|
```
|
||||||
|
> xo-server-auth-ldap
|
||||||
|
? uri ldap://ldap.company.net
|
||||||
|
? fill optional certificateAuthorities? No
|
||||||
|
? fill optional checkCertificate? No
|
||||||
|
? fill optional bind? No
|
||||||
|
? base ou=people,dc=company,dc=net
|
||||||
|
? fill optional filter? No
|
||||||
|
configuration saved in ./ldap.cache.conf
|
||||||
|
? Username john.smith
|
||||||
|
? Password *****
|
||||||
|
searching for entries...
|
||||||
|
0 entries found
|
||||||
|
could not authenticate john.smith
|
||||||
|
```
|
||||||
|
|
||||||
|
## Algorithm
|
||||||
|
|
||||||
|
1. If `bind` is defined, attempt to bind using this user.
|
||||||
|
2. Searches for the user in the directory starting from the `base`
|
||||||
|
with the defined `filter`.
|
||||||
|
3. If found, a bind is attempted using the distinguished name of this
|
||||||
|
user and the provided password.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```
|
||||||
|
# Install dependencies
|
||||||
|
> npm install
|
||||||
|
|
||||||
|
# Run the tests
|
||||||
|
> npm test
|
||||||
|
|
||||||
|
# Continuously compile
|
||||||
|
> npm run dev
|
||||||
|
|
||||||
|
# Continuously run the tests
|
||||||
|
> npm run dev-test
|
||||||
|
|
||||||
|
# Build for production (automatically called by npm install)
|
||||||
|
> npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributions
|
||||||
|
|
||||||
|
Contributions are *very* welcomed, either on the documentation or on
|
||||||
|
the code.
|
||||||
|
|
||||||
|
You may:
|
||||||
|
|
||||||
|
- report any [issue](https://github.com/vatesfr/xo-server-auth-ldap/issues)
|
||||||
|
you've encountered;
|
||||||
|
- fork and create a pull request.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
AGPL3 © [Vates SAS](http://vates.fr)
|
82
packages/xo-server-auth-ldap/package.json
Normal file
82
packages/xo-server-auth-ldap/package.json
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"name": "xo-server-auth-ldap",
|
||||||
|
"version": "0.6.0",
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"description": "LDAP authentication plugin for XO-Server",
|
||||||
|
"keywords": [
|
||||||
|
"ldap",
|
||||||
|
"orchestra",
|
||||||
|
"plugin",
|
||||||
|
"xen",
|
||||||
|
"xen-orchestra",
|
||||||
|
"xo-server"
|
||||||
|
],
|
||||||
|
"homepage": "https://github.com/vatesfr/xo-server-auth-ldap",
|
||||||
|
"bugs": "https://github.com/vatesfr/xo-server-auth-ldap/issues",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/vatesfr/xo-server-auth-ldap"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "Julien Fontanet",
|
||||||
|
"email": "julien.fontanet@vates.fr"
|
||||||
|
},
|
||||||
|
"preferGlobal": false,
|
||||||
|
"main": "dist/",
|
||||||
|
"bin": {
|
||||||
|
"xo-server-auth-ldap": "dist/test-cli.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"babel-runtime": "^6.3.19",
|
||||||
|
"event-to-promise": "^0.7.0",
|
||||||
|
"exec-promise": "^0.6.1",
|
||||||
|
"fs-promise": "^1.0.0",
|
||||||
|
"inquirer": "^2.0.0",
|
||||||
|
"ldapjs": "^1.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-cli": "^6.3.17",
|
||||||
|
"babel-eslint": "^7.0.0",
|
||||||
|
"babel-plugin-transform-runtime": "^6.3.13",
|
||||||
|
"babel-preset-latest": "^6.16.0",
|
||||||
|
"babel-preset-stage-0": "^6.3.13",
|
||||||
|
"cross-env": "^3.1.3",
|
||||||
|
"dependency-check": "^2.5.1",
|
||||||
|
"husky": "^0.12.0",
|
||||||
|
"rimraf": "^2.5.4",
|
||||||
|
"standard": "^8.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||||
|
"clean": "rimraf dist/",
|
||||||
|
"commit-msg": "npm test",
|
||||||
|
"depcheck": "dependency-check ./package.json",
|
||||||
|
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||||
|
"lint": "standard",
|
||||||
|
"posttest": "npm run lint && npm run depcheck",
|
||||||
|
"prebuild": "npm run clean",
|
||||||
|
"predev": "npm run clean",
|
||||||
|
"prepublish": "npm run build"
|
||||||
|
},
|
||||||
|
"babel": {
|
||||||
|
"plugins": [
|
||||||
|
"transform-runtime"
|
||||||
|
],
|
||||||
|
"presets": [
|
||||||
|
"latest",
|
||||||
|
"stage-0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"standard": {
|
||||||
|
"ignore": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"parser": "babel-eslint"
|
||||||
|
}
|
||||||
|
}
|
262
packages/xo-server-auth-ldap/src/index.js
Normal file
262
packages/xo-server-auth-ldap/src/index.js
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
/* eslint no-throw-literal: 0 */
|
||||||
|
|
||||||
|
import eventToPromise from 'event-to-promise'
|
||||||
|
import { createClient } from 'ldapjs'
|
||||||
|
import { escape } from 'ldapjs/lib/filters/escape'
|
||||||
|
import { readFile } from 'fs-promise'
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
const bind = (fn, thisArg) => function () {
|
||||||
|
return fn.apply(thisArg, arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
const noop = () => {}
|
||||||
|
|
||||||
|
export const promisify = (fn, thisArg) => function () {
|
||||||
|
const { length } = arguments
|
||||||
|
const args = new Array(length + 1)
|
||||||
|
for (let i = 0; i < length; ++i) {
|
||||||
|
args[i] = arguments[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
args[length] = (error, result) => error
|
||||||
|
? reject(error)
|
||||||
|
: resolve(result)
|
||||||
|
|
||||||
|
fn.apply(thisArg || this, args)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
const VAR_RE = /\{\{([^}]+)\}\}/g
|
||||||
|
const evalFilter = (filter, vars) => filter.replace(VAR_RE, (_, name) => {
|
||||||
|
const value = vars[name]
|
||||||
|
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error('invalid variable: ' + name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return escape(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const configurationSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
uri: {
|
||||||
|
description: 'URI of the LDAP server.',
|
||||||
|
type: 'string'
|
||||||
|
},
|
||||||
|
certificateAuthorities: {
|
||||||
|
description: `
|
||||||
|
Paths to CA certificates to use when connecting to SSL-secured LDAP servers.
|
||||||
|
|
||||||
|
If not specified, it will use a default set of well-known CAs.
|
||||||
|
`.trim(),
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
checkCertificate: {
|
||||||
|
description: 'Enforce the validity of the server\'s certificates. You can disable it when connecting to servers that use a self-signed certificate.',
|
||||||
|
type: 'boolean',
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
bind: {
|
||||||
|
description: 'Credentials to use before looking for the user record.',
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
dn: {
|
||||||
|
description: `
|
||||||
|
Distinguished name of the user permitted to search the LDAP directory for the user to authenticate.
|
||||||
|
|
||||||
|
For Microsoft Active Directory, it can also be \`<user>@<domain>\`.
|
||||||
|
`.trim(),
|
||||||
|
type: 'string'
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
description: 'Password of the user permitted of search the LDAP directory.',
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['dn', 'password']
|
||||||
|
},
|
||||||
|
base: {
|
||||||
|
description: 'The base is the part of the description tree where the users are looked for.',
|
||||||
|
type: 'string'
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
description: `
|
||||||
|
Filter used to find the user.
|
||||||
|
|
||||||
|
For Microsoft Active Directory, you can try one of the following filters:
|
||||||
|
|
||||||
|
- \`(cn={{name}})\`
|
||||||
|
- \`(sAMAccountName={{name}})\`
|
||||||
|
- \`(sAMAccountName={{name}}@<domain>)\`
|
||||||
|
- \`(userPrincipalName={{name}})\`
|
||||||
|
`.trim(),
|
||||||
|
type: 'string',
|
||||||
|
default: '(uid={{name}})'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['uri', 'base']
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
username: {
|
||||||
|
description: 'LDAP username',
|
||||||
|
type: 'string'
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
description: 'LDAP password',
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['username', 'password']
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
class AuthLdap {
|
||||||
|
constructor (xo) {
|
||||||
|
this._xo = xo
|
||||||
|
|
||||||
|
this._authenticate = bind(this._authenticate, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
async configure (conf) {
|
||||||
|
const clientOpts = this._clientOpts = {
|
||||||
|
url: conf.uri,
|
||||||
|
maxConnections: 5,
|
||||||
|
tlsOptions: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
bind,
|
||||||
|
checkCertificate = true,
|
||||||
|
certificateAuthorities
|
||||||
|
} = conf
|
||||||
|
|
||||||
|
if (bind) {
|
||||||
|
clientOpts.bindDN = bind.dn
|
||||||
|
clientOpts.bindCredentials = bind.password
|
||||||
|
}
|
||||||
|
|
||||||
|
const {tlsOptions} = clientOpts
|
||||||
|
|
||||||
|
tlsOptions.rejectUnauthorized = checkCertificate
|
||||||
|
if (certificateAuthorities) {
|
||||||
|
tlsOptions.ca = await Promise.all(
|
||||||
|
certificateAuthorities.map(path => readFile(path))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
bind: credentials,
|
||||||
|
base: searchBase,
|
||||||
|
filter: searchFilter = '(uid={{name}})'
|
||||||
|
} = conf
|
||||||
|
|
||||||
|
this._credentials = credentials
|
||||||
|
this._searchBase = searchBase
|
||||||
|
this._searchFilter = searchFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
load () {
|
||||||
|
this._xo.registerAuthenticationProvider(this._authenticate)
|
||||||
|
}
|
||||||
|
|
||||||
|
unload () {
|
||||||
|
this._xo.unregisterAuthenticationProvider(this._authenticate)
|
||||||
|
}
|
||||||
|
|
||||||
|
test ({ username, password }) {
|
||||||
|
return this._authenticate({
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
}).then(result => {
|
||||||
|
if (result === null) {
|
||||||
|
throw new Error('could not authenticate user')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async _authenticate ({ username, password }, logger = noop) {
|
||||||
|
if (username === undefined || password === undefined) {
|
||||||
|
logger('require `username` and `password` to authenticate!')
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = createClient(this._clientOpts)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Promisify some methods.
|
||||||
|
const bind = promisify(client.bind, client)
|
||||||
|
const search = promisify(client.search, client)
|
||||||
|
|
||||||
|
// Bind if necessary.
|
||||||
|
{
|
||||||
|
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, {
|
||||||
|
name: username
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
response.on('searchEntry', entry => {
|
||||||
|
logger('.')
|
||||||
|
entries.push(entry.json)
|
||||||
|
})
|
||||||
|
|
||||||
|
const {status} = await eventToPromise(response, 'end')
|
||||||
|
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 (error) {
|
||||||
|
logger(`failed to bind as ${entry.objectName}: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger(`could not authenticate ${username}`)
|
||||||
|
return null
|
||||||
|
} finally {
|
||||||
|
client.unbind()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
export default ({xo}) => new AuthLdap(xo)
|
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) => prompt({
|
||||||
|
default: defaultValue,
|
||||||
|
message,
|
||||||
|
name: 'value',
|
||||||
|
type: 'confirm'
|
||||||
|
}).then(_extractValue)
|
||||||
|
|
||||||
|
export const input = (message, {
|
||||||
|
default: defaultValue = null,
|
||||||
|
filter = undefined,
|
||||||
|
validate = undefined
|
||||||
|
} = EMPTY_OBJECT) => prompt({
|
||||||
|
default: defaultValue,
|
||||||
|
message,
|
||||||
|
name: 'value',
|
||||||
|
type: 'input',
|
||||||
|
validate
|
||||||
|
}).then(_extractValue)
|
||||||
|
|
||||||
|
export const list = (message, choices, {
|
||||||
|
default: defaultValue = null
|
||||||
|
} = EMPTY_OBJECT) => prompt({
|
||||||
|
default: defaultValue,
|
||||||
|
choices,
|
||||||
|
message,
|
||||||
|
name: 'value',
|
||||||
|
type: 'list'
|
||||||
|
}).then(_extractValue)
|
||||||
|
|
||||||
|
export const password = (message, {
|
||||||
|
default: defaultValue = null,
|
||||||
|
filter = undefined,
|
||||||
|
validate = undefined
|
||||||
|
} = EMPTY_OBJECT) => prompt({
|
||||||
|
default: defaultValue,
|
||||||
|
message,
|
||||||
|
name: 'value',
|
||||||
|
type: 'password',
|
||||||
|
validate
|
||||||
|
}).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) { // eslint-disable-line no-unmodified-loop-condition
|
||||||
|
await promptItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
n = schema.maxItems || Infinity
|
||||||
|
while (
|
||||||
|
i < n && // eslint-disable-line no-unmodified-loop-condition
|
||||||
|
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