UNPKG

@cloud-copilot/iam-simulate

Version:
406 lines 16.1 kB
import { ContextKeyImpl } from '../requestContext.js'; import { ArnEquals } from './arn/ArnEquals.js'; import { ArnLike } from './arn/ArnLike.js'; import { ArnNotEquals } from './arn/ArnNotEquals.js'; import { ArnNotLike } from './arn/ArnNotLike.js'; import { BinaryEquals } from './binary/BinaryEquals.js'; import { Bool } from './boolean/Bool.js'; import { DateEquals } from './date/DateEquals.js'; import { DateGreaterThan } from './date/DateGreaterThan.js'; import { DateGreaterThanEquals } from './date/DateGreaterThanEquals.js'; import { DateLessThan } from './date/DateLessThan.js'; import { DateLessThanEquals } from './date/DateLessThanEquals.js'; import { DateNotEquals } from './date/DateNotEquals.js'; import { IpAddress } from './ipaddress/IpAddress.js'; import { NotIpAddress } from './ipaddress/NotIpAddress.js'; import { NumericEquals } from './numeric/NumericEquals.js'; import { NumericGreaterThan } from './numeric/NumericGreaterThan.js'; import { NumericGreaterThanEquals } from './numeric/NumericGreaterThanEquals.js'; import { NumericLessThan } from './numeric/NumericLessThan.js'; import { NumericNotEquals } from './numeric/NumericNotEquals.js'; import { StringEquals } from './string/StringEquals.js'; import { StringEqualsIgnoreCase } from './string/StringEqualsIgnoreCase.js'; import { StringLike } from './string/StringLike.js'; import { StringNotEquals } from './string/StringNotEquals.js'; import { StringNotEqualsIgnoreCase } from './string/StringNotEqualsIgnoreCase.js'; import { StringNotLike } from './string/StringNotLike.js'; const allOperators = [ StringEquals, StringNotEquals, StringEqualsIgnoreCase, StringNotEqualsIgnoreCase, StringLike, StringNotLike, NumericEquals, NumericNotEquals, NumericLessThan, NumericNotEquals, NumericGreaterThan, NumericGreaterThanEquals, DateEquals, DateNotEquals, DateLessThan, DateLessThanEquals, DateGreaterThan, DateGreaterThanEquals, Bool, BinaryEquals, IpAddress, NotIpAddress, ArnLike, ArnEquals, ArnNotLike, ArnNotEquals ]; const baseOperations = {}; for (const operator of allOperators) { baseOperations[operator.name.toLowerCase()] = operator; } /** * Evaluate a set of conditions against a request * * @param request the request to test * @param conditions the conditions to test * @returns Match if all conditions match, NoMatch if any do not. Also returns all the details of the evaluation */ export function requestMatchesConditions(request, conditions, statementType, simulationParameters) { const results = conditions.map((condition) => ({ condition, explain: singleConditionMatchesRequest(request, condition, simulationParameters) })); const isIgnored = (c) => { if (simulationParameters.simulationMode !== 'Discovery') { return false; } if (simulationParameters.strictConditionKeys.has(c.condition.conditionKey().toLowerCase())) { return false; } // In Allows we ignore conditions that do not match if (statementType.toLowerCase() === 'allow') { return !c.explain.matches; } // In Denies we ignore conditions that do match if (statementType.toLowerCase() === 'deny') { return c.explain.matches; } throw new Error(`Unexpected condition explain result in discovery mode, statementType: ${statementType}`); }; const nonMatch = results.filter((r) => !isIgnored(r)).some((result) => !result.explain.matches); const ignoredMatches = results .filter((r) => isIgnored(r)) .some((result) => result.explain.matches); return { //If there is a non-match this is not ignored, it's a NoMatch //If there are matches that are ignored, it is also a NoMatch, // for instance in a Deny statement it may match a condition that is ignored, // but we still want a no match so we can show under what conditions it would be allowed matches: nonMatch || ignoredMatches ? 'NoMatch' : 'Match', details: { conditions: results.length == 0 ? undefined : results.map((r) => r.explain) }, //Ignored conditions only matter if the non ignored fields all match ignoredConditions: nonMatch ? undefined : ignoredConditions(results, isIgnored) }; } /** * Get the list of conditions that were ignored during discovery mode, if any * * @param conditions the conditions that were evaluated with their explains * @param statementType whether the statement is an allow or deny statement * @param simulationParameters the general parameters for the simulation * @returns an array of ignored conditions, or undefined if there are none */ function ignoredConditions(conditions, isIgnored) { const ignoredConditions = conditions.filter(isIgnored); if (ignoredConditions.length > 0) { return ignoredConditions.map((r) => r.condition); } return undefined; } /** * Checks to see if a single condition matches a request * * @param request the request to test * @param condition the condition to test * @returns the result of evaluating the condition */ export function singleConditionMatchesRequest(request, condition, simulationParameters) { const key = condition.conditionKey(); const baseOperation = baseOperations[condition.operation().baseOperator().toLowerCase()]; const keyExists = request.contextKeyExists(key); const keyValue = keyExists ? request.getContextKeyValue(key) : undefined; if (condition.operation().value().toLowerCase() == 'null' || condition.operation().baseOperator()?.toLowerCase() == 'null') { return testNull(condition, keyExists); } if (condition.operation().setOperator()) { const setOperator = condition.operation().setOperator(); if (setOperator === 'ForAnyValue') { return forAnyValueMatch(request, condition, keyValue, baseOperation); } else if (setOperator === 'ForAllValues') { return forAllValuesMatch(request, condition, keyValue, baseOperation); } else { throw new Error(`Unknown set operator: ${setOperator}`); } } return singleValueMatch(request, condition, baseOperation, keyValue); } /** * Tests a condition with a null operator * * @param condition the condition to test * @param keyExists whether the key exists in the request * @returns the result of evaluating the null operator */ function testNull(condition, keyExists) { const goalValue = keyExists ? 'false' : 'true'; const conditionValues = condition.conditionValues().map((value) => { return { value, matches: value.toLowerCase() === goalValue }; }); return { operator: condition.operation().value(), conditionKeyValue: condition.conditionKey(), values: condition.valueIsArray() ? conditionValues : conditionValues[0], matches: conditionValues.some((value) => value.matches) }; } function singleValueMatch(request, condition, baseOperation, keyValue) { const isNotOperator = condition.operation().baseOperator().toLowerCase().includes('not'); if (condition.operation().isIfExists() || isNotOperator) { //Check if it exists, return true if it doesn't //Double check what happens here if the key is not a valid key or is of the wrong type if (!keyValue) { const valueExplains = condition.conditionValues().map((value) => ({ value, matches: true })); return { operator: condition.operation().value(), conditionKeyValue: condition.conditionKey(), values: condition.valueIsArray() ? valueExplains : valueExplains[0], matches: true, matchedBecauseMissing: true, resolvedConditionKeyValue: keyValue }; } } if (!keyValue || !keyValue.isStringValue()) { //Set operator is required for a multi-value key //Confirmed this at re:Inforce 2025 IAM431. const valueExplains = condition.conditionValues().map((value) => ({ value, matches: false })); return { operator: condition.operation().value(), conditionKeyValue: condition.conditionKey(), values: condition.valueIsArray() ? valueExplains : valueExplains[0], matches: false, failedBecauseMissing: !keyValue, failedBecauseArray: keyValue?.isArrayValue() }; } if (!baseOperation) { const valueExplains = condition.conditionValues().map((value) => ({ value, matches: false })); return { operator: condition.operation().value(), conditionKeyValue: condition.conditionKey(), values: condition.valueIsArray() ? valueExplains : valueExplains[0], matches: false, missingOperator: true }; } const { matches, explains } = baseOperation.matches(request, keyValue.value, condition.conditionValues()); return { matches, operator: condition.operation().value(), conditionKeyValue: condition.conditionKey(), values: condition.valueIsArray() ? explains : explains[0], resolvedConditionKeyValue: keyValue.value }; } /** * Tests a condition with a ForAllValues set operator * * @param request the request to test * @param condition the condition with ForAllValues set operator * @param keyExists whether the key exists in the request * @param keyValue the value of the key in the request * @param baseOperation the base operation to test the key against * @returns the result of evaluating the ForAllValues set operator */ function forAllValuesMatch(request, condition, keyValue, baseOperation) { const matchingValueExplains = condition .conditionValues() .map((value) => ({ value, matches: true })); const notMatchingValueExplains = condition .conditionValues() .map((value) => ({ value, matches: false })); if (!keyValue) { return { operator: condition.operation().value(), conditionKeyValue: condition.conditionKey(), values: matchingValueExplains, matches: true, matchedBecauseMissing: true }; } // If the key only has a single value, convert it to an array to process if (keyValue.isStringValue()) { keyValue = new ContextKeyImpl(keyValue.name, [keyValue.value]); } if (!keyValue.isArrayValue()) { throw new Error('Key value is not an array, this is a bug.'); } if (!baseOperation) { return { operator: condition.operation().value(), conditionKeyValue: condition.conditionKey(), values: notMatchingValueExplains, matches: false, missingOperator: true }; } const valueExplains = keyValue.values.map((value) => { const { matches, explains } = baseOperation.matches(request, value, condition.conditionValues()); return { requestValue: value, matches, explains }; }); const anyNonMatches = valueExplains.some((valueExplain) => !valueExplain.matches); const overallMatch = !anyNonMatches; const unmatchedValues = []; const explains = {}; for (const valueExplain of valueExplains) { if (!baseOperation.isNegative && !valueExplain.matches) { unmatchedValues.push(valueExplain.requestValue); } else if (baseOperation.isNegative && valueExplain.matches) { unmatchedValues.push(valueExplain.requestValue); } for (const explain of valueExplain.explains) { let theExplain = explains[explain.value]; if (!theExplain) { explains[explain.value] = { value: explain.value, matches: overallMatch }; theExplain = explains[explain.value]; } if (explain.matches && !baseOperation.isNegative) { theExplain.matchingValues = theExplain.matchingValues || []; theExplain.matchingValues.push(valueExplain.requestValue); } else if (!explain.matches && baseOperation.isNegative) { theExplain.negativeMatchingValues = theExplain.negativeMatchingValues || []; theExplain.negativeMatchingValues.push(valueExplain.requestValue); } } } return { operator: condition.operation().value(), conditionKeyValue: condition.conditionKey(), values: Object.values(explains), matches: overallMatch, unmatchedValues }; } /** * Test a condition with a ForAnyValue set operator * * @param request the request to test * @param condition the condition with ForAnyValue set operator * @param keyExists whether the key exists in the request * @param keyValue the value of the key in the request * @param baseOperation the base operation to test the key against * @returns the result of evaluating the ForAnyValue set operator */ function forAnyValueMatch(request, condition, keyValue, baseOperation) { const failedValueExplains = condition.conditionValues().map((value) => ({ value, matches: false })); if (!keyValue) { return { operator: condition.operation().value(), conditionKeyValue: condition.conditionKey(), values: failedValueExplains, matches: false, failedBecauseMissing: true }; // return 'NoMatch' } // If the key only has a single value, convert it to an array to process if (keyValue.isStringValue()) { keyValue = new ContextKeyImpl(keyValue.name, [keyValue.value]); } if (!keyValue.isArrayValue()) { throw new Error('Key value is not an array, this is a bug.'); } if (!baseOperation) { return { operator: condition.operation().value(), conditionKeyValue: condition.conditionKey(), values: failedValueExplains, matches: false, missingOperator: true }; } const valueExplains = keyValue.values.map((value) => { const { matches, explains } = baseOperation.matches(request, value, condition.conditionValues()); return { requestValue: value, matches, explains }; }); const overallMatch = valueExplains.some((valueExplain) => valueExplain.matches); const unmatchedValues = []; const explains = {}; for (const valueExplain of valueExplains) { if (!baseOperation.isNegative && !valueExplain.matches) { unmatchedValues.push(valueExplain.requestValue); } else if (baseOperation.isNegative && valueExplain.matches) { unmatchedValues.push(valueExplain.requestValue); } for (const explain of valueExplain.explains) { let theExplain = explains[explain.value]; if (!theExplain) { explains[explain.value] = { value: explain.value, matches: overallMatch }; theExplain = explains[explain.value]; } if (explain.matches) { theExplain.matchingValues = theExplain.matchingValues || []; theExplain.matchingValues.push(valueExplain.requestValue); } } } return { operator: condition.operation().value(), conditionKeyValue: condition.conditionKey(), values: Object.values(explains), matches: overallMatch, unmatchedValues }; } //# sourceMappingURL=condition.js.map