@cloud-copilot/iam-collect
Version:
Collect IAM information from AWS Accounts
361 lines • 16.3 kB
JavaScript
"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