UNPKG

nitrogen-core

Version:

Core services used across ingestion, registry, and consumption servers.

870 lines (694 loc) 33.8 kB
var async = require('async') , core = require('../../lib') , crypto = require('crypto') , moment = require('moment') , mongoose = require('mongoose'); var DEVICE_AUTH_FAILURE_MESSAGE = "The device authentication details provided were not accepted."; var USER_AUTH_FAILURE_MESSAGE = "The email or password provided were not accepted."; // TODO: Remove once legacy user authentication endpoint is no longer needed. var legacyAccessTokenLookup = function(callback) { return function(err, principal) { if (err) return callback(err); core.services.accessTokens.findOrCreateToken(principal, function(err, accessToken) { if (err) return callback(err); core.log.debug("authenticated user principal: " + principal.id); callback(null, principal, accessToken); }); }; }; // TODO: Remove once legacy user authentication endpoint is removed. var legacyAuthentication = function(authBody, callback) { if (authBody.email && authBody.password) { authenticateUser(authBody.email, authBody.password, legacyAccessTokenLookup(callback)); } else { callback(core.utils.authenticationError('Please sign in with your email and password.')); } }; var accessTokenFor = function(authzPrincipal, principalId, options, callback) { findByIdCached(core.services.principals.servicePrincipal, principalId, function(err, accessTokenPrincipal) { if (err) return callback(err); if (!principalId) return callback(core.utils.notFoundError()); core.services.permissions.authorize({ principal: authzPrincipal.id, principal_for: principalId, action: 'admin' }, accessTokenPrincipal, function(err, permission) { if (err) return callback(err); if (!permission.authorized) { return callback(core.utils.authorizationError('Principal ' + authzPrincipal.id + ' does not have an admin permission for this principal.')); } core.services.accessTokens.create(accessTokenPrincipal, options, function(err, accessToken) { if (err) return callback(err); core.log.info("principal service: principal " + authzPrincipal.id + " created access token for principal: " + principalId + " via permission: " + permission); callback(null, accessToken); }); }); }); }; var authenticateSecret = function(principalId, secret, callback) { findById(core.services.principals.servicePrincipal, principalId, function(err, principal) { if (err) return callback(err); if (!principal) return callback(core.utils.authenticationError(DEVICE_AUTH_FAILURE_MESSAGE)); verifySecret(secret, principal, function(err) { if (err) return callback(err); return callback(err, principal); }); }); }; var authenticateUser = function(email, password, callback) { findByEmail(core.services.principals.servicePrincipal, email, function(err, principal) { if (err) return callback(err); if (!principal) return callback(core.utils.authenticationError(USER_AUTH_FAILURE_MESSAGE)); core.log.debug("found user email: " + email + " verifying password."); verifyPassword(password, principal, function(err) { if (err) return callback(err); return callback(null, principal); }); }); }; var cacheKeyPrincipalId = function(principalId) { return "id." + principalId; }; var clearCacheEntry = function(principalId, callback) { var cacheKey = cacheKeyPrincipalId(principalId); core.log.debug('principals: clearing cache entry ' + cacheKey); core.config.cache_provider.del('principals', cacheKey, callback); }; var changePassword = function(principal, newPassword, callback) { principal.password = newPassword; createUserCredentials(principal, function(err, principal) { if (err) return callback(err); // changing a user's password always invalidates all current access tokens. core.services.accessTokens.removeByPrincipal(principal, function(err) { if (err) return callback(err); // but create a new token for this user and return it in the callback. core.services.accessTokens.findOrCreateToken(principal, function(err, accessToken) { update(core.services.principals.servicePrincipal, principal.id, { salt: principal.salt, password_hash: principal.password_hash }, function(err, principal) { return callback(err, principal, accessToken); }); }); }); }); }; var create = function(principal, callback) { validate(principal, function(err) { if (err) return callback(err); checkForExistingPrincipal(principal, function(err, foundPrincipal) { if (err) return callback(err); if (foundPrincipal) return callback(core.utils.badRequestError('A user with that email already exists. Please sign in with your email and password.')); createCredentials(principal, function(err, principal) { if (err) return callback(err); if (!principal.is('service') && principal.secret) { principal.secret = undefined; } principal.save(function(err, principal) { if (err) return callback(err); createPermissions(principal, function(err) { if (err) return callback(err); core.log.info("created " + principal.type + " principal: " + principal.id); findByIdCached(core.services.principals.servicePrincipal, principal.id, function(err, updatedPrincipal) { if (err) return callback(err); notifySubscriptions(updatedPrincipal, function(err) { if (err) return callback(err); if (principal.is('reactor')) { return initializeIfFirstReactor(updatedPrincipal, callback); } else { return callback(err, updatedPrincipal); } }); }); }); }); }); }); }); }; var initializeIfFirstReactor = function(reactor, callback) { find(core.services.principals.servicePrincipal, { type: 'reactor' }, { limit: 2 }, function(err, reactors) { if (err) return callback(err); if (reactors.length !== 1) return callback(null, reactor); return initializeServiceReactor(reactor, callback); }); }; var checkForExistingPrincipal = function(principal, callback) { if (!core.services.principals.servicePrincipal) { core.log.info('principal service: not able to check for existing user because no service principal.'); return callback(null, null); } if (principal.is('user')) { findByEmail(core.services.principals.servicePrincipal, principal.email, callback); } else { findByIdCached(core.services.principals.servicePrincipal, principal.id, callback); } }; var createCredentials = function(principal, callback) { // only user credentials need to be hashed. non-users have public key. if (principal.is('user')) { core.services.apiKeys.assign(principal, function(err, apiKey) { if (err) return callback(err); principal.api_key = apiKey; createUserCredentials(principal, callback); }); } else { hashCredentials(principal, function(err, principal) { if (err) return callback(err); issueClaimCode(principal, function(err, code) { if (err) return callback(err); principal.claim_code = code; return callback(null, principal); }); }); } }; var createPermissions = function(principal, callback) { if (!principal.is('service')) { var permission = new core.models.Permission({ authorized: true, issued_to: principal.id, principal_for: principal.id, priority: core.models.Permission.DEFAULT_PRIORITY_BASE }); core.services.permissions.create(core.services.principals.servicePrincipal, permission, function(err) { if (err) return callback(err); if (principal.is('user')) return callback(); core.services.apiKeys.findById(principal.api_key, function(err, apiKey) { if (err) return callback(err); findByIdCached(core.services.principals.servicePrincipal, apiKey.owner, function(err, ownerPrincipal) { if (err) return callback(err); if (!ownerPrincipal || !ownerPrincipal.is('user')) return callback(); // if api_key owner is user, give them all permissions var permission = new core.models.Permission({ authorized: true, issued_to: ownerPrincipal.id, principal_for: principal.id, priority: core.models.Permission.DEFAULT_PRIORITY_BASE }); update(core.services.principals.servicePrincipal, principal.id, { claim_code: null }); core.services.permissions.create(core.services.principals.servicePrincipal, permission, callback); }); }); }); } else { core.log.info('principals service: adding blanket permission for service principal: ' + principal.id); var permission = new core.models.Permission({ authorized: true, issued_to: principal.id, priority: 0 }); core.services.permissions.createInternal(permission, callback); } }; var createSecret = function(principal, callback) { if (!core.config.device_secret_bytes) return callback( utils.internalError('principals service: Service is missing required configuration item device_secret_bytes.') ); crypto.randomBytes(core.config.device_secret_bytes, function(err, secretBuf) { if (err) return callback(err); principal.secret = secretBuf.toString('base64'); callback(null, principal); }); }; var createUserCredentials = function(principal, callback) { crypto.randomBytes(core.config.salt_length_bytes, function(err, saltBuf) { if (err) return callback(err); hashPassword(principal.password, saltBuf, function(err, hashedPasswordBuf) { if (err) return callback(err); principal.salt = saltBuf.toString('base64'); principal.password_hash = hashedPasswordBuf.toString('base64'); callback(null, principal); }); }); }; var filterForPrincipal = function(principal, filter) { if (typeof filter !== 'object') { core.log.warn('principals service: filterForPrincipal: squelching non object filter'); filter = {}; } // used only the first query during bootstrap before service principal is established. if (!principal && !core.services.principals.servicePrincipal) { return filter; } if (principal && principal.is('service')) { return filter; } filter.visible_to = principal._id; return filter; }; var find = function(principal, filter, options, callback) { core.models.Principal.find(filterForPrincipal(principal, filter), null, options, callback); }; var findByEmail = function(principal, email, callback) { core.models.Principal.findOne(filterForPrincipal(principal, { "email": email }), callback); }; var findByIdCached = function(authzPrincipal, id, callback) { var cacheKey = cacheKeyPrincipalId(id); core.log.debug('looking for principalId: ' + id + ' with cache key: ' + cacheKey); core.config.cache_provider.get('principals', cacheKey, function(err, principalObj) { if (err) return callback(err); if (principalObj) { // check to make sure it is visible to authz principal if (principalObj.visible_to.indexOf(authzPrincipal.id.toString()) !== -1) { core.log.debug("principals: " + cacheKey + ": cache hit"); var principal = new core.models.Principal(principalObj); // Mongoose by default will override the passed id with a new unique one. Set it back. principal._id = mongoose.Types.ObjectId(id); return callback(null, principal); } } core.log.debug("principals: " + cacheKey + ": cache miss."); // find and cache result return findById(authzPrincipal, id, callback); }); }; var findById = function(authzPrincipal, id, callback) { core.models.Principal.findOne(filterForPrincipal(authzPrincipal, { "_id": id }), function(err, principal) { if (err) return callback(err); if (!principal) return callback(null, null); var cacheKey = cacheKeyPrincipalId(id); core.log.debug("principals: setting cache entry for " + cacheKey); core.config.cache_provider.set('principals', cacheKey, principal.toObject(), moment().add(core.config.principals_cache_lifetime_minutes, 'minutes').toDate(), function(err) { return callback(err, principal); }); }); }; var checkClaimCode = function(code, callback) { find(core.services.principals.servicePrincipal, { claim_code: code }, {}, function (err, principals) { if (err) return callback(true); callback(principals.length > 0); }); }; var generateClaimCode = function() { var characterCode = ''; var numberCode = ''; var characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; for (var i=0; i < core.config.claim_code_length / 2; i++) { var idx = Math.floor(Math.random() * characters.length); characterCode += characters[idx]; numberCode += Math.floor(Math.random() * 10); } return characterCode + '-' + numberCode; }; var issueClaimCode = function(principal, callback) { if (principal.is('user')) return callback(null,null); var wasCollision = true; var claimCode = null; async.whilst( function() { return wasCollision; }, function(callback) { claimCode = generateClaimCode(); checkClaimCode(claimCode, function(collision) { wasCollision = collision; callback(); }); }, function(err) { if (err) return callback(err); callback(null, claimCode); } ); }; var hashPassword = function(password, saltBuf, callback) { crypto.pbkdf2(password, saltBuf, core.config.password_hash_iterations, core.config.password_hash_length, function(err, hash) { if (err) return callback(err); var hashBuf = new Buffer(hash, 'binary'); callback(null, hashBuf); }); }; var hashCredentials = function(principal, callback) { if (!principal.secret) return callback(null, principal); hashSecret(principal.secret, function(err, hashedSecret) { if (err) return callback(err); principal.secret_hash = hashedSecret; return callback(null, principal); }); } var hashSecret = function(secret, callback) { // have to create a buffer here because node's sha256 hash function expects binary encoding. var secretBuf = new Buffer(secret, 'base64'); var sha256 = crypto.createHash('sha256'); sha256.update(secretBuf.toString('binary'), 'binary'); callback(null, sha256.digest('base64')); }; var impersonate = function(authzPrincipal, impersonatedPrincipalId, callback) { findByIdCached(core.services.principals.servicePrincipal, impersonatedPrincipalId, function(err, impersonatedPrincipal) { if (err) return callback(err); if (!impersonatedPrincipal) return callback(core.utils.notFoundError()); core.services.permissions.authorize({ principal: authzPrincipal.id, principal_for: impersonatedPrincipalId, action: 'impersonate' }, impersonatedPrincipal, function(err, permission) { if (err) return callback(err); if (!permission.authorized) { return callback(core.utils.authorizationError('You are not authorized to impersonate this principal.')); } core.services.accessTokens.findOrCreateToken(impersonatedPrincipal, function(err, accessToken) { if (err) return callback(err); core.log.info("principal service: principal " + authzPrincipal.id + " impersonated principal: " + impersonatedPrincipalId + " via permission: " + permission); callback(null, impersonatedPrincipal, accessToken); }); }); }); }; var buildReactorCommands = function(reactor) { var commands = []; core.config.service_applications.forEach(function(app) { commands.push(new core.models.Message({ from: core.services.principals.servicePrincipal.id, to: reactor.id, type: 'reactorCommand', tags: [ nitrogen.CommandManager.commandTag(reactor.id) ], body: { command: 'install', execute_as: core.services.principals.servicePrincipal.id, instance_id: app.instance_id, module: app.module } })); commands.push(new core.models.Message({ from: core.services.principals.servicePrincipal.id, to: reactor.id, type: 'reactorCommand', tags: [ nitrogen.CommandManager.commandTag(reactor.id) ], body: { command: 'start', instance_id: app.instance_id, module: app.module, params: app.params } })); }); return commands; }; var initializeServiceReactor = function(reactor, callback) { var impersonatePerm = new core.models.Permission({ action: 'impersonate', issued_to: reactor.id, principal_for: core.services.principals.servicePrincipal.id, priority: nitrogen.Permission.NORMAL_PRIORITY, authorized: true }); core.services.permissions.create(core.services.principals.servicePrincipal, impersonatePerm, function(err, permission) { if (err) return callback(err); core.services.messages.createMany(core.services.principals.servicePrincipal, buildReactorCommands(reactor), function(err) { if (err) return callback(err); return callback(null, reactor); }); }); }; var initialize = function(callback) { // we don't use services find() here because it is a chicken and an egg visibility problem. // we aren't service so we can't find service. :) // make sure to sort by created_at so that we get the very first service principal that was created by this service // when it bootstrapped itself. core.models.Principal.find({ type: 'service' }, null, { sort: { created_at: 1 } }, function(err, principals) { if (err) return callback(err); if (principals.length === 0) { core.log.info("bootstrapping: creating service principal"); var servicePrincipal = new core.models.Principal({ name: 'Service', type: 'service', }); core.services.principals.createSecret(servicePrincipal, function(err, servicePrincipal) { if (err) return callback(err); create(servicePrincipal, function(err, servicePrincipal) { if (err) return callback(err); core.services.principals.servicePrincipal = servicePrincipal; return callback(); }); }); } else { core.services.principals.servicePrincipal = principals[0]; return callback(); } }); }; var notifySubscriptions = function(principal, callback) { core.log.debug('sending subscription item for principal: ' + principal); core.services.subscriptions.publish('principal', principal, callback); }; var removeById = function(authzPrincipal, principalId, callback) { findByIdCached(authzPrincipal, principalId, function (err, principal) { if (err) return callback(err); if (!principal) return callback(core.utils.notFoundError()); core.services.permissions.authorize({ principal: authzPrincipal.id, principal_for: principalId, action: 'admin' }, principal, function(err, permission) { if (err) return callback(err); if (!permission.authorized) { var authError = core.utils.authorizationError('You are not authorized to delete this principal.'); core.log.warn('principals: removeById: auth failure: ' + JSON.stringify(authError)); return callback(authError); } if (principal.is('user')) { // for user del for other principals, we just delete the permissions. core.services.permissions.remove(core.services.principals.servicePrincipal, { $or: [ { issued_to: principalId }, { principal_for: principalId } ] }, function(err) { if (err) return callback(err); core.models.Principal.remove({ _id: principalId }, function(err, removedCount) { if (err) return callback(err); clearCacheEntry(principalId, function(err) { return callback(err, removedCount); }) }); }); } else { // only delete the permissions the authorizing principal has for non-user principals. core.services.permissions.remove(core.services.principals.servicePrincipal, { issued_to: authzPrincipal.id, principal_for: principalId }, callback); } }); }); }; var resetPassword = function(authorizingPrincipal, principal, callback) { core.services.permissions.authorize({ principal: authorizingPrincipal.id, principal_for: principal.id, action: 'admin' }, principal, function(err, permission) { if (err) return callback(err); if (!permission.authorized) return callback(core.utils.authorizationError(permission)); core.log.info('principals service: reseting password for principal: ' + principal.id + ': ' + principal.email); generateRandomPassword(function(err, randomPassword) { if (err) return callback(err); changePassword(principal, randomPassword, function(err, principal) { if (err) return callback(err); var email = { to: principal.email, from: core.config.service_email_address, subject: "Password Reset", // TODO: Localization text: "A password reset was requested for your Nitrogen account. Your reset password is " + randomPassword + "\n" + "Please login and change it as soon as possible." }; core.services.email.send(email, function(err) { return callback(err, principal); }); }); }); }); }; var generateRandomPassword = function(callback) { crypto.randomBytes(core.config.reset_password_length, function(err, randomPasswordBuf) { if (err) return callback(err); var randomPasswordString = randomPasswordBuf.toString('base64').substr(0, core.config.reset_password_length); return callback(null, randomPasswordString); }); }; var update = function(authorizingPrincipal, id, updates, callback) { if (!authorizingPrincipal) return callback(utils.principalRequired()); if (!id) return callback(core.utils.badRequestError('Missing required argument id.')); findByIdCached(authorizingPrincipal, id, function(err, principal) { if (err) return callback(err); if (!principal) return callback(core.utils.badRequestError("Can't find principal for update.")); core.services.permissions.authorize({ principal: authorizingPrincipal.id, principal_for: id, action: 'admin' }, principal, function(err, permission) { if (err) return callback(err); if (!permission.authorized) return callback(core.utils.authorizationError(permission)); updates.updated_at = new Date(); core.models.Principal.update({ _id: id }, { $set: updates }, function (err, updateCount) { if (err) return callback(err); clearCacheEntry(id, function(err) { if (err) return callback(err); findByIdCached(authorizingPrincipal, id, function(err, updatedPrincipal) { if (err) return callback(err); notifySubscriptions(updatedPrincipal, function(err) { if (err) return callback(err); if (callback) return callback(err, updatedPrincipal); }); }); }); }); }); }); }; var updateLastConnection = function(principal, ip) { var updates = {}; // emit a ip message each time ip changes for principal. if (principal.last_ip != ip) { principal.last_ip = updates.last_ip = ip; var ipMessage = new core.models.Message({ type: 'ip', from: principal, body: { ip_address: ip } }); core.services.messages.create(core.services.principals.servicePrincipal, ipMessage, function(err, message) { if (err) core.log.info("principal service: creating ip message failed: " + err); }); } // only update the last_connection at most once a minute. if (new Date() - principal.last_connection > 60 * 1000) { principal.last_connection = updates.last_connection = new Date(); } if (Object.keys(updates).length > 0) { update(core.services.principals.servicePrincipal, principal.id, updates, function(err, principal) { if (err) return core.log.error("principal service: updating last connection failed: " + err); }); } }; var updateVisibleTo = function(principalId, callback) { core.log.debug("principal service: updating visible_to for: " + principalId); findByIdCached(core.services.principals.servicePrincipal, principalId, function(err, principal) { if (err) return callback(err); if (!principal) return callback(); core.log.debug("principal service: updating visible_to for principal id: " + principalId); core.services.permissions.find(core.services.principals.servicePrincipal, { $or : [ { action: 'view' }, { action: null } ], $or : [ { principal_for: principalId }, { principal_for: null } ] }, { sort: { priority: 1 } }, function(err, permissions) { if (err) return callback(err); var visibilityMap = {}; permissions.forEach(function(permission) { if (permission.issued_to) { if (!visibilityMap[permission.issued_to]) visibilityMap[permission.issued_to] = permission.authorized; } else { // // NEED TO THINK ABOUT THIS - THIS OVERRIDES ALL OF THE HIGHER PRIORITY AUTHORIZED=FALSE ACLS // visibilityMap['*'] = permission.authorized; } }); principal.visible_to = []; Object.keys(visibilityMap).forEach(function(key) { if (visibilityMap[key]) principal.visible_to.push(key); }); core.log.debug("principal service: final visible_to: " + JSON.stringify(principal.visible_to)); core.services.principals.update(core.services.principals.servicePrincipal, principalId, { visible_to: principal.visible_to }, callback); } ); }); }; var validate = function(principal, callback) { var validType = false; core.models.Principal.PRINCIPAL_TYPES.forEach(function(type) { validType = validType || principal.type === type; }); if (!validType) { var err = 'Principal type invalid. found: ' + principal.type; core.log.error(err); return callback(core.utils.badRequestError(err)); } if (principal.is('user')) { if (!principal.email) return callback(core.utils.badRequestError("User must have email")); if (!principal.password) return callback(core.utils.badRequestError("User must have password")); if (principal.password.length < core.config.minimum_password_length) return callback(core.utils.badRequestError("User password must be at least " + core.config.minimum_password_length + " characters.")); } else if (!principal.is('service')) { if (!principal.api_key) return callback(core.utils.badRequestError("Non-user principals must have api_key")); if (!principal.public_key && !principal.secret_hash && !principal.secret) return callback(core.utils.badRequestError("Non-user principal must have public_key or secret_hash.")); } callback(null); }; var verifyPassword = function(password, user, callback) { var saltBuf = new Buffer(user.salt, 'base64'); hashPassword(password, saltBuf, function(err, hashedPasswordBuf) { if (err) return callback(err); if (user.password_hash != hashedPasswordBuf.toString('base64')) return callback(core.utils.authenticationError(USER_AUTH_FAILURE_MESSAGE)); else return callback(null); }); }; var verifySecret = function(secret, principal, callback) { hashSecret(secret, function(err, hashedSecret) { if (err) return callback(err); if (hashedSecret !== principal.secret_hash) { core.log.warn("verification of secret for principal: " + principal.id + " failed"); core.log.warn("secret provided: " + secret); return callback(core.utils.authenticationError(DEVICE_AUTH_FAILURE_MESSAGE)); } callback(null); }); }; var verifySignature = function(nonceString, signature, callback) { core.services.nonce.find({ nonce: nonceString }, {}, function(err, nonces) { if (err) return callback(core.utils.internalError(err)); if (!nonces || nonces.length === 0) return callback(core.utils.authenticationError("Nonce not found.")); var nonce = nonces[0]; findByIdCached(core.services.principals.servicePrincipal, nonce.principal, function(err, principal) { if (err) return callback(core.utils.internalError(err)); if (!principal) return callback(core.utils.authenticationError("Nonce principal not found.")); if (!principal.public_key) return callback(core.utils.authenticationError("Principal does not use public key to authenticate.")); var verifier = crypto.createVerify("RSA-SHA256"); verifier.update(nonceString); var publicKeyBuf = new Buffer(principal.public_key, 'base64'); var result = verifier.verify(publicKeyBuf, signature, "base64"); core.services.nonce.remove({ nonce: nonceString }, function(err) { if (err) return callback(err); if (result) { return callback(null, principal); } else { return callback(core.utils.authenticationError("Signature authentication failed.")); } }); }); }); }; module.exports = { accessTokenFor: accessTokenFor, authenticateSecret: authenticateSecret, authenticateUser: authenticateUser, changePassword: changePassword, create: create, createSecret: createSecret, filterForPrincipal: filterForPrincipal, find: find, findByIdCached: findByIdCached, findById: findById, generateClaimCode: generateClaimCode, impersonate: impersonate, initialize: initialize, resetPassword: resetPassword, removeById: removeById, update: update, updateLastConnection: updateLastConnection, updateVisibleTo: updateVisibleTo, verifyPassword: verifyPassword, verifySecret: verifySecret, verifySignature: verifySignature, servicePrincipal: null, legacyAuthentication: legacyAuthentication };