Skip to content

Commit 1afd00e

Browse files
authored
Merge branch 'master' into monocdk-ignore-rosetta
2 parents 2168658 + b307b69 commit 1afd00e

6 files changed

Lines changed: 235 additions & 23 deletions

File tree

packages/@aws-cdk/aws-codebuild/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
"jest": "^27.3.1"
9696
},
9797
"dependencies": {
98+
"@aws-cdk/assets": "0.0.0",
9899
"@aws-cdk/aws-cloudwatch": "0.0.0",
99100
"@aws-cdk/aws-codecommit": "0.0.0",
100101
"@aws-cdk/aws-codestarnotifications": "0.0.0",

packages/@aws-cdk/aws-servicecatalog/README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,36 @@ const launchRole = new iam.Role(this, 'LaunchRole', {
307307
portfolio.setLaunchRole(product, launchRole);
308308
```
309309

310+
You can also set the launch role using just the name of a role which is locally deployed in end user accounts.
311+
This is useful for when roles and users are separately managed outside of the CDK.
312+
The given role must exist in both the account that creates the launch role constraint,
313+
as well as in any end user accounts that wish to provision a product with the launch role.
314+
315+
You can do this by passing in the role with an explicitly set name:
316+
317+
```ts fixture=portfolio-product
318+
import * as iam from '@aws-cdk/aws-iam';
319+
320+
const launchRole = new iam.Role(this, 'LaunchRole', {
321+
roleName: 'MyRole',
322+
assumedBy: new iam.ServicePrincipal('servicecatalog.amazonaws.com'),
323+
});
324+
325+
portfolio.setLocalLaunchRole(product, launchRole);
326+
```
327+
328+
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:
329+
330+
```ts fixture=portfolio-product
331+
import * as iam from '@aws-cdk/aws-iam';
332+
333+
const roleName = 'MyRole';
334+
335+
const launchRole: iam.IRole = portfolio.setLocalLaunchRoleName(product, roleName);
336+
```
337+
310338
See [Launch Constraint](https://docs.aws.amazon.com/servicecatalog/latest/adminguide/constraints-launch.html) documentation
311-
to understand permissions roles need.
339+
to understand the permissions roles need.
312340

313341
### Deploy with StackSets
314342

packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ export interface IPortfolio extends cdk.IResource {
112112

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

122125
/**
123-
* Configure deployment options using AWS Cloudformaiton StackSets
126+
* Force users to assume a certain role when launching a product.
127+
* The role will be referenced by name in the local account instead of a static role arn.
128+
* A role with this name will automatically be created and assumable by Service Catalog in this account.
129+
* This is useful when sharing the portfolio with multiple accounts.
130+
*
131+
* @param product A service catalog product.
132+
* @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.
133+
* @param options options for the constraint.
134+
*/
135+
setLocalLaunchRoleName(product: IProduct, launchRoleName: string, options?: CommonConstraintOptions): iam.IRole;
136+
137+
/**
138+
* Force users to assume a certain role when launching a product.
139+
* The role name will be referenced by in the local account and must be set explicitly.
140+
* This is useful when sharing the portfolio with multiple accounts.
141+
*
142+
* @param product A service catalog product.
143+
* @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.
144+
* @param options options for the constraint.
145+
*/
146+
setLocalLaunchRole(product: IProduct, launchRole: iam.IRole, options?: CommonConstraintOptions): void;
147+
148+
/**
149+
* Configure deployment options using AWS Cloudformation StackSets
124150
*
125151
* @param product A service catalog product.
126152
* @param options Configuration options for the constraint.
@@ -179,6 +205,20 @@ abstract class PortfolioBase extends cdk.Resource implements IPortfolio {
179205
AssociationManager.setLaunchRole(this, product, launchRole, options);
180206
}
181207

208+
public setLocalLaunchRoleName(product: IProduct, launchRoleName: string, options: CommonConstraintOptions = {}): iam.IRole {
209+
const launchRole: iam.IRole = new iam.Role(this, `LaunchRole${launchRoleName}`, {
210+
roleName: launchRoleName,
211+
assumedBy: new iam.ServicePrincipal('servicecatalog.amazonaws.com'),
212+
});
213+
AssociationManager.setLocalLaunchRoleName(this, product, launchRole.roleName, options);
214+
return launchRole;
215+
}
216+
217+
public setLocalLaunchRole(product: IProduct, launchRole: iam.IRole, options: CommonConstraintOptions = {}): void {
218+
InputValidator.validateRoleNameSetForLocalLaunchRole(launchRole);
219+
AssociationManager.setLocalLaunchRoleName(this, product, launchRole.roleName, options);
220+
}
221+
182222
public deployWithStackSets(product: IProduct, options: StackSetsConstraintOptions) {
183223
AssociationManager.deployWithStackSets(this, product, options);
184224
}

packages/@aws-cdk/aws-servicecatalog/lib/private/association-manager.ts

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -100,27 +100,15 @@ export class AssociationManager {
100100
}
101101

102102
public static setLaunchRole(portfolio: IPortfolio, product: IProduct, launchRole: iam.IRole, options: CommonConstraintOptions): void {
103-
const association = this.associateProductWithPortfolio(portfolio, product, options);
104-
// Check if a stackset deployment constraint has already been configured.
105-
if (portfolio.node.tryFindChild(this.stackSetConstraintLogicalId(association.associationKey))) {
106-
throw new Error(`Cannot set launch role when a StackSet rule is already defined for association ${this.prettyPrintAssociation(portfolio, product)}`);
107-
}
108-
109-
const constructId = this.launchRoleConstraintLogicalId(association.associationKey);
110-
if (!portfolio.node.tryFindChild(constructId)) {
111-
const constraint = new CfnLaunchRoleConstraint(portfolio as unknown as cdk.Resource, constructId, {
112-
acceptLanguage: options.messageLanguage,
113-
description: options.description,
114-
portfolioId: portfolio.portfolioId,
115-
productId: product.productId,
116-
roleArn: launchRole.roleArn,
117-
});
103+
this.setLaunchRoleConstraint(portfolio, product, options, {
104+
roleArn: launchRole.roleArn,
105+
});
106+
}
118107

119-
// Add dependsOn to force proper order in deployment.
120-
constraint.addDependsOn(association.cfnPortfolioProductAssociation);
121-
} else {
122-
throw new Error(`Cannot set multiple launch roles for association ${this.prettyPrintAssociation(portfolio, product)}`);
123-
}
108+
public static setLocalLaunchRoleName(portfolio: IPortfolio, product: IProduct, launchRoleName: string, options: CommonConstraintOptions): void {
109+
this.setLaunchRoleConstraint(portfolio, product, options, {
110+
localRoleName: launchRoleName,
111+
});
124112
}
125113

126114
public static deployWithStackSets(portfolio: IPortfolio, product: IProduct, options: StackSetsConstraintOptions) {
@@ -179,6 +167,34 @@ export class AssociationManager {
179167
};
180168
}
181169

170+
private static setLaunchRoleConstraint(
171+
portfolio: IPortfolio, product: IProduct, options: CommonConstraintOptions,
172+
roleOptions: LaunchRoleConstraintRoleOptions,
173+
): void {
174+
const association = this.associateProductWithPortfolio(portfolio, product, options);
175+
// Check if a stackset deployment constraint has already been configured.
176+
if (portfolio.node.tryFindChild(this.stackSetConstraintLogicalId(association.associationKey))) {
177+
throw new Error(`Cannot set launch role when a StackSet rule is already defined for association ${this.prettyPrintAssociation(portfolio, product)}`);
178+
}
179+
180+
const constructId = this.launchRoleConstraintLogicalId(association.associationKey);
181+
if (!portfolio.node.tryFindChild(constructId)) {
182+
const constraint = new CfnLaunchRoleConstraint(portfolio as unknown as cdk.Resource, constructId, {
183+
acceptLanguage: options.messageLanguage,
184+
description: options.description,
185+
portfolioId: portfolio.portfolioId,
186+
productId: product.productId,
187+
roleArn: roleOptions.roleArn,
188+
localRoleName: roleOptions.localRoleName,
189+
});
190+
191+
// Add dependsOn to force proper order in deployment.
192+
constraint.addDependsOn(association.cfnPortfolioProductAssociation);
193+
} else {
194+
throw new Error(`Cannot set multiple launch roles for association ${this.prettyPrintAssociation(portfolio, product)}`);
195+
}
196+
}
197+
182198
private static stackSetConstraintLogicalId(associationKey: string): string {
183199
return `StackSetConstraint${associationKey}`;
184200
}
@@ -213,3 +229,14 @@ export class AssociationManager {
213229
};
214230
}
215231

232+
interface LaunchRoleArnOption {
233+
readonly roleArn: string,
234+
readonly localRoleName?: never,
235+
}
236+
237+
interface LaunchRoleNameOption {
238+
readonly localRoleName: string,
239+
readonly roleArn?: never,
240+
}
241+
242+
type LaunchRoleConstraintRoleOptions = LaunchRoleArnOption | LaunchRoleNameOption;

packages/@aws-cdk/aws-servicecatalog/lib/private/validation.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as iam from '@aws-cdk/aws-iam';
12
import * as cdk from '@aws-cdk/core';
23

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

40+
/**
41+
* Validates that a role being used as a local launch role has the role name set
42+
*/
43+
public static validateRoleNameSetForLocalLaunchRole(role: iam.IRole): void {
44+
if (role.node.defaultChild) {
45+
if (cdk.Token.isUnresolved((role.node.defaultChild as iam.CfnRole).roleName)) {
46+
throw new Error(`Role ${role.node.id} used for Local Launch Role must have roleName explicitly set`);
47+
}
48+
}
49+
}
50+
3951
private static truncateString(string: string, maxLength: number): string {
4052
if (string.length > maxLength) {
4153
return string.substring(0, maxLength) + '[truncated]';

packages/@aws-cdk/aws-servicecatalog/test/portfolio.test.ts

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,7 @@ describe('portfolio associations and product constraints', () => {
568568
assumedBy: new iam.AccountRootPrincipal(),
569569
});
570570
launchRole = new iam.Role(stack, 'LaunchRole', {
571+
roleName: 'LaunchRole',
571572
assumedBy: new iam.ServicePrincipal('servicecatalog.amazonaws.com'),
572573
});
573574
}),
@@ -591,6 +592,59 @@ describe('portfolio associations and product constraints', () => {
591592
});
592593
}),
593594

595+
test('set a launch role constraint using local role name', () => {
596+
portfolio.addProduct(product);
597+
598+
portfolio.setLocalLaunchRoleName(product, 'LocalLaunchRole', {
599+
description: 'set launch role description',
600+
messageLanguage: servicecatalog.MessageLanguage.EN,
601+
});
602+
603+
Template.fromStack(stack).hasResourceProperties('AWS::ServiceCatalog::LaunchRoleConstraint', {
604+
PortfolioId: { Ref: 'MyPortfolio59CCA9C9' },
605+
ProductId: { Ref: 'MyProduct49A3C587' },
606+
Description: 'set launch role description',
607+
AcceptLanguage: 'en',
608+
LocalRoleName: { Ref: 'MyPortfolioLaunchRoleLocalLaunchRoleB2E6E22A' },
609+
});
610+
}),
611+
612+
test('set a launch role constraint using local role', () => {
613+
portfolio.addProduct(product);
614+
615+
portfolio.setLocalLaunchRole(product, launchRole, {
616+
description: 'set launch role description',
617+
messageLanguage: servicecatalog.MessageLanguage.EN,
618+
});
619+
620+
Template.fromStack(stack).hasResourceProperties('AWS::ServiceCatalog::LaunchRoleConstraint', {
621+
PortfolioId: { Ref: 'MyPortfolio59CCA9C9' },
622+
ProductId: { Ref: 'MyProduct49A3C587' },
623+
Description: 'set launch role description',
624+
AcceptLanguage: 'en',
625+
LocalRoleName: { Ref: 'LaunchRole2CFB2E44' },
626+
});
627+
}),
628+
629+
test('set a launch role constraint using imported local role', () => {
630+
portfolio.addProduct(product);
631+
632+
const importedLaunchRole = iam.Role.fromRoleArn(portfolio.stack, 'ImportedLaunchRole', 'arn:aws:iam::123456789012:role/ImportedLaunchRole');
633+
634+
portfolio.setLocalLaunchRole(product, importedLaunchRole, {
635+
description: 'set launch role description',
636+
messageLanguage: servicecatalog.MessageLanguage.EN,
637+
});
638+
639+
Template.fromStack(stack).hasResourceProperties('AWS::ServiceCatalog::LaunchRoleConstraint', {
640+
PortfolioId: { Ref: 'MyPortfolio59CCA9C9' },
641+
ProductId: { Ref: 'MyProduct49A3C587' },
642+
Description: 'set launch role description',
643+
AcceptLanguage: 'en',
644+
LocalRoleName: 'ImportedLaunchRole',
645+
});
646+
}),
647+
594648
test('set launch role constraint still adds without explicit association', () => {
595649
portfolio.setLaunchRole(product, launchRole);
596650

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

607661
expect(() => {
608662
portfolio.setLaunchRole(product, otherLaunchRole);
609-
}).toThrowError(/Cannot set multiple launch roles for association/);
663+
}).toThrow(/Cannot set multiple launch roles for association/);
664+
}),
665+
666+
test('local launch role must have roleName explicitly set', () => {
667+
const otherLaunchRole = new iam.Role(stack, 'otherLaunchRole', {
668+
assumedBy: new iam.ServicePrincipal('servicecatalog.amazonaws.com'),
669+
});
670+
671+
expect(() => {
672+
portfolio.setLocalLaunchRole(product, otherLaunchRole);
673+
}).toThrow(/Role otherLaunchRole used for Local Launch Role must have roleName explicitly set/);
674+
}),
675+
676+
test('fails to add multiple set launch roles - local launch role first', () => {
677+
portfolio.setLocalLaunchRoleName(product, 'LaunchRole');
678+
679+
expect(() => {
680+
portfolio.setLaunchRole(product, launchRole);
681+
}).toThrow(/Cannot set multiple launch roles for association/);
682+
}),
683+
684+
test('fails to add multiple set local launch roles - local launch role first', () => {
685+
portfolio.setLocalLaunchRoleName(product, 'LaunchRole');
686+
687+
expect(() => {
688+
portfolio.setLocalLaunchRole(product, launchRole);
689+
}).toThrow(/Cannot set multiple launch roles for association/);
690+
}),
691+
692+
test('fails to add multiple set local launch roles - local launch role name first', () => {
693+
portfolio.setLocalLaunchRole(product, launchRole);
694+
695+
expect(() => {
696+
portfolio.setLocalLaunchRoleName(product, 'LaunchRole');
697+
}).toThrow(/Cannot set multiple launch roles for association/);
698+
}),
699+
700+
test('fails to add multiple set launch roles - local launch role second', () => {
701+
portfolio.setLaunchRole(product, launchRole);
702+
703+
expect(() => {
704+
portfolio.setLocalLaunchRole(product, launchRole);
705+
}).toThrow(/Cannot set multiple launch roles for association/);
706+
}),
707+
708+
test('fails to add multiple set launch roles - local launch role second', () => {
709+
portfolio.setLaunchRole(product, launchRole);
710+
711+
expect(() => {
712+
portfolio.setLocalLaunchRoleName(product, 'LaunchRole');
713+
}).toThrow(/Cannot set multiple launch roles for association/);
610714
}),
611715

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

0 commit comments

Comments
 (0)