UNPKG

@cloud-copilot/iam-simulate

Version:
320 lines 11.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.convertIamString = convertIamString; exports.splitArnParts = splitArnParts; exports.getResourceSegments = getResourceSegments; exports.isDefined = isDefined; exports.isNotDefined = isNotDefined; exports.isWildcardOnlyAction = isWildcardOnlyAction; exports.getResourceTypesForAction = getResourceTypesForAction; exports.convertResourcePatternToRegex = convertResourcePatternToRegex; exports.lowerCaseAll = lowerCaseAll; exports.getVariablesFromString = getVariablesFromString; exports.isAssumedRoleArn = isAssumedRoleArn; exports.isIamUserArn = isIamUserArn; exports.isFederatedUserArn = isFederatedUserArn; const iam_data_1 = require("@cloud-copilot/iam-data"); const matchesNothing = new RegExp('a^'); const defaultStringReplaceOptions = { replaceWildcards: true, convertToRegex: true }; function convertIamString(value, request, replaceOptions) { const options = { ...defaultStringReplaceOptions, ...replaceOptions }; const errors = []; const newValue = value.replaceAll(/(\$\{.*?\})|(\*)|(\?)/gi, (match, args) => { if (match == '?') { return replacementValue(match, '\\?', '.', options); // return '.' } else if (match == '*') { return replacementValue(match, '\\*', '.*?', options); // return ".*?" } else if (match == '${*}') { return replacementValue(match, '\\$\\{\\*\\}', '\\*', options); // return "\\*" } else if (match == '${?}') { return replacementValue(match, '\\$\\{\\?\\}', '\\?', options); // return "\\?" } else if (match == '${$}') { return replacementValue(match, '\\$\\{\\$\\}', '\\$', options); // return "\\$" } // //This means it'a a variable const inTheBrackets = match.slice(2, -1); let defaultValue = undefined; const defaultParts = inTheBrackets.split(', '); if (defaultParts.length == 2) { const segmentAfterComma = defaultParts.at(1); if (segmentAfterComma?.startsWith("'") && segmentAfterComma.endsWith("'")) { defaultValue = segmentAfterComma.slice(1, -1); } } const variableName = defaultParts.at(0).trim(); const { value: requestValue, error: requestValueError } = getContextSingleValue(request, variableName); if (requestValue) { //TODO: Maybe escape the * in the resolved value to ${*} return options.convertToRegex ? escapeRegexCharacters(requestValue) : requestValue; } else if (defaultValue) { /* TODO: What happens in a request if a multi value context key is used in a string and there is a default value? Will it use the default value or will it fail the condition test? */ //TODO: Maybe escape the * in the resolved value to ${*} return options.convertToRegex ? escapeRegexCharacters(defaultValue) : defaultValue; } else { if (requestValueError == 'missing') { errors.push(`{${variableName}} not found in request context, and no default value provided. This will never match`); } else if (requestValueError == 'multivalue') { errors.push(`{${variableName}} is a multi value context key, and cannot be used for replacement. This will never match`); } /* https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_variables.html#policy-vars-no-value */ return match; } throw new Error('This should never happen'); }); if (!options.convertToRegex) { return newValue; } if (errors.length > 0) { return { pattern: matchesNothing, errors }; } return { pattern: new RegExp('^' + newValue + '$') }; } /** * Replace regex characters in a string with their escaped versions * * @param str the string to escape regex characters in * @returns the string with regex characters escaped */ function escapeRegexCharacters(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * Get the string value of a context key only if it is a single value key * * @param requestContext the request context to get the value from * @param contextKeyName the name of the context key to get the value of * @returns the value of the context key if it is a single value key, undefined otherwise */ function getContextSingleValue(request, contextKeyName) { if (!request.contextKeyExists(contextKeyName)) { return { error: 'missing' }; } const keyValue = request.getContextKeyValue(contextKeyName); if (keyValue.isStringValue()) { return { value: keyValue.value }; } return { error: 'multivalue' }; } /** * Get the replacement value for a string * * @param originalString the original string to replace the value of * @param rawString the string to replace the value in * @param wildcard the value to replace the wildcard with * @param replaceWildcards if the wildcard or raw string should be used * @returns */ function replacementValue(original, escaped, regex, options) { if (!options.convertToRegex) { return original; } if (options.replaceWildcards) { return regex; } return escaped; } /** * Split an ARN into its parts * * @param arn the arn to split * @returns the parts of the ARN */ function splitArnParts(arn) { const parts = arn.split(':'); const partition = parts.at(1); const service = parts.at(2); const region = parts.at(3); const accountId = parts.at(4); const resource = parts.slice(5).join(':'); return { partition, service, region, accountId, resource }; } /** * Get the product/id segments of the resource portion of an ARN. * The first segment is the product segment and the second segment is the resource id segment. * This could be split by a colon or a slash, so it checks for both. It also checks for S3 buckets/objects. * * @param resource The resource to get the resource segments. Must be an ARN resource. * @returns a tuple with the first segment being the product segment (including the separator) and the second segment being the resource id. */ function getResourceSegments(resource) { if (!resource.isArnResource()) { throw new Error(`Resource ${resource.value()} is not an ARN resource`); } const resourceString = resource.resource(); // This is terrible, and I hate it if (resource.service() === 's3' && resource.account() === '' && resource.region() === '') { return ['', resourceString]; } const slashIndex = resourceString.indexOf('/'); const colonIndex = resourceString.indexOf(':'); let splitIndex = slashIndex; if (slashIndex != -1 && colonIndex != -1) { splitIndex = Math.min(slashIndex, colonIndex) + 1; } else if (colonIndex == -1) { splitIndex = slashIndex + 1; } else if (slashIndex == -1) { splitIndex = colonIndex + 1; } else { throw new Error(`Unable to split resource ${resource}`); } return [resourceString.slice(0, splitIndex), resourceString.slice(splitIndex)]; } /** * Checks if a value is defined and not null and narrows the type to the defined type * * @param value the value to check if it is defined * @returns if the value is defined and not null */ function isDefined(value) { return value !== undefined && value !== null; } /** * Checks if a value is not defined or null * * @param value the value to check if it is not defined * @returns if the value is not defined or null */ function isNotDefined(value) { return !isDefined(value); } /** * Checks if an action is a wildcard only action * * @param service the service the action belongs to * @param action the action to check if it is a wildcard only action * @returns if the action is a wildcard only action * @throws an error if the service or action does not exist */ async function isWildcardOnlyAction(service, action) { const actionDetails = await (0, iam_data_1.iamActionDetails)(service, action); return actionDetails.resourceTypes.length === 0; } /** * Get the the possible resource types for an action and resource * * @param service the service the action belongs to * @param action the action to get the resource type for * @param resource the resource type matching the action, if any * @throws an error if the service or action does not exist, or if the action is a wildcard only action */ async function getResourceTypesForAction(service, action, resource) { const actionDetails = await (0, iam_data_1.iamActionDetails)(service, action); if (actionDetails.resourceTypes.length === 0) { throw new Error(`${service}:${action} does not have any resource types`); } const matchingResourceTypes = []; for (const rt of actionDetails.resourceTypes) { const resourceType = await (0, iam_data_1.iamResourceTypeDetails)(service, rt.name); const pattern = convertResourcePatternToRegex(resourceType.arn); const match = resource.match(new RegExp(pattern)); if (match) { matchingResourceTypes.push(resourceType); } } return matchingResourceTypes; } /** * Convert a resource pattern from iam-data to a regex pattern * * @param pattern the pattern to convert to a regex * @returns the regex pattern */ function convertResourcePatternToRegex(pattern) { const regex = pattern.replace(/\$\{.*?\}/g, (match) => { const name = match.substring(2, match.length - 1); const camelName = name.at(0)?.toLowerCase() + name.substring(1); return `(?<${camelName}>(.+?))`; }); return `^${regex}$`; } /** * Lowercase all strings in an array * * @param strings the strings to lowercase * @returns the lowercased strings */ function lowerCaseAll(strings) { return strings.map((s) => s.toLowerCase()); } /** * Gets the IAM variables from a string * * @param value the string to get the variables from * @returns the variables in the string, if any */ function getVariablesFromString(value) { const matches = value.match(/\$\{.*?\}/g); if (matches) { return matches.map((m) => { const inBrackets = m.slice(2, -1); if (inBrackets.includes(',')) { return inBrackets.split(',')[0].trim(); } return inBrackets; }); } return []; } const assumedRoleArnRegex = /^arn:aws:sts::\d{12}:assumed-role\/.*$/; /** * Tests if a principal string is an assumed role ARN * * @param principal the principal string to test * @returns true if the principal is an assumed role ARN, false otherwise */ function isAssumedRoleArn(principal) { return assumedRoleArnRegex.test(principal); } const userArnRegex = /^arn:aws:iam::\d{12}:user\/.*$/; /** * Test if a principal string is an IAM user ARN * * @param principal the principal string to test * @returns true if the principal is an IAM user ARN, false otherwise */ function isIamUserArn(principal) { return userArnRegex.test(principal); } const federatedUserArnRegex = /^arn:aws:sts::\d{12}:federated-user\/.*$/; /** * Test if a principal string is a federated user ARN * * @param principal the principal string to test * @returns true if the principal is a federated user ARN, false otherwise */ function isFederatedUserArn(principal) { return federatedUserArnRegex.test(principal); } //# sourceMappingURL=util.js.map