UNPKG

@cloud-copilot/iam-lens

Version:

Visibility in IAM in and across AWS accounts

626 lines 25.3 kB
import { splitArnParts } from '@cloud-copilot/iam-utils'; export class IamCollectClient { constructor(storageClient, clientOptions) { this.storageClient = storageClient; this._cache = {}; this._enableCaching = clientOptions?.enableCaching !== false; } // Generic cache helper async withCache(cacheKey, fetcher) { if (this._enableCaching && cacheKey in this._cache) { return this._cache[cacheKey]; } const value = await fetcher(); if (this._enableCaching) { this._cache[cacheKey] = value; } return value; } /** * Checks if an account exists in the store. * @param accountId The ID of the account to check. * @returns True if the account exists, false otherwise. */ async accountExists(accountId) { const accounts = await this.storageClient.listAccountIds(); return accounts.includes(accountId); } /** * Get all account IDs in the store. * * @returns all account IDs in the store */ async allAccounts() { return this.storageClient.listAccountIds(); } /** * Checks if a principal exists in the store. * @param principalArn The ARN of the principal to check. * @returns True if the principal exists, false otherwise. */ async principalExists(principalArn) { const accountId = splitArnParts(principalArn).accountId; const principalData = await this.storageClient.getResourceMetadata(accountId, principalArn, 'metadata'); return !!principalData; } /** * Gets the SCP Hierarchy for an account. The first element is the root, the last element is the account itself. * @param accountId The ID of the account to get the SCP Hierarchy for. * @returns The SCP Hierarchy for the account. */ async getScpHierarchyForAccount(accountId) { return this.getOrgPolicyHierarchyForAccount(accountId, 'scps'); } /** * Gets the policy hierarchy for an account for a given policy type. * @param accountId The ID of the account. * @param policyType The type of policy ('scps' or 'rcps'). * @returns The policy hierarchy for the account. */ async getOrgPolicyHierarchyForAccount(accountId, policyType) { const cacheKey = `orgPolicyHierarchy:${accountId}:${policyType}`; return this.withCache(cacheKey, async () => { const orgId = await this.getOrgIdForAccount(accountId); if (!orgId) { return []; } // SCPs and RCPs do not apply to the root account const orgMetadata = await this.getOrganizationMetadata(orgId); if (orgMetadata.rootAccountId === accountId) { return []; } const policyHierarchy = []; const orgHierarchy = await this.getOrgUnitHierarchyForAccount(accountId); for (const ouId of orgHierarchy) { const policies = await this.getOrgPoliciesForOrgUnit(orgId, ouId, policyType); policyHierarchy.push({ orgIdentifier: ouId, policies: policies.map((p) => ({ name: p.arn, policy: p.policy })) }); } const accountPolicies = await this.getOrgPoliciesForAccount(accountId, policyType); policyHierarchy.push({ orgIdentifier: accountId, policies: accountPolicies.map((p) => ({ name: p.arn, policy: p.policy })) }); return policyHierarchy; }); } /** * Gets the OUs for an account. The first element is the root, * the last element is the parent OU of the account. * @param accountId The ID of the account to get the OUs for. * @returns The OUs for the account. */ async getOrgUnitHierarchyForAccount(accountId) { const orgId = await this.getOrgIdForAccount(accountId); if (!orgId) { return []; } const ouIds = []; let ouId = await this.getOrgUnitIdForAccount(accountId); ouIds.push(ouId); while (ouId) { const parentOuId = await this.getParentOrgUnitIdForOrgUnit(orgId, ouId); if (parentOuId) { ouIds.unshift(parentOuId); } ouId = parentOuId; } return ouIds; } /** * Gets the org unit ID for an account. * @param accountId The ID of the account. * @returns The org unit ID for the account, or undefined if not found. */ async getOrgUnitIdForAccount(accountId) { const orgId = await this.getOrgIdForAccount(accountId); if (!orgId) { return undefined; } const accounts = (await this.getAccountDataForOrg(orgId)); return accounts[accountId].ou; } /** * Gets the parent org unit ID for a given org unit. * @param orgId The ID of the organization. * @param ouId The ID of the org unit. * @returns The parent org unit ID, or undefined if not found. */ async getParentOrgUnitIdForOrgUnit(orgId, ouId) { const ouData = await this.getOrgUnitsDataForOrg(orgId); const ou = ouData[ouId]; return ou.parent; } /** * Gets the SCPs for an account. * @param accountId The ID of the account. * @returns The SCPs for the account. */ async getScpsForAccount(accountId) { return this.getOrgPoliciesForAccount(accountId, 'scps'); } /** * Gets the org policies for an account for a given policy type. * @param accountId The ID of the account. * @param policyType The type of policy ('scps' or 'rcps'). * @returns The org policies for the account. */ async getOrgPoliciesForAccount(accountId, policyType) { const orgId = await this.getOrgIdForAccount(accountId); if (!orgId) { return []; } const accounts = (await this.getAccountDataForOrg(orgId)); const orgInformation = accounts[accountId]; const policyArns = orgInformation[policyType]; const policies = []; for (const policyArn of policyArns) { const policyInfo = await this.getOrgPolicy(orgId, policyType, policyArn); policies.push(policyInfo); } return policies; } /** * Gets the account data for an organization. * @param orgId The ID of the organization. * @returns The account data for the organization. */ async getAccountDataForOrg(orgId) { return this.storageClient.getOrganizationMetadata(orgId, 'accounts'); } /** * Gets the org units data for an organization. * @param orgId The ID of the organization. * @returns The org units data for the organization. */ async getOrgUnitsDataForOrg(orgId) { return this.storageClient.getOrganizationMetadata(orgId, 'ous'); } /** * Gets a specific org policy. * @param orgId The ID of the organization. * @param policyType The type of policy ('scps' or 'rcps'). * @param policyArn The ARN of the policy. * @returns The org policy. */ async getOrgPolicy(orgId, policyType, policyArn) { const policyId = policyArn.split('/').at(-1); const policyData = await this.storageClient.getOrganizationPolicyMetadata(orgId, policyType, policyId, 'metadata'); const policyDocument = await this.storageClient.getOrganizationPolicyMetadata(orgId, policyType, policyId, 'policy'); if (!policyDocument) { console.error(`Policy document not found for ${policyArn} in org ${orgId}`); } return { arn: policyData.arn, name: policyData.name, policy: policyDocument }; } /** * Gets the RCPs for an account. * @param accountId The ID of the account. * @returns The RCPs for the account. */ async getRcpsForAccount(accountId) { return this.getOrgPoliciesForAccount(accountId, 'rcps'); } /** * Gets the RCP hierarchy for an account. * @param accountId The ID of the account. * @returns The RCP hierarchy for the account. */ async getRcpHierarchyForAccount(accountId) { return this.getOrgPolicyHierarchyForAccount(accountId, 'rcps'); } /** * Gets the SCPs for an org unit. * @param orgId The ID of the organization. * @param orgUnitId The ID of the org unit. * @returns The SCPs for the org unit. */ async getScpsForOrgUnit(orgId, orgUnitId) { return this.getOrgPoliciesForOrgUnit(orgId, orgUnitId, 'scps'); } /** * Gets the org policies for an org unit for a given policy type. * @param orgId The ID of the organization. * @param orgUnitId The ID of the org unit. * @param policyType The type of policy ('scps' or 'rcps'). * @returns The org policies for the org unit. */ async getOrgPoliciesForOrgUnit(orgId, orgUnitId, policyType) { const orgUnitInformation = await this.getOrgUnitsDataForOrg(orgId); const orgUnit = orgUnitInformation[orgUnitId]; const orgPolicies = orgUnit[policyType]; const policies = []; for (const policyArn of orgPolicies) { const policyInfo = await this.getOrgPolicy(orgId, policyType, policyArn); policies.push(policyInfo); } return policies; } /** * Gets the RCPs for an org unit. * @param orgId The ID of the organization. * @param orgUnitId The ID of the org unit. * @returns The RCPs for the org unit. */ async getRcpsForOrgUnit(orgId, orgUnitId) { return this.getOrgPoliciesForOrgUnit(orgId, orgUnitId, 'rcps'); } /** * Gets the org ID for an account. * @param accountId The ID of the account. * @returns The org ID for the account, or undefined if not found. */ async getOrgIdForAccount(accountId) { const index = await this.storageClient.getIndex('accounts-to-orgs', {}); const accountToOrgMap = index.data; return accountToOrgMap[accountId]; } /** * Gets the account ID for a given S3 bucket name. * @param bucketName The name of the bucket. * @returns The account ID for the bucket, or undefined if not found. */ async getAccountIdForBucket(bucketName) { const index = await this.storageClient.getIndex('buckets-to-accounts', {}); const bucketToAccountMap = index.data; return bucketToAccountMap[bucketName]?.accountId; } /** * Gets the account ID for a given API Gateway ARN. * @param apiArn The ARN of the API Gateway. * @returns The account ID for the API Gateway, or undefined if not found. */ async getAccountIdForRestApi(apiArn) { const index = await this.storageClient.getIndex('apigateways-to-accounts', {}); const bucketToAccountMap = index.data; return bucketToAccountMap[apiArn]; } /** * Gets the managed policies attached to a user. * @param userArn The ARN of the user. * @returns The managed policies for the user. */ async getManagedPoliciesForUser(userArn) { const cacheKey = `userManagedPolicies:${userArn}`; return this.withCache(cacheKey, async () => { const accountId = splitArnParts(userArn).accountId; const managedPolicies = await this.storageClient.getResourceMetadata(accountId, userArn, 'managed-policies', []); const results = []; for (const policyArn of managedPolicies) { results.push(await this.getManagedPolicy(accountId, policyArn)); } return results; }); } async getManagedPolicy(accountId, policyArn) { const policyMetadata = await this.storageClient.getResourceMetadata(accountId, policyArn, 'metadata'); const policyDocument = await this.storageClient.getResourceMetadata(accountId, policyArn, 'current-policy'); if (!policyDocument) { console.error(`Policy document not found for ${policyArn} in account ${accountId}`); } return { arn: policyMetadata.arn, name: policyMetadata.name, policy: policyDocument }; } /** * Gets the inline policies attached to a user. * @param userArn The ARN of the user. * @returns The inline policies for the user. */ async getInlinePoliciesForUser(userArn) { const cacheKey = `userInlinePolicies:${userArn}`; return this.withCache(cacheKey, async () => { const accountId = splitArnParts(userArn).accountId; const inlinePolicies = await this.storageClient.getResourceMetadata(accountId, userArn, 'inline-policies', []); return inlinePolicies.map((p) => ({ name: p.PolicyName, policy: p.PolicyDocument })); }); } async getIamUserMetadata(userArn) { const accountId = splitArnParts(userArn).accountId; // The permissions boundary is stored as a policy ARN on the user resource metadata return this.storageClient.getResourceMetadata(accountId, userArn, 'metadata'); } /** * Gets the permissions boundary policy attached to a user, if any. * * @param userArn The ARN of the user. * @returns The permissions boundary policy as an OrgPolicy, or undefined if none is set. */ async getPermissionsBoundaryForUser(userArn) { const cacheKey = `userPermissionBoundary:${userArn}`; return this.withCache(cacheKey, async () => { const accountId = splitArnParts(userArn).accountId; // The permissions boundary is stored as a policy ARN on the user resource metadata const userMetadata = await this.getIamUserMetadata(userArn); if (!userMetadata) { return undefined; } const permissionsBoundaryArn = userMetadata.permissionBoundary; if (!permissionsBoundaryArn) { return undefined; } return this.getManagedPolicy(accountId, permissionsBoundaryArn); }); } /** * Gets the group ARNs that the user is a member of. * @param userArn The ARN of the user. * @returns An array of group ARNs the user belongs to. */ async getGroupsForUser(userArn) { const cacheKey = `groupsForUser:${userArn}`; return this.withCache(cacheKey, async () => { const accountId = splitArnParts(userArn).accountId; const groups = await this.storageClient.getResourceMetadata(accountId, userArn, 'groups', []); return groups; }); } /** * Gets the managed policies attached to a group. * * @param groupArn The ARN of the group. * @returns The managed policies for the group. */ async getManagedPoliciesForGroup(groupArn) { const cacheKey = `groupManagedPolicies:${groupArn}`; return this.withCache(cacheKey, async () => { const accountId = splitArnParts(groupArn).accountId; const managedPolicies = await this.storageClient.getResourceMetadata(accountId, groupArn, 'managed-policies', []); const results = []; for (const policyArn of managedPolicies) { results.push(await this.getManagedPolicy(accountId, policyArn)); } return results; }); } async getInlinePoliciesForGroup(groupArn) { const cacheKey = `groupInlinePolicies:${groupArn}`; return this.withCache(cacheKey, async () => { const accountId = splitArnParts(groupArn).accountId; const inlinePolicies = await this.storageClient.getResourceMetadata(accountId, groupArn, 'inline-policies', []); return inlinePolicies.map((p) => ({ name: p.PolicyName, policy: p.PolicyDocument })); }); } async getManagedPoliciesForRole(roleArn) { const cacheKey = `managedPoliciesForRole:${roleArn}`; return this.withCache(cacheKey, async () => { const accountId = splitArnParts(roleArn).accountId; const managedPolicies = await this.storageClient.getResourceMetadata(accountId, roleArn, 'managed-policies', []); const results = []; for (const policyArn of managedPolicies) { results.push(await this.getManagedPolicy(accountId, policyArn)); } return results; }); } async getInlinePoliciesForRole(roleArn) { const cacheKey = `inlinePoliciesForRole:${roleArn}`; return this.withCache(cacheKey, async () => { const accountId = splitArnParts(roleArn).accountId; const inlinePolicies = await this.storageClient.getResourceMetadata(accountId, roleArn, 'inline-policies', []); return inlinePolicies.map((p) => ({ name: p.PolicyName, policy: p.PolicyDocument })); }); } async getPermissionsBoundaryForRole(roleArn) { const cacheKey = `permissionBoundaryForRole:${roleArn}`; return this.withCache(cacheKey, async () => { const accountId = splitArnParts(roleArn).accountId; // The permissions boundary is stored as a policy ARN on the user resource metadata const roleMetadata = await this.getIamUserMetadata(roleArn); if (!roleMetadata) { return undefined; } const permissionsBoundaryArn = roleMetadata.permissionBoundary; if (!permissionsBoundaryArn) { return undefined; } return this.getManagedPolicy(accountId, permissionsBoundaryArn); }); } /** * Get the metadata for an organization. * * @param organizationId the id of the organization * @returns the metadata for the organization */ async getOrganizationMetadata(organizationId) { return this.storageClient.getOrganizationMetadata(organizationId, 'metadata'); } /** * Gets the resource policy for a given resource ARN and account. * * @param resourceArn The ARN of the resource. * @param accountId The ID of the account. * @returns The resource policy, or undefined if not found. */ async getResourcePolicyForArn(resourceArn, accountId) { const arnParts = splitArnParts(resourceArn); let metadataKey = 'policy'; if (arnParts.service === 'iam' && arnParts.resourceType === 'role') { metadataKey = 'trust-policy'; } const resourcePolicy = await this.storageClient.getResourceMetadata(accountId, resourceArn, metadataKey); return resourcePolicy; } /** * Gets the RAM share policy for a given resource ARN and account. * * @param resourceArn The ARN of the resource. * @param accountId The ID of the account. * @returns The RAM share policy, or undefined if not found. */ async getRamSharePolicyForArn(resourceArn, accountId) { const armSharePolicy = await this.storageClient.getRamResource(accountId, resourceArn); return armSharePolicy?.policy; } /** * Gets the tags for a given resource ARN and account. * * @param resourceArn The ARN of the resource. * @param accountId The ID of the account. * @returns The tags as a record, or undefined if not found. */ async getTagsForResource(resourceArn, accountId) { const tags = await this.storageClient.getResourceMetadata(accountId, resourceArn, 'tags'); return tags || {}; } /** * Gets a unique ID for an IAM resource based on its ARN and account ID. * Used specifically for IAM Users and Roles * * @param resourceArn the ARN of the IAM resource * @param accountId the ID of the account the resource belongs to * @returns a unique ID for the resource, or undefined if not found */ async getUniqueIdForIamResource(resourceArn) { const accountId = splitArnParts(resourceArn).accountId; const resourceMetadata = await this.storageClient.getResourceMetadata(accountId, resourceArn, 'metadata'); return resourceMetadata?.id; } /** * Get the account IDs for an organization. * * @param organizationId the ID of the organization * @returns a tuple containing a boolean indicating success and an array of account IDs */ async getAccountsForOrganization(organizationId) { const organizationAccounts = await this.getAccountDataForOrg(organizationId); if (!organizationAccounts) { return [false, []]; } const accountIds = Object.keys(organizationAccounts); return [true, accountIds]; } /** * Get the organization structure or an organization. * * @param orgId the ID of the organization * @returns returns the organization structure or undefined if not found */ async getOrganizationStructure(orgId) { return this.storageClient.getOrganizationMetadata(orgId, 'structure'); } async getAccountsForOrgPath(orgId, ouIds) { const orgUnits = await this.getOrganizationStructure(orgId); if (!orgUnits || ouIds.length === 0) { return [false, []]; } const rootOu = orgUnits[ouIds[0]]; // Now look through the structure to find the OU let currentStructure = rootOu; for (const ou of ouIds.slice(1)) { currentStructure = currentStructure.children?.[ou]; if (!currentStructure) { return [false, []]; // OU not found in the structure } } const getAccountId = (a) => a.split('/').at(-1); const accounts = []; if (currentStructure.accounts) { accounts.push(...currentStructure.accounts?.map(getAccountId)); } const children = Object.values(currentStructure.children || {}); // Traverse the children to collect all accounts while (children.length > 0) { const child = children.shift(); if (child?.accounts) { accounts.push(...child.accounts.map(getAccountId)); } if (child?.children) { children.push(...Object.values(child.children)); } } return [true, accounts]; } async getAllPrincipalsInAccount(accountId) { const iamUsers = await this.storageClient.findResourceMetadata(accountId, { service: 'iam', resourceType: 'user', account: accountId }); const iamRoles = await this.storageClient.findResourceMetadata(accountId, { service: 'iam', resourceType: 'role', account: accountId }); return [...iamUsers.map((user) => user.arn), ...iamRoles.map((role) => role.arn)]; } /** * Get the VPC endpoint policy for a given VPC endpoint ARN. * * @param vpcEndpointArn the ARN of the VPC endpoint * @returns the VPC endpoint policy, or undefined if not found */ async getVpcEndpointPolicyForArn(vpcEndpointArn) { const accountId = splitArnParts(vpcEndpointArn).accountId; const vpcEndpointPolicy = await this.storageClient.getResourceMetadata(accountId, vpcEndpointArn, 'endpoint-policy'); return vpcEndpointPolicy; } /** * Get the ARN of a VPC endpoint given its ID. * @param vpcEndpointId the ID of the VPC endpoint * @returns the ARN of the VPC endpoint, or undefined if not found */ async getVpcEndpointArnForVpcEndpointId(vpcEndpointId) { const index = await this.storageClient.getIndex('vpcs', { endpoints: {}, vpcs: {} }); return index.data.endpoints[vpcEndpointId]?.arn; } /** * Gets the VPC endpoint ID for a given VPC ID and service name. * * @param vpcId the ID of the VPC * @param service the service name of the VPC endpoint (e.g., s3, ec2, etc.) * @returns the VPC endpoint ID, or undefined if not found */ async getVpcEndpointIdForVpcService(vpcId, service) { const index = await this.storageClient.getIndex('vpcs', { endpoints: {}, vpcs: {} }); const vpc = index.data.vpcs[vpcId]; if (!vpc) { return undefined; } const endpoint = vpc.endpoints.find((ep) => ep.service === service); return endpoint?.id; } /** * Lookup the VPC ID for a given VPC endpoint ID. * * @param vpcEndpointId the ID of the VPC endpoint * @returns the VPC ID, or undefined if not found */ async getVpcIdForVpcEndpointId(vpcEndpointId) { const index = await this.storageClient.getIndex('vpcs', { endpoints: {}, vpcs: {} }); return index.data.endpoints[vpcEndpointId]?.vpc; } } //# sourceMappingURL=client.js.map