Skip to content

Commit a4f163f

Browse files
committed
Merge branches 'w/8.1/bugfix/ARSN-255-revampEvaluatePolicyForTagConditions' and 'q/1989/7.10/bugfix/ARSN-255-revampEvaluatePolicyForTagConditions' into tmp/octopus/q/8.1
3 parents 657f969 + b43cf22 + af50ef4 commit a4f163f

File tree

3 files changed

+455
-121
lines changed

3 files changed

+455
-121
lines changed

lib/policyEvaluator/evaluator.ts

Lines changed: 95 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ const operatorsWithVariables = ['StringEquals', 'StringNotEquals',
1313
const operatorsWithNegation = ['StringNotEquals',
1414
'StringNotEqualsIgnoreCase', 'StringNotLike', 'ArnNotEquals',
1515
'ArnNotLike', 'NumericNotEquals'];
16-
const tagConditions = new Set(['s3:ExistingObjectTag', 's3:RequestObjectTagKey', 's3:RequestObjectTagKeys']);
16+
const tagConditions = new Set([
17+
's3:ExistingObjectTag',
18+
's3:RequestObjectTagKey',
19+
's3:RequestObjectTagKeys',
20+
]);
1721

1822

1923
/**
@@ -24,11 +28,11 @@ const tagConditions = new Set(['s3:ExistingObjectTag', 's3:RequestObjectTagKey',
2428
* @param log - logger
2529
* @return true if applicable, false if not
2630
*/
27-
export const isResourceApplicable = (
31+
export function isResourceApplicable(
2832
requestContext: RequestContext,
2933
statementResource: string | string[],
3034
log: Logger,
31-
): boolean => {
35+
): boolean {
3236
const resource = requestContext.getResource();
3337
if (!Array.isArray(statementResource)) {
3438
// eslint-disable-next-line no-param-reassign
@@ -59,7 +63,7 @@ export const isResourceApplicable = (
5963
{ requestResource: resource });
6064
// If no match found, no resource is applicable
6165
return false;
62-
};
66+
}
6367

6468
/**
6569
* Check whether action in policy statement applies to request
@@ -69,11 +73,11 @@ export const isResourceApplicable = (
6973
* @param log - logger
7074
* @return true if applicable, false if not
7175
*/
72-
export const isActionApplicable = (
76+
export function isActionApplicable(
7377
requestAction: string,
7478
statementAction: string | string[],
7579
log: Logger,
76-
): boolean => {
80+
): boolean {
7781
if (!Array.isArray(statementAction)) {
7882
// eslint-disable-next-line no-param-reassign
7983
statementAction = [statementAction];
@@ -95,28 +99,29 @@ export const isActionApplicable = (
9599
{ requestAction });
96100
// If no match found, return false
97101
return false;
98-
};
102+
}
99103

100104
/**
101105
* Check whether request meets policy conditions
102-
* @param requestContext - info about request
103-
* @param statementCondition - Condition statement from policy
104-
* @param log - logger
105-
* @return contains whether conditions are allowed and whether they
106-
* contain any tag condition keys
106+
* @param {RequestContext} requestContext - info about request
107+
* @param {object} statementCondition - Condition statement from policy
108+
* @param {Logger} log - logger
109+
* @return {boolean|null} a condition evaluation result, one of:
110+
* - true: condition is met
111+
* - false: condition is not met
112+
* - null: condition evaluation requires additional info to be
113+
* provided (namely, for tag conditions, request tags and/or object
114+
* tags have to be provided to evaluate the condition)
107115
*/
108-
export const meetConditions = (
116+
export function meetConditions(
109117
requestContext: RequestContext,
110118
statementCondition: any,
111119
log: Logger,
112-
) => {
120+
): boolean | null {
121+
let hasTagConditions = false;
113122
// The Condition portion of a policy is an object with different
114123
// operators as keys
115-
const conditionEval = {};
116-
const operators = Object.keys(statementCondition);
117-
const length = operators.length;
118-
for (let i = 0; i < length; i++) {
119-
const operator = operators[i];
124+
for (const operator of Object.keys(statementCondition)) {
120125
const hasPrefix = operator.includes(':');
121126
const hasIfExistsCondition = operator.endsWith('IfExists');
122127
// If has "IfExists" added to operator name, or operator has "ForAnyValue" or
@@ -135,10 +140,6 @@ export const meetConditions = (
135140
// Note: this should be the actual operator name, not the bareOperator
136141
const conditionsWithSameOperator = statementCondition[operator];
137142
const conditionKeys = Object.keys(conditionsWithSameOperator);
138-
if (conditionKeys.some(key => tagConditions.has(key)) && !requestContext.getNeedTagEval()) {
139-
// @ts-expect-error
140-
conditionEval.tagConditions = true;
141-
}
142143
const conditionKeysLength = conditionKeys.length;
143144
for (let j = 0; j < conditionKeysLength; j++) {
144145
const key = conditionKeys[j];
@@ -155,6 +156,10 @@ export const meetConditions = (
155156
// tag key is included in condition key and needs to be
156157
// moved to value for evaluation, otherwise key/value are unchanged
157158
const [transformedKey, transformedValue] = transformTagKeyValue(key, value);
159+
if (tagConditions.has(transformedKey) && !requestContext.getNeedTagEval()) {
160+
hasTagConditions = true;
161+
continue;
162+
}
158163
// Pull key using requestContext
159164
// TODO: If applicable to S3, handle policy set operations
160165
// where a keyBasedOnRequestContext returns multiple values and
@@ -180,7 +185,7 @@ export const meetConditions = (
180185
log.trace('condition not satisfied due to ' +
181186
'missing info', { operator,
182187
conditionKey: transformedKey, policyValue: transformedValue });
183-
return { allow: false };
188+
return false;
184189
}
185190
// If condition operator prefix is included, the key should be an array
186191
if (prefix && !Array.isArray(keyBasedOnRequestContext)) {
@@ -195,14 +200,16 @@ export const meetConditions = (
195200
if (!operatorFunction(keyBasedOnRequestContext, transformedValue, prefix)) {
196201
log.trace('did not satisfy condition', { operator: bareOperator,
197202
keyBasedOnRequestContext, policyValue: transformedValue });
198-
return { allow: false };
203+
return false;
199204
}
200205
}
201206
}
202-
// @ts-expect-error
203-
conditionEval.allow = true;
204-
return conditionEval;
205-
};
207+
// one or more conditions required tag info to be evaluated
208+
if (hasTagConditions) {
209+
return null;
210+
}
211+
return true;
212+
}
206213

207214
/**
208215
* Evaluate whether a request is permitted under a policy.
@@ -215,13 +222,15 @@ export const meetConditions = (
215222
* @return Allow if permitted, Deny if not permitted or Neutral
216223
* if not applicable
217224
*/
218-
export const evaluatePolicy = (
225+
export function evaluatePolicy(
219226
requestContext: RequestContext,
220227
policy: any,
221228
log: Logger,
222-
): string => {
229+
): string {
223230
// TODO: For bucket policies need to add Principal evaluation
224-
let verdict = 'Neutral';
231+
let allow = false;
232+
let allowWithTagCondition = false;
233+
let denyWithTagCondition = false;
225234

226235
if (!Array.isArray(policy.Statement)) {
227236
// eslint-disable-next-line no-param-reassign
@@ -258,10 +267,18 @@ export const evaluatePolicy = (
258267
}
259268
const conditionEval = currentStatement.Condition ?
260269
meetConditions(requestContext, currentStatement.Condition, log) :
261-
null;
270+
true;
262271
// If do not meet conditions move on to next statement
263-
// @ts-expect-error
264-
if (conditionEval && !conditionEval.allow) {
272+
if (conditionEval === false) {
273+
continue;
274+
}
275+
// If condition needs tag info to be evaluated, mark and move on to next statement
276+
if (conditionEval === null) {
277+
if (currentStatement.Effect === 'Deny') {
278+
denyWithTagCondition = true;
279+
} else {
280+
allowWithTagCondition = true;
281+
}
265282
continue;
266283
}
267284
if (currentStatement.Effect === 'Deny') {
@@ -270,17 +287,27 @@ export const evaluatePolicy = (
270287
return 'Deny';
271288
}
272289
log.trace('Allow statement applies');
273-
// If statement is applicable, conditions are met and Effect is
274-
// to Allow, set verdict to Allow
290+
// statement is applicable, conditions are met and Effect is
291+
// to Allow
292+
allow = true;
293+
}
294+
let verdict;
295+
if (denyWithTagCondition) {
296+
// priority is on checking tags to potentially deny
297+
verdict = 'DenyWithTagCondition';
298+
} else if (allow) {
299+
// at least one statement is an allow
275300
verdict = 'Allow';
276-
// @ts-expect-error
277-
if (conditionEval && conditionEval.tagConditions) {
278-
verdict = 'NeedTagConditionEval';
279-
}
301+
} else if (allowWithTagCondition) {
302+
// all allow statements need tag checks
303+
verdict = 'AllowWithTagCondition';
304+
} else {
305+
// no statement matched to allow or deny
306+
verdict = 'Neutral';
280307
}
281308
log.trace('result of evaluating single policy', { verdict });
282309
return verdict;
283-
};
310+
}
284311

285312
/**
286313
* Evaluate whether a request is permitted under a policy.
@@ -293,24 +320,43 @@ export const evaluatePolicy = (
293320
* @return Allow if permitted, Deny if not permitted.
294321
* Default is to Deny. Deny overrides an Allow
295322
*/
296-
export const evaluateAllPolicies = (
323+
export function evaluateAllPolicies(
297324
requestContext: RequestContext,
298325
allPolicies: any[],
299326
log: Logger,
300-
): string => {
327+
): string {
301328
log.trace('evaluating all policies');
302-
let verdict = 'Deny';
329+
let allow = false;
330+
let allowWithTagCondition = false;
331+
let denyWithTagCondition = false;
303332
for (let i = 0; i < allPolicies.length; i++) {
304-
const singlePolicyVerdict =
305-
evaluatePolicy(requestContext, allPolicies[i], log);
333+
const singlePolicyVerdict = evaluatePolicy(requestContext, allPolicies[i], log);
306334
// If there is any Deny, just return Deny
307335
if (singlePolicyVerdict === 'Deny') {
308336
return 'Deny';
309337
}
310338
if (singlePolicyVerdict === 'Allow') {
339+
allow = true;
340+
} else if (singlePolicyVerdict === 'AllowWithTagCondition') {
341+
allowWithTagCondition = true;
342+
} else if (singlePolicyVerdict === 'DenyWithTagCondition') {
343+
denyWithTagCondition = true;
344+
} // else 'Neutral'
345+
}
346+
let verdict;
347+
if (allow) {
348+
if (denyWithTagCondition) {
349+
verdict = 'NeedTagConditionEval';
350+
} else {
311351
verdict = 'Allow';
312352
}
353+
} else {
354+
if (allowWithTagCondition) {
355+
verdict = 'NeedTagConditionEval';
356+
} else {
357+
verdict = 'Deny';
358+
}
313359
}
314-
log.trace('result of evaluating all pollicies', { verdict });
360+
log.trace('result of evaluating all policies', { verdict });
315361
return verdict;
316-
};
362+
}

lib/policyEvaluator/principal.ts

Lines changed: 18 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,22 @@ export default class Principal {
2323
* @param statement - Statement policy field
2424
* @return True if meet conditions
2525
*/
26-
static _evaluateCondition(
26+
static _evaluateStatement(
2727
params: Params,
2828
statement: Statement,
29-
// TODO Fix return type
30-
): any {
29+
): 'Neutral' | 'Allow' | 'Deny' {
30+
const reverse = !!statement.NotPrincipal;
31+
if (reverse) {
32+
// In case of anonymous NotPrincipal, this will neutral everyone
33+
return 'Neutral';
34+
}
3135
if (statement.Condition) {
32-
return meetConditions(params.rc, statement.Condition, params.log);
36+
const conditionEval = meetConditions(params.rc, statement.Condition, params.log);
37+
if (conditionEval === false || conditionEval === null) {
38+
return 'Neutral';
39+
}
3340
}
34-
return true;
41+
return statement.Effect;
3542
}
3643

3744
/**
@@ -48,19 +55,12 @@ export default class Principal {
4855
statement: Statement,
4956
valids: Valid,
5057
): 'Neutral' | 'Allow' | 'Deny' {
51-
const reverse = !!statement.NotPrincipal;
5258
const principal = (statement.Principal || statement.NotPrincipal)!;
53-
if (typeof principal === 'string' && principal === '*') {
54-
if (reverse) {
55-
// In case of anonymous NotPrincipal, this will neutral everyone
56-
return 'Neutral';
57-
}
58-
const conditionEval = Principal._evaluateCondition(params, statement);
59-
if (!conditionEval || conditionEval.allow === false) {
60-
return 'Neutral';
59+
const reverse = !!statement.NotPrincipal;
60+
if (typeof principal === 'string') {
61+
if (principal === '*') {
62+
return Principal._evaluateStatement(params, statement);
6163
}
62-
return statement.Effect;
63-
} else if (typeof principal === 'string') {
6464
return 'Deny';
6565
}
6666
let ref = [];
@@ -82,28 +82,8 @@ export default class Principal {
8282
}
8383
toCheck = Array.isArray(toCheck) ? toCheck : [toCheck];
8484
ref = Array.isArray(ref) ? ref : [ref];
85-
if (toCheck.indexOf('*') !== -1) {
86-
if (reverse) {
87-
return 'Neutral';
88-
}
89-
const conditionEval = Principal._evaluateCondition(params, statement);
90-
if (!conditionEval || conditionEval.allow === false) {
91-
return 'Neutral';
92-
}
93-
return statement.Effect;
94-
}
95-
const len = ref.length;
96-
for (let i = 0; i < len; ++i) {
97-
if (toCheck.indexOf(ref[i]) !== -1) {
98-
if (reverse) {
99-
return 'Neutral';
100-
}
101-
const conditionEval = Principal._evaluateCondition(params, statement);
102-
if (!conditionEval || conditionEval.allow === false) {
103-
return 'Neutral';
104-
}
105-
return statement.Effect;
106-
}
85+
if (toCheck.includes('*') || ref.some(r => toCheck.includes(r))) {
86+
return Principal._evaluateStatement(params, statement);
10787
}
10888
if (reverse) {
10989
return statement.Effect;

0 commit comments

Comments
 (0)