packages/constructs/L3/governance/roles-l3-construct/lib/roles-l3-construct.ts (288 lines of code) (raw):

/*! * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ import { MdaaManagedPolicy, MdaaRole } from '@aws-mdaa/iam-constructs'; import { MdaaL3Construct, MdaaL3ConstructProps } from '@aws-mdaa/l3-construct'; import { AccountPrincipal, ArnPrincipal, Condition, Effect, IPrincipal, IRole, ISamlProvider, ManagedPolicy, PolicyDocument, PolicyStatement, PrincipalWithConditions, SamlMetadataDocument, SamlPrincipal, SamlProvider, ServicePrincipal, } from 'aws-cdk-lib/aws-iam'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { MdaaNagSuppressions } from '@aws-mdaa/construct'; //NOSONAR import { Construct } from 'constructs'; import { resolve } from 'path'; import { parse } from 'yaml'; import { readFileSync } from 'fs'; import { ConfigurationElement } from '@aws-mdaa/config'; /** * Define UsageProfile types */ export enum BasePersona { DATA_ADMIN = 'data-admin', DATA_ENGINEER = 'data-engineer', DATA_SCIENTIST = 'data-scientist', } export interface PersonaConfigProps { readonly personas: { [key: string]: Array<string> }; } export interface FederationProps { /** * Arn of an existing provider */ readonly providerArn?: string; /** * Path to a SAML doc to be used to create a new provider */ readonly samlDoc?: string; } export interface GenerateManagedPolicyWithNameProps extends GenerateManagedPolicyProps { /** * Name of the managed policy. */ readonly name: string; } export interface GenerateManagedPolicyProps { /** * Managed policy document contents. */ readonly policyDocument: PolicyDocument; /** * CDK Nag suppressions if policyDocument generates Nags. */ readonly suppressions?: SuppressionProps[]; /** * If true (default false), policy name will be set verbatim instead of using the naming class */ readonly verbatimPolicyName?: boolean; /** * Additional policy statements that may be added to policyDocument */ readonly statements?: PolicyStatement[]; } export interface SuppressionProps { readonly id: string; readonly reason: string; } export interface GenerateRoleWithNameProps extends GenerateRoleProps { /** * Name of the role. */ readonly name: string; } export interface TrustedPrincipalProps { /** * The assume role trust config. */ readonly trustedPrincipal: string; readonly additionalTrustedActions?: string[]; } export interface GenerateRoleProps { /** * Intended base persona that the generated role should mimic * All the policies associated with specified persona will get associated with the generated role */ readonly basePersona?: BasePersona; /** * The assume role trust config. */ readonly trustedPrincipal: string; /** * Additional assume role trust configs. */ readonly additionalTrustedPrincipals?: TrustedPrincipalProps[]; /** * Conditions to apply to the assume role trust policy */ readonly assumeRoleTrustConditions?: { [key: string]: Condition }; /** * List of AWS Managed policies to associate to the role. */ readonly awsManagedPolicies?: string[]; /** * List of AWS Managed policies to associate to the role. */ readonly customerManagedPolicies?: string[]; /** * List of generated policies to associate to the role. */ readonly generatedPolicies?: string[]; /** * Suppressions if required by the role configuration. */ readonly suppressions?: SuppressionProps[]; } export interface RolesL3ConstructProps extends MdaaL3ConstructProps { /** * A map of federation names to federation definitions. */ readonly federations?: { [key: string]: FederationProps }; /** * A list of Managed Policies which will be created. */ readonly generatePolicies?: GenerateManagedPolicyWithNameProps[]; /** * A list of IAM roles which will be created. */ readonly generateRoles?: GenerateRoleWithNameProps[]; /** * If true (default), a set of Managed Policies will be generated by MDAA for use in pre-defined role personas */ readonly createPersonaManagedPolicies?: boolean; } interface MdaaPersonaAndManagedPolicies { /** * Map of persona names to list of managed policy names */ readonly personaToMdaaPolicyMap: { [personaName: string]: string[] }; /** * Map of managed policy-name to MDAA Managed Policy */ readonly mdaaPolicies: { [policyName: string]: MdaaManagedPolicy }; } export class RolesL3Construct extends MdaaL3Construct { protected readonly props: RolesL3ConstructProps; protected readonly personaToMdaaPolicyMap: { [personaName: string]: string[] }; protected readonly mdaaManagedPolicies: { [policyName: string]: MdaaManagedPolicy }; public readonly generatedRoles: { [key: string]: IRole }; constructor(scope: Construct, id: string, props: RolesL3ConstructProps) { super(scope, id, props); this.props = props; if (props.createPersonaManagedPolicies || props.createPersonaManagedPolicies == undefined) { const mdaaPersonaAndManagedPolicies = this.createMdaaManagedPolicies(); this.personaToMdaaPolicyMap = mdaaPersonaAndManagedPolicies.personaToMdaaPolicyMap; this.mdaaManagedPolicies = mdaaPersonaAndManagedPolicies.mdaaPolicies; } else { this.personaToMdaaPolicyMap = {}; this.mdaaManagedPolicies = {}; } const federationProviders = this.createFederations(); const generatedPolicies = this.createManagedPolicies(); this.generatedRoles = this.createRoles(federationProviders, generatedPolicies) || {}; } private createFederations(): { [key: string]: ISamlProvider } { const federations: { [key: string]: ISamlProvider } = {}; Object.keys(this.props.federations || {}).forEach(fedConfigName => { const fedConfig = (this.props.federations || {})[fedConfigName]; if (fedConfig.providerArn) { if (fedConfig.samlDoc) { throw new Error("Exactly one of 'providerArn' or 'samlDoc' should be specified in a Federation Config"); } federations[fedConfigName] = SamlProvider.fromSamlProviderArn( this.scope, `resolved-provider-${fedConfigName}`, fedConfig.providerArn, ); } else if (fedConfig.samlDoc) { if (fedConfig.providerArn) { throw new Error("Exactly one of 'providerArn' or 'samlDoc' should be specified in a Federation Config"); } federations[fedConfigName] = new SamlProvider(this.scope, `saml-provider-${fedConfigName}`, { name: this.props.naming.resourceName(fedConfigName), metadataDocument: SamlMetadataDocument.fromFile(fedConfig.samlDoc), }); } else { throw new Error("Exactly one of 'providerArn' or 'samlDoc' should be specified in a Federation Config"); } }); return federations; } private createManagedPolicies(): { [key: string]: ManagedPolicy } { const generatedPolicies: { [key: string]: ManagedPolicy } = {}; this.props.generatePolicies?.forEach(policyProps => { const policy = new MdaaManagedPolicy(this.scope, `policy-${policyProps.name}`, { naming: this.props.naming, managedPolicyName: policyProps.name, verbatimPolicyName: policyProps.verbatimPolicyName, document: policyProps.policyDocument, }); generatedPolicies[policyProps.name] = policy; if (policyProps.suppressions) { MdaaNagSuppressions.addConfigResourceSuppressions(policy, policyProps.suppressions, true); } }); return generatedPolicies; } private createMdaaManagedPolicies(): MdaaPersonaAndManagedPolicies { const personaToMdaaPolicyMap: { [key: string]: string[] } = {}; const personaConfig = this.loadPolicyConfig( '../policy-statements/persona-map.yaml', ) as unknown as PersonaConfigProps; const mdaaPolicySet = new Set<string>(); Object.entries(personaConfig.personas).forEach(([basePersona, personaProps]) => { personaProps.forEach(policyConfigFile => { mdaaPolicySet.add(policyConfigFile); if (this.getFileName(policyConfigFile)) { if (!personaToMdaaPolicyMap[basePersona]) { personaToMdaaPolicyMap[basePersona] = []; } personaToMdaaPolicyMap[basePersona].push(this.getFileName(policyConfigFile)); } }); }); const mdaaGeneratedPolicies: { [key: string]: MdaaManagedPolicy } = {}; mdaaPolicySet.forEach(policyConfigFile => { const name = this.getFileName(policyConfigFile); if (name) { const managedPolicyProps = this.loadPolicyConfig(`../policy-statements/${policyConfigFile}.yaml`) as { statements?: PolicyStatement[]; suppressions?: SuppressionProps[]; }; const policyStatements: PolicyStatement[] = (managedPolicyProps.statements || []).map(statement => { return PolicyStatement.fromJson(statement); }); // Create MDAA Managed Policy const mdaaPolicy = new MdaaManagedPolicy(this.scope, `caef-managed-policy-${name}`, { naming: this.props.naming, managedPolicyName: name, document: new PolicyDocument({ statements: policyStatements, }), }); // Add Suppression if (managedPolicyProps.suppressions) { MdaaNagSuppressions.addCodeResourceSuppressions(mdaaPolicy, managedPolicyProps.suppressions, true); } mdaaGeneratedPolicies[name] = mdaaPolicy; } }); return { personaToMdaaPolicyMap: personaToMdaaPolicyMap, mdaaPolicies: mdaaGeneratedPolicies, }; } private getFileName(policyConfigFile: string) { return policyConfigFile.split('/').pop() || ''; } private createRoles( federationProviders: { [key: string]: ISamlProvider }, generatedPolicies: { [key: string]: ManagedPolicy }, ): { [key: string]: IRole } | undefined { const generatedRoles = this.props.generateRoles?.map(generateRole => { const awsManagedPolicies = generateRole.awsManagedPolicies?.map(policyName => MdaaManagedPolicy.fromAwsManagedPolicyNameWithPartition(this, policyName), ); const customerManagedPolicies = generateRole.customerManagedPolicies?.map(policyName => ManagedPolicy.fromManagedPolicyName(this.scope, `${generateRole.name}-${policyName}`, policyName), ); const managedPolicies = [...(awsManagedPolicies || []), ...(customerManagedPolicies || [])]; const resolvedTrustPrincipal = this.resolveTrustedPrincipal(generateRole.trustedPrincipal, federationProviders); const trustPrincipal = generateRole.assumeRoleTrustConditions ? new PrincipalWithConditions(resolvedTrustPrincipal, generateRole.assumeRoleTrustConditions) : resolvedTrustPrincipal; const role = new MdaaRole(this.scope, generateRole.name, { assumedBy: trustPrincipal, roleName: generateRole.name, managedPolicies: managedPolicies, naming: this.props.naming, }); generateRole.additionalTrustedPrincipals?.forEach(trustPrincipalProps => { if (role.assumeRolePolicy) { const trustPrincipal = this.resolveTrustedPrincipal( trustPrincipalProps.trustedPrincipal, federationProviders, ); role.assumeRolePolicy.addStatements( new PolicyStatement({ actions: [trustPrincipal.assumeRoleAction, ...(trustPrincipalProps.additionalTrustedActions || [])], principals: [trustPrincipal], effect: Effect.ALLOW, }), ); } }); if (generateRole.basePersona) { // Attach Mdaa Generated Policies to the roles based on the persona defined in 'persona-map.yaml' this.personaToMdaaPolicyMap[generateRole.basePersona].forEach(policyName => { this.mdaaManagedPolicies[policyName].attachToRole(role); }); } if (generateRole.generatedPolicies) { generateRole.generatedPolicies.forEach(policyNamRef => { if (!generatedPolicies[policyNamRef]) { throw new Error(`Role ${generateRole.name} references non-existent policy: ${policyNamRef}`); } else { generatedPolicies[policyNamRef].attachToRole(role); } }); } if (generateRole.suppressions) { MdaaNagSuppressions.addConfigResourceSuppressions(role, generateRole.suppressions, true); } new StringParameter(role, `${generateRole.name}-ssm-generated-role-arn`, { parameterName: this.props.naming.ssmPath(`generated-role/${generateRole.name}/arn`, false), stringValue: role.roleArn, }); new StringParameter(role, `${generateRole.name}-ssm-generated-role-id`, { parameterName: this.props.naming.ssmPath(`generated-role/${generateRole.name}/id`, false), stringValue: role.roleId, }); return [generateRole.name, role]; }); return Object.fromEntries(generatedRoles || []); } private resolveTrustedPrincipal(ref: string, federationProviders: { [key: string]: ISamlProvider }): IPrincipal { if (ref.startsWith('service:')) { return new ServicePrincipal(ref.replace(/^service:\s*/, '')); } else if (ref.startsWith('account:')) { return new AccountPrincipal(ref.replace(/^account:\s*/, '')); } else if (ref.startsWith('arn:')) { return new ArnPrincipal(ref); } else if (ref.startsWith('federation:')) { const federation = federationProviders[ref.replace(/^federation:\s*/, '')]; if (!federation) { throw new Error(`Role references non-existent federation in config: ${ref}`); } return new SamlPrincipal(federation, {}); } else if (ref == 'this_account') { return new AccountPrincipal(this.account); } else { throw new Error("Trusted principal must start with service:, account:, federation: or equal 'this_account'"); } } private loadPolicyConfig(fileName: string) { // nosemgrep const configFilePath = resolve(__dirname, fileName); console.log('Reading config file from path' + configFilePath); try { // Read the configuration file // nosemgrep const rawConfigFile = readFileSync(configFilePath, 'utf8'); const rawConfig: ConfigurationElement = parse(rawConfigFile); return rawConfig; } catch (err) { console.log(err); throw err; } } }