@cloud-copilot/iam-simulate
Version:
Simulate evaluation of AWS IAM policies
320 lines • 11.8 kB
JavaScript
;
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