UNPKG

@quarks/quarks-iam

Version:

A modern authorization server built to authenticate your users and protect your APIs

180 lines (152 loc) 5.32 kB
/** * 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