Repository: cdklabs/cdk-enterprise-iac (language: python)

Issue:
add support for IAM Role Path in the AddPermissionBoundary Aspect
### Description

This project provides a way to add a consistent set of prefixes onto Roles, Policies and Instance Profiles, along with applying a set Permissions Boundary to all Roles. AWS [recommends](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) using IAM Paths to separate and group permissions, and it would be very useful to be able to specify this inside an Aspect as well.

### Use Case

Certain CDK constructs create hidden resources. For example, creating a Route53 ARecord with the `deleteExisting` [property](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_route53.ARecord.html#deleteexisting) will cause a Lambda Function to be deployed as a Custom Resource in order to perform the record delete. This function has a generated IAM role that fails to deploy due to the SCP's in place in our organisation (IAM Roles require a specific Path, prefix on RoleName, and PermissionBoundary).

### Proposed Solution

I can't write TS, so please excuse any mistakes.

```
/* add path to the props constructor with default */
constructor(props: AddPermissionBoundaryProps) {
    this._rolePath = props.rolePath || '/';
```

```
/* after the Permissions Boundary override, override Path as well */
node.addPropertyOverride('Path', this._rolePath);
```

### Other information

_No response_

### Acknowledge

- [ ] I may be able to implement this feature request
- [ ] This feature might incur a breaking change


Discussion / hints:
this would be super useful for me
@darylgraham Thanks for opening and sorry for the delay! I should have a PR ready by the end of the week!

Fix patch:
diff --git a/API.md b/API.md
index 96b054e7..cfa9c64c 100644
--- a/API.md
+++ b/API.md
@@ -4717,6 +4717,7 @@ const addPermissionBoundaryProps: AddPermissionBoundaryProps = { ... }
 | <code><a href="#@cdklabs/cdk-enterprise-iac.AddPermissionBoundaryProps.property.permissionsBoundaryPolicyName">permissionsBoundaryPolicyName</a></code> | <code>string</code> | Name of Permissions Boundary Policy to add to all IAM roles. |
 | <code><a href="#@cdklabs/cdk-enterprise-iac.AddPermissionBoundaryProps.property.instanceProfilePrefix">instanceProfilePrefix</a></code> | <code>string</code> | A prefix to prepend to the name of the IAM InstanceProfiles (Default: ''). |
 | <code><a href="#@cdklabs/cdk-enterprise-iac.AddPermissionBoundaryProps.property.policyPrefix">policyPrefix</a></code> | <code>string</code> | A prefix to prepend to the name of the IAM Policies and ManagedPolicies (Default: ''). |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.AddPermissionBoundaryProps.property.rolePath">rolePath</a></code> | <code>string</code> | An IAM path to add to all IAM roles (Default: ''). |
 | <code><a href="#@cdklabs/cdk-enterprise-iac.AddPermissionBoundaryProps.property.rolePrefix">rolePrefix</a></code> | <code>string</code> | A prefix to prepend to the name of IAM Roles (Default: ''). |
 
 ---
@@ -4757,6 +4758,18 @@ A prefix to prepend to the name of the IAM Policies and ManagedPolicies (Default
 
 ---
 
+##### `rolePath`<sup>Optional</sup> <a name="rolePath" id="@cdklabs/cdk-enterprise-iac.AddPermissionBoundaryProps.property.rolePath"></a>
+
+```typescript
+public readonly rolePath: string;
+```
+
+- *Type:* string
+
+An IAM path to add to all IAM roles (Default: '').
+
+---
+
 ##### `rolePrefix`<sup>Optional</sup> <a name="rolePrefix" id="@cdklabs/cdk-enterprise-iac.AddPermissionBoundaryProps.property.rolePrefix"></a>
 
 ```typescript
diff --git a/src/patches/addPermissionsBoundary.ts b/src/patches/addPermissionsBoundary.ts
index c7824480..e4d7587f 100644
--- a/src/patches/addPermissionsBoundary.ts
+++ b/src/patches/addPermissionsBoundary.ts
@@ -35,6 +35,10 @@ export interface AddPermissionBoundaryProps {
    * A prefix to prepend to the name of the IAM InstanceProfiles (Default: '').
    */
   readonly instanceProfilePrefix?: string;
+  /**
+   * An IAM path to add to all IAM roles (Default: '').
+   */
+  readonly rolePath?: string;
 }
 
 /**
@@ -49,12 +53,14 @@ export class AddPermissionBoundary implements IAspect {
   private _rolePrefix: string;
   private _policyPrefix: string;
   private _instanceProfilePrefix: string;
+  private _rolePath: string;
 
   constructor(props: AddPermissionBoundaryProps) {
     this._permissionsBoundaryPolicyName = props.permissionsBoundaryPolicyName;
     this._rolePrefix = props.rolePrefix || '';
     this._policyPrefix = props.policyPrefix || '';
     this._instanceProfilePrefix = props.instanceProfilePrefix || '';
+    this._rolePath = props.rolePath || '';
   }
 
   public checkAndOverride(
@@ -106,6 +112,9 @@ export class AddPermissionBoundary implements IAspect {
           'PermissionsBoundary',
           permissionsBoundaryPolicyArn
         );
+        if (this._rolePath) {
+          node.addPropertyOverride('Path', this._rolePath);
+        }
         const roleName =
           // eslint-disable-next-line dot-notation
           cfnResourceNode['cfnProperties'].roleName ||


Test patch:
diff --git a/test/patches/addPermissionsBoundary.test.ts b/test/patches/addPermissionsBoundary.test.ts
index 7cd307a0..e0e148ef 100644
--- a/test/patches/addPermissionsBoundary.test.ts
+++ b/test/patches/addPermissionsBoundary.test.ts
@@ -12,6 +12,7 @@ import {
   Role,
   ServicePrincipal,
 } from 'aws-cdk-lib/aws-iam';
+import { ARecord, HostedZone, RecordTarget } from 'aws-cdk-lib/aws-route53';
 import { Bucket } from 'aws-cdk-lib/aws-s3';
 import { AddPermissionBoundary } from '../../src/patches/addPermissionsBoundary';
 
@@ -58,9 +59,8 @@ describe('Permissions Boundary patch', () => {
       const roleName =
         'my-awesome-role-that-has-quite-a-long-name-seriously-it-is-so-long-it-exceeds-64-characters';
       const rolePrefix = 'MY_PREFIX';
-      new Role(stack, 'testRole', {
+      new Role(stack, roleName, {
         assumedBy: new ServicePrincipal('ec2.amazonaws.com'),
-        roleName,
       });
       Aspects.of(stack).add(
         new AddPermissionBoundary({
@@ -87,13 +87,11 @@ describe('Permissions Boundary patch', () => {
       let secondRoleName =
         'my-awesome-role-that-has-quite-a-long-name-seriously-it-is-so-long-it-exceeds-64-characters-2';
       const rolePrefix = 'MY_PREFIX';
-      new Role(stack, 'testFirstRole', {
+      new Role(stack, firstRoleName, {
         assumedBy: new ServicePrincipal('ec2.amazonaws.com'),
-        roleName: firstRoleName,
       });
-      new Role(stack, 'testSecondRole', {
+      new Role(stack, secondRoleName, {
         assumedBy: new ServicePrincipal('ec2.amazonaws.com'),
-        roleName: secondRoleName,
       });
       Aspects.of(stack).add(
         new AddPermissionBoundary({
@@ -184,6 +182,32 @@ describe('Permissions Boundary patch', () => {
         RoleName: roleName,
       });
     });
+    test('Role paths get added', () => {
+      const zone = new HostedZone(stack, 'TestHostedZone', {
+        zoneName: 'example.com',
+      });
+      // test case provided in feature request
+      new ARecord(stack, 'ARecord', {
+        zone,
+        target: RecordTarget.fromIpAddresses('1.2.3.4', '5.6.7.8'),
+        deleteExisting: true,
+      });
+      const rolePath = '/bananas/apples/';
+      Aspects.of(stack).add(
+        new AddPermissionBoundary({
+          permissionsBoundaryPolicyName: pbName,
+          rolePath,
+        })
+      );
+      const template = Template.fromStack(stack);
+      template.resourcePropertiesCountIs(
+        'AWS::IAM::Role',
+        {
+          Path: rolePath,
+        },
+        1
+      );
+    });
   });
   describe('Policies', () => {
     const policyPrefix = 'POLICY_PREFIX_';