Skip to content
Closed
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",
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.

Simpson ?

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.

You're the first person to notice 😃

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

@jmunoznaranjo jmunoznaranjo Jun 8, 2016

Choose a reason for hiding this comment

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

I suggest this file be moved to tests/utils or tests/conf in order to avoid having default values. The values can still be used by setting the env var. I've done a related comment in Config.json.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I'm not fond of this, for two main reasons:

  1. You still want people to be able to doa simple npm start to run the s3 server, without config beforehand (and the ids are a pain to write).
  2. This config file serves as both a default setup and a sample that people can start from. It is much easier to have it by default than to hide it within the tests.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Note that the tests should not be relying on this default conf, but rather define their own, if we wanted to isolate things properly.

14 changes: 10 additions & 4 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 @@ -150,8 +152,8 @@ class Config {
cert: certpath,
};
} else if (key || cert) {
throw new Error( 'bad config: both certFilePaths.key and ' +
'certFilePaths.cert must be defined');
throw new Error('bad config: both certFilePaths.key and ' +
'certFilePaths.cert must be defined');
}
/**
* Configure the backends for Authentication, Data and Metadata.
Expand All @@ -170,11 +172,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 Index from './index';

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

const authIndex = new Index(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
180 changes: 180 additions & 0 deletions lib/auth/in_memory/checker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import Logger from 'werelogs';

// Here, we expect the logger to have already been configured in S3
const log = new Logger('S3');
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 use the logger utility in lib/utilities so the log level is set by the config.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

will do, thanks (and will check how the circular dep behaves there)


function incr(count) {
if (count !== undefined) {
return count + 1;
}
return 1;
}

/**
* @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;
}

/**
* @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
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.

perhaps we should reconsider this rule at some point...

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I am personally split.
On one side, it can be really useful to detect monkey patching of objects, which can be useful to find improperly contained code.
On another side, it becomes an issue whenever you actually want to create a side-effect by modifying some input parameters.

I am accepting of the middle-ground saying that whenever it's actually done willingly, a suppressing comment is acceptable.

We can launch an issue on guidelines to discuss that if you wish to, I'm not against reviewing this specific rule.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

PS: Note that I'd rather explicitly silent this linter warning than silenting it through "re-binding" of the argument into a temp variable.

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.

Leaving aside the convenience (or lack of) of this rule, Object.assign can be used to avoid disabling the linter

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@jmunoznaranjo the point is to avoid doing a copy, since in this case, we want to actually modify the parameter, to update it. Object.assign is a "functional programming" way to do this, but this also means copying a lot of stuff, and killing performances when not necessary.

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.

Ok didn't consider perf

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]);
}
checkExists(data, 'User', 'keys', 'array', userObj);

if (userObj.keys) {
if (checkType(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) {
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', {
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.

@rahulreddy, thoughts?

Copy link
Copy Markdown
Contributor

@rahulreddy rahulreddy Jun 8, 2016

Choose a reason for hiding this comment

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

This happens only once i.e., during startup, so it should be ok. Also, the logger config is validated before the authChecker runs, so there should be no issue using logger here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Btw, how will it behave ? I mean, the Config.js is using the authchecker inside.

Which means that the authchecker will be called while the Config is being initialized. Isn't there a risk of stack overflow through recursion here ? I'll try it anyways.

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 was thinking that it should be ok as the config has been parsed and the var should be available to setup the logger, I don't see a recursion happening here, I could be wrong.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I was thinking of the following inclusion cycle:

  • Config includes checkAuth
  • checkAuth includes log utilities
  • log utilities include Config.

Then, when we instanciate the server:

  1. the first file imports the configuration
  2. load the configuration
  3. call authdataCheck
  4. imports Config.js (going back to 1.)

I didn't check yet, but I do think there is a risk 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.

I found this in node docs

Multiple calls to require('foo') may not cause the module code to be executed multiple times. This is an important feature. With it, "partially done" objects can be returned, thus allowing transitive dependencies to be loaded even when they would cause cycles.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Which is a good news, but there's still the point of how much can the config object be used, if it's incompletely built...
I'll test it anyways ^^' Will keep you posted

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 (nerr > 0) {
return true;
}

if (checkData.errors.length === 0) {
return false;
}

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

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);
}

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

return dumpErrors(checkData);
}
Loading