@cloud-copilot/iam-lens
Version:
Visibility in IAM in and across AWS accounts
193 lines • 11.2 kB
JavaScript
import { loadPolicy } from '@cloud-copilot/iam-policy';
import { shrinkJsonDocument } from '@cloud-copilot/iam-shrink';
import { splitArnParts } from '@cloud-copilot/iam-utils';
import { IamCollectClient } from '../collect/client.js';
import { getAllPoliciesForPrincipal } from '../principals.js';
import { addStatementToPermissionSet, buildPermissionSetFromPolicies, PermissionSet, toPolicyStatements } from './permissionSet.js';
import { allIamRolesAssumeRolePermissionSets, iamRolesSameAccount } from './resources/resourceTypes/iamRoles.js';
import { allKmsKeysAllActionsPermissionSets, kmsKeysSameAccount } from './resources/resourceTypes/kmsKeys.js';
import { s3BucketsCrossAccount, s3BucketsSameAccount } from './resources/resourceTypes/s3Buckets.js';
import { statementAppliesToPrincipal } from './resources/statements.js';
/**
* Get what actions a principal can perform based on their policies.
*
* @param collectClient the IAM collect client to use for retrieving policies.
* @param input the input containing the principal and options.
* @returns A promise that resolves to the permissions the principal can perform, or void if the implementation is incomplete.
*/
export async function principalCan(collectClient, input) {
const { principal } = input;
if (!principal) {
throw new Error('Principal must be provided for principal-can command');
}
const principalAccountId = splitArnParts(principal).accountId;
const principalPolicies = await getAllPoliciesForPrincipal(collectClient, principal);
const identityPolicies = [
...principalPolicies.managedPolicies,
...principalPolicies.inlinePolicies,
...(principalPolicies.groupPolicies?.map((group) => group.managedPolicies).flat() || []),
...(principalPolicies.groupPolicies?.map((group) => group.inlinePolicies).flat() || [])
].map((policy) => loadPolicy(policy.policy));
const allowedPermissions = await buildPermissionSetFromPolicies('Allow', identityPolicies);
const identityDenyPermissions = await buildPermissionSetFromPolicies('Deny', identityPolicies);
let finalPermissions = allowedPermissions;
const resourceDenyPermissions = new PermissionSet('Deny');
/*********** Start KMS Keys *************/
//📋 Get all current permissions
const { keyAllows: allKeysAllow, keyDenies: allKeysDeny } = await allKmsKeysAllActionsPermissionSets();
//📋 Capture what is currently allowed by identity policies
const identityKeyPermissions = allKeysAllow.intersection(allowedPermissions);
//📋 Do any subtractions first
// Remove all the KMS permissions from the identityAllows, add them back later
finalPermissions = finalPermissions.subtract(allKeysDeny).allow;
//📋 Get the permissions for the account
// Get all the KMS permission for the same account
const { accountAllows: keyAccountAllows, principalAllows: keyPrincipalAllows, denies: keyDenies } = await kmsKeysSameAccount(collectClient, principal);
//📋 Add direct permissions for the principal
// Add in the principal allows
finalPermissions.addAll(keyPrincipalAllows);
//📋 Intersect account allows with identity allows
// Add the account allows intersected with the identity allows
for (const keyAcctAllow of keyAccountAllows) {
finalPermissions.addAll(keyAcctAllow.intersection(identityKeyPermissions));
}
//📋 Add the denies.
// Add the denies for later
resourceDenyPermissions.addAll(keyDenies);
/*********** End KMS Keys *************/
/*********** Start Role Trust Policies *************/
const { roleAllows: allRolesAllow, roleDenies: allRolesDeny } = await allIamRolesAssumeRolePermissionSets();
const identityAssumeRolePermissions = allRolesAllow.intersection(allowedPermissions);
// Remove all the IAM role permissions from the identityAllows, add them back later
finalPermissions = finalPermissions.subtract(allRolesDeny).allow;
// Get all the KMS permission for the same account
const { accountAllows: roleAccountAllows, principalAllows: rolePrincipalAllows, denies: roleDenies } = await iamRolesSameAccount(collectClient, principal);
// Add in the principal allows
finalPermissions.addAll(rolePrincipalAllows);
// Add the account allows intersected with the identity allows
for (const roleAcctAllow of roleAccountAllows) {
finalPermissions.addAll(roleAcctAllow.intersection(identityAssumeRolePermissions));
}
// Add the denies for later
resourceDenyPermissions.addAll(roleDenies);
/*********** End Role Trust Policies *************/
/*********** Start Buckets *************/
const { allows: bucketAllows, denies: bucketDenies } = await s3BucketsSameAccount(collectClient, principal);
finalPermissions.addAll(bucketAllows);
resourceDenyPermissions.addAll(bucketDenies);
/*********** End Buckets *************/
// TODO: There is a slight wrinkle where same account resource policies can override implicit denies from Permission Boundaries.
if (principalPolicies.permissionBoundary) {
const boundaryPolicy = loadPolicy(principalPolicies.permissionBoundary.policy);
const boundaryPermissions = await buildPermissionSetFromPolicies('Allow', [boundaryPolicy]);
const boundaryDenies = await buildPermissionSetFromPolicies('Deny', [boundaryPolicy]);
identityDenyPermissions.addAll(boundaryDenies);
finalPermissions = finalPermissions.intersection(boundaryPermissions);
}
/*********** Start Cross Account Checks *************/
// 📋 List the accounts
// 📋 Get the RCPs, if any
// 📋 Get the bucket permission sets for the account
// 📋 Intersect with identity permissions and Permission boundary by using `finalPermissions`
// 📋 Subtract denies from identity permissions, SCPs, and Permission Boundary
const allAccounts = await collectClient.allAccounts();
const otherAccountAllows = new PermissionSet('Allow');
const otherAccountDenies = new PermissionSet('Deny');
for (const otherAccountId of allAccounts) {
if (otherAccountId === principalAccountId) {
continue;
}
const rcpPolicies = await collectClient.getRcpHierarchyForAccount(otherAccountId);
const otherAccountRcpDenies = new PermissionSet('Deny');
for (const level of rcpPolicies) {
const rcpPolicies = level.policies.map((rcp) => loadPolicy(rcp.policy));
for (const policy of rcpPolicies) {
for (const statement of policy.statements()) {
if (statement.isDeny()) {
// Only Add RCP denies that apply to the principal
const applies = await statementAppliesToPrincipal(statement, principal, collectClient);
if (applies === 'PrincipalMatch' || applies === 'AccountMatch') {
await addStatementToPermissionSet(statement, otherAccountRcpDenies);
}
}
}
}
}
const { allows: otherAccountBucketAllows, denies: otherAccountBucketDenies } = await s3BucketsCrossAccount(collectClient, otherAccountId, otherAccountRcpDenies, principal);
otherAccountAllows.addAll(otherAccountBucketAllows);
otherAccountDenies.addAll(otherAccountBucketDenies);
}
let effectiveOtherAccountAllows = otherAccountAllows.intersection(finalPermissions);
/*********** End Cross Account Checks *************/
const scpAllowsByLevel = [];
const rcpAllowsByLevel = [];
for (const level of principalPolicies.scps) {
const scpPolicies = level.policies.map((scp) => loadPolicy(scp.policy));
scpAllowsByLevel.push(await buildPermissionSetFromPolicies('Allow', scpPolicies));
for (const policy of scpPolicies) {
for (const statement of policy.statements()) {
if (statement.isDeny()) {
// Only Add SCP denies that apply to the principal
const applies = await statementAppliesToPrincipal(statement, principal, collectClient);
if (applies === 'PrincipalMatch' || applies === 'AccountMatch') {
await addStatementToPermissionSet(statement, identityDenyPermissions);
}
}
}
}
}
const principalAccountDenyPermissions = identityDenyPermissions.clone();
for (const level of principalPolicies.rcps) {
const rcpPolicies = level.policies.map((rcp) => loadPolicy(rcp.policy));
rcpAllowsByLevel.push(await buildPermissionSetFromPolicies('Allow', rcpPolicies));
for (const policy of rcpPolicies) {
for (const statement of policy.statements()) {
if (statement.isDeny()) {
// Only Add RCPs denies that apply to the principal
const applies = await statementAppliesToPrincipal(statement, principal, collectClient);
if (applies === 'PrincipalMatch' || applies === 'AccountMatch') {
await addStatementToPermissionSet(statement, principalAccountDenyPermissions);
}
}
}
}
}
for (const scpAllow of scpAllowsByLevel) {
finalPermissions = finalPermissions.intersection(scpAllow);
effectiveOtherAccountAllows = effectiveOtherAccountAllows.intersection(scpAllow);
}
for (const rcpAllow of rcpAllowsByLevel) {
finalPermissions = finalPermissions.intersection(rcpAllow);
}
//Put together all the denies
principalAccountDenyPermissions.addAll(resourceDenyPermissions);
/* Same account final permissions after denies */
const sameAccountPermissionsAfterDeny = finalPermissions.subtract(principalAccountDenyPermissions);
finalPermissions = sameAccountPermissionsAfterDeny.allow;
const deniedPermissions = sameAccountPermissionsAfterDeny.deny;
const allowStatements = toPolicyStatements(finalPermissions);
const denyStatements = toPolicyStatements(deniedPermissions);
/* Cross account final permissions */
// Combine all the denies that apply to cross account
const allCrossAccountDenies = principalAccountDenyPermissions.clone();
allCrossAccountDenies.addAll(otherAccountDenies);
// Subtract the denies from the cross account allows
const crossAccountPermissionsAfterDeny = effectiveOtherAccountAllows.subtract(allCrossAccountDenies);
const crossAccountAllowStatements = toPolicyStatements(crossAccountPermissionsAfterDeny.allow);
const crossAccountDenyStatements = toPolicyStatements(crossAccountPermissionsAfterDeny.deny);
/* Create a policy document for everything */
const policyDocument = {
Version: '2012-10-17',
Statement: [
...allowStatements,
...denyStatements,
...crossAccountAllowStatements,
...crossAccountDenyStatements
]
};
if (input.shrinkActionLists) {
await shrinkJsonDocument({ iterations: 0 }, policyDocument);
}
return policyDocument;
}
//# sourceMappingURL=principalCan.js.map