Compare commits

...

13 Commits

Author SHA1 Message Date
Jonathan Gramain 23f7e5af6a ARSN-296 bump version to 7.10.29-4 2023-01-23 13:12:14 +01:00
Jonathan Gramain cb31a07d8d ARSN-296 Revert "ARSN-252 - listing bug in DelimisterMaster"
This reverts commit 5f0df14ca8.
2023-01-23 13:12:14 +01:00
Jonathan Gramain 62be7f926d ARSN-296 Revert "ARSN-269 - listing bug in versioned bucket edge cases."
This reverts commit 88f16366a1.
2023-01-23 12:57:46 +01:00
Alexander Chan 2d205f8131 ARSN-283: bump version 7.10.29-3 2022-11-29 19:13:53 -08:00
Jonathan Gramain 0aa95823b7 bugfix: ARSN-262 fixes/tests in RequestContext
- remove "postXml" field, as it was a left-over from prototyping

- handle fields related to tag conditions: requestObjTags,
  existingObjTag, needTagEval, those were missing from constructor
  params

- fix a typo in serialization: requersterInfo -> requesterInfo

- new unit tests for RequestContext
  constructor/serialize/deserialize/getters

(cherry picked from commit 4f2b1ca960)
2022-11-29 19:06:54 -08:00
Jonathan Gramain 5f1e91b44c bugfix: ARSN-255 revamp evaluatePolicy logic for tag conditions
Rethink the logic of tag condition evaluation, so that the
"evaluateAllPolicies" function appropriately returns the verdict:
Allow or Deny or NeedTagConditionEval, the latter being when tag
values (request and/or object tags) are needed to settle the verdict
to Allow or Deny, in which case, Cloudserver knows it has to resend
the request to Vault along with tag info.

(cherry picked from commit 5cd1df8601)
2022-11-29 18:50:00 -08:00
Jonathan Gramain c5c736aa48 ARSN-255 [cleanup] better exports in evaluator.ts
Turn 'const' function objects into actual functions.

(cherry picked from commit ee38856f29)
2022-11-29 18:49:31 -08:00
Jonathan Gramain bca37e4b06 improvement: ARSN-260 improve efficiency of findConditionKey
Instead of pre-creating a Map with all supported condition keys before
returning the wanted one, use a switch/case construct to directly
return the attribute from the request context.

(cherry picked from commit dc229bb8aa)
2022-11-29 18:49:22 -08:00
Nicolas Humbert 5fd84e78e2 bump package version 2022-11-23 15:21:50 -05:00
williamlardier dd603482f0 ARSN-270: change bad permission names
(cherry picked from commit 7763685cb0)
2022-11-23 15:21:25 -05:00
Artem Bakalov c16d4e6954 Bump version to 7.10.29-1 2022-10-11 16:10:00 -07:00
Artem Bakalov 88f16366a1 ARSN-269 - listing bug in versioned bucket edge cases.
Simplifies testing that was used in ARSN-262. Adds a function allowDelimiterRangeSkip
to determine when a nextContinueMarker range can be skipped when .skipping is called.
This function uses a new state variable prefixKeySeen and the nextContinueMarker to determine
if a range of the form prefix/ can be skipped. An additional check is added when processing
delete markers of the form prefix/foo/(bar) so that the prefix/foo/ range can still be skipped
as an optimization.

(cherry picked from commit 87b060f2ae)
2022-10-11 16:09:03 -07:00
Artem Bakalov 5f0df14ca8 ARSN-252 - listing bug in DelimisterMaster
DelimiterMaster.filter is used to determine when a key range can be skipped in Metadata:RepdServer to optimize listing performance.
When a bucket is created with vFormat=v0, and subsequently a listing is done with a prefix, DelimiterMaster.filter was incorrectly
determining that a range could be skipped if a key was listed such that key == prefix. This case is now correctly handled in filterV0.

(cherry picked from commit f62c3d22ed)
2022-10-11 13:48:54 -07:00
9 changed files with 679 additions and 223 deletions

View File

@ -166,7 +166,6 @@ export default class RequestContext {
_policyArn: string; _policyArn: string;
_action?: string; _action?: string;
_needQuota: boolean; _needQuota: boolean;
_postXml?: string;
_requestObjTags: string | null; _requestObjTags: string | null;
_existingObjTag: string | null; _existingObjTag: string | null;
_needTagEval: boolean; _needTagEval: boolean;
@ -190,7 +189,9 @@ export default class RequestContext {
securityToken: string, securityToken: string,
policyArn: string, policyArn: string,
action?: string, action?: string,
postXml?: string, requestObjTags?: string,
existingObjTag?: string,
needTagEval?: false,
) { ) {
this._headers = headers; this._headers = headers;
this._query = query; this._query = query;
@ -220,10 +221,9 @@ export default 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 = requestObjTags || null;
this._requestObjTags = null; this._existingObjTag = existingObjTag || null;
this._existingObjTag = null; this._needTagEval = needTagEval || false;
this._needTagEval = false;
return this; return this;
} }
@ -236,7 +236,7 @@ export default class RequestContext {
apiMethod: this._apiMethod, apiMethod: this._apiMethod,
headers: this._headers, headers: this._headers,
query: this._query, query: this._query,
requersterInfo: this._requesterInfo, requesterInfo: this._requesterInfo,
requesterIp: this._requesterIp, requesterIp: this._requesterIp,
sslEnabled: this._sslEnabled, sslEnabled: this._sslEnabled,
awsService: this._awsService, awsService: this._awsService,
@ -252,7 +252,6 @@ export default 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, requestObjTags: this._requestObjTags,
existingObjTag: this._existingObjTag, existingObjTag: this._existingObjTag,
needTagEval: this._needTagEval, needTagEval: this._needTagEval,
@ -276,12 +275,27 @@ export default class RequestContext {
if (resource) { if (resource) {
obj.specificResource = resource; obj.specificResource = resource;
} }
return new RequestContext(obj.headers, obj.query, obj.generalResource, return new RequestContext(
obj.specificResource, obj.requesterIp, obj.sslEnabled, obj.headers,
obj.apiMethod, obj.awsService, obj.locationConstraint, obj.query,
obj.requesterInfo, obj.signatureVersion, obj.generalResource,
obj.authType, obj.signatureAge, obj.securityToken, obj.policyArn, obj.specificResource,
obj.action, obj.postXml); obj.requesterIp,
obj.sslEnabled,
obj.apiMethod,
obj.awsService,
obj.locationConstraint,
obj.requesterInfo,
obj.signatureVersion,
obj.authType,
obj.signatureAge,
obj.securityToken,
obj.policyArn,
obj.action,
obj.requestObjTags,
obj.existingObjTag,
obj.needTagEval,
);
} }
/** /**
@ -625,26 +639,6 @@ export default class RequestContext {
return this._needQuota; return this._needQuota;
} }
/**
* Set request post
*
* @param postXml - request post
* @return itself
*/
setPostXml(postXml: string) {
this._postXml = postXml;
return this;
}
/**
* Get request post
*
* @return request post
*/
getPostXml() {
return this._postXml;
}
/** /**
* Set request object tags * Set request object tags
* *

View File

@ -13,7 +13,11 @@ const operatorsWithVariables = ['StringEquals', 'StringNotEquals',
const operatorsWithNegation = ['StringNotEquals', const operatorsWithNegation = ['StringNotEquals',
'StringNotEqualsIgnoreCase', 'StringNotLike', 'ArnNotEquals', 'StringNotEqualsIgnoreCase', 'StringNotLike', 'ArnNotEquals',
'ArnNotLike', 'NumericNotEquals']; 'ArnNotLike', 'NumericNotEquals'];
const tagConditions = new Set(['s3:ExistingObjectTag', 's3:RequestObjectTagKey', 's3:RequestObjectTagKeys']); const tagConditions = new Set([
's3:ExistingObjectTag',
's3:RequestObjectTagKey',
's3:RequestObjectTagKeys',
]);
/** /**
@ -24,11 +28,11 @@ const tagConditions = new Set(['s3:ExistingObjectTag', 's3:RequestObjectTagKey',
* @param log - logger * @param log - logger
* @return true if applicable, false if not * @return true if applicable, false if not
*/ */
export const isResourceApplicable = ( export function isResourceApplicable(
requestContext: RequestContext, requestContext: RequestContext,
statementResource: string | string[], statementResource: string | string[],
log: Logger, log: Logger,
): boolean => { ): boolean {
const resource = requestContext.getResource(); const resource = requestContext.getResource();
if (!Array.isArray(statementResource)) { if (!Array.isArray(statementResource)) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
@ -59,7 +63,7 @@ export const isResourceApplicable = (
{ requestResource: resource }); { requestResource: resource });
// If no match found, no resource is applicable // If no match found, no resource is applicable
return false; return false;
}; }
/** /**
* Check whether action in policy statement applies to request * Check whether action in policy statement applies to request
@ -69,11 +73,11 @@ export const isResourceApplicable = (
* @param log - logger * @param log - logger
* @return true if applicable, false if not * @return true if applicable, false if not
*/ */
export const isActionApplicable = ( export function isActionApplicable(
requestAction: string, requestAction: string,
statementAction: string | string[], statementAction: string | string[],
log: Logger, log: Logger,
): boolean => { ): boolean {
if (!Array.isArray(statementAction)) { if (!Array.isArray(statementAction)) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
statementAction = [statementAction]; statementAction = [statementAction];
@ -95,32 +99,33 @@ export const isActionApplicable = (
{ requestAction }); { requestAction });
// If no match found, return false // If no match found, return false
return false; return false;
}; }
/** /**
* Check whether request meets policy conditions * Check whether request meets policy conditions
* @param requestContext - info about request * @param {RequestContext} requestContext - info about request
* @param statementCondition - Condition statement from policy * @param {object} statementCondition - Condition statement from policy
* @param log - logger * @param {Logger} log - logger
* @return contains whether conditions are allowed and whether they * @return {boolean|null} a condition evaluation result, one of:
* contain any tag condition keys * - true: condition is met
* - false: condition is not met
* - null: condition evaluation requires additional info to be
* provided (namely, for tag conditions, request tags and/or object
* tags have to be provided to evaluate the condition)
*/ */
export const meetConditions = ( export function meetConditions(
requestContext: RequestContext, requestContext: RequestContext,
statementCondition: any, statementCondition: any,
log: Logger, log: Logger,
) => { ): boolean | null {
let hasTagConditions = false;
// 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 = {}; for (const operator of Object.keys(statementCondition)) {
const operators = Object.keys(statementCondition);
const length = operators.length;
for (let i = 0; i < length; i++) {
const operator = operators[i];
const hasPrefix = operator.includes(':'); const hasPrefix = operator.includes(':');
const hasIfExistsCondition = operator.endsWith('IfExists'); const hasIfExistsCondition = operator.endsWith('IfExists');
// If has "IfExists" added to operator name, or operator has "ForAnyValue" or // If has "IfExists" added to operator name, or operator has "ForAnyValue" or
// "For All Values" prefix, find operator name without "IfExists" or prefix // "ForAllValues" prefix, find operator name without "IfExists" or prefix
let bareOperator = hasIfExistsCondition ? operator.slice(0, -8) : let bareOperator = hasIfExistsCondition ? operator.slice(0, -8) :
operator; operator;
let prefix: string | undefined; let prefix: string | undefined;
@ -135,10 +140,6 @@ export const meetConditions = (
// 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.has(key)) && !requestContext.getNeedTagEval()) {
// @ts-expect-error
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];
@ -155,6 +156,10 @@ export const meetConditions = (
// tag key is included in condition key and needs to be // tag key is included in condition key and needs to be
// moved to value for evaluation, otherwise key/value are unchanged // moved to value for evaluation, otherwise key/value are unchanged
const [transformedKey, transformedValue] = transformTagKeyValue(key, value); const [transformedKey, transformedValue] = transformTagKeyValue(key, value);
if (tagConditions.has(transformedKey) && !requestContext.getNeedTagEval()) {
hasTagConditions = true;
continue;
}
// 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
@ -180,11 +185,10 @@ export const meetConditions = (
log.trace('condition not satisfied due to ' + log.trace('condition not satisfied due to ' +
'missing info', { operator, 'missing info', { operator,
conditionKey: transformedKey, policyValue: transformedValue }); conditionKey: transformedKey, policyValue: transformedValue });
return { allow: false }; return false;
} }
// If condition operator prefix is included, the key should be an array // If condition operator prefix is included, the key should be an array
if (prefix && !Array.isArray(keyBasedOnRequestContext)) { if (prefix && !Array.isArray(keyBasedOnRequestContext)) {
// @ts-expect-error
keyBasedOnRequestContext = [keyBasedOnRequestContext]; keyBasedOnRequestContext = [keyBasedOnRequestContext];
} }
// Transalate operator into function using bareOperator // Transalate operator into function using bareOperator
@ -196,14 +200,16 @@ export const meetConditions = (
if (!operatorFunction(keyBasedOnRequestContext, transformedValue, prefix)) { if (!operatorFunction(keyBasedOnRequestContext, transformedValue, prefix)) {
log.trace('did not satisfy condition', { operator: bareOperator, log.trace('did not satisfy condition', { operator: bareOperator,
keyBasedOnRequestContext, policyValue: transformedValue }); keyBasedOnRequestContext, policyValue: transformedValue });
return { allow: false }; return false;
} }
} }
} }
// @ts-expect-error // one or more conditions required tag info to be evaluated
conditionEval.allow = true; if (hasTagConditions) {
return conditionEval; return null;
}; }
return true;
}
/** /**
* Evaluate whether a request is permitted under a policy. * Evaluate whether a request is permitted under a policy.
@ -216,13 +222,15 @@ export const meetConditions = (
* @return Allow if permitted, Deny if not permitted or Neutral * @return Allow if permitted, Deny if not permitted or Neutral
* if not applicable * if not applicable
*/ */
export const evaluatePolicy = ( export function evaluatePolicy(
requestContext: RequestContext, requestContext: RequestContext,
policy: any, policy: any,
log: Logger, log: Logger,
): string => { ): string {
// TODO: For bucket policies need to add Principal evaluation // TODO: For bucket policies need to add Principal evaluation
let verdict = 'Neutral'; let allow = false;
let allowWithTagCondition = false;
let denyWithTagCondition = false;
if (!Array.isArray(policy.Statement)) { if (!Array.isArray(policy.Statement)) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
@ -259,10 +267,18 @@ export const evaluatePolicy = (
} }
const conditionEval = currentStatement.Condition ? const conditionEval = currentStatement.Condition ?
meetConditions(requestContext, currentStatement.Condition, log) : meetConditions(requestContext, currentStatement.Condition, log) :
null; true;
// If do not meet conditions move on to next statement // If do not meet conditions move on to next statement
// @ts-expect-error if (conditionEval === false) {
if (conditionEval && !conditionEval.allow) { continue;
}
// If condition needs tag info to be evaluated, mark and move on to next statement
if (conditionEval === null) {
if (currentStatement.Effect === 'Deny') {
denyWithTagCondition = true;
} else {
allowWithTagCondition = true;
}
continue; continue;
} }
if (currentStatement.Effect === 'Deny') { if (currentStatement.Effect === 'Deny') {
@ -271,17 +287,27 @@ export const evaluatePolicy = (
return 'Deny'; return 'Deny';
} }
log.trace('Allow statement applies'); log.trace('Allow statement applies');
// If statement is applicable, conditions are met and Effect is // statement is applicable, conditions are met and Effect is
// to Allow, set verdict to Allow // to Allow
allow = true;
}
let verdict;
if (denyWithTagCondition) {
// priority is on checking tags to potentially deny
verdict = 'DenyWithTagCondition';
} else if (allow) {
// at least one statement is an allow
verdict = 'Allow'; verdict = 'Allow';
// @ts-expect-error } else if (allowWithTagCondition) {
if (conditionEval && conditionEval.tagConditions) { // all allow statements need tag checks
verdict = 'NeedTagConditionEval'; verdict = 'AllowWithTagCondition';
} } else {
// no statement matched to allow or deny
verdict = 'Neutral';
} }
log.trace('result of evaluating single policy', { verdict }); log.trace('result of evaluating single policy', { verdict });
return verdict; return verdict;
}; }
/** /**
* Evaluate whether a request is permitted under a policy. * Evaluate whether a request is permitted under a policy.
@ -294,24 +320,43 @@ export const evaluatePolicy = (
* @return Allow if permitted, Deny if not permitted. * @return Allow if permitted, Deny if not permitted.
* Default is to Deny. Deny overrides an Allow * Default is to Deny. Deny overrides an Allow
*/ */
export const evaluateAllPolicies = ( export function evaluateAllPolicies(
requestContext: RequestContext, requestContext: RequestContext,
allPolicies: any[], allPolicies: any[],
log: Logger, log: Logger,
): string => { ): string {
log.trace('evaluating all policies'); log.trace('evaluating all policies');
let verdict = 'Deny'; let allow = false;
let allowWithTagCondition = false;
let denyWithTagCondition = false;
for (let i = 0; i < allPolicies.length; i++) { for (let i = 0; i < allPolicies.length; i++) {
const singlePolicyVerdict = const singlePolicyVerdict = evaluatePolicy(requestContext, allPolicies[i], log);
evaluatePolicy(requestContext, allPolicies[i], log);
// If there is any Deny, just return Deny // If there is any Deny, just return Deny
if (singlePolicyVerdict === 'Deny') { if (singlePolicyVerdict === 'Deny') {
return 'Deny'; return 'Deny';
} }
if (singlePolicyVerdict === 'Allow') { if (singlePolicyVerdict === 'Allow') {
allow = true;
} else if (singlePolicyVerdict === 'AllowWithTagCondition') {
allowWithTagCondition = true;
} else if (singlePolicyVerdict === 'DenyWithTagCondition') {
denyWithTagCondition = true;
} // else 'Neutral'
}
let verdict;
if (allow) {
if (denyWithTagCondition) {
verdict = 'NeedTagConditionEval';
} else {
verdict = 'Allow'; verdict = 'Allow';
} }
} else {
if (allowWithTagCondition) {
verdict = 'NeedTagConditionEval';
} else {
verdict = 'Deny';
}
} }
log.trace('result of evaluating all pollicies', { verdict }); log.trace('result of evaluating all policies', { verdict });
return verdict; return verdict;
}; }

View File

@ -23,15 +23,22 @@ export default class Principal {
* @param statement - Statement policy field * @param statement - Statement policy field
* @return True if meet conditions * @return True if meet conditions
*/ */
static _evaluateCondition( static _evaluateStatement(
params: Params, params: Params,
statement: Statement, statement: Statement,
// TODO Fix return type ): 'Neutral' | 'Allow' | 'Deny' {
): any { const reverse = !!statement.NotPrincipal;
if (statement.Condition) { if (reverse) {
return meetConditions(params.rc, statement.Condition, params.log); // In case of anonymous NotPrincipal, this will neutral everyone
return 'Neutral';
} }
return true; if (statement.Condition) {
const conditionEval = meetConditions(params.rc, statement.Condition, params.log);
if (conditionEval === false || conditionEval === null) {
return 'Neutral';
}
}
return statement.Effect;
} }
/** /**
@ -48,19 +55,12 @@ export default class Principal {
statement: Statement, statement: Statement,
valids: Valid, valids: Valid,
): 'Neutral' | 'Allow' | 'Deny' { ): 'Neutral' | 'Allow' | 'Deny' {
const reverse = !!statement.NotPrincipal;
const principal = (statement.Principal || statement.NotPrincipal)!; const principal = (statement.Principal || statement.NotPrincipal)!;
if (typeof principal === 'string' && principal === '*') { const reverse = !!statement.NotPrincipal;
if (reverse) { if (typeof principal === 'string') {
// In case of anonymous NotPrincipal, this will neutral everyone if (principal === '*') {
return 'Neutral'; return Principal._evaluateStatement(params, statement);
} }
const conditionEval = Principal._evaluateCondition(params, statement);
if (!conditionEval || conditionEval.allow === false) {
return 'Neutral';
}
return statement.Effect;
} else if (typeof principal === 'string') {
return 'Deny'; return 'Deny';
} }
let ref = []; let ref = [];
@ -82,28 +82,8 @@ export default class Principal {
} }
toCheck = Array.isArray(toCheck) ? toCheck : [toCheck]; toCheck = Array.isArray(toCheck) ? toCheck : [toCheck];
ref = Array.isArray(ref) ? ref : [ref]; ref = Array.isArray(ref) ? ref : [ref];
if (toCheck.indexOf('*') !== -1) { if (toCheck.includes('*') || ref.some(r => toCheck.includes(r))) {
if (reverse) { return Principal._evaluateStatement(params, statement);
return 'Neutral';
}
const conditionEval = Principal._evaluateCondition(params, statement);
if (!conditionEval || conditionEval.allow === false) {
return 'Neutral';
}
return statement.Effect;
}
const len = ref.length;
for (let i = 0; i < len; ++i) {
if (toCheck.indexOf(ref[i]) !== -1) {
if (reverse) {
return 'Neutral';
}
const conditionEval = Principal._evaluateCondition(params, statement);
if (!conditionEval || conditionEval.allow === false) {
return 'Neutral';
}
return statement.Effect;
}
} }
if (reverse) { if (reverse) {
return statement.Effect; return statement.Effect;

View File

@ -4,14 +4,14 @@ const sharedActionMap = {
bucketDeleteEncryption: 's3:PutEncryptionConfiguration', bucketDeleteEncryption: 's3:PutEncryptionConfiguration',
bucketDeletePolicy: 's3:DeleteBucketPolicy', bucketDeletePolicy: 's3:DeleteBucketPolicy',
bucketDeleteWebsite: 's3:DeleteBucketWebsite', bucketDeleteWebsite: 's3:DeleteBucketWebsite',
bucketDeleteTagging: 's3:DeleteBucketTagging', bucketDeleteTagging: 's3:PutBucketTagging',
bucketGet: 's3:ListBucket', bucketGet: 's3:ListBucket',
bucketGetACL: 's3:GetBucketAcl', bucketGetACL: 's3:GetBucketAcl',
bucketGetCors: 's3:GetBucketCORS', bucketGetCors: 's3:GetBucketCORS',
bucketGetEncryption: 's3:GetEncryptionConfiguration', bucketGetEncryption: 's3:GetEncryptionConfiguration',
bucketGetLifecycle: 's3:GetLifecycleConfiguration', bucketGetLifecycle: 's3:GetLifecycleConfiguration',
bucketGetLocation: 's3:GetBucketLocation', bucketGetLocation: 's3:GetBucketLocation',
bucketGetNotification: 's3:GetBucketNotificationConfiguration', bucketGetNotification: 's3:GetBucketNotification',
bucketGetObjectLock: 's3:GetBucketObjectLockConfiguration', bucketGetObjectLock: 's3:GetBucketObjectLockConfiguration',
bucketGetPolicy: 's3:GetBucketPolicy', bucketGetPolicy: 's3:GetBucketPolicy',
bucketGetReplication: 's3:GetReplicationConfiguration', bucketGetReplication: 's3:GetReplicationConfiguration',
@ -23,7 +23,7 @@ const sharedActionMap = {
bucketPutCors: 's3:PutBucketCORS', bucketPutCors: 's3:PutBucketCORS',
bucketPutEncryption: 's3:PutEncryptionConfiguration', bucketPutEncryption: 's3:PutEncryptionConfiguration',
bucketPutLifecycle: 's3:PutLifecycleConfiguration', bucketPutLifecycle: 's3:PutLifecycleConfiguration',
bucketPutNotification: 's3:PutBucketNotificationConfiguration', bucketPutNotification: 's3:PutBucketNotification',
bucketPutObjectLock: 's3:PutBucketObjectLockConfiguration', bucketPutObjectLock: 's3:PutBucketObjectLockConfiguration',
bucketPutPolicy: 's3:PutBucketPolicy', bucketPutPolicy: 's3:PutBucketPolicy',
bucketPutReplication: 's3:PutReplicationConfiguration', bucketPutReplication: 's3:PutReplicationConfiguration',
@ -55,8 +55,8 @@ const actionMapRQ = {
// see http://docs.aws.amazon.com/AmazonS3/latest/API/ // see http://docs.aws.amazon.com/AmazonS3/latest/API/
// RESTBucketDELETEcors.html // RESTBucketDELETEcors.html
bucketDeleteCors: 's3:PutBucketCORS', bucketDeleteCors: 's3:PutBucketCORS',
bucketDeleteReplication: 's3:DeleteReplicationConfiguration', bucketDeleteReplication: 's3:PutReplicationConfiguration',
bucketDeleteLifecycle: 's3:DeleteLifecycleConfiguration', bucketDeleteLifecycle: 's3:PutLifecycleConfiguration',
completeMultipartUpload: 's3:PutObject', completeMultipartUpload: 's3:PutObject',
initiateMultipartUpload: 's3:PutObject', initiateMultipartUpload: 's3:PutObject',
objectDeleteVersion: 's3:DeleteObjectVersion', objectDeleteVersion: 's3:DeleteObjectVersion',
@ -72,6 +72,7 @@ const actionMapRQ = {
objectReplicate: 's3:ReplicateObject', objectReplicate: 's3:ReplicateObject',
objectPutRetentionVersion: 's3:PutObjectVersionRetention', objectPutRetentionVersion: 's3:PutObjectVersionRetention',
objectPutLegalHoldVersion: 's3:PutObjectVersionLegalHold', objectPutLegalHoldVersion: 's3:PutObjectVersionLegalHold',
listObjectVersions: 's3:ListBucketVersions',
...sharedActionMap, ...sharedActionMap,
}; };
@ -104,7 +105,7 @@ const actionMonitoringMapS3 = {
bucketGetCors: 'GetBucketCors', bucketGetCors: 'GetBucketCors',
bucketGetLifecycle: 'GetBucketLifecycleConfiguration', bucketGetLifecycle: 'GetBucketLifecycleConfiguration',
bucketGetLocation: 'GetBucketLocation', bucketGetLocation: 'GetBucketLocation',
bucketGetNotification: 'GetBucketNotificationConfiguration', bucketGetNotification: 'GetBucketNotification',
bucketGetObjectLock: 'GetObjectLockConfiguration', bucketGetObjectLock: 'GetObjectLockConfiguration',
bucketGetPolicy: 'GetBucketPolicy', bucketGetPolicy: 'GetBucketPolicy',
bucketGetReplication: 'GetBucketReplication', bucketGetReplication: 'GetBucketReplication',
@ -117,7 +118,7 @@ const actionMonitoringMapS3 = {
bucketPutACL: 'PutBucketAcl', bucketPutACL: 'PutBucketAcl',
bucketPutCors: 'PutBucketCors', bucketPutCors: 'PutBucketCors',
bucketPutLifecycle: 'PutBucketLifecycleConfiguration', bucketPutLifecycle: 'PutBucketLifecycleConfiguration',
bucketPutNotification: 'PutBucketNotificationConfiguration', bucketPutNotification: 'PutBucketNotification',
bucketPutObjectLock: 'PutObjectLockConfiguration', bucketPutObjectLock: 'PutObjectLockConfiguration',
bucketPutPolicy: 'PutBucketPolicy', bucketPutPolicy: 'PutBucketPolicy',
bucketPutReplication: 'PutBucketReplication', bucketPutReplication: 'PutBucketReplication',

View File

@ -11,31 +11,30 @@ import ipaddr from 'ipaddr.js';
* @param requestContext - info sent with request * @param requestContext - info sent with request
* @return condition key value * @return condition key value
*/ */
export const findConditionKey = ( export function findConditionKey(
key: string, key: string,
requestContext: RequestContext, requestContext: RequestContext,
): string => { ): any {
// TODO: Consider combining with findVariable function if no benefit // TODO: Consider combining with findVariable function if no benefit
// to keeping separate // to keeping separate
const headers = requestContext.getHeaders(); const headers = requestContext.getHeaders();
const query = requestContext.getQuery(); const query = requestContext.getQuery();
const requesterInfo = requestContext.getRequesterInfo(); const requesterInfo = requestContext.getRequesterInfo();
const map = new Map();
// Possible AWS Condition keys (http://docs.aws.amazon.com/IAM/latest/ // Possible AWS Condition keys (http://docs.aws.amazon.com/IAM/latest/
// UserGuide/reference_policies_elements.html#AvailableKeys) // UserGuide/reference_policies_elements.html#AvailableKeys)
switch (key) {
// aws:CurrentTime Used for date/time conditions // aws:CurrentTime Used for date/time conditions
// (see Date Condition Operators). // (see Date Condition Operators).
map.set('aws:CurrentTime', new Date().toISOString()); case 'aws:CurrentTime': return new Date().toISOString();
// 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()); case 'aws:EpochTime': return Date.now().toString();
// 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
// credentials. // credentials.
map.set('aws:TokenIssueTime', requestContext.getTokenIssueTime()); case 'aws:TokenIssueTime': return requestContext.getTokenIssueTime();
// aws:MultiFactorAuthPresent Used to check whether MFA was used // aws:MultiFactorAuthPresent Used to check whether MFA was used
// (see Boolean Condition Operators). // (see Boolean Condition Operators).
// Note: This key is only present if MFA was used. So, the following // Note: This key is only present if MFA was used. So, the following
@ -45,131 +44,132 @@ export const findConditionKey = (
// Instead use: // Instead use:
// "Condition" : // "Condition" :
// { "Null" : { "aws:MultiFactorAuthPresent" : true } } // { "Null" : { "aws:MultiFactorAuthPresent" : true } }
map.set('aws:MultiFactorAuthPresent', case 'aws:MultiFactorAuthPresent': return requestContext.getMultiFactorAuthPresent();
requestContext.getMultiFactorAuthPresent());
// aws:MultiFactorAuthAge Used to check how many seconds since // aws:MultiFactorAuthAge Used to check how many seconds since
// MFA credentials were issued. If MFA was not used, // MFA credentials were issued. If MFA was not used,
// this key is not present // this key is not present
map.set('aws:MultiFactorAuthAge', requestContext.getMultiFactorAuthAge()); case 'aws:MultiFactorAuthAge': return requestContext.getMultiFactorAuthAge();
// aws:principaltype states whether the principal is an account, // aws:principaltype states whether the principal is an account,
// user, federated, or assumed role // user, federated, or assumed role
// Note: Docs for conditions have "PrincipalType" but simulator // Note: Docs for conditions have "PrincipalType" but simulator
// and docs for variables have lowercase // and docs for variables have lowercase
map.set('aws:principaltype', requesterInfo.principaltype); case 'aws:principaltype': return requesterInfo.principaltype;
// aws:Referer Used to check who referred the client browser to // aws:Referer Used to check who referred the client browser to
// the address the request is being sent to. Only supported by some // the address the request is being sent to. Only supported by some
// 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); case 'aws:referer': return headers.referer;
// 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', case 'aws:SecureTransport': return requestContext.getSslEnabled() ? 'true' : 'false';
requestContext.getSslEnabled() ? 'true' : 'false');
// aws:SourceArn Used check the source of the request, // aws:SourceArn Used check the source of the request,
// using the ARN of the source. N/A here. // using the ARN of the source. N/A here.
map.set('aws:SourceArn', undefined); case 'aws:SourceArn': return undefined;
// aws:SourceIp Used to check the requester's IP address // aws:SourceIp Used to check the requester's IP address
// (see IP Address Condition Operators) // (see IP Address Condition Operators)
map.set('aws:SourceIp', requestContext.getRequesterIp()); case 'aws:SourceIp': return requestContext.getRequesterIp();
// aws:SourceVpc Used to restrict access to a specific // aws:SourceVpc Used to restrict access to a specific
// AWS Virtual Private Cloud. N/A here. // AWS Virtual Private Cloud. N/A here.
map.set('aws:SourceVpc', undefined); case 'aws:SourceVpc': return undefined;
// aws:SourceVpce Used to limit access to a specific VPC endpoint // aws:SourceVpce Used to limit access to a specific VPC endpoint
// N/A here // N/A here
map.set('aws:SourceVpce', undefined); case 'aws:SourceVpce': return undefined;
// aws:UserAgent Used to check the requester's client app. // aws:UserAgent Used to check the requester's client app.
// (see String Condition Operators) // (see String Condition Operators)
map.set('aws:UserAgent', headers['user-agent']); case 'aws:UserAgent': return headers['user-agent'];
// aws:userid Used to check the requester's unique user ID. // aws:userid Used to check the requester's unique user ID.
// (see String Condition Operators) // (see String Condition Operators)
map.set('aws:userid', requesterInfo.userid); case 'aws:userid': return requesterInfo.userid;
// aws:username Used to check the requester's friendly user name. // aws:username Used to check the requester's friendly user name.
// (see String Condition Operators) // (see String Condition Operators)
map.set('aws:username', requesterInfo.username); case 'aws:username': return requesterInfo.username;
// Possible condition keys for S3: // Possible condition keys for S3:
// s3:x-amz-acl is acl request for bucket or object put request // s3:x-amz-acl is acl request for bucket or object put request
map.set('s3:x-amz-acl', headers['x-amz-acl']); case 's3:x-amz-acl': return headers['x-amz-acl'];
// s3:x-amz-grant-PERMISSION (where permission can be: // s3:x-amz-grant-PERMISSION (where permission can be:
// read, write, read-acp, write-acp or full-control) // read, write, read-acp, write-acp or full-control)
// Value is the value of that header (ex. id of grantee) // Value is the value of that header (ex. id of grantee)
map.set('s3:x-amz-grant-read', headers['x-amz-grant-read']); case 's3:x-amz-grant-read': return headers['x-amz-grant-read'];
map.set('s3:x-amz-grant-write', headers['x-amz-grant-write']); case 's3:x-amz-grant-write': return headers['x-amz-grant-write'];
map.set('s3:x-amz-grant-read-acp', headers['x-amz-grant-read-acp']); case 's3:x-amz-grant-read-acp': return headers['x-amz-grant-read-acp'];
map.set('s3:x-amz-grant-write-acp', headers['x-amz-grant-write-acp']); case 's3:x-amz-grant-write-acp': return headers['x-amz-grant-write-acp'];
map.set('s3:x-amz-grant-full-control', headers['x-amz-grant-full-control']); case 's3:x-amz-grant-full-control': return headers['x-amz-grant-full-control'];
// s3:x-amz-copy-source is x-amz-copy-source header if applicable on // s3:x-amz-copy-source is x-amz-copy-source header if applicable on
// a put object // a put object
map.set('s3:x-amz-copy-source', headers['x-amz-copy-source']); case 's3:x-amz-copy-source': return headers['x-amz-copy-source'];
// s3:x-amz-metadata-directive is x-amz-metadata-directive header if // s3:x-amz-metadata-directive is x-amz-metadata-directive header if
// applicable on a put object copy. Determines whether metadata will // applicable on a put object copy. Determines whether metadata will
// be copied from original object or replaced. Values or "COPY" or // be copied from original object or replaced. Values or "COPY" or
// "REPLACE". Default is "COPY" // "REPLACE". Default is "COPY"
map.set('s3:x-amz-metadata-directive', headers['metadata-directive']); case 's3:x-amz-metadata-directive': return headers['metadata-directive'];
// s3:x-amz-server-side-encryption -- Used to require that object put // s3:x-amz-server-side-encryption -- Used to require that object put
// use server side encryption. Value is the encryption algo such as // use server side encryption. Value is the encryption algo such as
// "AES256" // "AES256"
map.set('s3:x-amz-server-side-encryption', case 's3:x-amz-server-side-encryption': return headers['x-amz-server-side-encryption'];
headers['x-amz-server-side-encryption']);
// s3:x-amz-storage-class -- x-amz-storage-class header value // s3:x-amz-storage-class -- x-amz-storage-class header value
// (STANDARD, etc.) // (STANDARD, etc.)
map.set('s3:x-amz-storage-class', headers['x-amz-storage-class']); case 's3:x-amz-storage-class': return headers['x-amz-storage-class'];
// s3:VersionId -- version id of object // s3:VersionId -- version id of object
map.set('s3:VersionId', query.versionId); case 's3:VersionId': return query.versionId;
// s3:LocationConstraint -- Used to restrict creation of bucket // s3:LocationConstraint -- Used to restrict creation of bucket
// in certain region. Only applicable for CreateBucket // in certain region. Only applicable for CreateBucket
map.set('s3:LocationConstraint', requestContext.getLocationConstraint()); case 's3:LocationConstraint': return requestContext.getLocationConstraint();
// s3:delimiter is delimiter for listing request // s3:delimiter is delimiter for listing request
map.set('s3:delimiter', query.delimiter); case 's3:delimiter': return query.delimiter;
// s3:max-keys is max-keys for listing request // s3:max-keys is max-keys for listing request
map.set('s3:max-keys', query['max-keys']); case 's3:max-keys': return query['max-keys'];
// s3:prefix is prefix for listing request // s3:prefix is prefix for listing request
map.set('s3:prefix', query.prefix); case 's3:prefix': return query.prefix;
// s3 auth v4 additional condition keys // s3 auth v4 additional condition keys
// (See http://docs.aws.amazon.com/AmazonS3/latest/API/ // (See http://docs.aws.amazon.com/AmazonS3/latest/API/
// bucket-policy-s3-sigv4-conditions.html) // bucket-policy-s3-sigv4-conditions.html)
// s3:signatureversion -- Either "AWS" for v2 or // s3:signatureversion -- Either "AWS" for v2 or
// "AWS4-HMAC-SHA256" for v4 // "AWS4-HMAC-SHA256" for v4
map.set('s3:signatureversion', requestContext.getSignatureVersion()); case 's3:signatureversion': return requestContext.getSignatureVersion();
// s3:authType -- Method of authentication: either "REST-HEADER", // s3:authType -- Method of authentication: either "REST-HEADER",
// "REST-QUERY-STRING" or "POST" // "REST-QUERY-STRING" or "POST"
map.set('s3:authType', requestContext.getAuthType()); case 's3:authType': return requestContext.getAuthType();
// s3:signatureAge is the length of time, in milliseconds, // s3:signatureAge is the length of time, in milliseconds,
// that a signature is valid in an authenticated request. So, // that a signature is valid in an authenticated request. So,
// can use this to limit the age to less than 7 days // can use this to limit the age to less than 7 days
map.set('s3:signatureAge', requestContext.getSignatureAge()); case 's3:signatureAge': return requestContext.getSignatureAge();
// s3:x-amz-content-sha256 - Valid value is "UNSIGNED-PAYLOAD" // s3:x-amz-content-sha256 - Valid value is "UNSIGNED-PAYLOAD"
// so can use this in a deny policy to deny any requests that do not // so can use this in a deny policy to deny any requests that do not
// have a signed payload // have a signed payload
map.set('s3:x-amz-content-sha256', headers['x-amz-content-sha256']); case 's3:x-amz-content-sha256': return headers['x-amz-content-sha256'];
// s3:ObjLocationConstraint is the location constraint set for an // s3:ObjLocationConstraint is the location constraint set for an
// object on a PUT request using the "x-amz-meta-scal-location-constraint" // object on a PUT request using the "x-amz-meta-scal-location-constraint"
// header // header
map.set('s3:ObjLocationConstraint', case 's3:ObjLocationConstraint': return headers['x-amz-meta-scal-location-constraint'];
headers['x-amz-meta-scal-location-constraint']); case 'sts:ExternalId': return requestContext.getRequesterExternalId();
map.set('sts:ExternalId', requestContext.getRequesterExternalId()); case 'iam:PolicyArn': return requestContext.getPolicyArn();
map.set('iam:PolicyArn', requestContext.getPolicyArn());
// s3:ExistingObjectTag - Used to check that existing object tag has // s3:ExistingObjectTag - Used to check that existing object tag has
// specific tag key and value. Extraction of correct tag key is done in CloudServer. // 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, // On first pass of policy evaluation, CloudServer information will not be included,
// so evaluation should be skipped // so evaluation should be skipped
map.set('s3:ExistingObjectTag', requestContext.getNeedTagEval() ? requestContext.getExistingObjTag() : undefined); case 's3:ExistingObjectTag':
return requestContext.getNeedTagEval()
? requestContext.getExistingObjTag() : undefined;
// s3:RequestObjectTag - Used to limit putting object tags to specific // s3:RequestObjectTag - Used to limit putting object tags to specific
// tag key and value. N/A here. // tag key and value. N/A here.
// Requires information from CloudServer // Requires information from CloudServer
// On first pass of policy evaluation, CloudServer information will not be included, // On first pass of policy evaluation, CloudServer information will not be included,
// so evaluation should be skipped // so evaluation should be skipped
map.set('s3:RequestObjectTagKey', requestContext.getNeedTagEval() ? requestContext.getRequestObjTags() : undefined); case 's3:RequestObjectTagKey':
return requestContext.getNeedTagEval()
? requestContext.getRequestObjTags() : undefined;
// s3:RequestObjectTagKeys - Used to limit putting object tags specific tag keys. // s3:RequestObjectTagKeys - Used to limit putting object tags specific tag keys.
// Requires information from CloudServer. // Requires information from CloudServer.
// On first pass of policy evaluation, CloudServer information will not be included, // On first pass of policy evaluation, CloudServer information will not be included,
// so evaluation should be skipped // so evaluation should be skipped
map.set('s3:RequestObjectTagKeys', case 's3:RequestObjectTagKeys':
requestContext.getNeedTagEval() && requestContext.getRequestObjTags() return requestContext.getNeedTagEval() && requestContext.getRequestObjTags()
? getTagKeys(requestContext.getRequestObjTags()!) ? getTagKeys(requestContext.getRequestObjTags()!)
: undefined, : undefined;
); default:
return map.get(key); return undefined;
}; }
}
// Wildcards are allowed in certain string comparison and arn comparisons // Wildcards are allowed in certain string comparison and arn comparisons
@ -229,7 +229,7 @@ function convertToEpochTime(time: string | string[]) {
* reference_policies_elements.html) * reference_policies_elements.html)
* @return true if condition passes and false if not * @return true if condition passes and false if not
*/ */
export const convertConditionOperator = (operator: string): boolean => { export function convertConditionOperator(operator: string): boolean {
// Policy Validator checks that the condition operator // Policy Validator checks that the condition operator
// is only one of these strings so should not have undefined // is only one of these strings so should not have undefined
// or security issue with object assignment // or security issue with object assignment
@ -444,4 +444,4 @@ export const convertConditionOperator = (operator: string): boolean => {
}, },
}; };
return operatorMap[operator]; return operatorMap[operator];
}; }

View File

@ -3,7 +3,7 @@
"engines": { "engines": {
"node": ">=16" "node": ">=16"
}, },
"version": "7.10.29", "version": "7.10.29-4",
"description": "Common utilities for the S3 project components", "description": "Common utilities for the S3 project components",
"main": "build/index.js", "main": "build/index.js",
"repository": { "repository": {

View File

@ -166,7 +166,7 @@ describe('network.Server: ', () => {
if (err) { if (err) {
return ws.onStop(() => { return ws.onStop(() => {
clearTimeout(requestTimeout); clearTimeout(requestTimeout);
if (err.code === 'EPROTO') { if (err.code === 'EPROTO' || err.code === 'ECONNRESET') {
return done(); return done();
} }
return done(err); return done(err);

View File

@ -1162,30 +1162,6 @@ describe('policyEvaluator', () => {
check(requestContext, rcModifiers, policy, 'Neutral'); check(requestContext, rcModifiers, policy, 'Neutral');
}); });
it('should allow with StringEquals operator and ExistingObjectTag ' +
'key if meet condition', () => {
policy.Statement.Condition = {
StringEquals: { 's3:ExistingObjectTag/tagKey': 'tagValue' },
};
const rcModifiers = {
_existingObjTag: 'tagKey=tagValue',
_needTagEval: true,
};
check(requestContext, rcModifiers, policy, 'Allow');
});
it('should allow StringEquals operator and RequestObjectTag ' +
'key if meet condition', () => {
policy.Statement.Condition = {
StringEquals: { 's3:RequestObjectTagKey/tagKey': 'tagValue' },
};
const rcModifiers = {
_requestObjTags: 'tagKey=tagValue',
_needTagEval: true,
};
check(requestContext, rcModifiers, policy, 'Allow');
});
it('should allow with ForAnyValue prefix if meet condition', () => { it('should allow with ForAnyValue prefix if meet condition', () => {
policy.Statement.Condition = { policy.Statement.Condition = {
'ForAnyValue:StringLike': { 's3:RequestObjectTagKeys': ['tagOne', 'tagTwo'] }, 'ForAnyValue:StringLike': { 's3:RequestObjectTagKeys': ['tagOne', 'tagTwo'] },
@ -1208,7 +1184,7 @@ describe('policyEvaluator', () => {
check(requestContext, rcModifiers, policy, 'Allow'); check(requestContext, rcModifiers, policy, 'Allow');
}); });
it('should not allow with ForAnyValue prefix if do not meet condition', () => { it('should be neutral with ForAnyValue prefix if do not meet condition', () => {
policy.Statement.Condition = { policy.Statement.Condition = {
'ForAnyValue:StringLike': { 's3:RequestObjectTagKeys': ['tagOne', 'tagTwo'] }, 'ForAnyValue:StringLike': { 's3:RequestObjectTagKeys': ['tagOne', 'tagTwo'] },
}; };
@ -1219,12 +1195,12 @@ describe('policyEvaluator', () => {
check(requestContext, rcModifiers, policy, 'Neutral'); check(requestContext, rcModifiers, policy, 'Neutral');
}); });
it('should not allow with ForAllValues prefix if do not meet condition', () => { it('should be neutral with ForAllValues prefix if do not meet condition', () => {
policy.Statement.Condition = { policy.Statement.Condition = {
'ForAllValues:StringLike': { 's3:RequestObjectTagKeys': ['tagOne', 'tagTwo'] }, 'ForAllValues:StringLike': { 's3:RequestObjectTagKeys': ['tagOne', 'tagTwo'] },
}; };
const rcModifiers = { const rcModifiers = {
_requestObjTags: 'tagThree=keyThree&tagFour=keyFour', _requestObjTags: 'tagOne=keyOne&tagThree=keyThree',
_needTagEval: true, _needTagEval: true,
}; };
check(requestContext, rcModifiers, policy, 'Neutral'); check(requestContext, rcModifiers, policy, 'Neutral');
@ -1241,6 +1217,203 @@ describe('policyEvaluator', () => {
check(requestContext, rcModifiers, policy, 'Neutral'); check(requestContext, rcModifiers, policy, 'Neutral');
}); });
}); });
describe('with multiple statements', () => {
beforeEach(() => {
requestContext = new RequestContext({}, {}, 'bucket',
undefined, undefined, undefined, 'objectPut', 's3');
requestContext.setRequesterInfo({});
});
const TestMatrix = [
{
statementsToEvaluate: [],
expectedPolicyEvaluation: 'Neutral',
},
{
statementsToEvaluate: [
{ effect: 'Allow', meetConditions: true },
],
expectedPolicyEvaluation: 'Allow',
},
{
statementsToEvaluate: [
{ effect: 'Allow', meetConditions: false },
],
expectedPolicyEvaluation: 'Neutral',
},
{
statementsToEvaluate: [
{ effect: 'Deny', meetConditions: true },
],
expectedPolicyEvaluation: 'Deny',
},
{
statementsToEvaluate: [
{ effect: 'Deny', meetConditions: false },
],
expectedPolicyEvaluation: 'Neutral',
},
{
statementsToEvaluate: [
{ effect: 'Allow', meetConditions: false },
{ effect: 'Allow', meetConditions: true },
],
expectedPolicyEvaluation: 'Allow',
},
{
statementsToEvaluate: [
{ effect: 'Allow', meetConditions: false },
{ effect: 'Allow', meetConditions: false },
],
expectedPolicyEvaluation: 'Neutral',
},
{
statementsToEvaluate: [
{ effect: 'Allow', meetConditions: false },
{ effect: 'Deny', meetConditions: false },
],
expectedPolicyEvaluation: 'Neutral',
},
{
statementsToEvaluate: [
{ effect: 'Allow', meetConditions: true },
{ effect: 'Deny', meetConditions: true },
],
expectedPolicyEvaluation: 'Deny',
},
{
statementsToEvaluate: [
{ effect: 'Deny', meetConditions: true },
{ effect: 'Allow', meetConditions: true },
],
expectedPolicyEvaluation: 'Deny',
},
{
statementsToEvaluate: [
{ effect: 'Allow', meetConditions: true },
{ effect: 'Deny', meetConditions: false },
],
expectedPolicyEvaluation: 'Allow',
},
{
statementsToEvaluate: [
{ effect: 'Allow', meetConditions: null },
],
expectedPolicyEvaluation: 'AllowWithTagCondition',
},
{
statementsToEvaluate: [
{ effect: 'Allow', meetConditions: null },
{ effect: 'Allow', meetConditions: null },
],
expectedPolicyEvaluation: 'AllowWithTagCondition',
},
{
statementsToEvaluate: [
{ effect: 'Deny', meetConditions: null },
],
expectedPolicyEvaluation: 'DenyWithTagCondition',
},
{
statementsToEvaluate: [
{ effect: 'Deny', meetConditions: null },
{ effect: 'Deny', meetConditions: null },
],
expectedPolicyEvaluation: 'DenyWithTagCondition',
},
{
statementsToEvaluate: [
{ effect: 'Allow', meetConditions: true },
{ effect: 'Allow', meetConditions: null },
],
expectedPolicyEvaluation: 'Allow',
},
{
statementsToEvaluate: [
{ effect: 'Allow', meetConditions: false },
{ effect: 'Allow', meetConditions: null },
],
expectedPolicyEvaluation: 'AllowWithTagCondition',
},
{
statementsToEvaluate: [
{ effect: 'Deny', meetConditions: true },
{ effect: 'Deny', meetConditions: null },
],
expectedPolicyEvaluation: 'Deny',
},
{
statementsToEvaluate: [
{ effect: 'Deny', meetConditions: true },
{ effect: 'Allow', meetConditions: null },
],
expectedPolicyEvaluation: 'Deny',
},
{
statementsToEvaluate: [
{ effect: 'Deny', meetConditions: false },
{ effect: 'Deny', meetConditions: null },
],
expectedPolicyEvaluation: 'DenyWithTagCondition',
},
{
statementsToEvaluate: [
{ effect: 'Allow', meetConditions: true },
{ effect: 'Deny', meetConditions: null },
],
expectedPolicyEvaluation: 'DenyWithTagCondition',
},
{
statementsToEvaluate: [
{ effect: 'Allow', meetConditions: null },
{ effect: 'Deny', meetConditions: null },
],
expectedPolicyEvaluation: 'DenyWithTagCondition',
},
];
TestMatrix.forEach(testCase => {
const policyDesc = testCase.statementsToEvaluate
.map(statement => `${statement.effect}(met:${statement.meetConditions})`)
.join(', ');
it(`policy with statements evaluating individually to [${policyDesc}] ` +
`should return ${testCase.expectedPolicyEvaluation}`, () => {
const policy = {
Version: '2012-10-17',
Statement: testCase.statementsToEvaluate.map(statement => {
let condition;
if (statement.meetConditions === true) {
condition = {
StringEquals: { 'aws:UserAgent': 'CyberSquaw' },
};
} else if (statement.meetConditions === false) {
condition = {
StringEquals: { 'aws:UserAgent': 'OtherAgent' },
};
} else if (statement.meetConditions === null) {
condition = {
StringEquals: { 's3:ExistingObjectTag/tagKey': 'tagValue' },
};
}
return {
Effect: statement.effect,
Action: 's3:PutObject',
Resource: 'arn:aws:s3:::bucket',
Condition: condition,
};
}),
};
requestContext.setHeaders({
'user-agent': 'CyberSquaw',
});
requestContext.setNeedTagEval(false);
const result = evaluatePolicy(requestContext, policy, log);
assert.strictEqual(result, testCase.expectedPolicyEvaluation);
});
});
});
}); });
describe('evaluate multiple policies', () => { describe('evaluate multiple policies', () => {
@ -1266,17 +1439,152 @@ describe('policyEvaluator', () => {
assert.strictEqual(result, 'Deny'); assert.strictEqual(result, 'Deny');
}); });
it('should deny access if request resource is not in any policy', it('should deny access if request resource is not in any policy', () => {
() => { requestContext = new RequestContext({}, {},
'notbucket', undefined,
undefined, undefined, 'objectGet', 's3');
requestContext.setRequesterInfo({});
const result = evaluateAllPolicies(requestContext, [
samples['Multi-Statement Policy'],
samples['Variable Bucket Policy'],
], log);
assert.strictEqual(result, 'Deny');
});
const TestMatrixPolicies = {
Allow: {
Version: '2012-10-17',
Statement: {
Effect: 'Allow',
Action: 's3:*',
Resource: '*',
},
},
Neutral: {
Version: '2012-10-17',
Statement: {
Effect: 'Allow',
Action: 's3:*',
Resource: 'arn:aws:s3:::other-bucket',
},
},
Deny: {
Version: '2012-10-17',
Statement: {
Effect: 'Deny',
Action: 's3:*',
Resource: '*',
},
},
AllowWithTagCondition: {
Version: '2012-10-17',
Statement: {
Effect: 'Allow',
Action: 's3:*',
Resource: '*',
Condition: {
StringEquals: {
's3:ExistingObjectTag/tagKey': 'tagValue',
},
},
},
},
DenyWithTagCondition: {
Version: '2012-10-17',
Statement: {
Effect: 'Deny',
Action: 's3:*',
Resource: '*',
Condition: {
StringEquals: {
's3:ExistingObjectTag/tagKey': 'tagValue',
},
},
},
},
};
const TestMatrix = [
{
policiesToEvaluate: [],
expectedPolicyEvaluation: 'Deny',
},
{
policiesToEvaluate: ['Allow'],
expectedPolicyEvaluation: 'Allow',
},
{
policiesToEvaluate: ['Neutral'],
expectedPolicyEvaluation: 'Deny',
},
{
policiesToEvaluate: ['Deny'],
expectedPolicyEvaluation: 'Deny',
},
{
policiesToEvaluate: ['Allow', 'Allow'],
expectedPolicyEvaluation: 'Allow',
},
{
policiesToEvaluate: ['Allow', 'Neutral'],
expectedPolicyEvaluation: 'Allow',
},
{
policiesToEvaluate: ['Neutral', 'Allow'],
expectedPolicyEvaluation: 'Allow',
},
{
policiesToEvaluate: ['Neutral', 'Neutral'],
expectedPolicyEvaluation: 'Deny',
},
{
policiesToEvaluate: ['Allow', 'Deny'],
expectedPolicyEvaluation: 'Deny',
},
{
policiesToEvaluate: ['AllowWithTagCondition'],
expectedPolicyEvaluation: 'NeedTagConditionEval',
},
{
policiesToEvaluate: ['Allow', 'AllowWithTagCondition'],
expectedPolicyEvaluation: 'Allow',
},
{
policiesToEvaluate: ['DenyWithTagCondition'],
expectedPolicyEvaluation: 'Deny',
},
{
policiesToEvaluate: ['Allow', 'DenyWithTagCondition'],
expectedPolicyEvaluation: 'NeedTagConditionEval',
},
{
policiesToEvaluate: ['AllowWithTagCondition', 'DenyWithTagCondition'],
expectedPolicyEvaluation: 'NeedTagConditionEval',
},
{
policiesToEvaluate: ['AllowWithTagCondition', 'DenyWithTagCondition', 'Deny'],
expectedPolicyEvaluation: 'Deny',
},
{
policiesToEvaluate: ['DenyWithTagCondition', 'AllowWithTagCondition', 'Allow'],
expectedPolicyEvaluation: 'NeedTagConditionEval',
},
];
TestMatrix.forEach(testCase => {
it(`policies evaluating individually to [${testCase.policiesToEvaluate.join(', ')}] `
+ `should return ${testCase.expectedPolicyEvaluation}`, () => {
requestContext = new RequestContext({}, {}, requestContext = new RequestContext({}, {},
'notbucket', undefined, 'my_favorite_bucket', undefined,
undefined, undefined, 'objectGet', 's3'); undefined, undefined, 'objectGet', 's3');
requestContext.setRequesterInfo({}); requestContext.setRequesterInfo({});
const result = evaluateAllPolicies(requestContext, const result = evaluateAllPolicies(
[samples['Multi-Statement Policy'], requestContext,
samples['Variable Bucket Policy']], log); testCase.policiesToEvaluate.map(policyName => TestMatrixPolicies[policyName]),
assert.strictEqual(result, 'Deny'); log);
assert.strictEqual(result, testCase.expectedPolicyEvaluation);
}); });
});
}); });
}); });

View File

@ -0,0 +1,128 @@
const assert = require('assert');
const RequestContext = require('../../../lib/policyEvaluator/RequestContext').default;
describe('RequestContext', () => {
const constructorParams = [
{ 'some-header': 'some-value' }, // headers
{ q1: 'v1', q2: 'v2' }, // query
'general-resource', // generalResource
'specific-resource', // specificResource
'127.0.0.1', // requesterIp
true, // sslEnabled
'GET', // apiMethod
's3', // awsService
'us-east-1', // locationConstraint
{ // requesterInfo
arn: 'arn:aws:iam::user/johndoe',
accountId: 'JOHNACCOUNT',
username: 'John Doe',
principalType: 'user',
},
'v4', // signatureVersion
'REST-HEADER', // authType
123456, // signatureAge
'security-token', // securityToken
'arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess', // policyArn
'objectGet', // action
'reqTagOne=valueOne&reqTagTwo=valueTwo', // requestObjTags
'existingTagOne=valueOne&existingTagTwo=valueTwo', // existingObjTag
true, // needTagEval
];
const rc = new RequestContext(...constructorParams);
const GetterTests = [
{ name: 'getAction', expectedValue: 'objectGet' },
{ name: 'getResource', expectedValue: 'arn:aws:s3:::general-resource/specific-resource' },
{ name: 'getHeaders', expectedValue: { 'some-header': 'some-value' } },
{ name: 'getQuery', expectedValue: { q1: 'v1', q2: 'v2' } },
{
name: 'getRequesterInfo',
expectedValue: {
accountId: 'JOHNACCOUNT',
arn: 'arn:aws:iam::user/johndoe',
username: 'John Doe',
principalType: 'user',
},
},
{ name: 'getRequesterIp', expectedValueToString: '127.0.0.1' },
{ name: 'getRequesterAccountId', expectedValue: undefined },
{ name: 'getRequesterEndArn', expectedValue: 'arn:aws:iam::user/johndoe' },
{ name: 'getRequesterExternalId', expectedValue: undefined },
{ name: 'getRequesterPrincipalArn', expectedValue: 'arn:aws:iam::user/johndoe' },
{ name: 'getRequesterType', expectedValue: 'user' },
{ name: 'getSslEnabled', expectedValue: true },
{ name: 'getSignatureVersion', expectedValue: 'v4' },
{ name: 'getAuthType', expectedValue: 'REST-HEADER' },
{ name: 'getSignatureAge', expectedValue: 123456 },
{ name: 'getLocationConstraint', expectedValue: 'us-east-1' },
{ name: 'getAwsService', expectedValue: 's3' },
{ name: 'getTokenIssueTime', expectedValue: null },
{ name: 'getMultiFactorAuthPresent', expectedValue: null },
{ name: 'getMultiFactorAuthAge', expectedValue: null },
{ name: 'getSecurityToken', expectedValue: 'security-token' },
{ name: 'getPolicyArn', expectedValue: 'arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess' },
{ name: 'isQuotaCheckNeeded', expectedValue: false },
{ name: 'getRequestObjTags', expectedValue: 'reqTagOne=valueOne&reqTagTwo=valueTwo' },
{ name: 'getExistingObjTag', expectedValue: 'existingTagOne=valueOne&existingTagTwo=valueTwo' },
{ name: 'getNeedTagEval', expectedValue: true },
];
GetterTests.forEach(testCase => {
it(`getter:${testCase.name}`, () => {
const getterResult = rc[testCase.name]();
if (testCase.expectedValueToString) {
assert.strictEqual(getterResult.toString(), testCase.expectedValueToString);
} else {
assert.deepStrictEqual(getterResult, testCase.expectedValue);
}
});
});
const SerializedFields = {
action: 'objectGet',
apiMethod: 'GET',
authType: 'REST-HEADER',
awsService: 's3',
existingObjTag: 'existingTagOne=valueOne&existingTagTwo=valueTwo',
generalResource: 'general-resource',
headers: {
'some-header': 'some-value',
},
locationConstraint: 'us-east-1',
multiFactorAuthAge: null,
multiFactorAuthPresent: null,
needTagEval: true,
policyArn: 'arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess',
query: {
q1: 'v1',
q2: 'v2',
},
requesterInfo: {
accountId: 'JOHNACCOUNT',
arn: 'arn:aws:iam::user/johndoe',
principalType: 'user',
username: 'John Doe',
},
requestObjTags: 'reqTagOne=valueOne&reqTagTwo=valueTwo',
requesterIp: '127.0.0.1',
securityToken: 'security-token',
signatureAge: 123456,
signatureVersion: 'v4',
specificResource: 'specific-resource',
sslEnabled: true,
tokenIssueTime: null,
};
it('serialize()', () => {
assert.deepStrictEqual(JSON.parse(rc.serialize()), SerializedFields);
});
it('deSerialize()', () => {
// check that everything that was serialized is deserialized
// properly into a new RequestContext object by making sure
// the serialized version of the latter corresponds to the
// input
const serialized = JSON.stringify(SerializedFields);
const deserializedRC = RequestContext.deSerialize(serialized);
const newSerialized = JSON.parse(deserializedRC.serialize());
assert.deepStrictEqual(newSerialized, SerializedFields);
});
});