backstrap-server
Version:
Backstrap is a lightweight framework for building APIs
1,318 lines (1,257 loc) • 45.1 kB
JavaScript
const util = require('util');
const fs = require('fs');
const crypto = require('crypto');
const path = require('path');
const rootDir = path.dirname(require.main.filename);
const jwt = require('./jwt.js');
var AccessControlExtension;
try {
AccessControlExtension = require(`${rootDir}/accessControl_ext.js`);
}
catch(e) {
console.error('INITIALIZATION ERROR -- accessControl_ext.js');
throw(e);
}
const permissions = {
some: 'some',
all: 'all'
}
class AccessControl {
constructor() {
this.utilities = null;
this.settings = null;
this.dataAccess = null;
this.roles = null;
this.file = null;
this.authSigningKey = null;
this.extension = null;
}
init(util, s, d, f) {
return new Promise((resolve, reject) => {
this.utilities = util;
this.settings = s;
this.dataAccess = d;
this.extension = new AccessControlExtension(this);
this.file = f;
try {
if(this.file.indexOf(rootDir) !== 0) this.file = `${rootDir}/${this.file}`;
let fileData = require(this.file);
this.roles = fileData['roles'];
if(this.settings.identity && this.settings.identity.provider && this.settings.identity.provider.toLowerCase() === 'auth0') {
let keyUrl = this.settings.identity.key_url || null;
let kid = this.settings.identity.kid || null;
jwt.getKey(keyUrl, kid)
.then((key) => {
this.authSigningKey = key;
Object.freeze(this);
resolve(true);
})
.catch((keyErr) => {
let errorObj = new ErrorObj(500,
'ac0020',
__filename,
'init',
'problem getting signing key from auth0',
'Initialization Failure. Please contact your administrator.',
keyErr);
reject(errorObj);
});
}
else {
Object.freeze(this);
resolve(true);
}
}
catch (e) {
let errorObj = new ErrorObj(403,
'ac0001',
__filename,
'init',
'unauthorized',
'You are not authorized to access this endpoint',
null);
reject(errorObj);
}
});
}
async createUser(userType, params, apiToken, thisUser) {
return new Promise((resolve, reject) => {
if(params) {
var username = params.username || params.email;
var email = params.email || null;
var password = params.password || null;
var roles = params.roles || null;
var exid = params.external_id || null;
}
if(userType !== 'api' || thisUser == null) {
var validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
if (email == null || !validEmailRegex.test(email)) {
var errorObj = new ErrorObj(500,
'ac0329',
__filename,
'createUser',
'invalid email address'
);
reject(errorObj);
return;
}
}
const createAPI = this.#createAPIUser.bind(this);
const createExt = this.#createExternalAPIUser.bind(this);
const createStd = this.#createStandardUser.bind(this);
var createUserCmd;
switch(userType) {
case 'api':
let uid = null;
if(thisUser) uid = thisUser.id;
createUserCmd = createAPI(email, roles, uid);
break;
case 'external-api':
createUserCmd = createExt(email, exid, roles);
break;
default:
createUserCmd = createStd(username, email, password, exid, roles, apiToken);
}
createUserCmd
.then((userObj) => {
resolve(userObj);
})
.catch((err) => {
typeof(err.AddToError) === 'function' ?
reject(err.AddToError(__filename, 'createUser'))
:
reject(new ErrorObj(500,
'ac0300',
__this.authSigningKeyname,
'createUser',
'create user error',
'There was a problem creating an account. Please try again.',
err
));
});
});
}
async signIn(params, apiToken) {
return new Promise((resolve, reject) => {
var username = params.username || null;
var email = params.email || null;
var password = params.password || null;
var token = params.token || null;
// CHECK THE ARGUMENTS PASSED IN AND GET THE USER FROM OUR DB.
// IF USERNAME/PASSWORD IS USED, GET THE USER VIA USERNAME
// IF TOKEN IS USED, CHECK THAT THE TOKEN IS VALID, THEN EXTRACT EXTERNAL ID TO
// GET THE USER FROM OUR DB
let initialProcessing = new Promise((ipResolve, ipReject) => {
// WE HAVE A TOKEN
if(token) {
let keyUrl = this.settings.identity.key_url || null;
let kid = this.settings.identity.kid || null;
jwt.getKey(keyUrl, kid)
.then((key) => {
return jwt.verifyToken(token, key);
})
.then((decodedToken) => {
let externalId = decodedToken.sub;
return this.dataAccess.getUserByExternalIdentityId(externalId, ['native','external']);
})
.then((usr) => {
ipResolve(usr);
})
.catch((jwtErr) => {
ipReject(jwtErr.AddToError(__name, 'signin'));
});
}
// WE HAVE USERNAME/PASSWORD
else if((username || email) && password) {
let identifier = username ? username : email;
if(identifier) {
this.dataAccess.getUserByUserName(identifier)
.then((usr) => {
// ONLY ADMINS AND SUPERUSERS CAN LOG IN WITH USER/PASSWORD IN NON-NATIVE ACCOUNTS (API USERS DON'T SIGN IN)
if(['native'].includes(usr.account_type) || usr.roles.includes('super-user') || usr.roles.includes('admin-user')) {
ipResolve(usr);
}
else {
let errorObj = new ErrorObj(401,
'ac0104',
__filename,
'signin',
'unauthorized',
'invalid credentials'
);
ipReject(errorObj);
}
})
.catch((usrErr) => {
let errorObj = new ErrorObj(401,
'ac0105',
__filename,
'signin',
'unauthorized',
'invalid credentials'
);
ipReject(errorObj);
})
}
else {
// NO USERNAME
let errorObj = new ErrorObj(401,
'ac0103',
__filename,
'signin',
'unauthorized',
'invalid credentials'
);
ipReject(errorObj);
}
}
else {
let errorObj = new ErrorObj(401,
'ac0102',
__filename,
'signin',
'unauthorized',
'invalid credentials'
);
ipReject(errorObj);
}
});
initialProcessing
.then((userObj) => {
// IF THIS IS A USERNAME/PASSWORD SIGNIN, CHECK THE CREDENTIALS AGAINST OUR DB
// IF THIS IS A TOKEN SIGNIN, JUST PASS THROUGH
return new Promise((innerResolve, innerReject) => {
// NATIVE SIGNIN -- USE OUR DB
if(this.settings.identity == null || this.settings.identity.length === 0 ||
(this.settings.identity.provider != null && this.settings.identity.provider.toLowerCase() === 'native')) {
this.checkCredentials(password, userObj)
.then(() => {
innerResolve(userObj);
})
.catch((credErr) => {
// JUST PASS ALONG THE ERROR, WE'LL MARK IT UP IN THE MAIN FAIL BLOCK OF THIS FUNCTION
innerReject(credErr);
});
}
// EXTERNAL SIGNIN -- TOKEN HAS ALREADY BEEN CHECKED, JUST PASS ALONG
else if(this.settings.identity.provider != null && this.settings.identity.provider.toLowerCase() === 'auth0') {
if(token) {
innerResolve(userObj);
}
// WE'RE USING EXTERNAL SIGNIN, BUT THIS IS A NATIVE ACCOUNT OR AN ADMIN/SUPERUSER USING USERNAME/PASSWORD
else if((userObj.account_type === 'native' || userObj.roles.includes('super-user') || userObj.roles.includes('admin-user')) && ((username || email) && password)) {
this.checkCredentials(password, userObj)
.then(() => {
innerResolve(userObj);
})
.catch((credErr) => {
// JUST PASS ALONG THE ERROR, WE'LL MARK IT UP IN THE MAIN FAIL BLOCK OF THIS FUNCTION
innerReject(credErr);
});
}
else {
// INSUFFICIENT CREDENTIALS
let errorObj = new ErrorObj(401,
'ac0103',
__filename,
'signin',
'unauthorized',
'invalid credentials'
);
innerReject(errorObj);
}
}
else {
// UNKNOWN IDENTITY PROVIDER
let errorObj = new ErrorObj(500,
'ac0101',
__filename,
'signin',
'Unknown identity provider',
'Ask your administrator to set up a known identity provider.'
);
innerReject(errorObj);
}
});
})
.then((userObj) => {
// START UP A SESSION
return Promise.all([userObj, this.startSession(userObj, params.clientInfo)]);
})
.then(([userObj, sess]) => {
return Promise.all([userObj, sess.token, this.validateToken(apiToken, true)]);
})
.then(([userObj, tkn, validTokenRes]) => {
var sess = null;
if (validTokenRes.is_valid === true && validTokenRes.session.anonymous === true) {
sess = validTokenRes.session;
return Promise.all([userObj, tkn, this.dataAccess.attachUserToSession(userObj.id, sess.id)]);
}
else {
return [userObj, tkn];
}
})
.then(([userObj, tkn]) => {
userObj[this.settings.token_header] = tkn;
resolve(userObj);
})
.catch((err) => {
let errorObj = new ErrorObj(401,
'ac0100',
__filename,
'signin',
'unauthorized',
'invalid credentials'
);
reject(errorObj);
});
});
}
async forgotPassword(email, username) {
return new Promise(async (resolve, reject) => {
let userObj = null;
this.dataAccess.findUser(null, username, email)
.then((userObjs) => {
return new Promise((validResolve, validReject) => {
if(userObjs.length === 1) {
userObj = userObjs[0];
if (userObj.locked) {
let errorObj = new ErrorObj(403,
'ac2006',
__filename,
'forgotPassword',
'bsuser is locked',
'Unauthorized',
null
);
validReject(errorObj);
}
else {
this.utilities.getHash(null,null,48)
.then((hash) => {
validResolve([userObj, hash]);
})
}
}
else {
let errorObj = new ErrorObj(404,
'ac2008',
__filename,
'forgotPassword',
'no user',
'That user could not be found.',
null
);
validReject(errorObj);
}
});
})
.then(([userObj, tkn]) => {
return [userObj, tkn, this.dataAccess.updateCredentialsForUser(userObj.id, null, null, tkn)];
})
.then(([userObj, tkn, saveRes]) => {
resolve({'user': userObj, 'token': tkn});
})
.catch((err) => {
if(err != null && err.err_code == 'da0200'){
resolve(null);
}
else if (err != null && typeof (err.AddToError) == 'function') {
err.setMessages('error with forgotPassword', 'Problem starting the password reset process');
reject(err.AddToError(__filename, 'forgotPassword'));
}
else {
let errorObj = new ErrorObj(500,
'ac1032',
__filename,
'forgotPassword',
'error with forgotPassword',
'Problem starting the password reset process',
err
);
reject(errorObj);
}
});
});
}
async checkCredentials(password, userObj) {
return new Promise((resolve, reject) => {
// IF USER IS LOCKED, BAIL OUT
if (userObj.locked) {
var errorObj = new ErrorObj(403,
'ac0300',
__filename,
'signin',
'Account is locked',
'Unauthorized',
null
);
reject(errorObj);
return;
}
// GOT A USER, GRAB THE ACTIVE CREDS
this.dataAccess.getCredentialsForUser(userObj.id)
.then((credRes) => {
let foundValidCreds = false;
for(let cIdx = 0; cIdx < credRes.length; cIdx++) {
let curCred = credRes[cIdx];
if(curCred.password != null && curCred.salt != null) {
let salt = curCred.salt;
let stored_password = curCred.password;
// SALT AND HASH PASSWORD
var saltedPassword = password + salt;
var hashedPassword = crypto.createHash('sha256').update(saltedPassword).digest('hex');
// CHECK IF HASHES MATCH
if (hashedPassword === stored_password) {
foundValidCreds = true;
break;
}
}
}
if(foundValidCreds) {
resolve();
}
else {
var errorObj = new ErrorObj(401,
'a0027',
__filename,
'checkCredentials',
'authentication failed'
);
reject(errorObj);
}
})
.catch((err) => {
let errorObj = new ErrorObj(401,
'a0027',
__filename,
'checkCredentials',
'authentication failed'
);
let eLogObj = JSON.parse(JSON.stringify(errorObj));
eLogObj.results = err;
this.utilities.writeErrorToLog(eLogObj);
reject(errorObj);
});
});
}
async startSession(userObj, clientInfo) {
return new Promise((resolve, reject) => {
this.#getSessionToken()
.then((tkn) => {
if(userObj != null) {
return this.dataAccess.startSession(tkn, userObj.id, clientInfo, false);
}
else {
return this.dataAccess.startSession(tkn, null, clientInfo, true);
}
})
.then((newSess) => {
resolve(newSess);
})
.catch((err) => {
if(err && typeof(err.AddToError) === 'function') {
reject(err.AddToError(__filename, 'startSession'));
}
else {
let errorObj = new ErrorObj(500,
'ac0200',
__filename,
'startSession',
'error starting session',
'There was a problem with your request. Please try again.',
err
);
reject(errorObj);
}
})
});
}
async reload() {
var ac = this;
return new Promise((resolve, reject) => {
ac.init(this.file)
.then((res) => {
resolve(res);
})
.catch((err) => {
let errorObj = new ErrorObj(500,
'ac0003',
__filename,
'reload',
'error while reloading access control'
);
reject(errorObj);
});
});
}
async save() {
return new Promise((resolve, reject) => {
let fileData = { roles: this.roles };
var fswrite = util.promisify(fs.writeFile);
fswrite(this.file, JSON.stringify(fileData, null, 4))
.then((write_res) => {
resolve(true);
})
.catch((err) => {
let errorObj = new ErrorObj(500,
'ac0004',
__filename,
'save',
'error writing to Security config file'
);
reject(errorObj);
});
});
}
async validateToken(tkn, continueWhenInvalid) {
return new Promise((resolve, reject) => {
if (tkn === undefined || tkn === null) {
if(continueWhenInvalid) {
resolve({is_valid: false});
}
else {
var errorObj = new ErrorObj(401,
'ac0005',
__filename,
'validateToken',
'no token provided'
);
reject(errorObj);
}
return;
}
this.dataAccess.getSession(null, tkn)
.then((sess) => {
if(sess) {
resolve({is_valid:true, session:sess});
}
else {
if(continueWhenInvalid) {
resolve({is_valid: false});
}
else {
let errorObj = new ErrorObj(401,
'ac1005',
__filename,
'validateToken',
'could not find session for this token',
'unauthorized',
null
);
reject(errorObj);
}
}
})
.catch((err) => {
if(continueWhenInvalid) {
resolve({is_valid: false});
}
else {
if (err !== undefined && err !== null && typeof (err.AddToError) === 'function') {
err.setStatus(401);
err.setMessages('could not find session for this token', 'unauthorized');
reject(err.AddToError(__filename, 'validateToken', 'could not find session for this token'));
}
else {
let errorObj = new ErrorObj(401,
'ac1004',
__filename,
'validateToken',
'could not find session for this token',
'unauthorized',
err
);
reject(errorObj);
}
}
});
});
}
async validateBasicAuth(authHeader, continueWhenInvalid) {
return new Promise((resolve, reject) => {
let [authType, authToken] = authHeader.split(' ');
if(authType.toLowerCase() === 'basic') {
let [clientId, clientSecret] = Buffer.from(authToken, 'base64').toString().split(':');
if(clientId && clientSecret) {
this.dataAccess.getUserByClientId(clientId, true)
.then((usr) => {
if(!usr.locked) {
let saltedSecret = clientSecret + usr.salt;
let hashedClientSecret = crypto.createHash('sha256').update(saltedSecret).digest('hex');
if(hashedClientSecret === usr.client_secret) {
// VALID
resolve({is_valid: true, client_id: clientId});
}
else {
if(continueWhenInvalid) {
resolve({is_valid: false});
}
else {
let errorObj = new ErrorObj(401,
'ac1009',
__filename,
'validateBasicAuth',
'Authentication Error',
'unauthorized',
null
);
reject(errorObj);
}
}
}
else {
if(continueWhenInvalid) {
resolve({is_valid: false});
}
else {
let errorObj = new ErrorObj(401,
'ac1008',
__filename,
'validateBasicAuth',
'User is locked',
'unauthorized',
null
);
reject(errorObj);
}
}
})
.catch((usrErr) => {
if(continueWhenInvalid) {
resolve({is_valid: false});
}
else {
let errorObj = new ErrorObj(401,
'ac1007',
__filename,
'validateBasicAuth',
'Authentication Error',
'unauthorized',
null
);
reject(errorObj);
}
})
}
else {
if(continueWhenInvalid) {
resolve({is_valid: false});
}
else {
let errorObj = new ErrorObj(401,
'ac1006',
__filename,
'validateBasicAuth',
'Malformed basic auth',
'unauthorized',
null
);
reject(errorObj);
}
}
}
else {
if(continueWhenInvalid) {
resolve({is_valid: false});
}
else {
let errorObj = new ErrorObj(401,
'ac1005',
__filename,
'validateBasicAuth',
'Malformed basic auth',
'unauthorized',
null
);
reject(errorObj);
}
}
});
}
async validateJwt(authHeader, continueWhenInvalid) {
return new Promise((resolve, reject) => {
let [authType, authToken] = authHeader.split(' ');
if(authType.toLowerCase() === 'bearer') {
jwt.verifyToken(authToken, this.authSigningKey)
.then((decodedToken) => {
let externalId = decodedToken.sub;
return this.dataAccess.getUserByExternalIdentityId(externalId, ['external-api']);
})
.then((usr) => {
if(usr.locked !== true) {
resolve({is_valid: true, user: usr});
}
else {
if(continueWhenInvalid) {
resolve({is_valid: false});
}
else {
let errorObj = new ErrorObj(401,
'ac1108',
__filename,
'validateJwt',
'User is locked',
'unauthorized',
null
);
reject(errorObj);
}
}
})
.catch((err) => {
if(continueWhenInvalid) {
resolve({is_valid: false});
}
else {
let errorObj = new ErrorObj(401,
'ac1107',
__filename,
'validateJwt',
'Authentication Error',
'unauthorized',
err
);
reject(errorObj);
}
})
}
else {
if(continueWhenInvalid) {
resolve({is_valid: false});
}
else {
let errorObj = new ErrorObj(401,
'ac1105',
__filename,
'validateJwt',
'Malformed bearer auth',
'unauthorized',
null
);
reject(errorObj);
}
}
});
}
async verifyAccess(req, serviceCall) {
return new Promise((resolve, reject) => {
var userObj = req.this_user;
if(userObj == null) {
let errorObj = new ErrorObj(403,
'ac0010',
__filename,
'verifyAccess',
'no user object found on the request',
'Unauthorized',
null
);
reject(errorObj);
return;
}
if(userObj.locked) {
let errorObj = new ErrorObj(403,
'ac0009',
__filename,
'verifyAccess',
'bsuser is locked',
'Unauthorized',
null
);
reject(errorObj);
return;
}
var accessGranted = false;
if (userObj.hasOwnProperty('roles')) {
userRolesLoop:
for (var roleIdx = 0; roleIdx < userObj.roles.length; roleIdx++) {
var userRole = userObj.roles[roleIdx];
allRolesLoop:
for (var allRolesIdx = 0; allRolesIdx < this.roles.length; allRolesIdx++) {
var securityRole = this.roles[allRolesIdx];
if (userRole === securityRole.name) {
areasLoop:
for (var areaIdx = 0; areaIdx < securityRole.areas.length; areaIdx++) {
var area = securityRole.areas[areaIdx];
if (area.name === serviceCall.area) {
if (area.permission === permissions.all) {
accessGranted = true;
break userRolesLoop;
}
else if (area.permission == permissions.some) {
routeLoop:
for (var routeIdx = 0; routeIdx < area.validRoutes.length; routeIdx++) {
var route = area.validRoutes[routeIdx];
if (route.controller === serviceCall.controller && route.version === serviceCall.version) {
if (route.permission === permissions.all) {
accessGranted = true;
break userRolesLoop;
}
else if (route.permission === permissions.some) {
methodLoop:
for (var methodIdx = 0; methodIdx < route.methods.length; methodIdx++) {
var method = route.methods[methodIdx];
if (method.verb === serviceCall.verb && method.call === serviceCall.call) {
accessGranted = true;
break userRolesLoop;
}
}
}
}
}
}
}
}
}
}
}
}
else {
// FAILURE
}
if (accessGranted) {
resolve(true);
}
else {
let errorObj = new ErrorObj(403,
'ac0007',
__filename,
'verifyAccess',
'not authorized to use this endpoint'
);
reject(errorObj);
}
});
}
async roleExists(roleName) {
return new Promise((resolve, reject) => {
roleName = roleName.toLowerCase();
var allRoles = [];
for (var rIdx = 0; rIdx < this.roles.length; rIdx++) {
allRoles.push(this.roles[rIdx].name.toLowerCase());
}
if (allRoles.indexOf(roleName) !== -1) {
resolve(true);
}
else {
var errorObj = new ErrorObj(404,
'ac0008',
__filename,
'roleExists',
'role not found'
);
reject(errorObj);
}
});
}
async grantRoles(userId, roles) {
return new Promise(async (resolve, reject) => {
try {
let didAddRoles = false;
let user = await this.dataAccess.getUserById(userId);
if(user) {
for(let rIdx = 0; rIdx < roles.length; rIdx++) {
let cRole = roles[rIdx];
if(!user.roles.includes(cRole)) {
user.roles.push(cRole);
didAddRoles = true;
}
}
if(didAddRoles) {
await this.dataAccess.updateUserInfo(userId, null, user.roles, null, null, null);
}
}
resolve();
}
catch(err) {
reject(new ErrorObj(500,
'ac0500',
__filename,
'grantRole',
'general error',
'There was a problem with your request',
err));
}
});
}
async revokeRoles(userId, roles) {
return new Promise(async (resolve, reject) => {
try {
let didRemoveRoles = false;
let user = await this.dataAccess.getUserById(userId);
if(user) {
for(let rIdx = 0; rIdx < roles.length; rIdx++) {
let cRole = roles[rIdx].toLowerCase();
if(user.roles.includes(cRole)) {
user.roles.splice(user.roles.indexOf(cRole), 1);
didRemoveRoles = true;
}
}
if(didRemoveRoles) {
await this.dataAccess.updateUserInfo(userId, null, user.roles, null, null, null);
}
}
resolve();
}
catch(err) {
reject(new ErrorObj(500,
'ac0510',
__filename,
'revokeRole',
'general error',
'There was a problem with your request',
err));
}
});
}
async setUserLocked(userId, isLocked) {
return new Promise(async (resolve, reject) => {
try {
let updRes = await this.dataAccess.updateUserInfo(userId, isLocked);
if(updRes.length < 1) {
reject(new ErrorObj(500,
'ac0520',
__filename,
'setUserLocked',
'update error',
'There was a problem locking/unlocking user.',
null));
}
}
catch(err) {
reject(500,
'ac0521',
__filename,
'setUserLocked',
'general error',
'There was a problem locking/unlocking user.',
err);
}
});
}
async getToken() {
return this.#getSessionToken();
}
async #getSessionToken() {
return new Promise((resolve, reject) => {
// DO A COLLISION CHECK. THIS IS PROBABLY OVERKILL SINCE OUR TOTAL POOL IS 256^48
// BUT WE REALLY DON'T WANT TWO SESSIONS WITH THE SAME TOKEN
this.dataAccess.getActiveTokens()
.then((tokens) => {
var tokenIsGood = false;
var token;
while (!tokenIsGood) {
token = crypto.randomBytes(48).toString('hex');
var sessions = tokens.filter(tkn => tkn === token);
if (sessions === null || sessions.length === 0) {
tokenIsGood = true;
}
else {
tokenIsGood = false;
}
}
resolve(token);
})
.catch((err) => {
if (err !== undefined && err !== null && typeof (err.AddToError) == 'function') {
if (err.message === 'no results found') {
var token = crypto.randomBytes(48).toString('hex');
resolve(token);
}
else {
reject(err.AddToError(__filename, 'getToken'));
}
}
else {
let errorObj = new ErrorObj(500,
'ac1000',
__filename,
'getToken',
'error getting token',
'Error getting token',
err
);
reject(errorObj);
}
});
});
}
async #createStandardUser(username, email, password = null, exid = null, roles, apiToken = null) {
return new Promise((resolve, reject) => {
var cryptoCall = util.promisify(crypto.randomBytes);
username = username || email;
roles = roles || ['default-user'];
this.utilities.validateEmail(email)
.then(() => {
return this.utilities.validateUsername(username);
})
.then(() => {
return new Promise((innerResolve, innerReject) => {
if(username && username !== '' && password && password !== '') {
cryptoCall(48)
.then((buf) => {
var salt = buf.toString('hex');
var saltedPassword = password + salt;
var hashedPassword = crypto.createHash('sha256').update(saltedPassword).digest('hex');
var userObj = {
'account_type': 'native',
'username': username,
'email': email,
'salt': salt,
'password': hashedPassword,
'roles': roles
};
if(exid) userObj.external_identity_id = exid;
return this.dataAccess.createUser(userObj);
})
.then((usr) => {
innerResolve(usr);
})
.catch((innerErr) => {
innerReject(innerErr)
})
}
else {
innerReject(new ErrorObj(400,
'ac0350',
__filename,
'createStandardUser',
'insufficient data to create user',
'You must provide username/email & password or an external identity provider id',
null));
}
})
})
.then((userObj) => {
return Promise.all([userObj, this.validateToken(apiToken, true)]);
})
.then(([userObj, validTokenRes]) => {
var sess;
if (validTokenRes.is_valid === true && validTokenRes.session.is_anonymous === true) {
sess = validTokenRes.session;
return Promise.all([userObj, this.dataAccess.attachUserToSession(userObj, sess)]);
}
else {
return [userObj, false];
}
})
.then(([userObj, isNewAnonSess, sessRes]) => {
if (isNewAnonSess) {
let sess = sessRes[0] ? sessRes[0].data : null;
return Promise.all([userObj, this.dataAccess.attachUserToSession(userObj, sess)]);
}
else {
return [userObj];
}
})
.then(([userObj]) => {
delete userObj.password;
delete userObj.salt;
// ADD EVENT TO SESSION
var resolveObj = userObj;
resolve(resolveObj);
})
.catch((err) => {
if (err !== undefined && err !== null && typeof (err.AddToError) == 'function') {
reject(err.AddToError(__filename, 'createStandardUser'));
}
else {
let errorObj = new ErrorObj(500,
'ac0330',
__filename,
'createStandardUser',
'error signing up',
'Error',
err
);
reject(errorObj);
}
});
});
}
async #createAPIUser(email, roles, parentAccountId) {
return new Promise((resolve, reject) => {
roles = roles || ['default-user'];
email = email || null;
let validateEmailOrSkip;
if(parentAccountId) {
validateEmailOrSkip = Promise.resolve(null);
}
else {
var validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
if (!validEmailRegex.test(email)) {
var errorObj = new ErrorObj(500,
'ac0331',
__filename,
'createAPIUser',
'no parent account & invalid email address'
);
reject(errorObj);
return;
}
validateEmailOrSkip = this.utilities.validateEmail(email);
}
validateEmailOrSkip
.then(() => {
return this.#generateApiUserCreds();
})
.then((creds) => {
var saltedSecret = creds.clientSecret + creds.salt;
var hashedSecret = crypto.createHash('sha256').update(saltedSecret).digest('hex');
let nextCmd;
if(parentAccountId){
nextCmd = this.dataAccess.saveApiCredentials(creds.clientId, creds.salt, hashedSecret, parentAccountId);
}
else {
var userObj = {
'account_type': 'api',
'email': email,
'client_id': creds.clientId,
'salt': creds.salt,
'client_secret': hashedSecret,
'roles': roles,
'locked': false
};
nextCmd = this.dataAccess.createUser(userObj);
}
return Promise.all([creds.clientSecret, nextCmd]);
})
.then(([clientSecret, userOrCreds]) => {
if(clientSecret) userOrCreds.client_secret = clientSecret;
if(userOrCreds.hasOwnProperty('email')) {
delete userOrCreds.id;
delete userOrCreds.salt;
resolve(userOrCreds);
}
else {
resolve({client_id: userOrCreds.client_id, client_secret: userOrCreds.client_secret});
}
})
.catch((err) => {
if (err !== undefined && err !== null && typeof (err.AddToError) == 'function') {
reject(err.AddToError(__filename, 'createAPIUser'));
}
else {
let errorObj = new ErrorObj(500,
'ac0332',
__filename,
'POST apiUser',
'error signing up api user',
'Error creating new api user',
err
);
reject(errorObj);
}
});
});
}
async #generateApiUserCreds() {
return new Promise((resolve, reject) => {
var cryptoCall = util.promisify(crypto.randomBytes);
cryptoCall(12)
.then((buf) => {
let clientId = buf.toString('hex');
return Promise.all([clientId, cryptoCall(24)]);
})
.then(([clientId, buf]) => {
let clientSecret = buf.toString('hex');
return Promise.all([clientId, clientSecret, cryptoCall(48)]);
})
.then(([clientId, clientSecret, buf]) => {
let salt = buf.toString('hex');
resolve({clientId: clientId, clientSecret: clientSecret, salt: salt});
})
.catch((err) => {
let errorObj = new ErrorObj(500,
'a1050',
__filename,
'generateApiUserCreds',
'error generating credentials for api user',
'Error generating credentials for api user',
err
);
reject(errorObj);
});
});
}
async #createExternalAPIUser(email, exid, roles) {
return new Promise((resolve, reject) => {
roles = roles || ['default-user'];
email = email || null;
exid = exid || null;
var validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
if (!validEmailRegex.test(email)) {
var errorObj = new ErrorObj(500,
'ac0431',
__filename,
'createExternalAPIUser',
'invalid email address'
);
reject(errorObj);
return;
}
if(exid == null) {
let errorObj = new ErrorObj(500,
'ac0433',
__filename,
'createExternalAPIUser',
'no external identifier'
);
reject(errorObj);
return;
}
this.utilities.validateEmail(email)
.then(() => {
let qry = "SELECT COUNT(*) FROM bs3_users WHERE external_id = $1 AND deleted_at IS NULL";
let params = [exid];
return this.dataAccess.runSql(qry, params);
})
.then((usrRes) => {
if(usrRes[0].count == 0) {
var userObj = {
'account_type': 'external-api',
'email': email,
'roles': roles,
'external_id': exid,
'locked': false
};
return this.dataAccess.createUser(userObj);
}
else {
let errorObj = new ErrorObj(500,
'ac0435',
__filename,
'createExternalUser',
'a user already exists with that external id',
'This external identifier is assigned to another user',
null
);
reject(errorObj);
return;
}
})
.then((userObj) => {
delete userObj.id;
resolve(userObj);
})
.catch((err) => {
if (err !== undefined && err !== null && typeof (err.AddToError) == 'function') {
reject(err.AddToError(__filename, 'createExternalUser'));
}
else {
let errorObj = new ErrorObj(500,
'ac0432',
__filename,
'createExternalUser',
'error signing up external user',
'Error creating new api user',
err
);
reject(errorObj);
}
});
});
}
}
const instance = new AccessControl();
module.exports = instance;