Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(servicecatalog): support local launch role name in launch role constraint #17371

Merged
merged 23 commits into from
Nov 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
87a3386
feat(aws-servicecatalog): support local launch role name in launch ro…
dponzo Nov 5, 2021
0d4915d
Update packages/@aws-cdk/aws-servicecatalog/README.md
dponzo Nov 8, 2021
aa05c51
Update packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts
dponzo Nov 8, 2021
e88edf7
Update packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts
dponzo Nov 8, 2021
30fad55
Update packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts
dponzo Nov 8, 2021
d7faffd
fixed readme role name
dponzo Nov 8, 2021
89c6f3e
fixed readme wording
dponzo Nov 8, 2021
a4e8047
fixed readme wording for portfolio local launch role
dponzo Nov 8, 2021
926b53c
Update packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts
dponzo Nov 8, 2021
8d5440f
Update packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts
dponzo Nov 8, 2021
ab205f4
empty line added
dponzo Nov 8, 2021
7309c4b
add an api that accepts a role and uses its name as well as an api th…
dponzo Nov 8, 2021
720cafa
add test for imported local launch role
dponzo Nov 8, 2021
91c08d1
made role options clearer and correctly named
dponzo Nov 9, 2021
5713838
Merge branch 'master' into local-launch-role
dponzo Nov 9, 2021
9ade69d
adding validation that a role name must be set on a role used as a lo…
dponzo Nov 9, 2021
3a96a19
onelined roleName
dponzo Nov 9, 2021
8bf2a33
Merge branch 'master' into local-launch-role
dponzo Nov 9, 2021
498cf0f
changed test semicolons to commas
dponzo Nov 9, 2021
9356514
Merge branch 'master' into local-launch-role
dponzo Nov 9, 2021
64de0ef
Update packages/@aws-cdk/aws-servicecatalog/lib/private/association-m…
dponzo Nov 10, 2021
d6f99ad
address pr comments
dponzo Nov 10, 2021
7a17db8
Merge branch 'master' into local-launch-role
dponzo Nov 10, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion packages/@aws-cdk/aws-servicecatalog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,36 @@ const launchRole = new iam.Role(this, 'LaunchRole', {
portfolio.setLaunchRole(product, launchRole);
```

You can also set the launch role using just the name of a role which is locally deployed in end user accounts.
This is useful for when roles and users are separately managed outside of the CDK.
The given role must exist in both the account that creates the launch role constraint,
as well as in any end user accounts that wish to provision a product with the launch role.

You can do this by passing in the role with an explicitly set name:

```ts fixture=portfolio-product
import * as iam from '@aws-cdk/aws-iam';

const launchRole = new iam.Role(this, 'LaunchRole', {
roleName: 'MyRole',
assumedBy: new iam.ServicePrincipal('servicecatalog.amazonaws.com'),
});

portfolio.setLocalLaunchRole(product, launchRole);
```

Or you can simply pass in a role name and CDK will create a role with that name that trusts service catalog in the account:

```ts fixture=portfolio-product
import * as iam from '@aws-cdk/aws-iam';

const roleName = 'MyRole';

const launchRole: iam.IRole = portfolio.setLocalLaunchRoleName(product, roleName);
```

dponzo marked this conversation as resolved.
Show resolved Hide resolved
See [Launch Constraint](https://docs.aws.amazon.com/servicecatalog/latest/adminguide/constraints-launch.html) documentation
to understand permissions roles need.
to understand the permissions roles need.

### Deploy with StackSets

Expand Down
42 changes: 41 additions & 1 deletion packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ export interface IPortfolio extends cdk.IResource {

/**
* Force users to assume a certain role when launching a product.
* This sets the launch role using the role arn which is tied to the account this role exists in.
* This is useful if you will be provisioning products from the account where this role exists.
* If you intend to share the portfolio across accounts, use a local launch role.
*
* @param product A service catalog product.
* @param launchRole The IAM role a user must assume when provisioning the product.
Expand All @@ -120,7 +123,30 @@ export interface IPortfolio extends cdk.IResource {
setLaunchRole(product: IProduct, launchRole: iam.IRole, options?: CommonConstraintOptions): void;

/**
* Configure deployment options using AWS Cloudformaiton StackSets
* Force users to assume a certain role when launching a product.
* The role will be referenced by name in the local account instead of a static role arn.
* A role with this name will automatically be created and assumable by Service Catalog in this account.
* This is useful when sharing the portfolio with multiple accounts.
*
* @param product A service catalog product.
* @param launchRoleName The name of the IAM role a user must assume when provisioning the product. A role with this name must exist in the account where the portolio is created and the accounts it is shared with.
* @param options options for the constraint.
*/
setLocalLaunchRoleName(product: IProduct, launchRoleName: string, options?: CommonConstraintOptions): iam.IRole;

/**
* Force users to assume a certain role when launching a product.
* The role name will be referenced by in the local account and must be set explicitly.
* This is useful when sharing the portfolio with multiple accounts.
*
* @param product A service catalog product.
* @param launchRole The IAM role a user must assume when provisioning the product. A role with this name must exist in the account where the portolio is created and the accounts it is shared with. The role name must be set explicitly.
* @param options options for the constraint.
*/
setLocalLaunchRole(product: IProduct, launchRole: iam.IRole, options?: CommonConstraintOptions): void;
dponzo marked this conversation as resolved.
Show resolved Hide resolved

/**
* Configure deployment options using AWS Cloudformation StackSets
*
* @param product A service catalog product.
* @param options Configuration options for the constraint.
Expand Down Expand Up @@ -179,6 +205,20 @@ abstract class PortfolioBase extends cdk.Resource implements IPortfolio {
AssociationManager.setLaunchRole(this, product, launchRole, options);
}

public setLocalLaunchRoleName(product: IProduct, launchRoleName: string, options: CommonConstraintOptions = {}): iam.IRole {
const launchRole: iam.IRole = new iam.Role(this, `LaunchRole${launchRoleName}`, {
roleName: launchRoleName,
assumedBy: new iam.ServicePrincipal('servicecatalog.amazonaws.com'),
});
AssociationManager.setLocalLaunchRoleName(this, product, launchRole.roleName, options);
return launchRole;
}

public setLocalLaunchRole(product: IProduct, launchRole: iam.IRole, options: CommonConstraintOptions = {}): void {
InputValidator.validateRoleNameSetForLocalLaunchRole(launchRole);
AssociationManager.setLocalLaunchRoleName(this, product, launchRole.roleName, options);
}

public deployWithStackSets(product: IProduct, options: StackSetsConstraintOptions) {
AssociationManager.deployWithStackSets(this, product, options);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,27 +100,15 @@ export class AssociationManager {
}

public static setLaunchRole(portfolio: IPortfolio, product: IProduct, launchRole: iam.IRole, options: CommonConstraintOptions): void {
const association = this.associateProductWithPortfolio(portfolio, product, options);
// Check if a stackset deployment constraint has already been configured.
if (portfolio.node.tryFindChild(this.stackSetConstraintLogicalId(association.associationKey))) {
throw new Error(`Cannot set launch role when a StackSet rule is already defined for association ${this.prettyPrintAssociation(portfolio, product)}`);
}

const constructId = this.launchRoleConstraintLogicalId(association.associationKey);
if (!portfolio.node.tryFindChild(constructId)) {
const constraint = new CfnLaunchRoleConstraint(portfolio as unknown as cdk.Resource, constructId, {
acceptLanguage: options.messageLanguage,
description: options.description,
portfolioId: portfolio.portfolioId,
productId: product.productId,
roleArn: launchRole.roleArn,
});
this.setLaunchRoleConstraint(portfolio, product, options, {
roleArn: launchRole.roleArn,
});
}

// Add dependsOn to force proper order in deployment.
constraint.addDependsOn(association.cfnPortfolioProductAssociation);
} else {
throw new Error(`Cannot set multiple launch roles for association ${this.prettyPrintAssociation(portfolio, product)}`);
}
public static setLocalLaunchRoleName(portfolio: IPortfolio, product: IProduct, launchRoleName: string, options: CommonConstraintOptions): void {
this.setLaunchRoleConstraint(portfolio, product, options, {
localRoleName: launchRoleName,
});
}

public static deployWithStackSets(portfolio: IPortfolio, product: IProduct, options: StackSetsConstraintOptions) {
Expand Down Expand Up @@ -179,6 +167,34 @@ export class AssociationManager {
};
}

private static setLaunchRoleConstraint(
portfolio: IPortfolio, product: IProduct, options: CommonConstraintOptions,
roleOptions: LaunchRoleConstraintRoleOptions,
): void {
const association = this.associateProductWithPortfolio(portfolio, product, options);
// Check if a stackset deployment constraint has already been configured.
if (portfolio.node.tryFindChild(this.stackSetConstraintLogicalId(association.associationKey))) {
throw new Error(`Cannot set launch role when a StackSet rule is already defined for association ${this.prettyPrintAssociation(portfolio, product)}`);
}

const constructId = this.launchRoleConstraintLogicalId(association.associationKey);
if (!portfolio.node.tryFindChild(constructId)) {
const constraint = new CfnLaunchRoleConstraint(portfolio as unknown as cdk.Resource, constructId, {
acceptLanguage: options.messageLanguage,
description: options.description,
portfolioId: portfolio.portfolioId,
productId: product.productId,
roleArn: roleOptions.roleArn,
localRoleName: roleOptions.localRoleName,
});

// Add dependsOn to force proper order in deployment.
constraint.addDependsOn(association.cfnPortfolioProductAssociation);
} else {
throw new Error(`Cannot set multiple launch roles for association ${this.prettyPrintAssociation(portfolio, product)}`);
}
}

private static stackSetConstraintLogicalId(associationKey: string): string {
return `StackSetConstraint${associationKey}`;
}
Expand Down Expand Up @@ -213,3 +229,14 @@ export class AssociationManager {
};
}

interface LaunchRoleArnOption {
readonly roleArn: string,
readonly localRoleName?: never,
}

interface LaunchRoleNameOption {
readonly localRoleName: string,
readonly roleArn?: never,
}

type LaunchRoleConstraintRoleOptions = LaunchRoleArnOption | LaunchRoleNameOption;
12 changes: 12 additions & 0 deletions packages/@aws-cdk/aws-servicecatalog/lib/private/validation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as iam from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';

/**
Expand Down Expand Up @@ -36,6 +37,17 @@ export class InputValidator {
this.validateRegex(resourceName, inputName, /^[\w\d.%+\-]+@[a-z\d.\-]+\.[a-z]{2,4}$/i, inputString);
}

/**
* Validates that a role being used as a local launch role has the role name set
*/
public static validateRoleNameSetForLocalLaunchRole(role: iam.IRole): void {
if (role.node.defaultChild) {
if (cdk.Token.isUnresolved((role.node.defaultChild as iam.CfnRole).roleName)) {
throw new Error(`Role ${role.node.id} used for Local Launch Role must have roleName explicitly set`);
}
}
}

private static truncateString(string: string, maxLength: number): string {
if (string.length > maxLength) {
return string.substring(0, maxLength) + '[truncated]';
Expand Down
106 changes: 105 additions & 1 deletion packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@ describe('portfolio associations and product constraints', () => {
assumedBy: new iam.AccountRootPrincipal(),
});
launchRole = new iam.Role(stack, 'LaunchRole', {
roleName: 'LaunchRole',
assumedBy: new iam.ServicePrincipal('servicecatalog.amazonaws.com'),
});
}),
Expand All @@ -591,6 +592,59 @@ describe('portfolio associations and product constraints', () => {
});
}),

test('set a launch role constraint using local role name', () => {
portfolio.addProduct(product);

portfolio.setLocalLaunchRoleName(product, 'LocalLaunchRole', {
description: 'set launch role description',
messageLanguage: servicecatalog.MessageLanguage.EN,
});

Template.fromStack(stack).hasResourceProperties('AWS::ServiceCatalog::LaunchRoleConstraint', {
PortfolioId: { Ref: 'MyPortfolio59CCA9C9' },
ProductId: { Ref: 'MyProduct49A3C587' },
Description: 'set launch role description',
AcceptLanguage: 'en',
LocalRoleName: { Ref: 'MyPortfolioLaunchRoleLocalLaunchRoleB2E6E22A' },
});
}),

test('set a launch role constraint using local role', () => {
portfolio.addProduct(product);

portfolio.setLocalLaunchRole(product, launchRole, {
description: 'set launch role description',
messageLanguage: servicecatalog.MessageLanguage.EN,
});

Template.fromStack(stack).hasResourceProperties('AWS::ServiceCatalog::LaunchRoleConstraint', {
PortfolioId: { Ref: 'MyPortfolio59CCA9C9' },
ProductId: { Ref: 'MyProduct49A3C587' },
Description: 'set launch role description',
AcceptLanguage: 'en',
LocalRoleName: { Ref: 'LaunchRole2CFB2E44' },
});
}),

test('set a launch role constraint using imported local role', () => {
portfolio.addProduct(product);

const importedLaunchRole = iam.Role.fromRoleArn(portfolio.stack, 'ImportedLaunchRole', 'arn:aws:iam::123456789012:role/ImportedLaunchRole');

portfolio.setLocalLaunchRole(product, importedLaunchRole, {
description: 'set launch role description',
messageLanguage: servicecatalog.MessageLanguage.EN,
});

Template.fromStack(stack).hasResourceProperties('AWS::ServiceCatalog::LaunchRoleConstraint', {
PortfolioId: { Ref: 'MyPortfolio59CCA9C9' },
ProductId: { Ref: 'MyProduct49A3C587' },
Description: 'set launch role description',
AcceptLanguage: 'en',
LocalRoleName: 'ImportedLaunchRole',
});
}),

test('set launch role constraint still adds without explicit association', () => {
portfolio.setLaunchRole(product, launchRole);

Expand All @@ -606,7 +660,57 @@ describe('portfolio associations and product constraints', () => {

expect(() => {
portfolio.setLaunchRole(product, otherLaunchRole);
}).toThrowError(/Cannot set multiple launch roles for association/);
}).toThrow(/Cannot set multiple launch roles for association/);
}),

test('local launch role must have roleName explicitly set', () => {
const otherLaunchRole = new iam.Role(stack, 'otherLaunchRole', {
assumedBy: new iam.ServicePrincipal('servicecatalog.amazonaws.com'),
});

expect(() => {
portfolio.setLocalLaunchRole(product, otherLaunchRole);
}).toThrow(/Role otherLaunchRole used for Local Launch Role must have roleName explicitly set/);
}),

test('fails to add multiple set launch roles - local launch role first', () => {
portfolio.setLocalLaunchRoleName(product, 'LaunchRole');

expect(() => {
portfolio.setLaunchRole(product, launchRole);
}).toThrow(/Cannot set multiple launch roles for association/);
}),

test('fails to add multiple set local launch roles - local launch role first', () => {
portfolio.setLocalLaunchRoleName(product, 'LaunchRole');

expect(() => {
portfolio.setLocalLaunchRole(product, launchRole);
}).toThrow(/Cannot set multiple launch roles for association/);
}),

test('fails to add multiple set local launch roles - local launch role name first', () => {
portfolio.setLocalLaunchRole(product, launchRole);

expect(() => {
portfolio.setLocalLaunchRoleName(product, 'LaunchRole');
}).toThrow(/Cannot set multiple launch roles for association/);
}),
dponzo marked this conversation as resolved.
Show resolved Hide resolved

test('fails to add multiple set launch roles - local launch role second', () => {
portfolio.setLaunchRole(product, launchRole);
dponzo marked this conversation as resolved.
Show resolved Hide resolved

expect(() => {
portfolio.setLocalLaunchRole(product, launchRole);
}).toThrow(/Cannot set multiple launch roles for association/);
}),

test('fails to add multiple set launch roles - local launch role second', () => {
portfolio.setLaunchRole(product, launchRole);

expect(() => {
portfolio.setLocalLaunchRoleName(product, 'LaunchRole');
}).toThrow(/Cannot set multiple launch roles for association/);
}),
dponzo marked this conversation as resolved.
Show resolved Hide resolved

test('fails to set launch role if stackset rule is already defined', () => {
Expand Down