Compare commits
13 Commits
developmen
...
hotfix/7.1
Author | SHA1 | Date |
---|---|---|
Jonathan Gramain | 23f7e5af6a | |
Jonathan Gramain | cb31a07d8d | |
Jonathan Gramain | 62be7f926d | |
Alexander Chan | 2d205f8131 | |
Jonathan Gramain | 0aa95823b7 | |
Jonathan Gramain | 5f1e91b44c | |
Jonathan Gramain | c5c736aa48 | |
Jonathan Gramain | bca37e4b06 | |
Nicolas Humbert | 5fd84e78e2 | |
williamlardier | dd603482f0 | |
Artem Bakalov | c16d4e6954 | |
Artem Bakalov | 88f16366a1 | |
Artem Bakalov | 5f0df14ca8 |
|
@ -166,7 +166,6 @@ export default class RequestContext {
|
|||
_policyArn: string;
|
||||
_action?: string;
|
||||
_needQuota: boolean;
|
||||
_postXml?: string;
|
||||
_requestObjTags: string | null;
|
||||
_existingObjTag: string | null;
|
||||
_needTagEval: boolean;
|
||||
|
@ -190,7 +189,9 @@ export default class RequestContext {
|
|||
securityToken: string,
|
||||
policyArn: string,
|
||||
action?: string,
|
||||
postXml?: string,
|
||||
requestObjTags?: string,
|
||||
existingObjTag?: string,
|
||||
needTagEval?: false,
|
||||
) {
|
||||
this._headers = headers;
|
||||
this._query = query;
|
||||
|
@ -220,10 +221,9 @@ export default class RequestContext {
|
|||
this._policyArn = policyArn;
|
||||
this._action = action;
|
||||
this._needQuota = _actionNeedQuotaCheck[apiMethod] === true;
|
||||
this._postXml = postXml;
|
||||
this._requestObjTags = null;
|
||||
this._existingObjTag = null;
|
||||
this._needTagEval = false;
|
||||
this._requestObjTags = requestObjTags || null;
|
||||
this._existingObjTag = existingObjTag || null;
|
||||
this._needTagEval = needTagEval || false;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -236,7 +236,7 @@ export default class RequestContext {
|
|||
apiMethod: this._apiMethod,
|
||||
headers: this._headers,
|
||||
query: this._query,
|
||||
requersterInfo: this._requesterInfo,
|
||||
requesterInfo: this._requesterInfo,
|
||||
requesterIp: this._requesterIp,
|
||||
sslEnabled: this._sslEnabled,
|
||||
awsService: this._awsService,
|
||||
|
@ -252,7 +252,6 @@ export default class RequestContext {
|
|||
securityToken: this._securityToken,
|
||||
policyArn: this._policyArn,
|
||||
action: this._action,
|
||||
postXml: this._postXml,
|
||||
requestObjTags: this._requestObjTags,
|
||||
existingObjTag: this._existingObjTag,
|
||||
needTagEval: this._needTagEval,
|
||||
|
@ -276,12 +275,27 @@ export default class RequestContext {
|
|||
if (resource) {
|
||||
obj.specificResource = resource;
|
||||
}
|
||||
return new RequestContext(obj.headers, obj.query, obj.generalResource,
|
||||
obj.specificResource, 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.postXml);
|
||||
return new RequestContext(
|
||||
obj.headers,
|
||||
obj.query,
|
||||
obj.generalResource,
|
||||
obj.specificResource,
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
|
|
|
@ -13,7 +13,11 @@ const operatorsWithVariables = ['StringEquals', 'StringNotEquals',
|
|||
const operatorsWithNegation = ['StringNotEquals',
|
||||
'StringNotEqualsIgnoreCase', 'StringNotLike', 'ArnNotEquals',
|
||||
'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
|
||||
* @return true if applicable, false if not
|
||||
*/
|
||||
export const isResourceApplicable = (
|
||||
export function isResourceApplicable(
|
||||
requestContext: RequestContext,
|
||||
statementResource: string | string[],
|
||||
log: Logger,
|
||||
): boolean => {
|
||||
): boolean {
|
||||
const resource = requestContext.getResource();
|
||||
if (!Array.isArray(statementResource)) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
|
@ -59,7 +63,7 @@ export const isResourceApplicable = (
|
|||
{ requestResource: resource });
|
||||
// If no match found, no resource is applicable
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether action in policy statement applies to request
|
||||
|
@ -69,11 +73,11 @@ export const isResourceApplicable = (
|
|||
* @param log - logger
|
||||
* @return true if applicable, false if not
|
||||
*/
|
||||
export const isActionApplicable = (
|
||||
export function isActionApplicable(
|
||||
requestAction: string,
|
||||
statementAction: string | string[],
|
||||
log: Logger,
|
||||
): boolean => {
|
||||
): boolean {
|
||||
if (!Array.isArray(statementAction)) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
statementAction = [statementAction];
|
||||
|
@ -95,32 +99,33 @@ export const isActionApplicable = (
|
|||
{ requestAction });
|
||||
// If no match found, return false
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether request meets policy conditions
|
||||
* @param requestContext - info about request
|
||||
* @param statementCondition - Condition statement from policy
|
||||
* @param log - logger
|
||||
* @return contains whether conditions are allowed and whether they
|
||||
* contain any tag condition keys
|
||||
* @param {RequestContext} requestContext - info about request
|
||||
* @param {object} statementCondition - Condition statement from policy
|
||||
* @param {Logger} log - logger
|
||||
* @return {boolean|null} a condition evaluation result, one of:
|
||||
* - 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,
|
||||
statementCondition: any,
|
||||
log: Logger,
|
||||
) => {
|
||||
): boolean | null {
|
||||
let hasTagConditions = false;
|
||||
// The Condition portion of a policy is an object with different
|
||||
// operators as keys
|
||||
const conditionEval = {};
|
||||
const operators = Object.keys(statementCondition);
|
||||
const length = operators.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const operator = operators[i];
|
||||
for (const operator of Object.keys(statementCondition)) {
|
||||
const hasPrefix = operator.includes(':');
|
||||
const hasIfExistsCondition = operator.endsWith('IfExists');
|
||||
// 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) :
|
||||
operator;
|
||||
let prefix: string | undefined;
|
||||
|
@ -135,10 +140,6 @@ export const meetConditions = (
|
|||
// Note: this should be the actual operator name, not the bareOperator
|
||||
const conditionsWithSameOperator = statementCondition[operator];
|
||||
const conditionKeys = Object.keys(conditionsWithSameOperator);
|
||||
if (conditionKeys.some(key => tagConditions.has(key)) && !requestContext.getNeedTagEval()) {
|
||||
// @ts-expect-error
|
||||
conditionEval.tagConditions = true;
|
||||
}
|
||||
const conditionKeysLength = conditionKeys.length;
|
||||
for (let j = 0; j < conditionKeysLength; j++) {
|
||||
const key = conditionKeys[j];
|
||||
|
@ -155,6 +156,10 @@ export const meetConditions = (
|
|||
// 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);
|
||||
if (tagConditions.has(transformedKey) && !requestContext.getNeedTagEval()) {
|
||||
hasTagConditions = true;
|
||||
continue;
|
||||
}
|
||||
// Pull key using requestContext
|
||||
// TODO: If applicable to S3, handle policy set operations
|
||||
// where a keyBasedOnRequestContext returns multiple values and
|
||||
|
@ -180,11 +185,10 @@ export const meetConditions = (
|
|||
log.trace('condition not satisfied due to ' +
|
||||
'missing info', { operator,
|
||||
conditionKey: transformedKey, policyValue: transformedValue });
|
||||
return { allow: false };
|
||||
return false;
|
||||
}
|
||||
// If condition operator prefix is included, the key should be an array
|
||||
if (prefix && !Array.isArray(keyBasedOnRequestContext)) {
|
||||
// @ts-expect-error
|
||||
keyBasedOnRequestContext = [keyBasedOnRequestContext];
|
||||
}
|
||||
// Transalate operator into function using bareOperator
|
||||
|
@ -196,14 +200,16 @@ export const meetConditions = (
|
|||
if (!operatorFunction(keyBasedOnRequestContext, transformedValue, prefix)) {
|
||||
log.trace('did not satisfy condition', { operator: bareOperator,
|
||||
keyBasedOnRequestContext, policyValue: transformedValue });
|
||||
return { allow: false };
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// @ts-expect-error
|
||||
conditionEval.allow = true;
|
||||
return conditionEval;
|
||||
};
|
||||
// one or more conditions required tag info to be evaluated
|
||||
if (hasTagConditions) {
|
||||
return null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* if not applicable
|
||||
*/
|
||||
export const evaluatePolicy = (
|
||||
export function evaluatePolicy(
|
||||
requestContext: RequestContext,
|
||||
policy: any,
|
||||
log: Logger,
|
||||
): string => {
|
||||
): string {
|
||||
// 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)) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
|
@ -259,10 +267,18 @@ export const evaluatePolicy = (
|
|||
}
|
||||
const conditionEval = currentStatement.Condition ?
|
||||
meetConditions(requestContext, currentStatement.Condition, log) :
|
||||
null;
|
||||
true;
|
||||
// If do not meet conditions move on to next statement
|
||||
// @ts-expect-error
|
||||
if (conditionEval && !conditionEval.allow) {
|
||||
if (conditionEval === false) {
|
||||
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;
|
||||
}
|
||||
if (currentStatement.Effect === 'Deny') {
|
||||
|
@ -271,17 +287,27 @@ export const evaluatePolicy = (
|
|||
return 'Deny';
|
||||
}
|
||||
log.trace('Allow statement applies');
|
||||
// If statement is applicable, conditions are met and Effect is
|
||||
// to Allow, set verdict to Allow
|
||||
// statement is applicable, conditions are met and Effect is
|
||||
// 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';
|
||||
// @ts-expect-error
|
||||
if (conditionEval && conditionEval.tagConditions) {
|
||||
verdict = 'NeedTagConditionEval';
|
||||
}
|
||||
} else if (allowWithTagCondition) {
|
||||
// all allow statements need tag checks
|
||||
verdict = 'AllowWithTagCondition';
|
||||
} else {
|
||||
// no statement matched to allow or deny
|
||||
verdict = 'Neutral';
|
||||
}
|
||||
log.trace('result of evaluating single policy', { verdict });
|
||||
return verdict;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate whether a request is permitted under a policy.
|
||||
|
@ -294,24 +320,43 @@ export const evaluatePolicy = (
|
|||
* @return Allow if permitted, Deny if not permitted.
|
||||
* Default is to Deny. Deny overrides an Allow
|
||||
*/
|
||||
export const evaluateAllPolicies = (
|
||||
export function evaluateAllPolicies(
|
||||
requestContext: RequestContext,
|
||||
allPolicies: any[],
|
||||
log: Logger,
|
||||
): string => {
|
||||
): string {
|
||||
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++) {
|
||||
const singlePolicyVerdict =
|
||||
evaluatePolicy(requestContext, allPolicies[i], log);
|
||||
const singlePolicyVerdict = evaluatePolicy(requestContext, allPolicies[i], log);
|
||||
// If there is any Deny, just return Deny
|
||||
if (singlePolicyVerdict === 'Deny') {
|
||||
return 'Deny';
|
||||
}
|
||||
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';
|
||||
}
|
||||
} 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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -23,15 +23,22 @@ export default class Principal {
|
|||
* @param statement - Statement policy field
|
||||
* @return True if meet conditions
|
||||
*/
|
||||
static _evaluateCondition(
|
||||
static _evaluateStatement(
|
||||
params: Params,
|
||||
statement: Statement,
|
||||
// TODO Fix return type
|
||||
): any {
|
||||
if (statement.Condition) {
|
||||
return meetConditions(params.rc, statement.Condition, params.log);
|
||||
): 'Neutral' | 'Allow' | 'Deny' {
|
||||
const reverse = !!statement.NotPrincipal;
|
||||
if (reverse) {
|
||||
// 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,
|
||||
valids: Valid,
|
||||
): 'Neutral' | 'Allow' | 'Deny' {
|
||||
const reverse = !!statement.NotPrincipal;
|
||||
const principal = (statement.Principal || statement.NotPrincipal)!;
|
||||
if (typeof principal === 'string' && principal === '*') {
|
||||
if (reverse) {
|
||||
// In case of anonymous NotPrincipal, this will neutral everyone
|
||||
return 'Neutral';
|
||||
const reverse = !!statement.NotPrincipal;
|
||||
if (typeof principal === 'string') {
|
||||
if (principal === '*') {
|
||||
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';
|
||||
}
|
||||
let ref = [];
|
||||
|
@ -82,28 +82,8 @@ export default class Principal {
|
|||
}
|
||||
toCheck = Array.isArray(toCheck) ? toCheck : [toCheck];
|
||||
ref = Array.isArray(ref) ? ref : [ref];
|
||||
if (toCheck.indexOf('*') !== -1) {
|
||||
if (reverse) {
|
||||
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 (toCheck.includes('*') || ref.some(r => toCheck.includes(r))) {
|
||||
return Principal._evaluateStatement(params, statement);
|
||||
}
|
||||
if (reverse) {
|
||||
return statement.Effect;
|
||||
|
|
|
@ -4,14 +4,14 @@ const sharedActionMap = {
|
|||
bucketDeleteEncryption: 's3:PutEncryptionConfiguration',
|
||||
bucketDeletePolicy: 's3:DeleteBucketPolicy',
|
||||
bucketDeleteWebsite: 's3:DeleteBucketWebsite',
|
||||
bucketDeleteTagging: 's3:DeleteBucketTagging',
|
||||
bucketDeleteTagging: 's3:PutBucketTagging',
|
||||
bucketGet: 's3:ListBucket',
|
||||
bucketGetACL: 's3:GetBucketAcl',
|
||||
bucketGetCors: 's3:GetBucketCORS',
|
||||
bucketGetEncryption: 's3:GetEncryptionConfiguration',
|
||||
bucketGetLifecycle: 's3:GetLifecycleConfiguration',
|
||||
bucketGetLocation: 's3:GetBucketLocation',
|
||||
bucketGetNotification: 's3:GetBucketNotificationConfiguration',
|
||||
bucketGetNotification: 's3:GetBucketNotification',
|
||||
bucketGetObjectLock: 's3:GetBucketObjectLockConfiguration',
|
||||
bucketGetPolicy: 's3:GetBucketPolicy',
|
||||
bucketGetReplication: 's3:GetReplicationConfiguration',
|
||||
|
@ -23,7 +23,7 @@ const sharedActionMap = {
|
|||
bucketPutCors: 's3:PutBucketCORS',
|
||||
bucketPutEncryption: 's3:PutEncryptionConfiguration',
|
||||
bucketPutLifecycle: 's3:PutLifecycleConfiguration',
|
||||
bucketPutNotification: 's3:PutBucketNotificationConfiguration',
|
||||
bucketPutNotification: 's3:PutBucketNotification',
|
||||
bucketPutObjectLock: 's3:PutBucketObjectLockConfiguration',
|
||||
bucketPutPolicy: 's3:PutBucketPolicy',
|
||||
bucketPutReplication: 's3:PutReplicationConfiguration',
|
||||
|
@ -55,8 +55,8 @@ const actionMapRQ = {
|
|||
// see http://docs.aws.amazon.com/AmazonS3/latest/API/
|
||||
// RESTBucketDELETEcors.html
|
||||
bucketDeleteCors: 's3:PutBucketCORS',
|
||||
bucketDeleteReplication: 's3:DeleteReplicationConfiguration',
|
||||
bucketDeleteLifecycle: 's3:DeleteLifecycleConfiguration',
|
||||
bucketDeleteReplication: 's3:PutReplicationConfiguration',
|
||||
bucketDeleteLifecycle: 's3:PutLifecycleConfiguration',
|
||||
completeMultipartUpload: 's3:PutObject',
|
||||
initiateMultipartUpload: 's3:PutObject',
|
||||
objectDeleteVersion: 's3:DeleteObjectVersion',
|
||||
|
@ -72,6 +72,7 @@ const actionMapRQ = {
|
|||
objectReplicate: 's3:ReplicateObject',
|
||||
objectPutRetentionVersion: 's3:PutObjectVersionRetention',
|
||||
objectPutLegalHoldVersion: 's3:PutObjectVersionLegalHold',
|
||||
listObjectVersions: 's3:ListBucketVersions',
|
||||
...sharedActionMap,
|
||||
};
|
||||
|
||||
|
@ -104,7 +105,7 @@ const actionMonitoringMapS3 = {
|
|||
bucketGetCors: 'GetBucketCors',
|
||||
bucketGetLifecycle: 'GetBucketLifecycleConfiguration',
|
||||
bucketGetLocation: 'GetBucketLocation',
|
||||
bucketGetNotification: 'GetBucketNotificationConfiguration',
|
||||
bucketGetNotification: 'GetBucketNotification',
|
||||
bucketGetObjectLock: 'GetObjectLockConfiguration',
|
||||
bucketGetPolicy: 'GetBucketPolicy',
|
||||
bucketGetReplication: 'GetBucketReplication',
|
||||
|
@ -117,7 +118,7 @@ const actionMonitoringMapS3 = {
|
|||
bucketPutACL: 'PutBucketAcl',
|
||||
bucketPutCors: 'PutBucketCors',
|
||||
bucketPutLifecycle: 'PutBucketLifecycleConfiguration',
|
||||
bucketPutNotification: 'PutBucketNotificationConfiguration',
|
||||
bucketPutNotification: 'PutBucketNotification',
|
||||
bucketPutObjectLock: 'PutObjectLockConfiguration',
|
||||
bucketPutPolicy: 'PutBucketPolicy',
|
||||
bucketPutReplication: 'PutBucketReplication',
|
||||
|
|
|
@ -11,31 +11,30 @@ import ipaddr from 'ipaddr.js';
|
|||
* @param requestContext - info sent with request
|
||||
* @return condition key value
|
||||
*/
|
||||
export const findConditionKey = (
|
||||
export function findConditionKey(
|
||||
key: string,
|
||||
requestContext: RequestContext,
|
||||
): string => {
|
||||
): any {
|
||||
// TODO: Consider combining with findVariable function if no benefit
|
||||
// to keeping separate
|
||||
const headers = requestContext.getHeaders();
|
||||
const query = requestContext.getQuery();
|
||||
const requesterInfo = requestContext.getRequesterInfo();
|
||||
|
||||
const map = new Map();
|
||||
// Possible AWS Condition keys (http://docs.aws.amazon.com/IAM/latest/
|
||||
// UserGuide/reference_policies_elements.html#AvailableKeys)
|
||||
|
||||
switch (key) {
|
||||
// aws:CurrentTime – Used for date/time conditions
|
||||
// (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
|
||||
// (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
|
||||
// credentials were issued (see Date Condition Operators).
|
||||
// Only present in requests that are signed using temporary security
|
||||
// credentials.
|
||||
map.set('aws:TokenIssueTime', requestContext.getTokenIssueTime());
|
||||
case 'aws:TokenIssueTime': return requestContext.getTokenIssueTime();
|
||||
// aws:MultiFactorAuthPresent – Used to check whether MFA was used
|
||||
// (see Boolean Condition Operators).
|
||||
// Note: This key is only present if MFA was used. So, the following
|
||||
|
@ -45,131 +44,132 @@ export const findConditionKey = (
|
|||
// Instead use:
|
||||
// "Condition" :
|
||||
// { "Null" : { "aws:MultiFactorAuthPresent" : true } }
|
||||
map.set('aws:MultiFactorAuthPresent',
|
||||
requestContext.getMultiFactorAuthPresent());
|
||||
case 'aws:MultiFactorAuthPresent': return requestContext.getMultiFactorAuthPresent();
|
||||
// aws:MultiFactorAuthAge – Used to check how many seconds since
|
||||
// MFA credentials were issued. If MFA was not used,
|
||||
// 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,
|
||||
// user, federated, or assumed role
|
||||
// Note: Docs for conditions have "PrincipalType" but simulator
|
||||
// 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
|
||||
// the address the request is being sent to. Only supported by some
|
||||
// services, such as S3. Value comes from the referer header in the
|
||||
// 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
|
||||
// using SSL (see Boolean Condition Operators).
|
||||
map.set('aws:SecureTransport',
|
||||
requestContext.getSslEnabled() ? 'true' : 'false');
|
||||
case 'aws:SecureTransport': return requestContext.getSslEnabled() ? 'true' : 'false';
|
||||
// aws:SourceArn – Used check the source of the request,
|
||||
// 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
|
||||
// (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 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
|
||||
// N/A here
|
||||
map.set('aws:SourceVpce', undefined);
|
||||
case 'aws:SourceVpce': return undefined;
|
||||
// aws:UserAgent – Used to check the requester's client app.
|
||||
// (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.
|
||||
// (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.
|
||||
// (see String Condition Operators)
|
||||
map.set('aws:username', requesterInfo.username);
|
||||
case 'aws:username': return requesterInfo.username;
|
||||
// Possible condition keys for S3:
|
||||
// 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:
|
||||
// read, write, read-acp, write-acp or full-control)
|
||||
// Value is the value of that header (ex. id of grantee)
|
||||
map.set('s3:x-amz-grant-read', headers['x-amz-grant-read']);
|
||||
map.set('s3:x-amz-grant-write', headers['x-amz-grant-write']);
|
||||
map.set('s3:x-amz-grant-read-acp', headers['x-amz-grant-read-acp']);
|
||||
map.set('s3:x-amz-grant-write-acp', 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-read': return headers['x-amz-grant-read'];
|
||||
case 's3:x-amz-grant-write': return headers['x-amz-grant-write'];
|
||||
case 's3:x-amz-grant-read-acp': return headers['x-amz-grant-read-acp'];
|
||||
case 's3:x-amz-grant-write-acp': return headers['x-amz-grant-write-acp'];
|
||||
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
|
||||
// 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
|
||||
// applicable on a put object copy. Determines whether metadata will
|
||||
// be copied from original object or replaced. Values or "COPY" or
|
||||
// "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
|
||||
// use server side encryption. Value is the encryption algo such as
|
||||
// "AES256"
|
||||
map.set('s3:x-amz-server-side-encryption',
|
||||
headers['x-amz-server-side-encryption']);
|
||||
case 's3:x-amz-server-side-encryption': return headers['x-amz-server-side-encryption'];
|
||||
// s3:x-amz-storage-class -- x-amz-storage-class header value
|
||||
// (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
|
||||
map.set('s3:VersionId', query.versionId);
|
||||
case 's3:VersionId': return query.versionId;
|
||||
// s3:LocationConstraint -- Used to restrict creation of bucket
|
||||
// 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
|
||||
map.set('s3:delimiter', query.delimiter);
|
||||
case 's3:delimiter': return query.delimiter;
|
||||
// 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
|
||||
map.set('s3:prefix', query.prefix);
|
||||
case 's3:prefix': return query.prefix;
|
||||
// s3 auth v4 additional condition keys
|
||||
// (See http://docs.aws.amazon.com/AmazonS3/latest/API/
|
||||
// bucket-policy-s3-sigv4-conditions.html)
|
||||
// s3:signatureversion -- Either "AWS" for v2 or
|
||||
// "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",
|
||||
// "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,
|
||||
// that a signature is valid in an authenticated request. So,
|
||||
// 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"
|
||||
// so can use this in a deny policy to deny any requests that do not
|
||||
// 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
|
||||
// object on a PUT request using the "x-amz-meta-scal-location-constraint"
|
||||
// header
|
||||
map.set('s3:ObjLocationConstraint',
|
||||
headers['x-amz-meta-scal-location-constraint']);
|
||||
map.set('sts:ExternalId', requestContext.getRequesterExternalId());
|
||||
map.set('iam:PolicyArn', requestContext.getPolicyArn());
|
||||
case 's3:ObjLocationConstraint': return headers['x-amz-meta-scal-location-constraint'];
|
||||
case 'sts:ExternalId': return requestContext.getRequesterExternalId();
|
||||
case 'iam:PolicyArn': return requestContext.getPolicyArn();
|
||||
// s3: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('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
|
||||
// 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('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.
|
||||
// Requires information from CloudServer.
|
||||
// On first pass of policy evaluation, CloudServer information will not be included,
|
||||
// so evaluation should be skipped
|
||||
map.set('s3:RequestObjectTagKeys',
|
||||
requestContext.getNeedTagEval() && requestContext.getRequestObjTags()
|
||||
? getTagKeys(requestContext.getRequestObjTags()!)
|
||||
: undefined,
|
||||
);
|
||||
return map.get(key);
|
||||
};
|
||||
case 's3:RequestObjectTagKeys':
|
||||
return requestContext.getNeedTagEval() && requestContext.getRequestObjTags()
|
||||
? getTagKeys(requestContext.getRequestObjTags()!)
|
||||
: undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Wildcards are allowed in certain string comparison and arn comparisons
|
||||
|
@ -229,7 +229,7 @@ function convertToEpochTime(time: string | string[]) {
|
|||
* reference_policies_elements.html)
|
||||
* @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
|
||||
// is only one of these strings so should not have undefined
|
||||
// or security issue with object assignment
|
||||
|
@ -444,4 +444,4 @@ export const convertConditionOperator = (operator: string): boolean => {
|
|||
},
|
||||
};
|
||||
return operatorMap[operator];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"version": "7.10.29",
|
||||
"version": "7.10.29-4",
|
||||
"description": "Common utilities for the S3 project components",
|
||||
"main": "build/index.js",
|
||||
"repository": {
|
||||
|
|
|
@ -166,7 +166,7 @@ describe('network.Server: ', () => {
|
|||
if (err) {
|
||||
return ws.onStop(() => {
|
||||
clearTimeout(requestTimeout);
|
||||
if (err.code === 'EPROTO') {
|
||||
if (err.code === 'EPROTO' || err.code === 'ECONNRESET') {
|
||||
return done();
|
||||
}
|
||||
return done(err);
|
||||
|
|
|
@ -1162,30 +1162,6 @@ describe('policyEvaluator', () => {
|
|||
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', () => {
|
||||
policy.Statement.Condition = {
|
||||
'ForAnyValue:StringLike': { 's3:RequestObjectTagKeys': ['tagOne', 'tagTwo'] },
|
||||
|
@ -1208,7 +1184,7 @@ describe('policyEvaluator', () => {
|
|||
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 = {
|
||||
'ForAnyValue:StringLike': { 's3:RequestObjectTagKeys': ['tagOne', 'tagTwo'] },
|
||||
};
|
||||
|
@ -1219,12 +1195,12 @@ describe('policyEvaluator', () => {
|
|||
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 = {
|
||||
'ForAllValues:StringLike': { 's3:RequestObjectTagKeys': ['tagOne', 'tagTwo'] },
|
||||
};
|
||||
const rcModifiers = {
|
||||
_requestObjTags: 'tagThree=keyThree&tagFour=keyFour',
|
||||
_requestObjTags: 'tagOne=keyOne&tagThree=keyThree',
|
||||
_needTagEval: true,
|
||||
};
|
||||
check(requestContext, rcModifiers, policy, 'Neutral');
|
||||
|
@ -1241,6 +1217,203 @@ describe('policyEvaluator', () => {
|
|||
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', () => {
|
||||
|
@ -1266,17 +1439,152 @@ describe('policyEvaluator', () => {
|
|||
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({}, {},
|
||||
'notbucket', undefined,
|
||||
'my_favorite_bucket', 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 result = evaluateAllPolicies(
|
||||
requestContext,
|
||||
testCase.policiesToEvaluate.map(policyName => TestMatrixPolicies[policyName]),
|
||||
log);
|
||||
assert.strictEqual(result, testCase.expectedPolicyEvaluation);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue