@quarks/quarks-iam
Version:
A modern authorization server built to authenticate your users and protect your APIs
180 lines (152 loc) • 5.32 kB
JavaScript
/**
* Module dependencies
*/
var async = require('async')
var ADStrategy = require('passport-adauth').Strategy
var LdapStrategy = require('passport-ldapauth').Strategy
var User = require('../models/User')
var Role = require('../models/Role')
/**
* Regexes for utility functions
*/
var domainDnRegex = /,?\s*((?:dc=[^,=\s][^,=]+[^,=\s],?\s*){2,})$/i
var dnPartRegex = /([a-z]+)=([^,\\ ][^,\\]*(?:\\.[^,\\]*)*)/gi
var badExtra = /[^,\s]/g
/**
* Extracts a lowercased domain from a distinguished name for comparison
* purposes.
*
* e.g. "CN=User,OU=MyBusiness,DC=example,DC=com" will return "example.com."
*/
function dnToDomain (dn) {
if (!dn || typeof dn !== 'string') { return null }
var matches = domainDnRegex.exec(dn)
if (matches) {
return matches[1].replace(dnPartRegex, '$1.').toLowerCase()
} else {
return null
}
}
/**
* Normalizes distinguished names, removing any extra whitespace, lowercasing
* the DN, and running some basic validation checks.
*
* If the DN is found to be malformed, will return null.
*/
function normalizeDN (dn) {
if (!dn || typeof dn !== 'string') { return null }
var normalizedDN = ''
// Take advantage of replace() to grab the matched segments of the DN. If what
// is left over contains unexpected characters, we assume the original DN was
// malformed.
var extra = dn.replace(dnPartRegex, function (match) {
normalizedDN += match.toLowerCase() + ','
return ''
})
if (!normalizedDN) { return null }
if (badExtra.test(extra)) { return null }
return normalizedDN.substr(0, normalizedDN.length - 1)
}
/**
* Verifier
*/
function verifier (provider, config) {
return function (req, user, done) {
user.id = (config.serverType || provider.serverType) === 'AD'
? user.objectGUID : user[provider.mapping.id]
User.connect(req, null, user, function (err, connectUser, info) {
if (err) { return done(err) }
if (connectUser && connectUser._groups) {
// Put the distinguished names of the directory server groups the user is
// in into an array.
var rolesToAdd = user._groups.map(function (group) {
return group.dn
})
var rolesToRemove = []
// Create an object which maps normalized DNs to their original format
var ldapGroupDNs = {}
rolesToAdd.forEach(function (dn) {
ldapGroupDNs[normalizeDN(dn)] = dn
})
Role.listByUsers(connectUser, function (err, roles) {
if (err) { return done(err) }
var userDomain = dnToDomain(user.dn)
roles.forEach(function (role) {
if (role) {
var roleDN = normalizeDN(role.name)
// Only modify existing user roles if they are:
// * A valid distinguished name
// * In the same domain as the directory server
if (roleDN && dnToDomain(role.name) === userDomain) {
if (ldapGroupDNs[roleDN]) {
rolesToAdd.splice(rolesToAdd.indexOf(ldapGroupDNs[roleDN]), 1)
} else {
rolesToRemove.push(role.name)
}
}
}
})
// Does not create roles if they have not been defined yet in Connect.
// This is intentional. It allows Connect's roles to remain free of
// clutter, holding only the directory server groups that are needed as
// roles in Connect.
//
// Likewise, if a group is deleted or renamed in the directory server,
// it is up to the administrator to remove or update the related role in
// Connect.
async.parallel([
function (next) {
async.each(rolesToAdd, function (roleName, callback) {
User.addRoles(connectUser, roleName, function (err, result) {
if (err) { return callback(err) }
rolesToAdd.splice(rolesToAdd.indexOf(roleName), 1)
callback()
})
}, next)
},
function (next) {
async.each(rolesToRemove, function (roleName, callback) {
User.removeRoles(connectUser, roleName, function (err, result) {
if (err) { return callback(err) }
rolesToRemove.splice(rolesToRemove.indexOf(roleName), 1)
callback()
})
}, next)
}
], function (err) {
done(err, connectUser, info)
})
})
} else if (connectUser) {
done(err, connectUser, info)
} else {
done(null, null, info)
}
})
}
}
LdapStrategy.verifier = verifier
/**
* Initialize
*/
function initialize (provider, configuration) {
var serverType = configuration.serverType || provider.serverType || 'LDAP'
var strategy
if (serverType === 'AD') {
strategy = new ADStrategy(
{ server: configuration, passReqToCallback: true },
verifier(provider, configuration)
)
} else {
strategy = new LdapStrategy(
{ server: configuration, passReqToCallback: true },
verifier(provider, configuration)
)
}
return strategy
}
LdapStrategy.initialize = initialize
/**
* Exports
*/
module.exports = LdapStrategy