thegameofcode/cipherlayer

View on GitHub
src/managers/user.js

Summary

Maintainability
F
4 days
Test Coverage
'use strict';

const request = require('request');
const _ = require('lodash');
const ciphertoken = require('ciphertoken');
const async = require('async');

const config = require('../../config');
const log = require('../logger/service');
const daoMng = require('./dao');
const tokenMng = require('./token');
const redisMng = require('./redis');
const crypto = require('./crypto');
const cryptoMng = crypto(config.password);
const phoneMng = require('./phone');
const emailMng = require('./email');

const isValidJSON = require('./json_validator');

const ERR_INVALID_PWD = {
    err: 'invalid_password_format',
    code: 400
};

let _settings = {};

// This is Chris's contribution to the coding of this project!!!
const ERR_INVALID_USER_DOMAIN = 'Sorry your email domain is not authorised for this service';

function setPlatformData(userId, platform, data, cbk) {
    daoMng.updateArrayItem(userId, 'platforms', 'platform', data, function (err, updates) {
        if (err) {
            return cbk(err);
        }

        if (updates < 1) {
            return cbk({err: 'platform_not_updated', des: 'updated command worked but no platform were updated'});
        }

        return cbk();
    });
}

function createUser(body, pin, cbk) {
    if (!body[_settings.passThroughEndpoint.username]) {
        return cbk({
            err: 'auth_proxy_error',
            des: 'invalid userinfo',
            code: 400
        });
    }
    body[_settings.passThroughEndpoint.username] = body[_settings.passThroughEndpoint.username].toLowerCase();

    isValidDomain(body[_settings.passThroughEndpoint.username], function (isValid) {
        if (!isValid) {
            const err = {
                err: 'user_domain_not_allowed',
                des: ERR_INVALID_USER_DOMAIN,
                code: 400
            };
            log.warn(err);
            return cbk(err, null);
        }

        if (!body[_settings.passThroughEndpoint.password]) {
            if (!body.sf) {
                return cbk({
                    err: 'invalid_security_token',
                    des: 'you must provide a password or a salesforce token to create the user',
                    code: 400
                });
            }
        } else {
            if (!validatePwd(body.password, _settings.password.regexValidation)) {
                const invalidPasswordError = ERR_INVALID_PWD;
                invalidPasswordError.des = _settings.password.message;
                return cbk(invalidPasswordError);
            }
        }

        const user = {
            username: body[_settings.passThroughEndpoint.username],
            password: body[_settings.passThroughEndpoint.password]
        };

        daoMng.getFromUsername(user.username, function (err, foundUser) {
            if (err && err.message !== daoMng.ERROR_USER_NOT_FOUND) {
                return cbk(err);
            }

            if (foundUser) {
                return cbk({
                    err: 'auth_proxy_user_error',
                    des: 'user already exists',
                    code: 403
                });
            }

            if (body.fb) {
                user.platforms = [{
                    platform: 'fb',
                    accessToken: body.fb.accessToken
                }];
                delete body.fb;
                createUserPrivateCall(body, user, cbk);
                return;
            }

            const phone = body.phone;
            const countryISO = body.country;
            phoneMng(_settings).verifyPhone(user.username, phone, countryISO, pin, function (err) {
                if (err) {
                    return cbk(err);
                }

                if (body.sf) {
                    delete(body[_settings.passThroughEndpoint.password]);
                    tokenMng.getAccessTokenInfo(body.sf, function (err, tokenInfo) {
                        if (err) {
                            return cbk({
                                err: 'invalid_platform_token',
                                des: 'you must provide a valid salesforce token',
                                code: 400
                            });
                        }

                        user.platforms = [{
                            platform: 'sf',
                            accessToken: tokenInfo.data.accessToken,
                            refreshToken: tokenInfo.data.refreshToken,
                            expiry: new Date().getTime() + _settings.salesforce.expiration * 60 * 1000
                        }];
                        return createUserPrivateCall(body, user, cbk);
                    });
                } else {
                    emailMng(_settings).emailVerification(body[_settings.passThroughEndpoint.email || 'email'], body, function (err, destinationEmail) {
                        if (err) {
                            return cbk(err);
                        }
                        if (destinationEmail) {
                            return cbk({
                                des: destinationEmail,
                                code: 200
                            });
                        }
                        return createUserPrivateCall(body, user, cbk);
                    });
                }
            });
        });
    });
}

function createUserByToken(token, cbk) {
    if (!token) {
        return cbk({
            err: 'auth_proxy_error',
            des: 'empty param verifyToken',
            code: 400
        });
    }

    // Decipher the body
    const tokenSettings = {
        cipherKey: _settings.accessToken.cipherKey,
        firmKey: _settings.accessToken.signKey,
        // Same expiration as the redisKey
        tokenExpirationMinutes: _settings.emailVerification.redis.expireInSec
    };

    ciphertoken.getTokenSet(tokenSettings, token, function (err, bodyData) {
        if (err) {
            return cbk(err);
        }
        const body = bodyData.data;
        let profileSchema;

        if (!config.validators) {
            profileSchema = require('./json_formats/profile_create.json');
        } else {
            profileSchema = require((config.validators.profile.path ? config.validators.profile.path : './json_formats/') + config.validators.profile.filename);
        }

        // Validate the current bodyData with the schema profile_create.json
        if (!isValidJSON(body, profileSchema) || !body.transactionId) {
            return cbk({
                err: 'invalid_profile_data',
                des: 'The data format provided is not valid.',
                code: 400
            });
        }
        // Verify the transactionId
        const redisKey = _settings.emailVerification.redis.key.replace('{username}', body[config.passThroughEndpoint.email || 'email']);

        redisMng.getKeyValue(redisKey, function (err, transactionId) {
            if (err) {
                return cbk({
                    err: 'auth_proxy_error',
                    des: 'error getting redis key',
                    code: 403
                });
            }

            if (body.transactionId !== transactionId) {
                return cbk({
                    err: 'invalid_profile_data',
                    des: 'Incorrect or expired transaction.',
                    code: 400
                });
            }

            const user = {
                username: body[_settings.passThroughEndpoint.username],
                password: body[_settings.passThroughEndpoint.password]
            };
            delete(body[_settings.passThroughEndpoint.password]);

            isValidDomain(user.username, function (isValid) {
                if (!isValid) {
                    const domainNotAllowedError = {
                        err: 'user_domain_not_allowed',
                        des: ERR_INVALID_USER_DOMAIN,
                        code: 400
                    };
                    log.warn({err: domainNotAllowedError});
                    return cbk(domainNotAllowedError, null);
                }

                daoMng.getFromUsername(user.username, function (err, foundUser) {
                    if (err && err.message !== daoMng.ERROR_USER_NOT_FOUND) {
                        return cbk({
                            err: 'auth_proxy_error',
                            des: 'error checking user from db',
                            code: 403
                        });
                    }

                    if (foundUser) {
                        if (_settings.emailVerification.errOnUserExists === false) {
                            //do not return an error when the user already exists
                            //this allow the redirect flow for user activation to continue properly
                            //insted of returning a JSON error
                            const data = {};
                            if (foundUser.roles) {
                                data.roles = foundUser.roles;
                            }

                            tokenMng.createBothTokens(foundUser._id, data, function (err, tokens) {
                                if (err) {
                                    log.error({err}, 'error creating tokens');
                                    return cbk({
                                        err: err.message,
                                        code: 409
                                    });
                                }
                                tokens.expiresIn = _settings.accessToken.expiration * 60;
                                return cbk(null, tokens);
                            });
                        } else {
                            return cbk({
                                err: 'auth_proxy_error',
                                des: 'user already exists',
                                code: 403
                            });
                        }
                    } else {
                        delete(body[_settings.passThroughEndpoint.password]);
                        createUserPrivateCall(body, user, cbk);
                    }
                });
            });
        });
    });
}

function createUserPrivateCall(body, user, cbk) {
    const clonedBody = _.clone(body);
    delete clonedBody.password;
    const options = {
        url: `http://${_settings.private_host}:${_settings.private_port}${_settings.passThroughEndpoint.path}`,
        headers: {
            'Content-Type': 'application/json; charset=utf-8'
        },
        method: 'POST',
        body: clonedBody,
        json: true
    };

    log.info({url: options.url}, '=> POST');
    request(options, function (err, private_res, body) {
        if (err) {
            log.error(`<= error: ${err}`);
            return cbk({
                err: 'auth_proxy_error',
                des: 'there was an internal error when redirecting the call to protected service',
                code: 500
            });
        }

        if (private_res.statusCode !== 201) {
            log.warn({request_body: options.body, response_body: body}, 'user creation attempt failed');
            if (body.err && body.des) {
                return cbk({
                    err: body.err,
                    des: body.des,
                    code: private_res.statusCode
                });
            }
            return cbk({
                err: 'user_creation_failed',
                des: body,
                code: 400
            });
        }

        log.info(`<= ${private_res.statusCode}`);
        user.id = body.id;
        user.roles = body.roles || [];

        if (!user.password) {
            user.password = cryptoMng.randomPassword(config.password.generatedRegex);
        }

        cryptoMng.encrypt(user.password, function (encrypted) {
            user.password = encrypted;

            daoMng.addUser(user, function (err, createdUser) {
                if (err) {
                    log.error({err}, 'error adding user to DB');
                    return cbk({
                        err: err.err,
                        des: 'error adding user to DB',
                        code: 409
                    });
                }

                daoMng.getFromUsernamePassword(createdUser.username, createdUser.password, function (err, foundUser) {
                    if (err) {
                        log.error({err}, 'error obtaining user');
                        return cbk({
                            err: err.message,
                            code: 409
                        });
                    }

                    const data = {};
                    if (foundUser.roles) {
                        data.roles = foundUser.roles;
                    }

                    async.series([
                        function (done) {
                            // Add "realms" & "capabilities"
                            daoMng.getRealms(function (err, realms) {
                                if (err) {
                                    log.error({err}, 'error obtaining user realms');
                                    return done();
                                }

                                if (!realms || !realms.length) {
                                    log.info({des: 'there are no REALMS in DB'});
                                    return done();
                                }

                                async.eachSeries(realms, function (realm, next) {
                                    if (!realm.allowedDomains || !realm.allowedDomains.length) {
                                        return next();
                                    }
                                    async.eachSeries(realm.allowedDomains, function (domain, more) {
                                        // wildcard
                                        const check = domain.replace(/\*/g, '.*');
                                        const match = foundUser.username.match(check);
                                        if (!match || foundUser.username !== match[0]) {
                                            return more();
                                        }

                                        if (!data.realms) {
                                            data.realms = [];
                                        }
                                        data.realms.push(realm.name);

                                        async.each(Object.keys(realm.capabilities), function (capName, added) {
                                            if (!data.capabilities) {
                                                data.capabilities = {};
                                            }

                                            data.capabilities[capName] = realm.capabilities[capName];
                                            added();
                                        }, more);
                                    }, next);
                                }, done);
                            });
                        }
                    ], function () {
                        tokenMng.createBothTokens(foundUser._id, data, function (err, tokens) {
                            if (err) {
                                log.error({err}, 'error creating tokens');
                                return cbk({
                                    err: err.message,
                                    code: 409
                                });
                            }
                            tokens.expiresIn = _settings.accessToken.expiration * 60;
                            return cbk(null, tokens);
                        });
                    });

                });
            });
        });

    });
}

function setPassword(id, body, cbk) {
    if (!body.password) {
        return cbk({
            err: 'auth_proxy_error',
            des: 'invalid body request',
            code: 400
        });
    }

    if (!validatePwd(body.password, _settings.password.regexValidation)) {
        const err = ERR_INVALID_PWD;
        err.des = _settings.password.message;
        return cbk(err);
    }
    cryptoMng.encrypt(body.password, function (encryptedPwd) {
        daoMng.updateField(id, 'password', encryptedPwd, function (err) {
            return cbk(err, 1);
        });
    });
}

function validateOldPassword(username, oldPassword, cbk) {

    daoMng.getAllUserFields(username, function (err, user) {
        if (err) {
            return cbk(err);
        }

        cryptoMng.encrypt(oldPassword, function (encrypted) {
            if (user.password !== encrypted) {
                return cbk({
                    err: 'invalid_old_password',
                    des: 'invalid password',
                    code: 401
                });
            }

            return cbk();
        });
    });
}

function isValidDomain(email, cbk) {
    // settings overrides realms configuration
    if (_settings.allowedDomains) {
        for (let i = 0; i < _settings.allowedDomains.length; i++) {
            const domain = _settings.allowedDomains[i];

            // wildcard
            const check = domain.replace(/\*/g, '.*');
            const match = email.match(check);
            if ((match !== null && email === match[0])) {
                return cbk(true);
            }
        }
    }

    // if domain is not override on settings, we look for db realms
    daoMng.getRealms(function (err, realms) {
        if (err) {
            log.error({err}, 'error finding realms in db');
            return cbk(false);
        }

        if ((!realms || !realms.length)) {
            return cbk(false);
        }

        for (let i in realms) {
            const realm = realms[i];
            if (!realm.allowedDomains || !realm.allowedDomains.length) {
                continue;
            }

            for (let j in realm.allowedDomains) {
                // wildcard
                const domain = realm.allowedDomains[j];
                const check = domain.replace(/\*/g, '.*');
                const match = email.match(check);

                if (match !== null && email === match[0]) {
                    return cbk(true);
                }
            }
        }
        return cbk(false);
    });
}

function removeRealmFromUser(userId, name, cbk) {
    daoMng.removeFromArrayFieldById(userId, 'realms', name, function (err) {
        if (err) {
            return cbk(err);
        }
        return cbk();
    });
}

function addRealmToUser(userId, name, cbk) {
    async.waterfall([
        function (done) {
            daoMng.getRealmFromName(name, function (err, realm) {
                if (err) {
                    return done(err);
                }
                return done(null, realm);
            });
        },
        function (realm, done) {
            daoMng.addToArrayFieldById(userId, 'realms', realm.name, function (err, added) {
                if (err) {
                    return done(err);
                }
                if (added !== 1) {
                    return done({
                        err: 'realm not added to user',
                        code: 400
                    });
                }
                return done();
            });
        }
    ], function (err) {
        if (err) {
            log.error({err});
            return cbk(err);
        }
        return cbk();
    });
}

function validatePwd(pwd, regexp) {
    return (new RegExp(regexp)).test(pwd);
}

module.exports = function (settings) {
    _settings = _.assign({}, config, settings);

    return {
        setPlatformData,
        createUser,
        createUserByToken,
        setPassword,
        validateOldPassword,
        removeRealmFromUser,
        addRealmToUser
    };
};