Asymmetrik/crowd-authenticator

View on GitHub
src/crowd-authenticator.js

Summary

Maintainability
A
3 hrs
Test Coverage
let
    crypto = require('crypto'),
    Group = require('atlassian-crowd-client/lib/models/group'),
    User = require('atlassian-crowd-client/lib/models/user');

function allSettled(promise) {
    return promise.then(
        (result) => { return { result: result, status: 'resolved' }; },
        (err) => { return { error: err, status: 'rejected' }; });
}

function nodePromiseResolver() {
    let toResolve, toReject;
    let promise = new Promise((resolve, reject) => {
        toResolve = resolve;
        toReject = reject;
    });

    return {
        nodeResolver: (err, val) => {
            if(null != err) {
                toReject(err);
            }
            else {
                toResolve(val);
            }
        },
        promise: promise
    };
}

function initializeConfig(config) {
    config = config || {};
    config.passwordStrategy = config.passwordStrategy || defaultPasswordStrategy;
    config.groupPrefix = config.groupPrefix || 'crowd-authenticator:';
    config.defaultGroups = config.defaultGroups || [];

    return config;
}

function defaultPasswordStrategy() {
    return Promise.resolve()
        .then(() => {
            let nodePromise = nodePromiseResolver();
            crypto.randomBytes(16, nodePromise.nodeResolver);
            return nodePromise.promise;
        })
        .then((buf) => { return buf.toString('hex'); });
}

function subtractLists(aList, bList) {

    aList = aList || [];
    bList = bList || [];

    // Figure out which layers need to be removed (a - b)
    return aList
        .filter((a) => {
            return !(bList.find((b) => { return ( a === b); }));
        });

}

function validateCrowdUserInfo(userInfo) {
    let err;

    if (null == userInfo) {
        err = 'Missing Crowd UserInfo object';
    }
    else if (null == userInfo.displayname) {
        err = 'Missing Crowd UserInfo.displayname';
    }
    else if (null == userInfo.username) {
        err = 'Missing Crowd UserInfo.username';
    }
    else if (null == userInfo.email) {
        err = 'Missing Crowd UserInfo.email';
    }

    return (null == err) ? err : `Crowd Authenticator Error: ${err}`;
}

module.exports = function(crowdClient, config) {

    config = initializeConfig(config);

    let createCrowdUser = (u) => {

        return config.passwordStrategy()
            .then((password) => {

                // Create a new user
                return new User(u.firstname, u.lastname, u.displayname,
                    u.email, u.username, password,
                    true);

            });

    };

    let getOrCreateCrowdUser = (crowdUserInfo) => {

        return crowdClient.user.get(crowdUserInfo.username, true)
            .catch((err) => {

                // If it's the user not found error, create the user
                if(null != err && err.type === 'USER_NOT_FOUND') {

                    // Create the user
                    return createCrowdUser(crowdUserInfo)
                        .then((user) => {
                            return crowdClient.user.create(user);
                        });

                }
                // Any other error is terminal
                else {
                    throw err;
                }
            });
    };

    let syncCrowdUserGroups = (crowdUserInfo) => {

        // Get all of the user groups from crowd
        return crowdClient.user.groups.list(crowdUserInfo.username)
            .then((groups) => {

                let crowdGroups = groups || [];
                let authGroups = crowdUserInfo.groups || [];

                let crowdAuthGroups = crowdGroups.filter((g) => { return g.startsWith(config.groupPrefix); });
                authGroups = authGroups.map((g) => { return `${config.groupPrefix}${g}`; });

                let defaultGroups = config.defaultGroups;

                let toRemove = [];
                let toAdd = [];

                // Need to remove crowdGroups that start with the prefix but aren't in the authGroups list
                toRemove = toRemove.concat(subtractLists(crowdAuthGroups, authGroups));

                // Need to add auth groups that are not in crowdGroups
                toAdd = toAdd.concat(subtractLists(authGroups, crowdGroups));

                // Need to add default groups that are not in crowd groups
                toAdd = toAdd.concat(subtractLists(defaultGroups, crowdGroups));

                // We need to ensure all the toAdd groups exist
                let createGroupsPromises = toAdd.map((g) => {
                    return crowdClient.group.create(new Group(g));
                });

                // First run all the promises to create the groups
                return Promise.all(createGroupsPromises.map(allSettled))
                    .then(() => {

                        let getPromises = (arr, fn) => {
                            return arr.map((g) => { return fn(crowdUserInfo.username, g); });
                        };

                        let syncGroupPromises =    getPromises(toAdd, crowdClient.user.groups.add)
                                .concat(getPromises(toRemove, crowdClient.user.groups.remove));

                        return Promise.all(syncGroupPromises.map(allSettled));
                    });

            });

    };

    // Return an object that wraps the authenticate object
    return {

        /**
         * Main authenticate call
         * @param authRequest
         */
        authenticate: function(crowdUserInfo) {

            // Validate the authId
            let err = validateCrowdUserInfo(crowdUserInfo);
            if(null != err) {
                return Promise.reject('crowd-authenticator error: Must provide a valid crowdUserInfo object');
            }

            // Try to authenticate the user using the configure auth strategy
            return Promise.resolve()
                .then(() => {

                    /**
                     * The user was authenticated by the auth strategy, so get or create the user
                      */
                    return getOrCreateCrowdUser(crowdUserInfo);

                })
                .then(() => {

                    /**
                     * Sync the user groups
                     */
                    return syncCrowdUserGroups(crowdUserInfo);

                })
                .then(() => {

                    // Authenticate the user and create the session information
                    return crowdClient.session.createUnvalidated(crowdUserInfo.username);

                });

        },

        allSettled: allSettled,
        nodePromiseResolver: nodePromiseResolver,

        validateCrowdUserInfo: validateCrowdUserInfo,
        initializeConfig: initializeConfig,
        defaultPasswordStrategy: defaultPasswordStrategy,
        subtractLists: subtractLists,

        createCrowdUser: createCrowdUser,
        getOrCreateCrowdUser: getOrCreateCrowdUser,
        syncCrowdUserGroups: syncCrowdUserGroups

    };

};