Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions conf/authdata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"accounts": [{
"name": "Bart",
"email": "sampleaccount1@sampling.com",
"arn": "aws::iam:123456789012:root",
"canonicalID": "79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be",
"shortid": "123456789012",
"keys": [{
"access": "accessKey1",
"secret": "verySecretKey1"
}],
"users": [{
"name": "Bart Jr",
"email": "user1.sampleaccount2@sampling.com",
"arn": "aws::iam:123456789013:bart",
"keys": [{
"access": "accessKey1/1",
"secret": "verySecretKey1"
}]
}]
}, {
"name": "Lisa",
"email": "sampleaccount2@sampling.com",
"arn": "aws::iam:accessKey2:user/Lisa",
"canonicalID": "79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2bf",
"shortid": "123456789012",
"keys": [{
"access": "accessKey2",
"secret": "verySecretKey2"
}]
}]
}
10 changes: 8 additions & 2 deletions lib/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import assert from 'assert';
import fs from 'fs';
import path from 'path';

import authDataChecker from './auth/in_memory/checker';

/**
* Reads from a config file and returns the content as a config object
*/
Expand Down Expand Up @@ -175,11 +177,15 @@ class Config {
if (auth === 'file' || auth === 'mem') {
// Auth only checks for 'mem' since mem === file
auth = 'mem';
let authfile = `${__dirname}/auth/in_memory/vault.json`;
let authfile = `${__dirname}/../conf/authdata.json`;
if (process.env.S3AUTH_CONFIG) {
authfile = process.env.S3AUTH_CONFIG;
}
this.authData = require(authfile);
const authData = require(authfile);
if (authDataChecker(authData)) {
throw new Error('bad config: invalid auth config file.');
}
this.authData = authData;
}
if (process.env.S3SPROXYD) {
data = process.env.S3SPROXYD;
Expand Down
43 changes: 22 additions & 21 deletions lib/auth/in_memory/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { errors } from 'arsenal';
import crypto from 'crypto';

import config from '../../Config';
import Indexer from './indexer';

import { calculateSigningKey, hashSignature } from './vaultUtilities';

const authIndex = new Indexer(config.authData);

const backend = {
/** verifySignatureV2
* @param {string} stringToSign - string to sign built per AWS rules
Expand All @@ -16,21 +19,22 @@ const backend = {
*/
verifySignatureV2: (stringToSign, signatureFromRequest,
accessKey, options, callback) => {
const account = config.authData.accountsKeyedbyAccessKey[accessKey];
if (!account) {
const entity = authIndex.getByKey(accessKey);
if (!entity) {
return callback(errors.InvalidAccessKeyId);
}
const secretKey = account.secretKey;
const secretKey = entity.keys
.filter(kv => kv.access === accessKey)[0].secret;
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,
accountDisplayName: entity.accountDisplayName,
canonicalID: entity.canonicalID,
arn: entity.arn,
IAMdisplayName: entity.IAMdisplayName,
};
const vaultReturnObject = {
message: {
Expand All @@ -54,22 +58,23 @@ const backend = {
*/
verifySignatureV4: (stringToSign, signatureFromRequest, accessKey,
region, scopeDate, options, callback) => {
const account = config.authData.accountsKeyedbyAccessKey[accessKey];
if (!account) {
const entity = authIndex.getByKey(accessKey);
if (!entity) {
return callback(errors.InvalidAccessKeyId);
}
const secretKey = account.secretKey;
const secretKey = entity.keys
.filter(kv => kv.access === accessKey)[0].secret;
const signingKey = calculateSigningKey(secretKey, region, scopeDate);
const reconstructedSig = crypto.createHmac('sha256', signingKey)
.update(stringToSign).digest('hex');
if (signatureFromRequest !== reconstructedSig) {
return callback(errors.SignatureDoesNotMatch);
}
const userInfoToSend = {
accountDisplayName: account.displayName,
canonicalID: account.canonicalID,
arn: account.arn,
IAMdisplayName: account.IAMdisplayName,
accountDisplayName: entity.accountDisplayName,
canonicalID: entity.canonicalID,
arn: entity.arn,
IAMdisplayName: entity.IAMdisplayName,
};
const vaultReturnObject = {
message: {
Expand All @@ -92,13 +97,10 @@ const backend = {
getCanonicalIds: (emails, log, cb) => {
const results = {};
emails.forEach(email => {
const lowercasedEmail = email.toLowerCase();
if (!config.authData.accountsKeyedbyEmail[lowercasedEmail]) {
if (!authIndex.getByEmail(email)) {
results[email] = 'NotFound';
} else {
results[email] =
config.authData.accountsKeyedbyEmail[lowercasedEmail]
.canonicalID;
results[email] = authIndex.getByEmail(email).canonicalID;
}
});
const vaultReturnObject = {
Expand All @@ -123,8 +125,7 @@ const backend = {
getEmailAddresses: (canonicalIDs, options, cb) => {
const results = {};
canonicalIDs.forEach(canonicalId => {
const foundAccount = config.authData
.accountsKeyedbyCanID[canonicalId];
const foundAccount = authIndex.getByCanId(canonicalId);
if (!foundAccount || !foundAccount.email) {
results[canonicalId] = 'NotFound';
} else {
Expand Down
182 changes: 182 additions & 0 deletions lib/auth/in_memory/checker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import Logger from 'werelogs';

// Here, we expect the logger to have already been configured in S3
const log = new Logger('S3');

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add function description here.

* @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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add function description here.

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) {
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 (accountObj.users) {
if (checkType(data, 'Account', 'users', 'array', accountObj)) {
accountObj.users.forEach(userObj => checkUser(data, userObj));
}
}

if (accountObj.keys) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same you can use checkExists() here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not this time, it's an optional field here :).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, my bad :)

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) {
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) {
let nerr = dumpCountError('CanonicalID', checkData.canonicalIds);
nerr += dumpCountError('Email', checkData.emails);
nerr += dumpCountError('ARN', checkData.arns);
nerr += dumpCountError('AccessKey', checkData.keys);

if (checkData.errors.length > 0) {
checkData.errors.forEach(msg => {
log.error(msg.txt, msg.obj);
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm reading this correctly, we log errors for duplicate items and then don't log any other formatting errors if there are duplicates? Why? Also, a few more comments would be helpful here.

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
* @return {boolean} true on erroneous data
* false on success
*/
export default function check(authdata) {
const checkData = {
errors: [],
emails: [],
arns: [],
canonicalIds: [],
keys: [],
};

if (authdata.accounts === undefined) {
checkData.errors.push({
txt: 'no "accounts" array defined in Auth config',
});
return dumpErrors(checkData);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't understand, why check here?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm not sure I get the question, but my go-to answer is: There are no accounts to browse anyways, might as well return early.

}

authdata.accounts.forEach(account => {
checkAccount(checkData, account);
});

return dumpErrors(checkData);
}
Loading