UNPKG

@cloud-copilot/iam-simulate

Version:
410 lines 17 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.requestMatchesConditions = requestMatchesConditions; exports.singleConditionMatchesRequest = singleConditionMatchesRequest; const requestContext_js_1 = require("../requestContext.js"); const ArnEquals_js_1 = require("./arn/ArnEquals.js"); const ArnLike_js_1 = require("./arn/ArnLike.js"); const ArnNotEquals_js_1 = require("./arn/ArnNotEquals.js"); const ArnNotLike_js_1 = require("./arn/ArnNotLike.js"); const BinaryEquals_js_1 = require("./binary/BinaryEquals.js"); const Bool_js_1 = require("./boolean/Bool.js"); const DateEquals_js_1 = require("./date/DateEquals.js"); const DateGreaterThan_js_1 = require("./date/DateGreaterThan.js"); const DateGreaterThanEquals_js_1 = require("./date/DateGreaterThanEquals.js"); const DateLessThan_js_1 = require("./date/DateLessThan.js"); const DateLessThanEquals_js_1 = require("./date/DateLessThanEquals.js"); const DateNotEquals_js_1 = require("./date/DateNotEquals.js"); const IpAddress_js_1 = require("./ipaddress/IpAddress.js"); const NotIpAddress_js_1 = require("./ipaddress/NotIpAddress.js"); const NumericEquals_js_1 = require("./numeric/NumericEquals.js"); const NumericGreaterThan_js_1 = require("./numeric/NumericGreaterThan.js"); const NumericGreaterThanEquals_js_1 = require("./numeric/NumericGreaterThanEquals.js"); const NumericLessThan_js_1 = require("./numeric/NumericLessThan.js"); const NumericNotEquals_js_1 = require("./numeric/NumericNotEquals.js"); const StringEquals_js_1 = require("./string/StringEquals.js"); const StringEqualsIgnoreCase_js_1 = require("./string/StringEqualsIgnoreCase.js"); const StringLike_js_1 = require("./string/StringLike.js"); const StringNotEquals_js_1 = require("./string/StringNotEquals.js"); const StringNotEqualsIgnoreCase_js_1 = require("./string/StringNotEqualsIgnoreCase.js"); const StringNotLike_js_1 = require("./string/StringNotLike.js"); const allOperators = [ StringEquals_js_1.StringEquals, StringNotEquals_js_1.StringNotEquals, StringEqualsIgnoreCase_js_1.StringEqualsIgnoreCase, StringNotEqualsIgnoreCase_js_1.StringNotEqualsIgnoreCase, StringLike_js_1.StringLike, StringNotLike_js_1.StringNotLike, NumericEquals_js_1.NumericEquals, NumericNotEquals_js_1.NumericNotEquals, NumericLessThan_js_1.NumericLessThan, NumericNotEquals_js_1.NumericNotEquals, NumericGreaterThan_js_1.NumericGreaterThan, NumericGreaterThanEquals_js_1.NumericGreaterThanEquals, DateEquals_js_1.DateEquals, DateNotEquals_js_1.DateNotEquals, DateLessThan_js_1.DateLessThan, DateLessThanEquals_js_1.DateLessThanEquals, DateGreaterThan_js_1.DateGreaterThan, DateGreaterThanEquals_js_1.DateGreaterThanEquals, Bool_js_1.Bool, BinaryEquals_js_1.BinaryEquals, IpAddress_js_1.IpAddress, NotIpAddress_js_1.NotIpAddress, ArnLike_js_1.ArnLike, ArnEquals_js_1.ArnEquals, ArnNotLike_js_1.ArnNotLike, ArnNotEquals_js_1.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 */ 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 */ 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 requestContext_js_1.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 requestContext_js_1.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