Skip to content

Commit c300194

Browse files
committed
Merge remote-tracking branch 'origin/improvement/ARSN-593-arn-condition-operators' into w/8.5/improvement/ARSN-593-arn-condition-operators
2 parents 5370257 + 429f9a3 commit c300194

7 files changed

Lines changed: 1459 additions & 1284 deletions

File tree

lib/policyEvaluator/evaluator.ts

Lines changed: 69 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,27 @@ import { transformTagKeyValue } from './utils/objectTags';
66
import RequestContext from './RequestContext';
77
import { Logger } from 'werelogs';
88

9-
const operatorsWithVariables = ['StringEquals', 'StringNotEquals',
10-
'StringEqualsIgnoreCase', 'StringNotEqualsIgnoreCase',
11-
'StringLike', 'StringNotLike', 'ArnEquals', 'ArnNotEquals',
12-
'ArnLike', 'ArnNotLike'];
13-
const operatorsWithNegation = ['StringNotEquals',
14-
'StringNotEqualsIgnoreCase', 'StringNotLike', 'ArnNotEquals',
15-
'ArnNotLike', 'NumericNotEquals'];
16-
const tagConditions = new Set([
17-
's3:ExistingObjectTag',
18-
's3:RequestObjectTagKey',
19-
's3:RequestObjectTagKeys',
20-
]);
21-
9+
const operatorsWithVariables = [
10+
'StringEquals',
11+
'StringNotEquals',
12+
'StringEqualsIgnoreCase',
13+
'StringNotEqualsIgnoreCase',
14+
'StringLike',
15+
'StringNotLike',
16+
'ArnEquals',
17+
'ArnNotEquals',
18+
'ArnLike',
19+
'ArnNotLike',
20+
];
21+
const operatorsWithNegation = [
22+
'StringNotEquals',
23+
'StringNotEqualsIgnoreCase',
24+
'StringNotLike',
25+
'ArnNotEquals',
26+
'ArnNotLike',
27+
'NumericNotEquals',
28+
];
29+
const tagConditions = new Set(['s3:ExistingObjectTag', 's3:RequestObjectTagKey', 's3:RequestObjectTagKeys']);
2230

2331
/**
2432
* Check whether resource in policy statement applies to request resource
@@ -45,21 +53,16 @@ export function isResourceApplicable(
4553
const requestRelativeId = requestResourceArr.slice(5).join(':');
4654
for (let i = 0; i < statementResource.length; i++) {
4755
// Handle variables (must handle BEFORE wildcards)
48-
const policyResource =
49-
substituteVariables(statementResource[i], requestContext);
56+
const policyResource = substituteVariables(statementResource[i], requestContext);
5057
// Handle wildcards
51-
const arnSegmentsMatch =
52-
checkArnMatch(policyResource, requestRelativeId,
53-
requestResourceArr, true);
58+
const arnSegmentsMatch = checkArnMatch(policyResource, requestRelativeId, requestResourceArr);
5459
if (arnSegmentsMatch) {
55-
log.trace('policy resource is applicable to request',
56-
{ requestResource: resource, policyResource });
60+
log.trace('policy resource is applicable to request', { requestResource: resource, policyResource });
5761
return true;
5862
}
5963
continue;
6064
}
61-
log.trace('no policy resource is applicable to request',
62-
{ requestResource: resource });
65+
log.trace('no policy resource is applicable to request', { requestResource: resource });
6366
// If no match found, no resource is applicable
6467
return false;
6568
}
@@ -72,29 +75,24 @@ export function isResourceApplicable(
7275
* @param log - logger
7376
* @return true if applicable, false if not
7477
*/
75-
export function isActionApplicable(
76-
requestAction: string,
77-
statementAction: string | string[],
78-
log: Logger,
79-
): boolean {
78+
export function isActionApplicable(requestAction: string, statementAction: string | string[], log: Logger): boolean {
8079
if (!Array.isArray(statementAction)) {
8180
statementAction = [statementAction];
8281
}
8382
const length = statementAction.length;
8483
for (let i = 0; i < length; i++) {
8584
// No variables in actions so no need to handle
86-
const regExStrOfStatementAction =
87-
handleWildcards(statementAction[i]);
85+
const regExStrOfStatementAction = handleWildcards(statementAction[i]);
8886
const actualRegEx = new RegExp(regExStrOfStatementAction, 'i');
8987
if (actualRegEx.test(requestAction)) {
9088
log.trace('policy action is applicable to request action', {
91-
requestAction, policyAction: statementAction[i],
89+
requestAction,
90+
policyAction: statementAction[i],
9291
});
9392
return true;
9493
}
9594
}
96-
log.trace('no action in policy applicable to request action',
97-
{ requestAction });
95+
log.trace('no action in policy applicable to request action', { requestAction });
9896
// If no match found, return false
9997
return false;
10098
}
@@ -111,11 +109,7 @@ export function isActionApplicable(
111109
* provided (namely, for tag conditions, request tags and/or object
112110
* tags have to be provided to evaluate the condition)
113111
*/
114-
export function meetConditions(
115-
requestContext: RequestContext,
116-
statementCondition: any,
117-
log: Logger,
118-
): boolean | null {
112+
export function meetConditions(requestContext: RequestContext, statementCondition: any, log: Logger): boolean | null {
119113
let hasTagConditions = false;
120114
// The Condition portion of a policy is an object with different
121115
// operators as keys
@@ -124,16 +118,13 @@ export function meetConditions(
124118
const hasIfExistsCondition = operator.endsWith('IfExists');
125119
// If has "IfExists" added to operator name, or operator has "ForAnyValue" or
126120
// "ForAllValues" prefix, find operator name without "IfExists" or prefix
127-
let bareOperator = hasIfExistsCondition ? operator.slice(0, -8) :
128-
operator;
121+
let bareOperator = hasIfExistsCondition ? operator.slice(0, -8) : operator;
129122
let prefix: string | undefined;
130123
if (hasPrefix) {
131124
[prefix, bareOperator] = bareOperator.split(':');
132125
}
133-
const operatorCanHaveVariables =
134-
operatorsWithVariables.indexOf(bareOperator) > -1;
135-
const isNegationOperator =
136-
operatorsWithNegation.indexOf(bareOperator) > -1;
126+
const operatorCanHaveVariables = operatorsWithVariables.indexOf(bareOperator) > -1;
127+
const isNegationOperator = operatorsWithNegation.indexOf(bareOperator) > -1;
137128
// Loop through conditions with the same operator
138129
// Note: this should be the actual operator name, not the bareOperator
139130
const conditionsWithSameOperator = statementCondition[operator];
@@ -147,8 +138,7 @@ export function meetConditions(
147138
}
148139
// Handle variables
149140
if (operatorCanHaveVariables) {
150-
value = value.map((item: any) =>
151-
substituteVariables(item, requestContext));
141+
value = value.map((item: any) => substituteVariables(item, requestContext));
152142
}
153143
// if condition key is RequestObjectTag or ExistingObjectTag,
154144
// tag key is included in condition key and needs to be
@@ -164,25 +154,29 @@ export function meetConditions(
164154
// condition has "ForAnyValue" or "ForAllValues".
165155
// (see http://docs.aws.amazon.com/IAM/latest/UserGuide/
166156
// reference_policies_multi-value-conditions.html)
167-
let keyBasedOnRequestContext =
168-
findConditionKey(transformedKey, requestContext);
157+
let keyBasedOnRequestContext = findConditionKey(transformedKey, requestContext);
169158
// Handle IfExists and negation operators
170-
if ((keyBasedOnRequestContext === undefined ||
171-
keyBasedOnRequestContext === null) &&
172-
(hasIfExistsCondition || isNegationOperator)) {
173-
log.trace('satisfies condition due to IfExists operator or ' +
174-
'negation operator', { method: 'evaluators.evaluatePolicy' });
159+
if (
160+
(keyBasedOnRequestContext === undefined || keyBasedOnRequestContext === null) &&
161+
(hasIfExistsCondition || isNegationOperator)
162+
) {
163+
log.trace('satisfies condition due to IfExists operator or ' + 'negation operator', {
164+
method: 'evaluators.evaluatePolicy',
165+
});
175166
continue;
176167
}
177168
// If no IfExists qualifier, the key does not exist and the
178169
// condition operator is not Null, the
179170
// condition is not met so return false.
180-
if ((keyBasedOnRequestContext === null ||
181-
keyBasedOnRequestContext === undefined) &&
182-
bareOperator !== 'Null') {
183-
log.trace('condition not satisfied due to ' +
184-
'missing info', { operator,
185-
conditionKey: transformedKey, policyValue: transformedValue });
171+
if (
172+
(keyBasedOnRequestContext === null || keyBasedOnRequestContext === undefined) &&
173+
bareOperator !== 'Null'
174+
) {
175+
log.trace('condition not satisfied due to ' + 'missing info', {
176+
operator,
177+
conditionKey: transformedKey,
178+
policyValue: transformedValue,
179+
});
186180
return false;
187181
}
188182
// If condition operator prefix is included, the key should be an array
@@ -196,8 +190,11 @@ export function meetConditions(
196190
// are the only operators where wildcards are allowed
197191
// @ts-expect-error
198192
if (!operatorFunction(keyBasedOnRequestContext, transformedValue, prefix)) {
199-
log.trace('did not satisfy condition', { operator: bareOperator,
200-
keyBasedOnRequestContext, policyValue: transformedValue });
193+
log.trace('did not satisfy condition', {
194+
operator: bareOperator,
195+
keyBasedOnRequestContext,
196+
policyValue: transformedValue,
197+
});
201198
return false;
202199
}
203200
}
@@ -220,11 +217,7 @@ export function meetConditions(
220217
* @return Allow if permitted, Deny if not permitted or Neutral
221218
* if not applicable
222219
*/
223-
export function evaluatePolicy(
224-
requestContext: RequestContext,
225-
policy: any,
226-
log: Logger,
227-
): string {
220+
export function evaluatePolicy(requestContext: RequestContext, policy: any, log: Logger): string {
228221
// TODO: For bucket policies need to add Principal evaluation
229222
let allow = false;
230223
let allowWithTagCondition = false;
@@ -237,34 +230,30 @@ export function evaluatePolicy(
237230
const currentStatement = policy.Statement[i];
238231
// If affirmative resource is in policy and request resource is
239232
// not applicable, move on to next statement
240-
if (currentStatement.Resource && !isResourceApplicable(requestContext,
241-
currentStatement.Resource, log)) {
233+
if (currentStatement.Resource && !isResourceApplicable(requestContext, currentStatement.Resource, log)) {
242234
continue;
243235
}
244236
// If NotResource is in policy and resource matches NotResource
245237
// in policy, move on to next statement
246-
if (currentStatement.NotResource &&
247-
isResourceApplicable(requestContext,
248-
currentStatement.NotResource, log)) {
238+
if (currentStatement.NotResource && isResourceApplicable(requestContext, currentStatement.NotResource, log)) {
249239
continue;
250240
}
251241
// If affirmative action is in policy and request action is not
252242
// applicable, move on to next statement
253-
if (currentStatement.Action &&
254-
!isActionApplicable(requestContext.getAction(),
255-
currentStatement.Action, log)) {
243+
if (currentStatement.Action && !isActionApplicable(requestContext.getAction(), currentStatement.Action, log)) {
256244
continue;
257245
}
258246
// If NotAction is in policy and action matches NotAction in policy,
259247
// move on to next statement
260-
if (currentStatement.NotAction &&
261-
isActionApplicable(requestContext.getAction(),
262-
currentStatement.NotAction, log)) {
248+
if (
249+
currentStatement.NotAction &&
250+
isActionApplicable(requestContext.getAction(), currentStatement.NotAction, log)
251+
) {
263252
continue;
264253
}
265-
const conditionEval = currentStatement.Condition ?
266-
meetConditions(requestContext, currentStatement.Condition, log) :
267-
true;
254+
const conditionEval = currentStatement.Condition
255+
? meetConditions(requestContext, currentStatement.Condition, log)
256+
: true;
268257
// If do not meet conditions move on to next statement
269258
if (conditionEval === false) {
270259
continue;
@@ -318,11 +307,7 @@ export function evaluatePolicy(
318307
* @return Allow if permitted, Deny if not permitted.
319308
* Default is to Deny. Deny overrides an Allow
320309
*/
321-
export function evaluateAllPolicies(
322-
requestContext: RequestContext,
323-
allPolicies: any[],
324-
log: Logger,
325-
): string {
310+
export function evaluateAllPolicies(requestContext: RequestContext, allPolicies: any[], log: Logger): string {
326311
return standardEvaluateAllPolicies(requestContext, allPolicies, log).verdict;
327312
}
328313
export function standardEvaluateAllPolicies(

lib/policyEvaluator/utils/checkArnMatch.ts

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import LRUCache from '../../algos/cache/LRUCache';
22
import { handleWildcards } from './wildcards';
33
import { policyArnAllowedEmptyAccountId } from '../../constants';
44

5-
// Compiled matchers are pure functions of (policyArn, caseSensitive), so the
6-
// cache needs no invalidation; the LRU cap bounds the cardinality introduced
5+
// Compiled matchers are pure functions of the policy arn, so the cache
6+
// needs no invalidation; the LRU cap bounds the cardinality introduced
77
// by policy-variable substitution (per-request values in the policy arn).
88
const ARN_MATCHER_CACHE_ENTRIES = 10000;
99
const arnMatcherCache = new LRUCache(ARN_MATCHER_CACHE_ENTRIES);
@@ -28,24 +28,20 @@ function hasWildcards(portion: string): boolean {
2828
return WILDCARD_RE.test(portion);
2929
}
3030

31-
function compileArnMatcher(policyArn: string, caseSensitive: boolean): ArnMatcher {
31+
function compileArnMatcher(policyArn: string): ArnMatcher {
3232
const policyArnArr = policyArn.split(':');
3333
// The relativeId is the last part of the ARN (for instance, a bucket and
3434
// object name in S3)
3535
// Join on ":" in case there were ":" in the relativeID at the end
3636
// of the arn
3737
let relativeId: PortionMatcher;
3838
if (policyArnArr.length === 6 && !hasWildcards(policyArnArr[5])) {
39-
relativeId = {
40-
literal: caseSensitive ? policyArnArr[5] : policyArnArr[5].toLowerCase(),
41-
};
39+
relativeId = { literal: policyArnArr[5] };
4240
} else {
4341
// Translate the joined relative-id as one anchored unit so an embedded
4442
// ':' (legal in S3 keys) matches; <6-portion ARNs have none, match any.
4543
const source = policyArnArr.length >= 6 ? handleWildcards(policyArnArr.slice(5).join(':')) : '';
46-
relativeId = {
47-
regExp: new RegExp(caseSensitive ? source : source.toLowerCase()),
48-
};
44+
relativeId = { regExp: new RegExp(source) };
4945
}
5046
const segments: PortionMatcher[] = [];
5147
for (let j = 0; j < 5; j++) {
@@ -68,40 +64,32 @@ function portionMatches(matcher: PortionMatcher, value: string): boolean {
6864

6965
/**
7066
* Checks whether an ARN from a request matches an ARN in a policy
71-
* to compare against each portion of the ARN from the request
67+
* to compare against each portion of the ARN from the request.
68+
* The comparison is case-sensitive, as on AWS.
7269
* @param policyArn - arn from policy
7370
* @param requestRelativeId - last part of the arn from the request
7471
* @param requestArnArr - all parts of request arn split on ":"
75-
* @param caseSensitive - whether the comparison should be
76-
* case sensitive
7772
* @return true if match, false if not
7873
*/
79-
export default function checkArnMatch(
80-
policyArn: string,
81-
requestRelativeId: string,
82-
requestArnArr: string[],
83-
caseSensitive: boolean,
84-
): boolean {
85-
const cacheKey = `${caseSensitive ? 'cs' : 'ci'}:${policyArn}`;
86-
let matcher: ArnMatcher = arnMatcherCache.get(cacheKey);
74+
export default function checkArnMatch(policyArn: string, requestRelativeId: string, requestArnArr: string[]): boolean {
75+
let matcher: ArnMatcher = arnMatcherCache.get(policyArn);
8776
if (matcher === undefined) {
88-
matcher = compileArnMatcher(policyArn, caseSensitive);
89-
arnMatcherCache.add(cacheKey, matcher);
77+
matcher = compileArnMatcher(policyArn);
78+
arnMatcherCache.add(policyArn, matcher);
9079
}
9180
// Check to see if the relative-id matches first since most likely
9281
// to diverge. If not a match, the resource is not applicable so return
9382
// false
94-
if (!portionMatches(matcher.relativeId, caseSensitive ? requestRelativeId : requestRelativeId.toLowerCase())) {
83+
if (!portionMatches(matcher.relativeId, requestRelativeId)) {
9584
return false;
9685
}
9786
// Check the other parts of the ARN to make sure they match. If not,
9887
// return false.
9988
for (let j = 0; j < 5; j++) {
100-
const requestSegment = caseSensitive ? requestArnArr[j] : requestArnArr[j].toLowerCase();
10189
if (j === 4 && matcher.skipAccountIdCheck) {
10290
continue;
10391
}
104-
if (!portionMatches(matcher.segments[j], requestSegment)) {
92+
if (!portionMatches(matcher.segments[j], requestArnArr[j])) {
10593
return false;
10694
}
10795
}

0 commit comments

Comments
 (0)