Compare commits

...

3 Commits

Author SHA1 Message Date
Electra Chong d09b25877c parameterize checker 2017-06-19 13:01:55 -07:00
Electra Chong ed15e244af add authdata checker 2017-06-16 15:04:22 -07:00
Electra Chong a3b5127141 wip rf backend 2017-06-15 16:22:16 -07:00
5 changed files with 499 additions and 42 deletions

View File

@ -10,6 +10,11 @@ const constructStringToSignV4 = require('./v4/constructStringToSign');
const convertUTCtoISO8601 = require('./v4/timeUtils').convertUTCtoISO8601;
const crypto = require('crypto');
const vaultUtilities = require('./in_memory/vaultUtilities');
const backend = require('./in_memory/s3/backend');
const indexer = require('./in_memory/s3/indexer');
const checker = require('./in_memory/s3/checker');
let vault = null;
const auth = {};
const checkFunctions = {
@ -207,4 +212,9 @@ module.exports = {
client: {
generateV4Headers,
},
inMemory: {
backend,
indexer,
checker,
},
};

View File

@ -12,7 +12,39 @@ const accountsKeyedbyEmail =
const calculateSigningKey = require('./vaultUtilities').calculateSigningKey;
const hashSignature = require('./vaultUtilities').hashSignature;
const backend = {
const _indexer = {
getEntityByKey(accessKey) {
return accountsKeyedbyAccessKey[accessKey];
},
getEntityByEmail(email) {
return accountsKeyedbyEmail[email];
},
getEntityByCanId(canonicalId) {
return accountsKeyedbyCanID[canonicalId];
},
getSecretKey(account) {
return account.secretKey;
},
getAcctDisplayName(account) {
return account.displayName;
},
};
function _formatResponse(userInfoToSend) {
return {
message: {
body: userInfoToSend,
},
};
}
class BackendTemplate {
constructor(indexer, formatter) {
this.indexer = indexer;
this.formatResponse = formatter;
}
/** verifySignatureV2
* @param {string} stringToSign - string to sign built per AWS rules
* @param {string} signatureFromRequest - signature sent with request
@ -21,31 +53,27 @@ const backend = {
* @param {function} callback - callback with either error or user info
* @return {function} calls callback
*/
verifySignatureV2: (stringToSign, signatureFromRequest,
accessKey, options, callback) => {
const account = accountsKeyedbyAccessKey[accessKey];
if (!account) {
verifySignatureV2(stringToSign, signatureFromRequest,
accessKey, options, callback) {
const entity = this.indexer.getEntityByKey(accessKey);
if (!entity) {
return callback(errors.InvalidAccessKeyId);
}
const secretKey = account.secretKey;
const secretKey = this.indexer.getSecretKey(entity, accessKey);
const reconstructedSig =
hashSignature(stringToSign, secretKey, options.algo);
if (signatureFromRequest !== reconstructedSig) {
return callback(errors.SignatureDoesNotMatch);
}
const userInfoToSend = {
accountDisplayName: account.displayName,
canonicalID: account.canonicalID,
arn: account.arn,
IAMdisplayName: account.IAMdisplayName,
};
const vaultReturnObject = {
message: {
body: userInfoToSend,
},
accountDisplayName: this.indexer.getAcctDisplayName(entity),
canonicalID: entity.canonicalID,
arn: entity.arn,
IAMdisplayName: entity.IAMdisplayName,
};
const vaultReturnObject = this.formatResponse(userInfoToSend);
return callback(null, vaultReturnObject);
},
}
/** verifySignatureV4
@ -59,13 +87,13 @@ const backend = {
* @param {function} callback - callback with either error or user info
* @return {function} calls callback
*/
verifySignatureV4: (stringToSign, signatureFromRequest, accessKey,
region, scopeDate, options, callback) => {
const account = accountsKeyedbyAccessKey[accessKey];
if (!account) {
verifySignatureV4(stringToSign, signatureFromRequest, accessKey,
region, scopeDate, options, callback) {
const entity = this.indexer.getEntityByKey(accessKey);
if (!entity) {
return callback(errors.InvalidAccessKeyId);
}
const secretKey = account.secretKey;
const secretKey = this.indexer.getSecretKey(entity, accessKey);
const signingKey = calculateSigningKey(secretKey, region, scopeDate);
const reconstructedSig = crypto.createHmac('sha256', signingKey)
.update(stringToSign, 'binary').digest('hex');
@ -73,18 +101,14 @@ const backend = {
return callback(errors.SignatureDoesNotMatch);
}
const userInfoToSend = {
accountDisplayName: account.displayName,
canonicalID: account.canonicalID,
arn: account.arn,
IAMdisplayName: account.IAMdisplayName,
};
const vaultReturnObject = {
message: {
body: userInfoToSend,
},
accountDisplayName: this.indexer.getAcctDisplayName(entity),
canonicalID: entity.canonicalID,
arn: entity.arn,
IAMdisplayName: entity.IAMdisplayName,
};
const vaultReturnObject = this.formatResponse(userInfoToSend);
return callback(null, vaultReturnObject);
},
}
/**
* Gets canonical ID's for a list of accounts
@ -96,15 +120,16 @@ const backend = {
* object with email addresses as keys and canonical IDs
* as values
*/
getCanonicalIds: (emails, log, cb) => {
getCanonicalIds(emails, log, cb) {
const results = {};
emails.forEach(email => {
const lowercasedEmail = email.toLowerCase();
if (!accountsKeyedbyEmail[lowercasedEmail]) {
const entity = this.indexer.getEntityByEmail[lowercasedEmail];
if (!entity) {
results[email] = 'NotFound';
} else {
results[email] =
accountsKeyedbyEmail[lowercasedEmail].canonicalID;
entity.canonicalID;
}
});
const vaultReturnObject = {
@ -113,7 +138,7 @@ const backend = {
},
};
return cb(null, vaultReturnObject);
},
}
/**
* Gets email addresses (referred to as diplay names for getACL's)
@ -126,14 +151,14 @@ const backend = {
* an object from Vault containing account canonicalID
* as each object key and an email address as the value (or "NotFound")
*/
getEmailAddresses: (canonicalIDs, options, cb) => {
getEmailAddresses(canonicalIDs, options, cb) {
const results = {};
canonicalIDs.forEach(canonicalId => {
const foundAccount = accountsKeyedbyCanID[canonicalId];
if (!foundAccount || !foundAccount.email) {
const foundEntity = this.indexer.getEntityByCanId(canonicalId);
if (!foundEntity || !foundEntity.email) {
results[canonicalId] = 'NotFound';
} else {
results[canonicalId] = foundAccount.email;
results[canonicalId] = foundEntity.email;
}
});
const vaultReturnObject = {
@ -142,7 +167,12 @@ const backend = {
},
};
return cb(null, vaultReturnObject);
},
};
}
}
module.exports = backend;
const backend = new BackendTemplate(_indexer, _formatResponse);
module.exports = {
backend,
BackendTemplate,
};

View File

@ -0,0 +1,58 @@
const BackendTemplate = require('../backend').BackendTemplate;
function _buildArn(generalResource, specificResource) {
return `arn:aws:s3:::${generalResource}/${specificResource}`;
}
class Backend extends BackendTemplate {
/**
* Mocks Vault's response to a policy evaluation request
* Since policies not actually implemented in memory backend,
* we allow users to proceed with request.
* @param {object} requestContextParams - parameters needed to construct
* requestContext in Vault
* @param {object} requestContextParams.constantParams -
* params that have the
* same value for each requestContext to be constructed in Vault
* @param {object} requestContextParams.paramaterize - params that have
* arrays as values since a requestContext needs to be constructed with
* each option in Vault
* @param {object[]} requestContextParams.paramaterize.specificResource -
* specific resources paramaterized as an array of objects containing
* properties `key` and optional `versionId`
* @param {string} userArn - arn of requesting user
* @param {object} log - log object
* @param {function} cb - callback with either error or an array
* of authorization results
* @returns {undefined}
* @callback called with (err, vaultReturnObject)
*/
checkPolicies(requestContextParams, userArn, log, cb) {
let results;
const parameterizeParams = requestContextParams.parameterize;
if (parameterizeParams && parameterizeParams.specificResource) {
// object is parameterized
results = parameterizeParams.specificResource.map(obj => ({
isAllowed: true,
arn: _buildArn(requestContextParams
.constantParams.generalResource, obj.key),
versionId: obj.versionId,
}));
} else {
results = [{
isAllowed: true,
arn: _buildArn(requestContextParams
.constantParams.generalResource, requestContextParams
.constantParams.specificResource),
}];
}
const vaultReturnObject = {
message: {
body: results,
},
};
return cb(null, vaultReturnObject);
}
}
module.exports = Backend;

View File

@ -0,0 +1,194 @@
const werelogs = require('werelogs');
function incr(count) {
if (count !== undefined) {
return count + 1;
}
return 1;
}
/**
* This function ensures that the field `name` inside `container` is of the
* expected `type` inside `obj`. If any error is found, an entry is added into
* the error collector object.
*
* @param {object} data - the error collector object
* @param {string} container - the name of the entity that contains
* what we're checking
* @param {string} name - the name of the entity we're checking for
* @param {string} type - expected typename of the entity we're checking
* @param {object} obj - the object we're checking the fields of
* @return {boolean} true if the type is Ok and no error found
* false if an error was found and reported
*/
function checkType(data, container, name, type, obj) {
if ((type === 'array' && !Array.isArray(obj[name]))
|| (type !== 'array' && typeof obj[name] !== type)) {
data.errors.push({
txt: 'property is not of the expected type',
obj: {
entity: container,
property: name,
type: typeof obj[name],
expectedType: type,
},
});
return false;
}
return true;
}
/**
* This function ensures that the field `name` inside `obj` which is a
* `container`. If any error is found, an entry is added into the error
* collector object.
*
* @param {object} data - the error collector object
* @param {string} container - the name of the entity that contains
* what we're checking
* @param {string} name - the name of the entity we're checking for
* @param {string} type - expected typename of the entity we're checking
* @param {object} obj - the object we're checking the fields of
* @return {boolean} true if the field exists and type is Ok
* false if an error was found and reported
*/
function checkExists(data, container, name, type, obj) {
if (obj[name] === undefined) {
data.errors.push({
txt: 'missing property in auth entity',
obj: {
entity: container,
property: name,
},
});
return false;
}
return checkType(data, container, name, type, obj);
}
function checkUser(data, userObj) {
if (checkExists(data, 'User', 'arn', 'string', userObj)) {
// eslint-disable-next-line no-param-reassign
data.arns[userObj.arn] = incr(data.arns[userObj.arn]);
}
if (checkExists(data, 'User', 'email', 'string', userObj)) {
// eslint-disable-next-line no-param-reassign
data.emails[userObj.email] = incr(data.emails[userObj.email]);
}
if (checkExists(data, 'User', 'keys', 'array', userObj)) {
userObj.keys.forEach(keyObj => {
// eslint-disable-next-line no-param-reassign
data.keys[keyObj.access] = incr(data.keys[keyObj.access]);
});
}
}
function checkAccount(data, accountObj, checkSas) {
if (checkExists(data, 'Account', 'email', 'string', accountObj)) {
// eslint-disable-next-line no-param-reassign
data.emails[accountObj.email] = incr(data.emails[accountObj.email]);
}
if (checkExists(data, 'Account', 'arn', 'string', accountObj)) {
// eslint-disable-next-line no-param-reassign
data.arns[accountObj.arn] = incr(data.arns[accountObj.arn]);
}
if (checkExists(data, 'Account', 'canonicalID', 'string', accountObj)) {
// eslint-disable-next-line no-param-reassign
data.canonicalIds[accountObj.canonicalID] =
incr(data.canonicalIds[accountObj.canonicalID]);
}
if (checkSas &&
checkExists(data, 'Account', 'sasBlob', 'string', accountObj)) {
// eslint-disable-next-line no-param-reassign
data.sasBlobs[accountObj.sasBlob] =
incr(data.sasBlobs[accountObj.sasBlob]);
}
if (accountObj.users) {
if (checkType(data, 'Account', 'users', 'array', accountObj)) {
accountObj.users.forEach(userObj => checkUser(data, userObj));
}
}
if (accountObj.keys) {
if (checkType(data, 'Account', 'keys', 'array', accountObj)) {
accountObj.keys.forEach(keyObj => {
// eslint-disable-next-line no-param-reassign
data.keys[keyObj.access] = incr(data.keys[keyObj.access]);
});
}
}
}
function dumpCountError(property, obj, log) {
let count = 0;
Object.keys(obj).forEach(key => {
if (obj[key] > 1) {
log.error('property should be unique', {
property,
value: key,
count: obj[key],
});
++count;
}
});
return count;
}
function dumpErrors(checkData, log) {
let nerr = dumpCountError('CanonicalID', checkData.canonicalIds, log);
nerr += dumpCountError('Email', checkData.emails, log);
nerr += dumpCountError('ARN', checkData.arns, log);
nerr += dumpCountError('AccessKey', checkData.keys, log);
nerr += dumpCountError('SAS Blob', checkData.sasBlobs, log);
if (checkData.errors.length > 0) {
checkData.errors.forEach(msg => {
log.error(msg.txt, msg.obj);
});
}
if (checkData.errors.length === 0 && nerr === 0) {
return false;
}
log.fatal('invalid authentication config file (cannot start)');
return true;
}
/**
* @param {object} authdata - the authentication config file's data
* @param {object} loggerConfig - config for logger if needed to dump errors
* @param {string} loggerConfig.level - logger level
* @param {string} loggerConfig.dump - logger dump level
* @param {(boolean|null)} checkSas - whether to check Azure SAS for ea. account
* @return {boolean} true on erroneous data
* false on success
*/
function check(authdata, loggerConfig, checkSas) {
const checkData = {
errors: [],
emails: [],
arns: [],
canonicalIds: [],
keys: [],
sasBlobs: [],
};
if (authdata.accounts === undefined) {
checkData.errors.push({
txt: 'no "accounts" array defined in Auth config',
});
return dumpErrors(checkData);
}
authdata.accounts.forEach(account => {
checkAccount(checkData, account, checkSas);
});
const log = new werelogs.Logger('S3', loggerConfig);
return dumpErrors(checkData, log);
}
module.exports = check;

View File

@ -0,0 +1,165 @@
/**
* Class that provides an internal indexing over the simple data provided by
* the authentication configuration file for the memory backend. This allows
* accessing the different authentication entities through various types of
* keys.
*
* @class Indexer
*/
module.exports = class Indexer {
/**
* @constructor
* @param {object} authdata - the authentication config file's data
* @return {undefined}
*/
constructor(authdata) {
this.accountsBy = {
canId: {},
accessKey: {},
email: {},
};
this.usersBy = {
accessKey: {},
email: {},
};
/*
* This may happen if the file backend is not configured for S3.
* As such, we're managing the error here to avoid screwing up there.
*/
if (!authdata) {
return;
}
this._build(authdata);
}
_indexUser(account, user) {
const userData = {
arn: account.arn,
canonicalID: account.canonicalID,
shortid: account.shortid,
accountDisplayName: account.accountDisplayName,
IAMdisplayName: user.name,
email: user.email.toLowerCase(),
keys: [],
};
this.usersBy.email[userData.email] = userData;
user.keys.forEach(key => {
userData.keys.push(key);
this.usersBy.accessKey[key.access] = userData;
});
}
_indexAccount(account) {
const accountData = {
arn: account.arn,
canonicalID: account.canonicalID,
shortid: account.shortid,
accountDisplayName: account.name,
email: account.email.toLowerCase(),
keys: [],
};
this.accountsBy.canId[accountData.canonicalID] = accountData;
this.accountsBy.email[accountData.email] = accountData;
if (account.keys !== undefined) {
account.keys.forEach(key => {
accountData.keys.push(key);
this.accountsBy.accessKey[key.access] = accountData;
});
}
if (account.users !== undefined) {
account.users.forEach(user => {
this._indexUser(accountData, user);
});
}
}
_build(authdata) {
authdata.accounts.forEach(account => {
this._indexAccount(account);
});
}
/**
* This method returns the account associated to a canonical ID.
*
* @param {string} canId - The canonicalId of the account
* @return {Object} account - The account object
* @return {Object} account.arn - The account's ARN
* @return {Object} account.canonicalID - The account's canonical ID
* @return {Object} account.shortid - The account's internal shortid
* @return {Object} account.accountDisplayName - The account's display name
* @return {Object} account.email - The account's lowercased email
*/
getEntityByCanId(canId) {
return this.accountsBy.canId[canId];
}
/**
* This method returns the entity (either an account or a user) associated
* to a canonical ID.
*
* @param {string} key - The accessKey of the entity
* @return {Object} entity - The entity object
* @return {Object} entity.arn - The entity's ARN
* @return {Object} entity.canonicalID - The canonical ID for the entity's
* account
* @return {Object} entity.shortid - The entity's internal shortid
* @return {Object} entity.accountDisplayName - The entity's account
* display name
* @return {Object} entity.IAMDisplayName - The user's display name
* (if the entity is an user)
* @return {Object} entity.email - The entity's lowercased email
*/
getEntityByKey(key) {
if (this.accountsBy.accessKey.hasOwnProperty(key)) {
return this.accountsBy.accessKey[key];
}
return this.usersBy.accessKey[key];
}
/**
* This method returns the entity (either an account or a user) associated
* to an email address.
*
* @param {string} email - The email address
* @return {Object} entity - The entity object
* @return {Object} entity.arn - The entity's ARN
* @return {Object} entity.canonicalID - The canonical ID for the entity's
* account
* @return {Object} entity.shortid - The entity's internal shortid
* @return {Object} entity.accountDisplayName - The entity's account
* display name
* @return {Object} entity.IAMDisplayName - The user's display name
* (if the entity is an user)
* @return {Object} entity.email - The entity's lowercased email
*/
getEntityByEmail(email) {
const lowerCasedEmail = email.toLowerCase();
if (this.usersBy.email.hasOwnProperty(lowerCasedEmail)) {
return this.usersBy.email[lowerCasedEmail];
}
return this.accountsBy.email[lowerCasedEmail];
}
/**
* This method returns the secret key associated with the entity.
* @param {Object} entity - the entity object
* @param {string} accessKey - access key
* @returns {string} secret key
*/
getSecretKey(entity, accessKey) {
return entity.keys
.filter(kv => kv.access === accessKey)[0].secret;
}
/**
* This method returns the account display name associated with the entity.
* @param {Object} entity - the entity object
* @returns {string} account display name
*/
getAcctDisplayName(entity) {
return entity.accountDisplayName;
}
};