-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement ApplyPermissionsBoundary
- Loading branch information
Showing
6 changed files
with
291 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/* | ||
* Copyright 2025 Amazon.com, Inc. or its affiliates. | ||
*/ | ||
|
||
/** | ||
* Example Usage: | ||
* | ||
* ```typescript | ||
* import { App, Aspects } from "aws-cdk-lib"; | ||
* import { ApplyPermissionsBoundaryAspect } from "osml-cdk-constructs"; | ||
* | ||
* const app = new App(); | ||
* | ||
* // Define the IAM permissions boundary ARN | ||
* const permissionsBoundaryArn = `arn:aws:iam::${process.env.CDK_DEFAULT_ACCOUNT}:policy/MyPermissionsBoundary`; | ||
* | ||
* // Apply the aspect to enforce permissions boundaries on all IAM roles | ||
* Aspects.of(app).add(new ApplyPermissionsBoundaryAspect({ permissionsBoundaryArn })); | ||
* | ||
* // Define your CDK stacks here | ||
* // new MyStack(app, "MyStack"); | ||
* | ||
* app.synth(); | ||
* ``` | ||
* | ||
* This ensures that all IAM roles created in the CDK application | ||
* will inherit the specified permissions boundary automatically. | ||
*/ | ||
|
||
import { IAspect } from "aws-cdk-lib"; | ||
import { CfnRole, Role } from "aws-cdk-lib/aws-iam"; | ||
import { IConstruct } from "constructs"; | ||
|
||
/** | ||
* Represents the properties required to define the ApplyPermissionsBoundary. | ||
* | ||
* @interface ApplyPermissionsBoundaryProps | ||
*/ | ||
export interface ApplyPermissionsBoundaryProps { | ||
/** | ||
* The ARN of the IAM permissions boundary policy to apply. | ||
* | ||
* @type {string} | ||
*/ | ||
permissionsBoundaryArn: string; | ||
} | ||
|
||
/** | ||
* Represents an aspect that enforces an IAM permissions boundary on all IAM roles | ||
* created within the CDK application. | ||
* | ||
* This ensures that every role explicitly inherits the permissions boundary, | ||
* maintaining compliance with security policies and governance frameworks. | ||
*/ | ||
export class ApplyPermissionsBoundary implements IAspect { | ||
/** | ||
* The ARN of the permissions boundary policy to be applied. | ||
*/ | ||
private readonly permissionsBoundaryArn: string; | ||
|
||
/** | ||
* Creates an instance of ApplyPermissionsBoundary. | ||
* | ||
* @param {ApplyPermissionsBoundaryProps} props - The properties required to configure this aspect. | ||
* @returns {ApplyPermissionsBoundary} - The ApplyPermissionsBoundary instance. | ||
*/ | ||
constructor(props: ApplyPermissionsBoundaryProps) { | ||
this.permissionsBoundaryArn = props.permissionsBoundaryArn; | ||
} | ||
|
||
/** | ||
* Visits constructs in the CDK tree and applies the IAM permissions boundary | ||
* to all IAM roles. | ||
* | ||
* This method checks if the construct is an instance of Role, retrieves its | ||
* CloudFormation resource (CfnRole), and explicitly sets the permissions boundary. | ||
* | ||
* @param {IConstruct} node - The construct node being visited. | ||
*/ | ||
visit(node: IConstruct): void { | ||
if (node instanceof Role) { | ||
const cfnRole = node.node.defaultChild as CfnRole; | ||
if (cfnRole) { | ||
cfnRole.permissionsBoundary = this.permissionsBoundaryArn; | ||
console.debug( | ||
`✅ Applied Permissions Boundary to IAM Role: ${node.roleName}` | ||
); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
/* | ||
* Copyright 2025 Amazon.com, Inc. or its affiliates. | ||
*/ | ||
|
||
import { App, Aspects, Stack } from "aws-cdk-lib"; | ||
import { CfnRole, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam"; | ||
|
||
import { ApplyPermissionsBoundary } from "../../../lib"; | ||
|
||
describe("ApplyPermissionsBoundary Aspect Tests", () => { | ||
let app: App; | ||
let stack: Stack; | ||
let aspect: ApplyPermissionsBoundary; | ||
let role: Role; | ||
|
||
/** | ||
* Sets up a fresh CDK app and stack before each test. | ||
*/ | ||
beforeEach(() => { | ||
app = new App(); | ||
stack = new Stack(app, "TestStack"); | ||
|
||
// Create a test IAM Role | ||
role = new Role(stack, "TestRole", { | ||
assumedBy: new ServicePrincipal("ec2.amazonaws.com") | ||
}); | ||
|
||
// Instantiate the aspect with a test permissions boundary | ||
aspect = new ApplyPermissionsBoundary({ | ||
permissionsBoundaryArn: | ||
"arn:aws:iam::123456789012:policy/TestPermissionsBoundaryPolicy" | ||
}); | ||
}); | ||
|
||
/** | ||
* Validates that the aspect correctly stores the permissions boundary ARN. | ||
*/ | ||
it("should correctly resolve the permissions boundary ARN", () => { | ||
expect(aspect["permissionsBoundaryArn"]).toBe( | ||
"arn:aws:iam::123456789012:policy/TestPermissionsBoundaryPolicy" | ||
); | ||
}); | ||
|
||
/** | ||
* Ensures that the permissions boundary is applied when the aspect is used. | ||
*/ | ||
it("should apply the permissions boundary to IAM roles", () => { | ||
Aspects.of(stack).add(aspect); | ||
app.synth(); // Synthesize the stack to apply aspects | ||
|
||
const cfnRole = role.node.defaultChild as CfnRole; | ||
expect(cfnRole).toBeDefined(); | ||
expect(cfnRole.permissionsBoundary).toBeDefined(); | ||
expect(cfnRole.permissionsBoundary).toBe(aspect["permissionsBoundaryArn"]); | ||
}); | ||
|
||
/** | ||
* Ensures that non-IAM constructs remain unaffected by the aspect. | ||
*/ | ||
it("should not apply permissions boundary to non-IAM constructs", () => { | ||
const newApp = new App(); | ||
const nonRoleStack = new Stack(newApp, "NonRoleStack"); | ||
const newAspect = new ApplyPermissionsBoundary({ | ||
permissionsBoundaryArn: | ||
"arn:aws:iam::123456789012:policy/TestPermissionsBoundaryPolicy" | ||
}); | ||
|
||
Aspects.of(nonRoleStack).add(newAspect); | ||
newApp.synth(); // Independent synth call to verify behavior | ||
|
||
// Ensure no permissions boundary is incorrectly assigned to non-IAM constructs | ||
expect( | ||
nonRoleStack.node.tryFindChild("permissionsBoundary") | ||
).toBeUndefined(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
/* | ||
* Copyright 2025 Amazon.com, Inc. or its affiliates. | ||
*/ | ||
|
||
import { BaseConfig, ConfigType } from "../../../lib"; | ||
|
||
describe("BaseConfig Class Tests", () => { | ||
it("should initialize properties from the provided config object", () => { | ||
const config: ConfigType = { | ||
someProperty: "custom value", | ||
anotherProperty: 42 | ||
}; | ||
|
||
const baseConfig = new BaseConfig(config); | ||
|
||
expect((baseConfig as never)["someProperty"]).toBe("custom value"); | ||
expect((baseConfig as never)["anotherProperty"]).toBe(42); | ||
}); | ||
|
||
it("should not override properties if they are not provided in config", () => { | ||
class MyConfig extends BaseConfig { | ||
public someProperty: string; | ||
public anotherProperty: number; | ||
|
||
constructor(config: ConfigType = {}) { | ||
super(config); | ||
this.someProperty = this.someProperty ?? "default value"; | ||
this.anotherProperty = this.anotherProperty ?? 100; | ||
} | ||
} | ||
|
||
const configInstance = new MyConfig(); | ||
|
||
expect(configInstance.someProperty).toBe("default value"); | ||
expect(configInstance.anotherProperty).toBe(100); | ||
}); | ||
|
||
it("should override default values if provided in config", () => { | ||
class MyConfig extends BaseConfig { | ||
public someProperty: string; | ||
public anotherProperty: number; | ||
|
||
constructor(config: ConfigType = {}) { | ||
super(config); | ||
this.someProperty = this.someProperty ?? "default value"; | ||
this.anotherProperty = this.anotherProperty ?? 100; | ||
} | ||
} | ||
|
||
const configInstance = new MyConfig({ | ||
someProperty: "custom", | ||
anotherProperty: 50 | ||
}); | ||
|
||
expect(configInstance.someProperty).toBe("custom"); | ||
expect(configInstance.anotherProperty).toBe(50); | ||
}); | ||
|
||
it("should handle an empty configuration object gracefully", () => { | ||
const baseConfig = new BaseConfig(); | ||
expect(baseConfig).toBeDefined(); | ||
}); | ||
|
||
it("should allow additional properties to be dynamically assigned", () => { | ||
const config: ConfigType = { newProperty: "new value" }; | ||
const baseConfig = new BaseConfig(config); | ||
|
||
expect((baseConfig as never)["newProperty"]).toBe("new value"); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
/* | ||
* Copyright 2025 Amazon.com, Inc. or its affiliates. | ||
*/ | ||
|
||
import { Runtime } from "aws-cdk-lib/aws-lambda"; | ||
|
||
import { defaultConfig, RegionalConfig } from "../../../lib"; | ||
|
||
describe("RegionalConfig Class Tests", () => { | ||
it("should return default configuration for an unknown region", () => { | ||
const config = RegionalConfig.getConfig("unknown-region"); | ||
expect(config).toEqual(defaultConfig); | ||
}); | ||
|
||
it("should return the correct configuration for 'us-west-1' region", () => { | ||
const config = RegionalConfig.getConfig("us-west-1"); | ||
expect(config.maxVpcAzs).toBe(2); | ||
expect(config.sageMakerGpuEndpointInstanceType).toBe("ml.g4dn.2xlarge"); | ||
expect(config.ecrCdkDeployRuntime).toBe(defaultConfig.ecrCdkDeployRuntime); | ||
expect(config.s3Endpoint).toBe(defaultConfig.s3Endpoint); | ||
}); | ||
|
||
it("should return the correct configuration for 'us-gov-west-1' region", () => { | ||
const config = RegionalConfig.getConfig("us-gov-west-1"); | ||
expect(config.ecrCdkDeployRuntime).toBe(Runtime.PROVIDED_AL2); | ||
expect(config.s3Endpoint).toBe("s3.us-gov-west-1.amazonaws.com"); | ||
expect(config.maxVpcAzs).toBe(defaultConfig.maxVpcAzs); | ||
expect(config.sageMakerGpuEndpointInstanceType).toBe( | ||
defaultConfig.sageMakerGpuEndpointInstanceType | ||
); | ||
}); | ||
|
||
it("should return the correct configuration for 'us-isob-east-1' region", () => { | ||
const config = RegionalConfig.getConfig("us-isob-east-1"); | ||
expect(config.ecrCdkDeployRuntime).toBe(Runtime.PROVIDED_AL2); | ||
expect(config.maxVpcAzs).toBe(2); | ||
expect(config.s3Endpoint).toBe("s3.us-isob-east-1.sc2s.sgov.gov"); | ||
}); | ||
|
||
it("should return the correct configuration for 'us-iso-east-1' region", () => { | ||
const config = RegionalConfig.getConfig("us-iso-east-1"); | ||
expect(config.ecrCdkDeployRuntime).toBe(Runtime.PROVIDED_AL2); | ||
expect(config.s3Endpoint).toBe("s3.us-iso-east-1.c2s.ic.gov"); | ||
}); | ||
|
||
it("should merge partial region-specific configs with default values", () => { | ||
const config = RegionalConfig.getConfig("us-west-1"); | ||
expect(config.ecrCdkDeployRuntime).toBe(defaultConfig.ecrCdkDeployRuntime); | ||
expect(config.s3Endpoint).toBe(defaultConfig.s3Endpoint); | ||
}); | ||
}); |