Skip to content

Commit

Permalink
feat: implement ApplyPermissionsBoundary
Browse files Browse the repository at this point in the history
  • Loading branch information
drduhe committed Feb 4, 2025
1 parent 188e708 commit 8073de4
Show file tree
Hide file tree
Showing 6 changed files with 291 additions and 2 deletions.
1 change: 0 additions & 1 deletion .eslintrc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ rules:
require-await: off
"@typescript-eslint/no-unused-expressions":
- error
- allowShortCircuit: true
- allowTernary: true
"@typescript-eslint/no-unsafe-assignment": warn
"@typescript-eslint/interface-name-prefix": off
Expand Down
4 changes: 3 additions & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/*
* Copyright 2023-2024 Amazon.com, Inc. or its affiliates.
* Copyright 2023-2025 Amazon.com, Inc. or its affiliates.
*/

export * from "./osml/utils/base_config";
export * from "./osml/utils/apply_permissions_boundary";
export * from "./osml/data_intake/di_dataplane";
export * from "./osml/data_intake/roles/di_lambda_role";
export * from "./osml/model_runner/mr_monitoring";
Expand All @@ -29,3 +30,4 @@ export * from "./osml/data_catalog/dc_dataplane";
export * from "./osml/data_catalog/roles/dc_lambda_role";
export * from "./osml/tile_server/ts_dataplane";
export * from "./osml/tile_server/roles/ts_task_role";
export * from "./osml/utils/regional_config";
91 changes: 91 additions & 0 deletions lib/osml/utils/apply_permissions_boundary.ts
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}`
);
}
}
}
}
76 changes: 76 additions & 0 deletions test/osml/utils/apply_permissions_boundary.test.ts
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();
});
});
70 changes: 70 additions & 0 deletions test/osml/utils/base_config.test.ts
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");
});
});
51 changes: 51 additions & 0 deletions test/osml/utils/regional_config.test.ts
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);
});
});

0 comments on commit 8073de4

Please sign in to comment.