packages/@aws-cdk/cloudformation-diff/lib/iam/iam-changes.ts (376 lines of code) (raw):

import { PropertyScrutinyType, ResourceScrutinyType } from '@aws-cdk/service-spec-types'; import * as chalk from 'chalk'; import type { ISsoInstanceACAConfig, ISsoPermissionSet } from './iam-identity-center'; import { SsoAssignment, SsoInstanceACAConfig, SsoPermissionSet } from './iam-identity-center'; import type { ManagedPolicyJson } from './managed-policy'; import { ManagedPolicyAttachment } from './managed-policy'; import type { Statement, StatementJson } from './statement'; import { parseLambdaPermission, parseStatements } from './statement'; import type { MaybeParsed } from '../diff/maybe-parsed'; import type { PropertyChange, PropertyMap, ResourceChange } from '../diff/types'; import { DiffableCollection } from '../diffable'; import { renderIntrinsics } from '../render-intrinsics'; import { deepRemoveUndefined, dropIfEmpty, flatMap, makeComparator } from '../util'; export interface IamChangesProps { propertyChanges: PropertyChange[]; resourceChanges: ResourceChange[]; } /** * Changes to IAM statements and IAM identity center */ export class IamChanges { public static IamPropertyScrutinies = [ PropertyScrutinyType.InlineIdentityPolicies, PropertyScrutinyType.InlineResourcePolicy, PropertyScrutinyType.ManagedPolicies, ]; public static IamResourceScrutinies = [ ResourceScrutinyType.ResourcePolicyResource, ResourceScrutinyType.IdentityPolicyResource, ResourceScrutinyType.LambdaPermission, ResourceScrutinyType.SsoAssignmentResource, ResourceScrutinyType.SsoInstanceACAConfigResource, ResourceScrutinyType.SsoPermissionSet, ]; // each entry in a DiffableCollection is used to generate a single row of the security changes table that is presented for cdk diff and cdk deploy. public readonly statements = new DiffableCollection<Statement>(); public readonly managedPolicies = new DiffableCollection<ManagedPolicyAttachment>(); public readonly ssoPermissionSets = new DiffableCollection<SsoPermissionSet>(); public readonly ssoAssignments = new DiffableCollection<SsoAssignment>(); public readonly ssoInstanceACAConfigs = new DiffableCollection<SsoInstanceACAConfig>(); constructor(props: IamChangesProps) { for (const propertyChange of props.propertyChanges) { this.readPropertyChange(propertyChange); } for (const resourceChange of props.resourceChanges) { this.readResourceChange(resourceChange); } this.statements.calculateDiff(); this.managedPolicies.calculateDiff(); this.ssoPermissionSets.calculateDiff(); this.ssoAssignments.calculateDiff(); this.ssoInstanceACAConfigs.calculateDiff(); } public get hasChanges() { return (this.statements.hasChanges || this.managedPolicies.hasChanges || this.ssoPermissionSets.hasChanges || this.ssoAssignments.hasChanges || this.ssoInstanceACAConfigs.hasChanges); } /** * Return whether the changes include broadened permissions * * Permissions are broadened if positive statements are added or * negative statements are removed, or if managed policies are added. */ public get permissionsBroadened(): boolean { return this.statements.additions.some(s => !s.isNegativeStatement) || this.statements.removals.some(s => s.isNegativeStatement) || this.managedPolicies.hasAdditions || this.ssoPermissionSets.hasAdditions || this.ssoAssignments.hasAdditions || this.ssoInstanceACAConfigs.hasAdditions; } /** * Return a summary table of changes */ public summarizeStatements(): string[][] { const ret: string[][] = []; const header = ['', 'Resource', 'Effect', 'Action', 'Principal', 'Condition']; // First generate all lines, then sort on Resource so that similar resources are together for (const statement of this.statements.additions) { const renderedStatement = statement.render(); ret.push([ '+', renderedStatement.resource, renderedStatement.effect, renderedStatement.action, renderedStatement.principal, renderedStatement.condition, ].map(s => chalk.green(s))); } for (const statement of this.statements.removals) { const renderedStatement = statement.render(); ret.push([ '-', renderedStatement.resource, renderedStatement.effect, renderedStatement.action, renderedStatement.principal, renderedStatement.condition, ].map(s => chalk.red(s))); } // Sort by 2nd column ret.sort(makeComparator((row: string[]) => [row[1]])); ret.splice(0, 0, header); return ret; } public summarizeManagedPolicies(): string[][] { const ret: string[][] = []; const header = ['', 'Resource', 'Managed Policy ARN']; for (const att of this.managedPolicies.additions) { ret.push([ '+', att.identityArn, att.managedPolicyArn, ].map(s => chalk.green(s))); } for (const att of this.managedPolicies.removals) { ret.push([ '-', att.identityArn, att.managedPolicyArn, ].map(s => chalk.red(s))); } // Sort by 2nd column ret.sort(makeComparator((row: string[]) => [row[1]])); ret.splice(0, 0, header); return ret; } public summarizeSsoAssignments(): string[][] { const ret: string[][] = []; const header = ['', 'Resource', 'InstanceArn', 'PermissionSetArn', 'PrincipalId', 'PrincipalType', 'TargetId', 'TargetType']; for (const att of this.ssoAssignments.additions) { ret.push([ '+', att.cfnLogicalId || '', att.ssoInstanceArn || '', att.permissionSetArn || '', att.principalId || '', att.principalType || '', att.targetId || '', att.targetType || '', ].map(s => chalk.green(s))); } for (const att of this.ssoAssignments.removals) { ret.push([ '-', att.cfnLogicalId || '', att.ssoInstanceArn || '', att.permissionSetArn || '', att.principalId || '', att.principalType || '', att.targetId || '', att.targetType || '', ].map(s => chalk.red(s))); } // Sort by resource name to ensure a unique value is used for sorting ret.sort(makeComparator((row: string[]) => [row[1]])); ret.splice(0, 0, header); return ret; } public summarizeSsoInstanceACAConfigs(): string[][] { const ret: string[][] = []; const header = ['', 'Resource', 'InstanceArn', 'AccessControlAttributes']; function formatAccessControlAttribute(aca: ISsoInstanceACAConfig.AccessControlAttribute): string { return `Key: ${aca?.Key}, Values: [${aca?.Value?.Source.join(', ')}]`; } for (const att of this.ssoInstanceACAConfigs.additions) { ret.push([ '+', att.cfnLogicalId || '', att.ssoInstanceArn || '', att.accessControlAttributes?.map(formatAccessControlAttribute).join('\n') || '', ].map(s => chalk.green(s))); } for (const att of this.ssoInstanceACAConfigs.removals) { ret.push([ '-', att.cfnLogicalId || '', att.ssoInstanceArn || '', att.accessControlAttributes?.map(formatAccessControlAttribute).join('\n') || '', ].map(s => chalk.red(s))); } // Sort by resource name to ensure a unique value is used for sorting ret.sort(makeComparator((row: string[]) => [row[1]])); ret.splice(0, 0, header); return ret; } public summarizeSsoPermissionSets(): string[][] { const ret: string[][] = []; const header = ['', 'Resource', 'InstanceArn', 'PermissionSet name', 'PermissionsBoundary', 'CustomerManagedPolicyReferences']; function formatManagedPolicyRef(s: ISsoPermissionSet.CustomerManagedPolicyReference | undefined): string { return `Name: ${s?.Name || ''}, Path: ${s?.Path || ''}`; } function formatSsoPermissionsBoundary(ssoPb: ISsoPermissionSet.PermissionsBoundary | undefined): string { // ManagedPolicyArn OR CustomerManagedPolicyReference can be specified -- but not both. if (ssoPb?.ManagedPolicyArn !== undefined) { return `ManagedPolicyArn: ${ssoPb?.ManagedPolicyArn || ''}`; } else if (ssoPb?.CustomerManagedPolicyReference !== undefined) { return `CustomerManagedPolicyReference: {\n ${formatManagedPolicyRef(ssoPb?.CustomerManagedPolicyReference)}\n}`; } else { return ''; } } for (const att of this.ssoPermissionSets.additions) { ret.push([ '+', att.cfnLogicalId || '', att.ssoInstanceArn || '', att.name || '', formatSsoPermissionsBoundary(att.ssoPermissionsBoundary), att.ssoCustomerManagedPolicyReferences?.map(formatManagedPolicyRef).join('\n') || '', ].map(s => chalk.green(s))); } for (const att of this.ssoPermissionSets.removals) { ret.push([ '-', att.cfnLogicalId || '', att.ssoInstanceArn || '', att.name || '', formatSsoPermissionsBoundary(att.ssoPermissionsBoundary), att.ssoCustomerManagedPolicyReferences?.map(formatManagedPolicyRef).join('\n') || '', ].map(s => chalk.red(s))); } // Sort by resource name to ensure a unique value is used for sorting ret.sort(makeComparator((row: string[]) => [row[1]])); ret.splice(0, 0, header); return ret; } /** * Return a machine-readable version of the changes. * This is only used in tests. * * @internal */ public _toJson(): IamChangesJson { return deepRemoveUndefined({ statementAdditions: dropIfEmpty(this.statements.additions.map(s => s._toJson())), statementRemovals: dropIfEmpty(this.statements.removals.map(s => s._toJson())), managedPolicyAdditions: dropIfEmpty(this.managedPolicies.additions.map(s => s._toJson())), managedPolicyRemovals: dropIfEmpty(this.managedPolicies.removals.map(s => s._toJson())), }); } private readPropertyChange(propertyChange: PropertyChange) { switch (propertyChange.scrutinyType) { case PropertyScrutinyType.InlineIdentityPolicies: // AWS::IAM::{ Role | User | Group }.Policies this.statements.addOld(...this.readIdentityPolicies(propertyChange.oldValue, propertyChange.resourceLogicalId)); this.statements.addNew(...this.readIdentityPolicies(propertyChange.newValue, propertyChange.resourceLogicalId)); break; case PropertyScrutinyType.InlineResourcePolicy: // Any PolicyDocument on a resource (including AssumeRolePolicyDocument) this.statements.addOld(...this.readResourceStatements(propertyChange.oldValue, propertyChange.resourceLogicalId)); this.statements.addNew(...this.readResourceStatements(propertyChange.newValue, propertyChange.resourceLogicalId)); break; case PropertyScrutinyType.ManagedPolicies: // Just a list of managed policies this.managedPolicies.addOld(...this.readManagedPolicies(propertyChange.oldValue, propertyChange.resourceLogicalId)); this.managedPolicies.addNew(...this.readManagedPolicies(propertyChange.newValue, propertyChange.resourceLogicalId)); break; } } private readResourceChange(resourceChange: ResourceChange) { switch (resourceChange.scrutinyType) { case ResourceScrutinyType.IdentityPolicyResource: // AWS::IAM::Policy this.statements.addOld(...this.readIdentityPolicyResource(resourceChange.oldProperties)); this.statements.addNew(...this.readIdentityPolicyResource(resourceChange.newProperties)); break; case ResourceScrutinyType.ResourcePolicyResource: // AWS::*::{Bucket,Queue,Topic}Policy this.statements.addOld(...this.readResourcePolicyResource(resourceChange.oldProperties)); this.statements.addNew(...this.readResourcePolicyResource(resourceChange.newProperties)); break; case ResourceScrutinyType.LambdaPermission: this.statements.addOld(...this.readLambdaStatements(resourceChange.oldProperties)); this.statements.addNew(...this.readLambdaStatements(resourceChange.newProperties)); break; case ResourceScrutinyType.SsoPermissionSet: this.ssoPermissionSets.addOld(...this.readSsoPermissionSet(resourceChange.oldProperties, resourceChange.resourceLogicalId)); this.ssoPermissionSets.addNew(...this.readSsoPermissionSet(resourceChange.newProperties, resourceChange.resourceLogicalId)); break; case ResourceScrutinyType.SsoAssignmentResource: this.ssoAssignments.addOld(...this.readSsoAssignments(resourceChange.oldProperties, resourceChange.resourceLogicalId)); this.ssoAssignments.addNew(...this.readSsoAssignments(resourceChange.newProperties, resourceChange.resourceLogicalId)); break; case ResourceScrutinyType.SsoInstanceACAConfigResource: this.ssoInstanceACAConfigs.addOld(...this.readSsoInstanceACAConfigs(resourceChange.oldProperties, resourceChange.resourceLogicalId)); this.ssoInstanceACAConfigs.addNew(...this.readSsoInstanceACAConfigs(resourceChange.newProperties, resourceChange.resourceLogicalId)); break; } } /** * Parse a list of policies on an identity */ private readIdentityPolicies(policies: any, logicalId: string): Statement[] { if (policies === undefined || !Array.isArray(policies)) { return []; } const appliesToPrincipal = 'AWS:${' + logicalId + '}'; return flatMap(policies, (policy: any) => { // check if the Policy itself is not an intrinsic, like an Fn::If const unparsedStatement = policy.PolicyDocument?.Statement ? policy.PolicyDocument.Statement : policy; return defaultPrincipal(appliesToPrincipal, parseStatements(renderIntrinsics(unparsedStatement))); }); } /** * Parse an IAM::Policy resource */ private readIdentityPolicyResource(properties: any): Statement[] { if (properties === undefined) { return []; } properties = renderIntrinsics(properties); const principals = (properties.Groups || []).concat(properties.Users || []).concat(properties.Roles || []); return flatMap(principals, (principal: string) => { const ref = 'AWS:' + principal; return defaultPrincipal(ref, parseStatements(properties.PolicyDocument.Statement)); }); } private readSsoInstanceACAConfigs(properties: any, logicalId: string): SsoInstanceACAConfig[] { if (properties === undefined) { return []; } properties = renderIntrinsics(properties); return [new SsoInstanceACAConfig({ cfnLogicalId: '${' + logicalId + '}', ssoInstanceArn: properties.InstanceArn, accessControlAttributes: properties.AccessControlAttributes, })]; } private readSsoAssignments(properties: any, logicalId: string): SsoAssignment[] { if (properties === undefined) { return []; } properties = renderIntrinsics(properties); return [new SsoAssignment({ cfnLogicalId: '${' + logicalId + '}', ssoInstanceArn: properties.InstanceArn, permissionSetArn: properties.PermissionSetArn, principalId: properties.PrincipalId, principalType: properties.PrincipalType, targetId: properties.TargetId, targetType: properties.TargetType, })]; } private readSsoPermissionSet(properties: any, logicalId: string): SsoPermissionSet[] { if (properties === undefined) { return []; } properties = renderIntrinsics(properties); return [new SsoPermissionSet({ cfnLogicalId: '${' + logicalId + '}', name: properties.Name, ssoInstanceArn: properties.InstanceArn, ssoCustomerManagedPolicyReferences: properties.CustomerManagedPolicyReferences, ssoPermissionsBoundary: properties.PermissionsBoundary, })]; } private readResourceStatements(policy: any, logicalId: string): Statement[] { if (policy === undefined) { return []; } const appliesToResource = '${' + logicalId + '.Arn}'; return defaultResource(appliesToResource, parseStatements(renderIntrinsics(policy.Statement))); } /** * Parse an AWS::*::{Bucket,Topic,Queue}policy */ private readResourcePolicyResource(properties: any): Statement[] { if (properties === undefined) { return []; } properties = renderIntrinsics(properties); const policyKeys = Object.keys(properties).filter(key => key.indexOf('Policy') > -1); // Find the key that identifies the resource(s) this policy applies to const resourceKeys = Object.keys(properties).filter(key => !policyKeys.includes(key) && !key.endsWith('Name')); let resources = resourceKeys.length === 1 ? properties[resourceKeys[0]] : ['???']; // For some resources, this is a singleton string, for some it's an array if (!Array.isArray(resources)) { resources = [resources]; } return flatMap(resources, (resource: string) => { return defaultResource(resource, parseStatements(properties[policyKeys[0]].Statement)); }); } private readManagedPolicies(policyArns: any, logicalId: string): ManagedPolicyAttachment[] { if (!policyArns) { return []; } const rep = '${' + logicalId + '}'; return ManagedPolicyAttachment.parseManagedPolicies(rep, renderIntrinsics(policyArns)); } private readLambdaStatements(properties?: PropertyMap): Statement[] { if (!properties) { return []; } return [parseLambdaPermission(renderIntrinsics(properties))]; } } /** * Set an undefined or wildcarded principal on these statements */ function defaultPrincipal(principal: string, statements: Statement[]) { statements.forEach(s => s.principals.replaceEmpty(principal)); statements.forEach(s => s.principals.replaceStar(principal)); return statements; } /** * Set an undefined or wildcarded resource on these statements */ function defaultResource(resource: string, statements: Statement[]) { statements.forEach(s => s.resources.replaceEmpty(resource)); statements.forEach(s => s.resources.replaceStar(resource)); return statements; } export interface IamChangesJson { statementAdditions?: Array<MaybeParsed<StatementJson>>; statementRemovals?: Array<MaybeParsed<StatementJson>>; managedPolicyAdditions?: Array<MaybeParsed<ManagedPolicyJson>>; managedPolicyRemovals?: Array<MaybeParsed<ManagedPolicyJson>>; }