@cloud-copilot/iam-lens
Version:
Visibility in IAM in and across AWS accounts
279 lines • 11.8 kB
JavaScript
import { actionSupportsAwsResourceInfoContextKeys, bucketArn, convertAssumedRoleArnToRoleArn, isS3BucketOrObjectArn, splitArnParts } from '@cloud-copilot/iam-utils';
import { IamCollectClient } from '../collect/client.js';
import { isArnPrincipal, isServicePrincipal } from '../principals.js';
import {} from './simulate.js';
export const knownContextKeys = [
'aws:SecureTransport',
'aws:CurrentTime',
'aws:EpochTime',
'aws:PrincipalArn',
'aws:PrincipalAccount',
'aws:PrincipalOrgId',
'aws:PrincipalOrgPaths',
'aws:PrincipalType',
'aws:username',
'aws:ResourceAccount',
'aws:ResourceOrgID',
'aws:ResourceOrgPaths',
'aws:PrincipalIsAWSService',
'aws:PrincipalServiceName',
'aws:SourceAccount',
'aws:SourceOrgID',
'aws:SourceOrgPaths',
'aws:SourceOwner',
'kms:CallerAccount'
];
export const CONTEXT_KEYS = {
assumedRoot: 'aws:AssumedRoot',
userId: 'aws:userid',
vpc: 'aws:SourceVpc',
vpcEndpointId: 'aws:SourceVpce',
vpcEndpointAccount: 'aws:VpceAccount',
vpcEndpointOrgId: 'aws:VpceOrgID',
vpcEndpointOrgPaths: 'aws:VpceOrgPaths',
vpcArn: 'aws:SourceVpcArn'
};
/**
* Checks if a context has a specific key (case-insensitive).
*
* @param context - The context to check.
* @param key - The key to check for.
* @returns True if the context has the key, false otherwise.
*/
export function contextHasKey(context, key) {
return !!contextValue(context, key);
}
/**
* Get the value of a context key (case-insensitive).
*
* @param context - The context to check.
* @param key - The key to get the value for.
* @returns The value of the context key, or undefined if it doesn't exist.
*/
export function contextValue(context, key) {
const matchingKey = Object.keys(context).find((contextKey) => contextKey.toLowerCase() === key.toLowerCase());
return matchingKey ? context[matchingKey] : undefined;
}
/**
* Get the context keys for a simulation request.
*
* @param collectClient the collect client to use for fetching data
* @param simulationRequest the simulation request to create context keys for
* @param service the service the request is for
* @param contextKeyOverrides the context key overrides to apply
* @returns a promise that resolves to the context keys for the simulation request
*/
export async function createContextKeys(collectClient, simulationRequest, service, contextKeyOverrides) {
const contextKeys = {
'aws:SecureTransport': 'true',
'aws:CurrentTime': new Date().toISOString(),
'aws:EpochTime': Math.floor(Date.now() / 1000).toString()
};
if (isArnPrincipal(simulationRequest.principal)) {
const arnParts = splitArnParts(simulationRequest.principal);
const principalArnForContext = arnParts.resourceType === 'assumed-role'
? convertAssumedRoleArnToRoleArn(simulationRequest.principal)
: simulationRequest.principal;
contextKeys['aws:PrincipalArn'] = principalArnForContext;
const principalAccountId = arnParts.accountId;
contextKeys['aws:PrincipalAccount'] = arnParts.accountId || '';
const orgId = await collectClient.getOrgIdForAccount(principalAccountId);
if (orgId) {
contextKeys['aws:PrincipalOrgId'] = orgId;
const orgStructure = await collectClient.getOrgUnitHierarchyForAccount(principalAccountId);
contextKeys['aws:PrincipalOrgPaths'] = makeOrgPaths(orgId, orgStructure);
}
const { tags } = await collectClient.getTagsForResource(simulationRequest.principal, principalAccountId);
for (const [key, value] of Object.entries(tags)) {
contextKeys[`aws:PrincipalTag/${key}`] = value;
}
contextKeys['aws:PrincipalIsAWSService'] = 'false';
if (service.toLowerCase() === 'kms') {
contextKeys['kms:CallerAccount'] = principalAccountId;
}
if (simulationRequest.principal.endsWith(':root')) {
contextKeys['aws:PrincipalType'] = 'Account';
contextKeys['aws:userid'] = principalAccountId;
}
else if (arnParts.resourceType === 'user') {
contextKeys['aws:PrincipalType'] = 'User';
const userUniqueId = await collectClient.getUniqueIdForIamResource(simulationRequest.principal);
contextKeys['aws:userid'] = userUniqueId || 'UNKNOWN';
const userName = arnParts.resourcePath?.split('/').at(-1);
contextKeys['aws:username'] = userName;
}
else if (arnParts.resourceType === 'federated-user') {
contextKeys['aws:PrincipalType'] = 'FederatedUser';
contextKeys['aws:userid'] = `${arnParts.accountId}:${arnParts.resourcePath}`;
}
else if (arnParts.resourceType === 'assumed-role' || arnParts.resourceType === 'role') {
contextKeys['aws:PrincipalType'] = 'AssumedRole';
//TODO: Set aws:userId for role principals
if (arnParts.resourceType === 'assumed-role') {
const sessionName = arnParts.resourcePath?.split('/').at(-1);
const roleArn = convertAssumedRoleArnToRoleArn(simulationRequest.principal);
const roleUniqueId = await collectClient.getUniqueIdForIamResource(roleArn);
contextKeys['aws:userid'] = `${roleUniqueId || 'UNKNOWN'}:${sessionName}`;
}
}
}
//Resource context keys
if (actionSupportsAwsResourceInfoContextKeys(simulationRequest.action)) {
contextKeys['aws:ResourceAccount'] = simulationRequest.resourceAccount;
const resourceOrgId = await collectClient.getOrgIdForAccount(simulationRequest.resourceAccount);
if (resourceOrgId) {
contextKeys['aws:ResourceOrgID'] = resourceOrgId;
const orgStructure = await collectClient.getOrgUnitHierarchyForAccount(simulationRequest.resourceAccount);
contextKeys['aws:ResourceOrgPaths'] = makeOrgPaths(resourceOrgId, orgStructure);
}
}
let resourceTagsAreKnown = false;
if (simulationRequest.resourceArn) {
const isBucket = isS3BucketOrObjectArn(simulationRequest.resourceArn);
const { tags: resourceTags, present: resourceTagsPreset } = await collectClient.getTagsForResource(isBucket ? bucketArn(simulationRequest.resourceArn) : simulationRequest.resourceArn, simulationRequest.resourceAccount);
resourceTagsAreKnown = resourceTagsPreset;
for (const [key, value] of Object.entries(resourceTags)) {
contextKeys[`aws:ResourceTag/${key}`] = value;
if (isBucket) {
contextKeys[`s3:BucketTag/${key}`] = value;
}
}
}
//Service Principal context keys
if (isServicePrincipal(simulationRequest.principal)) {
contextKeys['aws:PrincipalIsAWSService'] = 'true';
contextKeys['aws:PrincipalServiceName'] = simulationRequest.principal;
contextKeys['aws:SourceAccount'] = simulationRequest.resourceAccount;
contextKeys['aws:SourceOwner'] = simulationRequest.resourceAccount;
contextKeys['aws:SourceOrgID'] = contextKeys['aws:ResourceOrgID'];
contextKeys['aws:SourceOrgPaths'] = contextKeys['aws:ResourceOrgPaths'];
}
//Apply any custom context key overrides
for (const [key, value] of Object.entries(contextKeyOverrides)) {
contextKeys[key] = value;
}
//Add VPC context keys
const vpcKeys = await getVpcKeys(contextKeys, service, collectClient);
for (const [key, value] of Object.entries(vpcKeys)) {
contextKeys[key] = value;
}
return {
resourceTagsAreKnown,
contextKeys
};
}
/**
* Get the VPC keys that should be added to the context for a simulation.
*
* @param context the existing context
* @param service the service the request is for
* @param collectClient the IAM collect client
* @returns a record of VPC context keys
*/
export async function getVpcKeys(context, service, collectClient) {
const vpcKeys = {};
let vpcEndpointId = contextValue(context, CONTEXT_KEYS.vpcEndpointId);
const hasVpcEndpoint = !!vpcEndpointId;
let vpcId = contextValue(context, CONTEXT_KEYS.vpc);
let vpcArn = contextValue(context, CONTEXT_KEYS.vpcArn);
//If we know the VPC ID but not the endpoint, lookup the endpoint
if (!vpcEndpointId && vpcId && typeof vpcId === 'string') {
vpcEndpointId = await collectClient.getVpcEndpointIdForVpcService(vpcId, service);
}
//If we know the VPC ARN but not the endpoint, lookup the endpoint
if (!vpcEndpointId && vpcArn && typeof vpcArn === 'string') {
vpcEndpointId = await collectClient.getVpcEndpointIdForVpcService(vpcArn, service);
}
if (vpcEndpointId && !vpcId) {
if (typeof vpcEndpointId == 'string') {
const vpcId = await collectClient.getVpcIdForVpcEndpointId(vpcEndpointId);
if (vpcId) {
vpcKeys[CONTEXT_KEYS.vpc] = vpcId;
}
}
}
if (vpcEndpointId && !hasVpcEndpoint) {
vpcKeys[CONTEXT_KEYS.vpcEndpointId] = vpcEndpointId;
}
if (vpcEndpointId &&
typeof vpcEndpointId === 'string' &&
serviceSupportsExtraVpcEndpointData(service)) {
const vpcEndpointAccount = await collectClient.getAccountIdForVpcEndpointId(vpcEndpointId);
const vpcEndpointOrgId = await collectClient.getOrgIdForVpcEndpointId(vpcEndpointId);
const vpcEndpointOrgStructure = await collectClient.getOrgUnitHierarchyForVpcEndpointId(vpcEndpointId);
const vpcArn = await collectClient.getVpcArnForVpcEndpointId(vpcEndpointId);
if (vpcEndpointAccount && !contextHasKey(context, CONTEXT_KEYS.vpcEndpointAccount)) {
vpcKeys[CONTEXT_KEYS.vpcEndpointAccount] = vpcEndpointAccount;
}
if (vpcEndpointOrgId && !contextHasKey(context, CONTEXT_KEYS.vpcEndpointOrgId)) {
vpcKeys[CONTEXT_KEYS.vpcEndpointOrgId] = vpcEndpointOrgId;
}
if (vpcArn && !contextHasKey(context, CONTEXT_KEYS.vpcArn)) {
vpcKeys[CONTEXT_KEYS.vpcArn] = vpcArn;
}
if (vpcEndpointOrgId &&
vpcEndpointOrgStructure &&
!contextHasKey(context, CONTEXT_KEYS.vpcEndpointOrgPaths)) {
vpcKeys[CONTEXT_KEYS.vpcEndpointOrgPaths] = makeOrgPaths(vpcEndpointOrgId, vpcEndpointOrgStructure);
}
}
return vpcKeys;
}
const servicesThatSupportExtraVpcEndpointData = new Set([
'apprunner',
'discovery',
'athena',
'servicediscovery',
'applicationinsights',
'cloudformation',
'comprehendmedical',
'compute-optimizer',
'ecr',
'ecs',
'kinesisanalytics',
'route53',
'datasync',
'ebs',
'scheduler',
'firehose',
'medical-imaging',
'healthlake',
'omics',
'iam',
'iotfleetwise',
'iotwireless',
'kms',
'lambda',
'payment-cryptography',
'polly',
'acm-pca',
'rbin',
'rekognition',
'servicequotas',
's3',
'storagegateway',
'ssm-contacts',
'textract',
'transcribe',
'transfer'
]);
/**
* Check if a service supports extra VPC endpoint data.
*
* @param service the service to check
* @returns true if the service supports extra VPC endpoint data, false otherwise
*/
export function serviceSupportsExtraVpcEndpointData(service) {
return servicesThatSupportExtraVpcEndpointData.has(service.toLowerCase());
}
/**
* Create a string array for an aws:xxOrgPaths context key value.
*
* @param orgId the organization ID
* @param hierarchy the organizational hierarchy
* @returns a string array representing the organizational paths
*/
function makeOrgPaths(orgId, hierarchy) {
return [`${orgId}/${hierarchy.join('/')}/`];
}
//# sourceMappingURL=contextKeys.js.map