New implementation.

This commit is contained in:
Julien Fontanet 2015-04-01 16:08:40 +02:00
parent 4617025bd4
commit f2e7963e1f
3 changed files with 110 additions and 22 deletions

View File

@ -19,10 +19,38 @@ of XO-Server:
plugins:
auth-ldap:
uri: "ldap://ldap.example.org",
uri: "ldap://ldap.example.org"
# Credentials to use before looking for the user record.
#
# Default to anonymous.
bind:
# Distinguished name of the user permitted to search the LDAP
# directory for the user to authenticate.
dn: 'cn=admin,ou=people,dc=example,dc=org'
# Password of the user permitted to search the LDAP directory.
password: 'secret'
# The base is the part of the directory tree where the users are
# looked for.
base: "ou=people,dc=example,dc=org"
# Filter used to find the user.
#
# Default is `'(uid={{name}})'`.
#filter: '(uid={{name}})'
```
## 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
### Installing dependencies

View File

@ -25,6 +25,7 @@
"dependencies": {
"babel-runtime": "^4",
"bluebird": "^2.9.21",
"event-to-promise": "^0.3.2",
"ldapjs": "^0.7.1"
},
"devDependencies": {

View File

@ -1,40 +1,99 @@
import Bluebird from 'bluebird'
import Bluebird, {coroutine, promisify} from 'bluebird'
import eventToPromise from 'event-to-promise'
import {createClient} from 'ldapjs'
import {escape} from 'ldapjs/lib/filters/escape'
// ===================================================================
const VAR_RE = /\{\{([^}]+)\}\}/g
function evalFilter (filter, vars) {
return filter.replace(VAR_RE, (_, name) => {
const value = vars[name]
if (value === undefined) {
throw new Error('invalid variable: ' + name)
}
return escape(value)
})
}
function createAuthenticator (conf) {
}
// ===================================================================
class AuthLdap {
constructor (conf) {
const base = conf.base ? ',' + conf.base : ''
const clientOpts = {
url: conf.uri
url: conf.uri,
maxConnections: 5
}
this._provider = (credentials) => {
const {username, password} = credentials
{
const {bind} = conf
if (bind) {
clientOpts.bindDN = bind.dn
clientOpts.bindCredentials = bind.password
}
}
const {base: searchBase} = conf
const searchFilter = conf.filter || '(uid={{name}})'
this._provider = coroutine(function * ({username, password}) {
if (username === undefined || password === undefined) {
return Bluebird.reject(new Error('invalid credentials'))
throw null
}
return new Bluebird((resolve, reject) => {
const client = createClient(clientOpts)
const client = createClient(clientOpts)
client.bind(
'uid=' + escape(username) + base,
password,
(error) => {
if (error) {
reject(error)
} else {
resolve({ username })
}
try {
// Promisify some methods.
const bind = promisify(client.bind, client)
const search = promisify(client.search, client)
client.unbind()
// Bind if necessary.
{
const {bind: credentials} = conf
if (credentials) {
yield bind(credentials.dn, credentials.password)
}
)
})
}
}
// Search for the user.
const entries = []
{
const response = yield search(searchBase, {
scope: 'sub',
filter: evalFilter(searchFilter, {
name: username
})
})
response.on('searchEntry', entry => {
entries.push(entry.json)
})
const {status} = yield eventToPromise(response, 'end')
if (status) {
throw new Error('unexpected search response status: ' + status)
}
}
// Try to find an entry which can be bind with the given password.
for (let entry of entries) {
try {
yield bind(entry.objectName, password)
return { username }
} catch (error) {}
}
throw null
} finally {
client.unbind()
}
})
}
load (xo) {