UNPKG

@cloud-copilot/iam-collect

Version:

Collect IAM information from AWS Accounts

361 lines 16.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OrganizationSync = void 0; exports.getOrganizationDetails = getOrganizationDetails; exports.getOrganizationRoot = getOrganizationRoot; exports.getTagsForOu = getTagsForOu; exports.getTagsForAccount = getTagsForAccount; exports.getChildOrgUnits = getChildOrgUnits; exports.getAccountsForParent = getAccountsForParent; const client_organizations_1 = require("@aws-sdk/client-organizations"); const ClientPool_js_1 = require("../../aws/ClientPool.js"); const client_tools_js_1 = require("../../utils/client-tools.js"); const tags_js_1 = require("../../utils/tags.js"); const typedSync_js_1 = require("../typedSync.js"); exports.OrganizationSync = { awsService: 'organizations', name: 'organization', global: true, execute: async function (accountId, region, credentials, storage, endpoint, syncOptions) { const organizationClient = ClientPool_js_1.AwsClientPool.defaultInstance.client(client_organizations_1.OrganizationsClient, credentials, region, endpoint); const organization = await getOrganizationDetails(organizationClient); if (!organization) { await saveOrgForAccount(storage, accountId, undefined); return; } const organizationId = organization.Id; const root = await getOrganizationRoot(organizationClient); if (!root) { await saveOrgForAccount(storage, accountId, undefined); return; } const features = root.PolicyTypes?.reduce((acc, type) => { acc[type.Type] = type.Status === client_organizations_1.PolicyTypeStatus.ENABLED; return acc; }, {}) || {}; const scpsEnabled = !!features[client_organizations_1.PolicyType.SERVICE_CONTROL_POLICY]; const rcpsEnabled = !!features[client_organizations_1.PolicyType.RESOURCE_CONTROL_POLICY]; const allAccounts = {}; const allOus = {}; const ouDetails = {}; allOus[root.Id] = { parent: undefined, scps: await getPoliciesForTarget(organizationClient, root.Id, client_organizations_1.PolicyType.SERVICE_CONTROL_POLICY, scpsEnabled), rcps: await getPoliciesForTarget(organizationClient, root.Id, client_organizations_1.PolicyType.RESOURCE_CONTROL_POLICY, rcpsEnabled) }; ouDetails[root.Id] = await getOuDetails(organizationClient, root); const structure = { [root.Id]: { children: {}, accounts: [] } }; // const children = await getChildOrgUnits(organizationClient, root.Id!) const parents = [structure]; let parent = parents.pop(); while (parent) { for (const key in parent) { // Get structure information const children = await getChildOrgUnits(organizationClient, key); for (const child of children) { ouDetails[child.Id] = await getOuDetails(organizationClient, child); const childId = child.Id; allOus[childId] = { parent: key, scps: await getPoliciesForTarget(organizationClient, root.Id, client_organizations_1.PolicyType.SERVICE_CONTROL_POLICY, scpsEnabled), rcps: await getPoliciesForTarget(organizationClient, root.Id, client_organizations_1.PolicyType.RESOURCE_CONTROL_POLICY, rcpsEnabled) }; parent[key].children ||= {}; parent[key].children[childId] = { children: undefined, accounts: undefined }; } const accounts = await getAccountsForParent(organizationClient, key); if (accounts.length > 0) { parent[key].accounts = []; } for (const account of accounts) { const accountTags = await getTagsForAccount(organizationClient, account.Id); allAccounts[account.Id] = { ou: key, scps: await getPoliciesForTarget(organizationClient, account.Id, client_organizations_1.PolicyType.SERVICE_CONTROL_POLICY, scpsEnabled), rcps: await getPoliciesForTarget(organizationClient, account.Id, client_organizations_1.PolicyType.RESOURCE_CONTROL_POLICY, rcpsEnabled), tags: accountTags }; parent[key].accounts.push(account.Arn); } // parent[key].accounts = accounts.map((a) => a.Arn!) if (parent[key].children) { parents.push(parent[key].children); } } parent = parents.pop(); } storage.saveOrganizationMetadata(organizationId, 'structure', structure); storage.saveOrganizationMetadata(organizationId, 'metadata', { id: organizationId, arn: organization.Arn, rootOu: root.Id, rootAccountArn: organization.MasterAccountArn, rootAccountId: organization.MasterAccountId, features }); storage.saveOrganizationMetadata(organizationId, 'accounts', allAccounts); storage.saveOrganizationMetadata(organizationId, 'ous', allOus); // Sync OUs const persistedOus = await storage.listOrganizationalUnits(organizationId); const newOus = new Set(Object.keys(ouDetails)); const deletedOus = persistedOus.filter((ou) => !newOus.has(ou)); for (const ouToDelete of deletedOus) { await storage.deleteOrganizationalUnit(organizationId, ouToDelete); } for (const ouId of Object.keys(ouDetails)) { const ou = ouDetails[ouId]; await storage.saveOrganizationalUnitMetadata(organizationId, ouId, 'metadata', ou.metadata); await storage.saveOrganizationalUnitMetadata(organizationId, ouId, 'tags', ou.tags); } // Sync policies await syncPolicies(organizationId, organizationClient, storage, client_organizations_1.PolicyType.SERVICE_CONTROL_POLICY, 'scps', scpsEnabled); await syncPolicies(organizationId, organizationClient, storage, client_organizations_1.PolicyType.RESOURCE_CONTROL_POLICY, 'rcps', rcpsEnabled); // Sync organization resource policy await syncOrganizationResourcePolicy(organizationClient, storage, organizationId); await saveOrgForAccount(storage, accountId, organizationId); } }; /** * Get the details of an organization or an account. * * @param client The OrganizationsClient to use * @returns the details of the organization the account belongs to or undefined if the account is not part of an organization or does not have permission. */ async function getOrganizationDetails(client) { const command = new client_organizations_1.DescribeOrganizationCommand(); try { const response = await (0, client_tools_js_1.runAndCatch404)(() => client.send(command)); if (!response) { return undefined; } return response.Organization; } catch (e) { if (e.name === 'AWSOrganizationsNotInUseException') { return undefined; } } return undefined; } /** * Get the root Organizational Unit for an organization * * @param client The OrganizationsClient to use * @returns the root Organizational Unit for the organization if it exists */ async function getOrganizationRoot(client) { return (0, client_tools_js_1.runAndCatchAccessDenied)(async () => { const roots = await (0, typedSync_js_1.paginateResource)(client, client_organizations_1.ListRootsCommand, 'Roots', { inputKey: 'NextToken', outputKey: 'NextToken' }, {}); return roots.at(0); }); } /** * Get the tags for an Organizational Unit * * @param client The OrganizationsClient to use * @param ouId The AWS id of the Organizational Unit to get the tags for * @returns The tags for the Organizational Unit */ async function getTagsForOu(client, ouId) { return getTags(client, ouId); } /** * Get the tags for an account * @param client The OrganizationsClient to use * @param accountId The AWS id of the account to get the tags for * @returns The tags for the account */ async function getTagsForAccount(client, accountId) { return getTags(client, accountId); } /** * Get the tags for a resource in AWS Organizations * * @param client The OrganizationsClient to use * @param resourceId The AWS id of the resource to get the tags for * @returns The tags for the resource */ async function getTags(client, resourceId) { const command = new client_organizations_1.ListTagsForResourceCommand({ ResourceId: resourceId }); const response = await (0, client_tools_js_1.runAndCatch404)(() => client.send(command)); if (!response) { return {}; } return (0, tags_js_1.convertTagsToRecord)(response.Tags); } /** * Get the organizational units for a parent organizational unit * * @param client The OrganizationsClient to use * @param parentId The AWS id of the parent organizational unit * @returns The organizational units directly under the parent */ async function getChildOrgUnits(client, parentId) { return await (0, typedSync_js_1.paginateResource)(client, client_organizations_1.ListOrganizationalUnitsForParentCommand, 'OrganizationalUnits', { inputKey: 'NextToken', outputKey: 'NextToken' }, { ParentId: parentId }); } /** * Get the accounts for a parent organizational unit * * @param client The OrganizationsClient to use * @param parentId The AWS id of the parent organizational unit * @returns The accounts directly under the parent */ async function getAccountsForParent(client, parentId) { const accounts = await (0, typedSync_js_1.paginateResource)(client, client_organizations_1.ListAccountsForParentCommand, 'Accounts', { inputKey: 'NextToken', outputKey: 'NextToken' }, { ParentId: parentId }); return accounts; } /** * Get the details of an Organizational Unit (OU). * * @param organizationClient the OrganizationsClient to use * @param ou the Organizational Unit to get the details for * @returns an object containing the OU's tags and metadata */ async function getOuDetails(organizationClient, ou) { return { tags: await getTagsForOu(organizationClient, ou.Id), metadata: { arn: ou.Arn, name: ou.Name } }; } /** * Get the policies for a target * * @param client the OrganizationsClient to use * @param targetId the id of the target to get the policies for * @param policyType the type of policy to get * @param enabled whether the policy type is enabled * @returns the Arns of the policies for the target */ async function getPoliciesForTarget(client, targetId, policyType, enabled) { if (!enabled) { return []; } const policies = await (0, typedSync_js_1.paginateResource)(client, client_organizations_1.ListPoliciesForTargetCommand, 'Policies', { inputKey: 'NextToken', outputKey: 'NextToken' }, { TargetId: targetId, Filter: policyType }); return policies.map((policy) => policy.Arn); } /** * Sync the policies for an organization and a specific policy type. * * @param organizationId the id of the organization to sync policies for * @param organizationClient the OrganizationsClient to use * @param storage the AwsIamStore to use for persistence * @param policyType the type of policy to sync (e.g., SERVICE_CONTROL_POLICY, RESOURCE_CONTROL_POLICY) * @param fileType the type of policy file to sync to storage (e.g., 'scps', 'rcps') * @param enabled whether the policy type is enabled in the organization */ async function syncPolicies(organizationId, organizationClient, storage, policyType, fileType, enabled) { const existingPolicies = await storage.listOrganizationPolicies(organizationId, fileType); if (!enabled) { for (const policyId of existingPolicies) { await storage.deleteOrganizationPolicy(organizationId, fileType, policyId); } return; } const policies = await (0, typedSync_js_1.paginateResource)(organizationClient, client_organizations_1.ListPoliciesCommand, 'Policies', { inputKey: 'NextToken', outputKey: 'NextToken' }, { Filter: policyType }); const newPolicyIds = new Set(policies.map((p) => p.Id.toLowerCase())); const policiesToDelete = existingPolicies.filter((id) => !newPolicyIds.has(id)); // Delete policies that are no longer present in the organization for (const policyToDelete of policiesToDelete) { await storage.deleteOrganizationPolicy(organizationId, fileType, policyToDelete); } for (const policy of policies) { const metadata = { arn: policy.Arn, name: policy.Name, description: policy.Description, awsManaged: policy.AwsManaged }; await storage.saveOrganizationPolicyMetadata(organizationId, fileType, policy.Id, 'metadata', metadata); const content = await getPolicyContent(organizationClient, policy.Id); await storage.saveOrganizationPolicyMetadata(organizationId, fileType, policy.Id, 'policy', content); const tags = await getTags(organizationClient, policy.Id); await storage.saveOrganizationPolicyMetadata(organizationId, fileType, policy.Id, 'tags', tags); } } /** * Get the content of a policy by its ID. * * @param organizationClient the OrganizationsClient to use * @param policyId the ID of the policy to get the content for * @returns the content of the policy as a parsed JSON object, or undefined if the policy does not exist or has no content */ async function getPolicyContent(organizationClient, policyId) { const command = new client_organizations_1.DescribePolicyCommand({ PolicyId: policyId }); const response = await (0, client_tools_js_1.runAndCatch404)(() => organizationClient.send(command)); if (response?.Policy?.Content) { return JSON.parse(response.Policy.Content); } return undefined; } /** * Sync the organization resource policy. * * @param organizationClient the OrganizationsClient to use * @param storage the AwsIamStore to use for persistence * @param organizationId the id of the organization to sync the resource policy for */ async function syncOrganizationResourcePolicy(organizationClient, storage, organizationId) { const policy = await getOrganizationResourcePolicy(organizationClient, organizationId); await storage.saveOrganizationMetadata(organizationId, 'policy', policy); } /** * Get the resource policy for an organization. * * @param organizationClient the OrganizationsClient to use * @param organizationId the id of the organization to get the resource policy for * @returns the resource policy as a parsed JSON object, or undefined if the policy does not exist or has no content */ async function getOrganizationResourcePolicy(organizationClient, organizationId) { const command = new client_organizations_1.DescribeResourcePolicyCommand({ PolicyId: organizationId }); try { const response = await organizationClient.send(command); if (response?.ResourcePolicy?.Content) { return JSON.parse(response.ResourcePolicy.Content); } return undefined; } catch (error) { if (error.name === 'ResourcePolicyNotFoundException') { return undefined; } throw error; } } /** * Save the organization ID for an account if it is not already saved. * * @param storage the AwsIamStore to use for persistence * @param accountId the ID of the account to save the organization ID for * @param organizationId the ID of the organization to save */ async function saveOrgForAccount(storage, accountId, organizationId) { if (organizationId) { await storage.saveAccountMetadata(accountId, 'organization', { organizationId }); } else { await storage.saveAccountMetadata(accountId, 'organization', undefined); } } //# sourceMappingURL=organizations.js.map