UNPKG

express-gateway

Version:

A microservices API gateway built on top of ExpressJS

365 lines (303 loc) 12.6 kB
const uuid62 = require('uuid62'); const uuidv4 = require('uuid/v4'); const refParser = require('json-schema-ref-parser'); const mergeAllOf = require('json-schema-merge-allof'); const utils = require('../utils'); const config = require('../../config'); const { validate } = require('../../../lib/schemas'); const credentialDao = require('./credential.dao.js'); const s = {}; const dereferencePromise = refParser.dereference(config.models.credentials).then(derefSchema => mergeAllOf(derefSchema)); s.insertScopes = function (scopes) { return validateNewScopes(scopes) .then(newScopes => { if (!newScopes) { return true; // no scopes to insert } return credentialDao.insertScopes(newScopes).then(v => !!v); }); }; s.removeScopes = function (scopes) { return credentialDao.removeScopes(scopes).then(v => !!v); }; s.existsScope = function (scope) { return scope ? credentialDao.existsScope(scope) : Promise.resolve(false); }; s.getAllScopes = function () { return credentialDao.getAllScopes(); }; s.insertCredential = function (id, type, credentialDetails) { credentialDetails = credentialDetails || {}; if (!id || typeof id !== 'string' || !type) { throw new Error('Invalid credentials'); // TODO: replace with validation error } if (!config.models.credentials.properties[type]) { throw new Error(`Invalid credential type: ${type}`); // TODO: replace with validation error } // check if credential already exists const checkSingleCredExistence = () => { return this.getCredential(id, type) .then(cred => { if (cred && cred.isActive) { throw new Error('Credential already exists and is active'); // TODO: replace with validation error } }); }; // TODO: not a good approach, new way TBD const areMultipleCredsAllowed = ['key-auth', 'jwt'].includes(type); const flow = areMultipleCredsAllowed ? dereferencePromise : checkSingleCredExistence().then(() => dereferencePromise); return flow.then(resolvedSchema => { const credentialConfig = resolvedSchema.properties[type]; const newCredential = { isActive: 'true' }; utils.appendCreatedAt(newCredential); utils.appendUpdatedAt(newCredential); if (areMultipleCredsAllowed) { return Promise.all([ validateNewCredentialScopes(credentialConfig, credentialDetails), validateNewCredentialProperties(credentialConfig, credentialDetails) ]).then(([scopes, credentialProps]) => { Object.assign(newCredential, credentialProps); newCredential.keyId = credentialDetails.keyId || uuid62.v4(); newCredential.keySecret = credentialDetails.keySecret || uuid62.v4(); newCredential.scopes = JSON.stringify(scopes); newCredential.consumerId = id; return Promise.all([ credentialDao.insertCredential(newCredential.keyId, type, newCredential), credentialDao.associateCredentialWithScopes(newCredential.keyId, type, scopes) ]); }).then(() => { if (newCredential.scopes && newCredential.scopes.length > 0) { newCredential.scopes = JSON.parse(newCredential.scopes); } if (typeof newCredential.isActive === 'string') { newCredential.isActive = newCredential.isActive === 'true'; } return newCredential; }); } return Promise.all([ validateNewCredentialScopes(credentialConfig, credentialDetails), validateAndHashPassword(credentialConfig, credentialDetails), validateNewCredentialProperties(credentialConfig, credentialDetails) ]) .then(([scopes, { hash, password }, credentialProps]) => { if (scopes) { newCredential.scopes = JSON.stringify(scopes); } newCredential[credentialConfig.properties.passwordKey.default] = hash; delete credentialProps[credentialConfig.properties.passwordKey.default]; Object.assign(newCredential, credentialProps); return Promise.all([ password, credentialDao.insertCredential(id, type, newCredential), credentialDao.associateCredentialWithScopes(id, type, scopes) ]); }).then(([password]) => { const credential = newCredential; delete credential[credentialConfig.properties.passwordKey.default]; credential.id = id; if (password) { credential[credentialConfig.properties.passwordKey.default] = password; } if (credential.scopes && credential.scopes.length > 0) { credential.scopes = JSON.parse(credential.scopes); } if (typeof credential.isActive === 'string') { credential.isActive = credential.isActive === 'true'; } return credential; }); }); }; s.getCredential = function (id, type, options) { if (!id || !type || typeof id !== 'string' || typeof type !== 'string') { throw new Error('invalid credential'); // TODO: replace with validation error } return credentialDao.getCredential(id, type) .then(credential => { if (!credential) { return null; } return processCredential(credential, options); }); }; s.getCredentials = function (consumerId, options) { return credentialDao.getAllCredentials(consumerId) .then(credentials => credentials.map(processCredential)); }; s.deactivateCredential = function (id, type) { if (!id || !type) { throw new Error('invalid credential'); // TODO: replace with validation error } return this.getCredential(id, type) // verify credential exists .then((credential) => { if (credential) { return credentialDao.deactivateCredential(id, type).then(() => true); } else throw new Error('credential does not exist'); // TODO: replace with validation error }); }; s.activateCredential = function (id, type) { if (!id || !type) { throw new Error('invalid credential'); // TODO: replace with validation error } return this.getCredential(id, type) // verify credential exists .then((credential) => { if (credential) { return credentialDao.activateCredential(id, type).then(() => true); } else throw new Error('credential does not exist'); // TODO: replace with validation error }); }; s.updateCredential = function (id, type, properties) { return this.getCredential(id, type) .then((credential) => { if (!credential) { throw new Error('credential does not exist'); // TODO: replace with validation error } return validateUpdatedCredentialProperties(type, properties); }) .then((credentialProperties) => { if (!credentialProperties) { return null; } utils.appendUpdatedAt(credentialProperties); return credentialDao.updateCredential(id, type, credentialProperties); }); }; s.removeCredential = function (id, type) { if (!id || !type) { throw new Error('invalid credential'); // TODO: replace with validation error } return credentialDao.removeCredential(id, type); }; s.removeAllCredentials = function (id) { if (!id) { throw new Error('invalid credential'); // TODO: replace with validation error } return credentialDao.removeAllCredentials(id); }; s.addScopesToCredential = function (id, type, scopes) { return Promise.all([ validateExistingScopes(scopes), this.getCredential(id, type) ]).then(([_scopes, credential]) => { if (!credential) { throw new Error('credential not found'); } const existingScopes = credential.scopes ? (Array.isArray(credential.scopes) ? credential.scopes : [credential.scopes]) : []; // Set has unique items const newScopes = [...new Set(_scopes.concat(existingScopes))]; return Promise.all([ credentialDao.updateCredential(id, type, { scopes: JSON.stringify(newScopes) }), credentialDao.associateCredentialWithScopes(id, type, _scopes) ]).then(() => true); }); }; s.removeScopesFromCredential = function (id, type, scopes) { return this.getCredential(id, type) .then((credential) => { if (!credential) { throw new Error('Credential not found'); } const newScopes = credential.scopes.filter(val => scopes.indexOf(val) === -1); return Promise.all([ credentialDao.updateCredential(id, type, { scopes: JSON.stringify(newScopes) }), credentialDao.dissociateCredentialFromScopes(id, type, scopes) ]).then(() => true); }); }; s.setScopesForCredential = function (id, type, scopes) { return this.getCredential(id, type) .then((credential) => { if (!credential) { throw new Error('credential not found'); } return credentialDao.updateCredential(id, type, { scopes: JSON.stringify(scopes) }); }).then(() => true); }; function processCredential (credential, options = { includePassword: false }) { if (credential.scopes && credential.scopes.length > 0) { credential.scopes = JSON.parse(credential.scopes); } const credentialModel = config.models.credentials.properties[credential.type]; if (!options.includePassword && credentialModel.properties && credentialModel.properties.passwordKey) { delete credential[credentialModel.properties.passwordKey.default]; delete credential.passwordKey; } delete credential.autoGeneratePassword; return credential; } function validateAndHashPassword (credentialConfig, credentialDetails) { if (credentialDetails[credentialConfig.properties.passwordKey.default]) { return utils.saltAndHash(credentialDetails[credentialConfig.properties.passwordKey.default]) .then(hash => ({ hash })); } if (!credentialConfig.properties.autoGeneratePassword.default) { throw new Error(`${credentialConfig.properties.passwordKey.default} is required`); // TODO: replace with validation error } const password = uuidv4(); return utils.saltAndHash(password) .then((hash) => ({ hash, password })); } function validateNewCredentialScopes (credentialConfig, credentialDetails) { if (!credentialConfig.properties || !credentialConfig.properties.scopes) { return Promise.resolve(null); } if (credentialDetails.scopes) { return validateExistingScopes(credentialDetails.scopes); } if (credentialConfig.required && credentialConfig.required.includes('scopes')) { throw new Error('scopes are required'); // TODO: replace with validation error } if (credentialConfig.properties.scopes.default) { return Promise.resolve(credentialConfig.properties.scopes.default); } return Promise.resolve(null); } // This function validates all user defined properties, excluding scopes function validateNewCredentialProperties (credentialConfig, credentialDetail) { // Tmp — horrible hack to remove. const credentialDetails = JSON.parse(JSON.stringify(credentialDetail)); delete credentialDetails.scopes; const validationResult = validate(credentialConfig, credentialDetails); if (!validationResult.isValid) { return Promise.reject(new Error(validationResult.error)); }; return Promise.resolve(credentialDetails); } // This function validates all user defined properties, excluding scopes function validateUpdatedCredentialProperties (type, credentialDetails) { const newCredentialProperties = {}; const credentialConfig = config.models.credentials.properties[type]; for (const prop in credentialConfig.properties) { if (prop === 'scopes') { continue; } if (credentialDetails[prop]) { if (typeof credentialDetails[prop] !== 'string') { throw new Error('credential property must be a string'); // TODO: replace with validation error } if (credentialConfig.properties[prop].isMutable !== false) { newCredentialProperties[prop] = credentialDetails[prop]; } else throw new Error(`${prop} is immutable`); } } return Object.keys(newCredentialProperties).length > 0 ? Promise.resolve(newCredentialProperties) : Promise.resolve(null); } function validateNewScopes (scopes) { return grabScopesAndExecute(scopes, val => !val).catch(() => { throw new Error('One or more scopes already exist.'); }); } function validateExistingScopes (scopes) { return grabScopesAndExecute(scopes, val => val).catch(() => { throw new Error('One or more scopes don\'t exist'); }); } function grabScopesAndExecute (scopes, fn) { return Promise.all(scopes.map(s.existsScope)) .then(res => { if (res.every(fn)) { return scopes; } throw new Error('SCOPE_VALIDATION_FAILED'); }); } module.exports = s;