Compare commits

...

4 Commits

Author SHA1 Message Date
Dora Korpar 0632028ea3 [squash] if tag conditions 2020-09-17 12:49:44 -07:00
Dora Korpar 65e5fe33a4 [squash] finish prefix eval and handle first pass eval 2020-09-16 22:05:33 -07:00
Dora Korpar fae2163c16 ft: S3-3177 policy tag condition keys 2020-09-16 02:41:10 -07:00
Dora Korpar 129f74d558 ft: S3C-3177-new-iampolicy-conditions 2020-09-01 11:12:36 -07:00
4 changed files with 195 additions and 23 deletions

View File

@ -234,7 +234,7 @@ class RequestContext {
requesterIp, sslEnabled, apiMethod, requesterIp, sslEnabled, apiMethod,
awsService, locationConstraint, requesterInfo, awsService, locationConstraint, requesterInfo,
signatureVersion, authType, signatureAge, securityToken, policyArn, signatureVersion, authType, signatureAge, securityToken, policyArn,
action) { action, postXml) {
this._headers = headers; this._headers = headers;
this._query = query; this._query = query;
this._requesterIp = requesterIp; this._requesterIp = requesterIp;
@ -263,6 +263,10 @@ class RequestContext {
this._policyArn = policyArn; this._policyArn = policyArn;
this._action = action; this._action = action;
this._needQuota = _actionNeedQuotaCheck[apiMethod] === true; this._needQuota = _actionNeedQuotaCheck[apiMethod] === true;
this._postXml = postXml;
this._requestObjTags = null;
this._existingObjTag = null;
this._needTagEval = false;
return this; return this;
} }
@ -291,6 +295,10 @@ class RequestContext {
securityToken: this._securityToken, securityToken: this._securityToken,
policyArn: this._policyArn, policyArn: this._policyArn,
action: this._action, action: this._action,
postXml: this._postXml,
requestObjTags: this._requestObjTags,
existingObjTag: this._existingObjTag,
needTagEval: this._needTagEval,
}; };
return JSON.stringify(requestInfo); return JSON.stringify(requestInfo);
} }
@ -316,7 +324,7 @@ class RequestContext {
obj.apiMethod, obj.awsService, obj.locationConstraint, obj.apiMethod, obj.awsService, obj.locationConstraint,
obj.requesterInfo, obj.signatureVersion, obj.requesterInfo, obj.signatureVersion,
obj.authType, obj.signatureAge, obj.securityToken, obj.policyArn, obj.authType, obj.signatureAge, obj.securityToken, obj.policyArn,
obj.action); obj.action, obj.postXml);
} }
/** /**
@ -659,6 +667,86 @@ class RequestContext {
isQuotaCheckNeeded() { isQuotaCheckNeeded() {
return this._needQuota; return this._needQuota;
} }
/**
* Set request post
*
* @param {string} postXml - request post
* @return {RequestContext} itself
*/
setPostXml(postXml) {
this._postXml = postXml;
return this;
}
/**
* Get request post
*
* @return {string} request post
*/
getPostXml() {
return this._postXml;
}
/**
* Set request object tags
*
* @param {string} requestObjTags - object tag(s) included in request in query string form
* @return {RequestContext} itself
*/
setRequestObjTags(requestObjTags) {
this._requestObjTags = requestObjTags;
return this;
}
/**
* Get request object tags
*
* @return {string} request object tag(s)
*/
getRequestObjTags() {
return this._requestObjTags;
}
/**
* Set info on existing tag on object included in request
*
* @param {string} existingObjTag - existing object tag in query string form
* @return {RequestContext} itself
*/
setExistingObjTag(existingObjTag) {
this._existingObjTag = existingObjTag;
return this;
}
/**
* Get existing object tag
*
* @return {string} existing object tag
*/
getExistingObjTag() {
return this._existingObjTag;
}
/**
* Set whether IAM policy tag condition keys should be evaluated
*
* @param {boolean} needTagEval - whether to evaluate tags
* @return {RequestContext} itself
*/
setNeedTagEval(needTagEval) {
this._needTagEval = needTagEval;
return this;
}
/**
* Get needTagEval param
*
* @return {boolean} needTagEval - whether IAM policy tags condition keys should be evaluated
*/
getNeedTagEval() {
return this._needTagEval;
}
} }
module.exports = RequestContext; module.exports = RequestContext;

View File

@ -6,6 +6,7 @@ const conditions = require('./utils/conditions.js');
const findConditionKey = conditions.findConditionKey; const findConditionKey = conditions.findConditionKey;
const convertConditionOperator = conditions.convertConditionOperator; const convertConditionOperator = conditions.convertConditionOperator;
const checkArnMatch = require('./utils/checkArnMatch.js'); const checkArnMatch = require('./utils/checkArnMatch.js');
const { transformTagKeyValue } = require('./utils/objectTags');
const evaluators = {}; const evaluators = {};
@ -16,6 +17,7 @@ const operatorsWithVariables = ['StringEquals', 'StringNotEquals',
const operatorsWithNegation = ['StringNotEquals', const operatorsWithNegation = ['StringNotEquals',
'StringNotEqualsIgnoreCase', 'StringNotLike', 'ArnNotEquals', 'StringNotEqualsIgnoreCase', 'StringNotLike', 'ArnNotEquals',
'ArnNotLike', 'NumericNotEquals']; 'ArnNotLike', 'NumericNotEquals'];
const tagConditions = ['s3:ExistingObjectTag', 's3:RequestObjectTagKey', 's3:RequestObjectTagKeys'];
/** /**
@ -96,20 +98,27 @@ evaluators.isActionApplicable = (requestAction, statementAction, log) => {
* @param {RequestContext} requestContext - info about request * @param {RequestContext} requestContext - info about request
* @param {Object} statementCondition - Condition statement from policy * @param {Object} statementCondition - Condition statement from policy
* @param {Object} log - logger * @param {Object} log - logger
* @return {boolean} true if meet conditions, false if not * @return {Object} contains whether conditions are allowed and whether they
* contain any tag condition keys
*/ */
evaluators.meetConditions = (requestContext, statementCondition, log) => { evaluators.meetConditions = (requestContext, statementCondition, log) => {
// The Condition portion of a policy is an object with different // The Condition portion of a policy is an object with different
// operators as keys // operators as keys
const conditionEval = {};
const operators = Object.keys(statementCondition); const operators = Object.keys(statementCondition);
const length = operators.length; const length = operators.length;
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
const operator = operators[i]; const operator = operators[i];
const hasPrefix = operator.startsWith('ForAnyValue') || operator.startsWith('ForAllValues');
const hasIfExistsCondition = operator.endsWith('IfExists'); const hasIfExistsCondition = operator.endsWith('IfExists');
// If has "IfExists" added to operator name, find operator name // If has "IfExists" added to operator name, or operator has "ForAnyValue" or
// without "IfExists" // "For All Values" prefix, find operator name without "IfExists" or prefix
const bareOperator = hasIfExistsCondition ? operator.slice(0, -8) : let bareOperator = hasIfExistsCondition ? operator.slice(0, -8) :
operator; operator;
let prefix;
if (hasPrefix) {
[prefix, bareOperator] = bareOperator.split(':');
}
const operatorCanHaveVariables = const operatorCanHaveVariables =
operatorsWithVariables.indexOf(bareOperator) > -1; operatorsWithVariables.indexOf(bareOperator) > -1;
const isNegationOperator = const isNegationOperator =
@ -118,6 +127,9 @@ evaluators.meetConditions = (requestContext, statementCondition, log) => {
// Note: this should be the actual operator name, not the bareOperator // Note: this should be the actual operator name, not the bareOperator
const conditionsWithSameOperator = statementCondition[operator]; const conditionsWithSameOperator = statementCondition[operator];
const conditionKeys = Object.keys(conditionsWithSameOperator); const conditionKeys = Object.keys(conditionsWithSameOperator);
if (conditionKeys.some(key => tagConditions.includes(key))) {
conditionEval.tagConditions = true;
}
const conditionKeysLength = conditionKeys.length; const conditionKeysLength = conditionKeys.length;
for (let j = 0; j < conditionKeysLength; j++) { for (let j = 0; j < conditionKeysLength; j++) {
const key = conditionKeys[j]; const key = conditionKeys[j];
@ -130,14 +142,18 @@ evaluators.meetConditions = (requestContext, statementCondition, log) => {
value = value.map(item => value = value.map(item =>
substituteVariables(item, requestContext)); substituteVariables(item, requestContext));
} }
// if condition key is RequestObjectTag or ExistingObjectTag,
// tag key is included in condition key and needs to be
// moved to value for evaluation, otherwise key/value are unchanged
const [transformedKey, transformedValue] = transformTagKeyValue(key, value);
// Pull key using requestContext // Pull key using requestContext
// TODO: If applicable to S3, handle policy set operations // TODO: If applicable to S3, handle policy set operations
// where a keyBasedOnRequestContext returns multiple values and // where a keyBasedOnRequestContext returns multiple values and
// condition has "ForAnyValue" or "ForAllValues". // condition has "ForAnyValue" or "ForAllValues".
// (see http://docs.aws.amazon.com/IAM/latest/UserGuide/ // (see http://docs.aws.amazon.com/IAM/latest/UserGuide/
// reference_policies_multi-value-conditions.html) // reference_policies_multi-value-conditions.html)
const keyBasedOnRequestContext = let keyBasedOnRequestContext =
findConditionKey(key, requestContext); findConditionKey(transformedKey, requestContext);
// Handle IfExists and negation operators // Handle IfExists and negation operators
if ((keyBasedOnRequestContext === undefined || if ((keyBasedOnRequestContext === undefined ||
keyBasedOnRequestContext === null) && keyBasedOnRequestContext === null) &&
@ -154,22 +170,27 @@ evaluators.meetConditions = (requestContext, statementCondition, log) => {
bareOperator !== 'Null') { bareOperator !== 'Null') {
log.trace('condition not satisfied due to ' + log.trace('condition not satisfied due to ' +
'missing info', { operator, 'missing info', { operator,
conditionKey: key, policyValue: value }); conditionKey: transformedKey, policyValue: transformedValue });
return false; return { allow: false };
}
// If condition operator prefix is included, the key should be an array
if (prefix && !Array.isArray(keyBasedOnRequestContext)) {
keyBasedOnRequestContext = [keyBasedOnRequestContext];
} }
// Transalate operator into function using bareOperator // Transalate operator into function using bareOperator
const operatorFunction = convertConditionOperator(bareOperator); const operatorFunction = convertConditionOperator(bareOperator);
// Note: Wildcards are handled in the comparison operator function // Note: Wildcards are handled in the comparison operator function
// itself since StringLike, StringNotLike, ArnLike and ArnNotLike // itself since StringLike, StringNotLike, ArnLike and ArnNotLike
// are the only operators where wildcards are allowed // are the only operators where wildcards are allowed
if (!operatorFunction(keyBasedOnRequestContext, value)) { if (!operatorFunction(keyBasedOnRequestContext, transformedValue, prefix)) {
log.trace('did not satisfy condition', { operator: bareOperator, log.trace('did not satisfy condition', { operator: bareOperator,
keyBasedOnRequestContext, policyValue: value }); keyBasedOnRequestContext, policyValue: transformedValue });
return false; return { allow: false };
} }
} }
} }
return true; conditionEval.allow = true;
return conditionEval;
}; };
/** /**
@ -220,10 +241,10 @@ evaluators.evaluatePolicy = (requestContext, policy, log) => {
currentStatement.NotAction, log)) { currentStatement.NotAction, log)) {
continue; continue;
} }
const conditionEval = evaluators.meetConditions(requestContext,
currentStatement.Condition, log);
// If do not meet conditions move on to next statement // If do not meet conditions move on to next statement
if (currentStatement.Condition && if (currentStatement.Condition && !conditionEval.allow) {
!evaluators.meetConditions(requestContext,
currentStatement.Condition, log)) {
continue; continue;
} }
if (currentStatement.Effect === 'Deny') { if (currentStatement.Effect === 'Deny') {
@ -235,6 +256,9 @@ evaluators.evaluatePolicy = (requestContext, policy, log) => {
// If statement is applicable, conditions are met and Effect is // If statement is applicable, conditions are met and Effect is
// to Allow, set verdict to Allow // to Allow, set verdict to Allow
verdict = 'Allow'; verdict = 'Allow';
if (conditionEval.tagConditions) {
verdict = 'NeedTagConditionEval';
}
} }
log.trace('result of evaluating single policy', { verdict }); log.trace('result of evaluating single policy', { verdict });
return verdict; return verdict;

View File

@ -4,6 +4,7 @@
const checkIPinRangeOrMatch = require('../../ipCheck').checkIPinRangeOrMatch; const checkIPinRangeOrMatch = require('../../ipCheck').checkIPinRangeOrMatch;
const handleWildcards = require('./wildcards.js').handleWildcards; const handleWildcards = require('./wildcards.js').handleWildcards;
const checkArnMatch = require('./checkArnMatch.js'); const checkArnMatch = require('./checkArnMatch.js');
const { getTagKeys } = require('./objectTags');
const conditions = {}; const conditions = {};
/** /**
@ -29,6 +30,11 @@ conditions.findConditionKey = (key, requestContext) => {
// aws:EpochTime Used for date/time conditions // aws:EpochTime Used for date/time conditions
// (see Date Condition Operators). // (see Date Condition Operators).
map.set('aws:EpochTime', Date.now().toString()); map.set('aws:EpochTime', Date.now().toString());
// aws:ExistingObjectTag - Used to check that existing object tag has
// specific tag key and value. Extraction of correct tag key is done in CloudServer.
// On first pass of policy evaluation, CloudServer information will not be included,
// so evaluation should be skipped
map.set('aws:ExistingObjectTag', requestContext.getNeedTagEval() ? requestContext.getExistingObjTag() : undefined);
// aws:TokenIssueTime Date/time that temporary security // aws:TokenIssueTime Date/time that temporary security
// credentials were issued (see Date Condition Operators). // credentials were issued (see Date Condition Operators).
// Only present in requests that are signed using temporary security // Only present in requests that are signed using temporary security
@ -59,6 +65,18 @@ conditions.findConditionKey = (key, requestContext) => {
// services, such as S3. Value comes from the referer header in the // services, such as S3. Value comes from the referer header in the
// HTTPS request made to AWS. // HTTPS request made to AWS.
map.set('aws:referer', headers.referer); map.set('aws:referer', headers.referer);
// aws:RequestObjectTag - Used to limit putting object tags to specific
// tag key and value. N/A here.
// Requires information from CloudServer
// On first pass of policy evaluation, CloudServer information will not be included,
// so evaluation should be skipped
map.set('aws:RequestObjectTag', requestContext.getNeedTagEval() ? requestContext.getRequestObjTags() : undefined);
// aws:RequestObjectTagKeys - Used to limit putting object tags specific tag keys.
// Requires information from CloudServer.
// On first pass of policy evaluation, CloudServer information will not be included,
// so evaluation should be skipped
map.set('aws:RequestObjectTagKeys',
requestContext.getNeedTagEval() ? getTagKeys(requestContext.getRequestObjTags()) : undefined);
// aws:SecureTransport Used to check whether the request was sent // aws:SecureTransport Used to check whether the request was sent
// using SSL (see Boolean Condition Operators). // using SSL (see Boolean Condition Operators).
map.set('aws:SecureTransport', map.set('aws:SecureTransport',
@ -232,12 +250,21 @@ conditions.convertConditionOperator = operator => {
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
return !operatorMap.StringEqualsIgnoreCase(key, value); return !operatorMap.StringEqualsIgnoreCase(key, value);
}, },
StringLike: function stringLike(key, value) { StringLike: function stringLike(key, value, prefix) {
function policyValRegex(testKey) {
return value.some(item => { return value.some(item => {
const wildItem = handleWildcards(item); const wildItem = handleWildcards(item);
const wildRegEx = new RegExp(wildItem); const wildRegEx = new RegExp(wildItem);
return wildRegEx.test(key); return wildRegEx.test(testKey);
}); });
}
if (prefix === 'ForAnyValue') {
return key.some(k => policyValRegex(k));
}
if (prefix === 'ForAllValues') {
return key.every(k => policyValRegex(k));
}
return policyValRegex(key);
}, },
StringNotLike: function stringNotLike(key, value) { StringNotLike: function stringNotLike(key, value) {
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap

View File

@ -0,0 +1,33 @@
/**
* Removes tag key value from condition key and adds it to value if needed
* @param {string} key - condition key
* @param {string} value - condition value
* @return {array} key/value pair to use
*/
function transformTagKeyValue(key, value) {
if (!key.includes('/')) {
return [key, value];
}
// if key is RequestObjectTag or ExistingObjectTag,
// remove tag key from condition key and add to value
// and transform value into query string
const [conditionKey, tagKey] = key.split('/');
const transformedValue = [tagKey, value].join('=');
return [conditionKey, [transformedValue]];
}
/**
* Gets array of tag key names from request tag query string
* @param {string} tagQuery - request tags in query string format
* @return {array} array of tag key names
*/
function getTagKeys(tagQuery) {
const tagsArray = tagQuery.split(',');
const keysArray = tagsArray.map(tag => tag.split('=')[0]);
return keysArray;
}
module.exports = {
transformTagKeyValue,
getTagKeys,
};