diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/README.md b/packages/@aws-cdk/aws-servicecatalogappregistry/README.md index 6fcee1d94bf9c..e96a13bc8b2db 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/README.md +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/README.md @@ -31,6 +31,9 @@ enables organizations to create and manage repositores of applications and assoc - [Associations](#associations) - [Associating application with an attribute group](#attribute-group-association) - [Associating application with a stack](#resource-association) +- [Sharing](#sharing) + - [Sharing an application](#sharing-an-application) + - [Sharing an attribute group](#sharing-an-attribute-group) The `@aws-cdk/aws-servicecatalogappregistry` package contains resources that enable users to automate governance and management of their AWS resources at scale. @@ -124,3 +127,59 @@ const myStack = new Stack(app, 'MyStack'); declare const application: appreg.Application; application.associateStack(myStack); ``` + +## Sharing + +You can share your AppRegistry applications and attribute groups with AWS Organizations, Organizational Units (OUs), AWS accounts within an organization, as well as IAM roles and users. AppRegistry requires that AWS Organizations is enabled in an account before deploying a share of an application or attribute group. + +### Sharing an application + +```ts +import * as iam from '@aws-cdk/aws-iam'; +declare const application: appreg.Application; +declare const myRole: iam.IRole; +declare const myUser: iam.IUser; +application.shareApplication({ + accounts: ['123456789012'], + organizationArns: ['arn:aws:organizations::123456789012:organization/o-my-org-id'], + roles: [myRole], + users: [myUser], +}); +``` + +E.g., sharing an application with multiple accounts and allowing the accounts to associate resources to the application. + +```ts +import * as iam from '@aws-cdk/aws-iam'; +declare const application: appreg.Application; +application.shareApplication({ + accounts: ['123456789012', '234567890123'], + sharePermission: appreg.SharePermission.ALLOW_ACCESS, +}); +``` + +### Sharing an attribute group + +```ts +import * as iam from '@aws-cdk/aws-iam'; +declare const attributeGroup: appreg.AttributeGroup; +declare const myRole: iam.IRole; +declare const myUser: iam.IUser; +attributeGroup.shareAttributeGroup({ + accounts: ['123456789012'], + organizationArns: ['arn:aws:organizations::123456789012:organization/o-my-org-id'], + roles: [myRole], + users: [myUser], +}); +``` + +E.g., sharing an application with multiple accounts and allowing the accounts to associate applications to the attribute group. + +```ts +import * as iam from '@aws-cdk/aws-iam'; +declare const attributeGroup: appreg.AttributeGroup; +attributeGroup.shareAttributeGroup({ + accounts: ['123456789012', '234567890123'], + sharePermission: appreg.SharePermission.ALLOW_ACCESS, +}); +``` diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts index 7fcdc313102a0..256de4099afd0 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts @@ -1,10 +1,15 @@ -import * as crypto from 'crypto'; +import { CfnResourceShare } from '@aws-cdk/aws-ram'; import * as cdk from '@aws-cdk/core'; +import { Names } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { IAttributeGroup } from './attribute-group'; +import { getPrincipalsforSharing, hashValues, ShareOptions, SharePermission } from './common'; import { InputValidator } from './private/validation'; import { CfnApplication, CfnAttributeGroupAssociation, CfnResourceAssociation } from './servicecatalogappregistry.generated'; +const APPLICATION_READ_ONLY_RAM_PERMISSION_ARN = 'arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly'; +const APPLICATION_ALLOW_ACCESS_RAM_PERMISSION_ARN = 'arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationAllowAssociation'; + /** * A Service Catalog AppRegistry Application. */ @@ -23,15 +28,24 @@ export interface IApplication extends cdk.IResource { /** * Associate thisapplication with an attribute group. + * * @param attributeGroup AppRegistry attribute group */ associateAttributeGroup(attributeGroup: IAttributeGroup): void; /** * Associate this application with a CloudFormation stack. + * * @param stack a CFN stack */ associateStack(stack: cdk.Stack): void; + + /** + * Share this application with other IAM entities, accounts, or OUs. + * + * @param shareOptions The options for the share. + */ + shareApplication(shareOptions: ShareOptions): void; } /** @@ -88,10 +102,43 @@ abstract class ApplicationBase extends cdk.Resource implements IApplication { } } + /** + * Share an application with accounts, organizations and OUs, and IAM roles and users. + * The application will become available to end users within those principals. + * + * @param shareOptions The options for the share. + */ + public shareApplication(shareOptions: ShareOptions): void { + const principals = getPrincipalsforSharing(shareOptions); + const shareName = `RAMShare${hashValues(Names.nodeUniqueId(this.node), this.node.children.length.toString())}`; + new CfnResourceShare(this, shareName, { + name: shareName, + allowExternalPrincipals: false, + principals: principals, + resourceArns: [this.applicationArn], + permissionArns: [this.getApplicationSharePermissionARN(shareOptions)], + }); + } + /** * Create a unique id */ protected abstract generateUniqueHash(resourceAddress: string): string; + + /** + * Get the correct permission ARN based on the SharePermission + */ + private getApplicationSharePermissionARN(shareOptions: ShareOptions): string { + switch (shareOptions.sharePermission) { + case SharePermission.ALLOW_ACCESS: + return APPLICATION_ALLOW_ACCESS_RAM_PERMISSION_ARN; + case SharePermission.READ_ONLY: + return APPLICATION_READ_ONLY_RAM_PERMISSION_ARN; + + default: + return shareOptions.sharePermission ?? APPLICATION_READ_ONLY_RAM_PERMISSION_ARN; + } + } } /** @@ -156,12 +203,3 @@ export class Application extends ApplicationBase { InputValidator.validateLength(this.node.path, 'application description', 0, 1024, props.description); } } - -/** - * Generates a unique hash identfifer using SHA256 encryption algorithm - */ -function hashValues(...values: string[]): string { - const sha256 = crypto.createHash('sha256'); - values.forEach(val => sha256.update(val)); - return sha256.digest('hex').slice(0, 12); -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/attribute-group.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/attribute-group.ts index 02af6db135a6e..ea5a893422aa5 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/attribute-group.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/attribute-group.ts @@ -1,7 +1,13 @@ +import { CfnResourceShare } from '@aws-cdk/aws-ram'; import * as cdk from '@aws-cdk/core'; +import { getPrincipalsforSharing, hashValues, ShareOptions, SharePermission } from './common'; import { Construct } from 'constructs'; import { InputValidator } from './private/validation'; import { CfnAttributeGroup } from './servicecatalogappregistry.generated'; +import { Names } from '@aws-cdk/core'; + +const ATTRIBUTE_GROUP_READ_ONLY_RAM_PERMISSION_ARN = 'arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryAttributeGroupReadOnly'; +const ATTRIBUTE_GROUP_ALLOW_ACCESS_RAM_PERMISSION_ARN = 'arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryAttributeGroupAllowAssociation'; /** * A Service Catalog AppRegistry Attribute Group. @@ -18,6 +24,13 @@ export interface IAttributeGroup extends cdk.IResource { * @attribute */ readonly attributeGroupId: string; + + /** + * Share the attribute group resource with other IAM entities, accounts, or OUs. + * + * @param shareOptions The options for the share. + */ + shareAttributeGroup(shareOptions: ShareOptions): void; } /** @@ -45,6 +58,33 @@ export interface AttributeGroupProps { abstract class AttributeGroupBase extends cdk.Resource implements IAttributeGroup { public abstract readonly attributeGroupArn: string; public abstract readonly attributeGroupId: string; + + public shareAttributeGroup(shareOptions: ShareOptions): void { + const principals = getPrincipalsforSharing(shareOptions); + const shareName = `RAMShare${hashValues(Names.nodeUniqueId(this.node), this.node.children.length.toString())}`; + new CfnResourceShare(this, shareName, { + name: shareName, + allowExternalPrincipals: false, + principals: principals, + resourceArns: [this.attributeGroupArn], + permissionArns: [this.getAttributeGroupSharePermissionARN(shareOptions)], + }); + } + + /** + * Get the correct permission ARN based on the SharePermission + */ + protected getAttributeGroupSharePermissionARN(shareOptions: ShareOptions): string { + switch (shareOptions.sharePermission) { + case SharePermission.ALLOW_ACCESS: + return ATTRIBUTE_GROUP_ALLOW_ACCESS_RAM_PERMISSION_ARN; + case SharePermission.READ_ONLY: + return ATTRIBUTE_GROUP_READ_ONLY_RAM_PERMISSION_ARN; + + default: + return shareOptions.sharePermission ?? ATTRIBUTE_GROUP_READ_ONLY_RAM_PERMISSION_ARN; + } + } } /** diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/common.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/common.ts new file mode 100644 index 0000000000000..148ddb1637c03 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/common.ts @@ -0,0 +1,87 @@ +import * as crypto from 'crypto'; +import * as iam from '@aws-cdk/aws-iam'; + +/** + * Supported permissions for sharing applications or attribute groups with principals using AWS RAM. + */ +export enum SharePermission { + /** + * Allows principals in the share to only view the application or attribute group. + */ + READ_ONLY, + + /** + * Allows principals in the share to associate resources and attribute groups with applications. + */ + ALLOW_ACCESS, +}; + +/** + * The options that are passed into a share of an Application or Attribute Group. + */ +export interface ShareOptions { + /** + * A list of AWS accounts that the application will be shared with. + * + * @default - No accounts specified for share + */ + readonly accounts?: string[]; + + /** + * A list of AWS Organization or Organizational Units (OUs) ARNs that the application will be shared with. + * + * @default - No AWS Organizations or OUs specified for share + */ + readonly organizationArns?: string[]; + + /** + * A list of AWS IAM roles that the application will be shared with. + * + * @default - No IAM roles specified for share + */ + readonly roles?: iam.IRole[]; + + /** + * An option to manage access to the application or attribute group. + * + * @default - Principals will be assigned read only permissions on the application or attribute group. + */ + readonly sharePermission?: SharePermission | string; + + /** + * A list of AWS IAM users that the application will be shared with. + * + * @default - No IAM Users specified for share + */ + readonly users?: iam.IUser[]; +} + +/** + * Generates a unique hash identfifer using SHA256 encryption algorithm. + */ +export function hashValues(...values: string[]): string { + const sha256 = crypto.createHash('sha256'); + values.forEach(val => sha256.update(val)); + return sha256.digest('hex').slice(0, 12); +} + +/** + * Reformats share targets into a collapsed list necessary for handler. + * + * @param options The share target options + * @returns flat list of target ARNs + */ +export function getPrincipalsforSharing(options: ShareOptions): string[] { + const principals = [ + ...options.accounts ?? [], + ...options.organizationArns ?? [], + ...options.users ? options.users.map(user => user.userArn) : [], + ...options.roles ? options.roles.map(role => role.roleArn) : [], + ]; + + if (principals.length == 0) { + throw new Error('An entity must be provided for the share'); + } + + return principals; +} diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/index.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/index.ts index 8f4ec75704657..adbf2a9febfe6 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/index.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/index.ts @@ -1,5 +1,6 @@ export * from './application'; export * from './attribute-group'; +export * from './common'; // AWS::ServiceCatalogAppRegistry CloudFormation Resources: export * from './servicecatalogappregistry.generated'; diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/package.json b/packages/@aws-cdk/aws-servicecatalogappregistry/package.json index edb96a90a27de..f4d1243ef7542 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/package.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/package.json @@ -92,10 +92,14 @@ }, "dependencies": { "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-ram": "0.0.0", "constructs": "^10.0.0" }, "peerDependencies": { "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-ram": "0.0.0", "constructs": "^10.0.0" }, "engines": { diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/cdk.out index 90bef2e09ad39..588d7b269d34f 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/cdk.out @@ -1 +1 @@ -{"version":"17.0.0"} \ No newline at end of file +{"version":"20.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/integ-servicecatalogappregistry-application.assets.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/integ-servicecatalogappregistry-application.assets.json index 17507827bfe90..7f5db7d1e1328 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/integ-servicecatalogappregistry-application.assets.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/integ-servicecatalogappregistry-application.assets.json @@ -1,7 +1,7 @@ { - "version": "17.0.0", + "version": "20.0.0", "files": { - "d561cf6d9aa2d98689712d70accb1c3f56f2a54d6cbb1268d35bd72e05675791": { + "d03aa6239eb3b20f4b72fb3dd44a4082d06d7a5451d0ac3855bd1aa78aecfbe9": { "source": { "path": "integ-servicecatalogappregistry-application.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "d561cf6d9aa2d98689712d70accb1c3f56f2a54d6cbb1268d35bd72e05675791.json", + "objectKey": "d03aa6239eb3b20f4b72fb3dd44a4082d06d7a5451d0ac3855bd1aa78aecfbe9.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/integ-servicecatalogappregistry-application.template.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/integ-servicecatalogappregistry-application.template.json index 10dfe70894fd7..a083f478debfd 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/integ-servicecatalogappregistry-application.template.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/integ-servicecatalogappregistry-application.template.json @@ -39,6 +39,32 @@ } } }, + "TestApplicationRAMSharead8ba81b8cdd40199FD1": { + "Type": "AWS::RAM::ResourceShare", + "Properties": { + "Name": "RAMSharead8ba81b8cdd", + "AllowExternalPrincipals": false, + "PermissionArns": [ + "arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly" + ], + "Principals": [ + { + "Fn::GetAtt": [ + "MyRoleF48FFE04", + "Arn" + ] + } + ], + "ResourceArns": [ + { + "Fn::GetAtt": [ + "TestApplication2FBC585F", + "Arn" + ] + } + ] + } + }, "TestAttributeGroupB1CB284F": { "Type": "AWS::ServiceCatalogAppRegistry::AttributeGroup", "Properties": { @@ -61,6 +87,38 @@ "Name": "myAttributeGroupTest", "Description": "my attribute group description" } + }, + "MyRoleF48FFE04": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/manifest.json index 34833a17ec6d5..e4ec7abf302a2 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "17.0.0", + "version": "20.0.0", "artifacts": { "Tree": { "type": "cdk:tree", @@ -33,11 +33,32 @@ "data": "TestApplicationAttributeGroupAssociation4ba7f5842818B8EE1C6F" } ], + "/integ-servicecatalogappregistry-application/TestApplication/RAMSharead8ba81b8cdd": [ + { + "type": "aws:cdk:logicalId", + "data": "TestApplicationRAMSharead8ba81b8cdd40199FD1" + } + ], "/integ-servicecatalogappregistry-application/TestAttributeGroup/Resource": [ { "type": "aws:cdk:logicalId", "data": "TestAttributeGroupB1CB284F" } + ], + "/integ-servicecatalogappregistry-application/MyRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyRoleF48FFE04" + } + ], + "TestApplicationRAMShareintegservicecatalogappregistryapplicationTestApplicationF7821DB233482C38": [ + { + "type": "aws:cdk:logicalId", + "data": "TestApplicationRAMShareintegservicecatalogappregistryapplicationTestApplicationF7821DB233482C38", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" + ] + } ] }, "displayName": "integ-servicecatalogappregistry-application" diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/tree.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/tree.json index 1f5ec26e247d0..7e7c3a99234d4 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/tree.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.integ.snapshot/tree.json @@ -9,7 +9,7 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.0.9" + "version": "10.1.51" } }, "integ-servicecatalogappregistry-application": { @@ -82,6 +82,40 @@ "fqn": "@aws-cdk/aws-servicecatalogappregistry.CfnAttributeGroupAssociation", "version": "0.0.0" } + }, + "RAMSharead8ba81b8cdd": { + "id": "RAMSharead8ba81b8cdd", + "path": "integ-servicecatalogappregistry-application/TestApplication/RAMSharead8ba81b8cdd", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::RAM::ResourceShare", + "aws:cdk:cloudformation:props": { + "name": "RAMSharead8ba81b8cdd", + "allowExternalPrincipals": false, + "permissionArns": [ + "arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly" + ], + "principals": [ + { + "Fn::GetAtt": [ + "MyRoleF48FFE04", + "Arn" + ] + } + ], + "resourceArns": [ + { + "Fn::GetAtt": [ + "TestApplication2FBC585F", + "Arn" + ] + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ram.CfnResourceShare", + "version": "0.0.0" + } } }, "constructInfo": { @@ -129,6 +163,56 @@ "fqn": "@aws-cdk/aws-servicecatalogappregistry.AttributeGroup", "version": "0.0.0" } + }, + "MyRole": { + "id": "MyRole", + "path": "integ-servicecatalogappregistry-application/MyRole", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-servicecatalogappregistry-application/MyRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } } }, "constructInfo": { diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.test.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.test.ts index f2d88f4c5abe6..33f1ca2628cdb 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.test.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.test.ts @@ -1,4 +1,5 @@ import { Template } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as appreg from '../lib'; @@ -216,4 +217,110 @@ describe('Application', () => { Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); }); }); + + describe('Resource sharing of an application', () => { + let application: appreg.Application; + + beforeEach(() => { + application = new appreg.Application(stack, 'MyApplication', { + applicationName: 'MyApplication', + }); + }); + + test('fails for sharing application without principals', () => { + expect(() => { + application.shareApplication({}); + }).toThrow(/An entity must be provided for the share/); + }); + + test('share application with an organization', () => { + application.shareApplication({ + organizationArns: ['arn:aws:organizations::123456789012:organization/o-70oi5564q1'], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', { + AllowExternalPrincipals: false, + Name: 'RAMSharee6e0e560e6f8', + Principals: ['arn:aws:organizations::123456789012:organization/o-70oi5564q1'], + ResourceArns: [{ 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Arn'] }], + PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly'], + }); + }); + + test('share application with an account', () => { + application.shareApplication({ + accounts: ['123456789012'], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', { + AllowExternalPrincipals: false, + Name: 'RAMSharee6e0e560e6f8', + Principals: ['123456789012'], + ResourceArns: [{ 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Arn'] }], + PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly'], + }); + }); + + test('share application with an IAM role', () => { + const myRole = iam.Role.fromRoleArn(stack, 'MyRole', 'arn:aws:iam::123456789012:role/myRole'); + + application.shareApplication({ + roles: [myRole], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', { + AllowExternalPrincipals: false, + Name: 'RAMSharee6e0e560e6f8', + Principals: ['arn:aws:iam::123456789012:role/myRole'], + ResourceArns: [{ 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Arn'] }], + PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly'], + }); + }); + + test('share application with an IAM user', () => { + const myUser = iam.User.fromUserArn(stack, 'MyUser', 'arn:aws:iam::123456789012:user/myUser'); + + application.shareApplication({ + users: [myUser], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', { + AllowExternalPrincipals: false, + Name: 'RAMSharee6e0e560e6f8', + Principals: ['arn:aws:iam::123456789012:user/myUser'], + ResourceArns: [{ 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Arn'] }], + PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly'], + }); + }); + + test('share application with organization, give explicit read only access to an application', () => { + application.shareApplication({ + organizationArns: ['arn:aws:organizations::123456789012:organization/o-70oi5564q1'], + sharePermission: appreg.SharePermission.READ_ONLY, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', { + AllowExternalPrincipals: false, + Name: 'RAMSharee6e0e560e6f8', + Principals: ['arn:aws:organizations::123456789012:organization/o-70oi5564q1'], + ResourceArns: [{ 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Arn'] }], + PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly'], + }); + }); + + test('share application with organization, allow access to associate resources and attribute group with an application', () => { + application.shareApplication({ + organizationArns: ['arn:aws:organizations::123456789012:organization/o-70oi5564q1'], + sharePermission: appreg.SharePermission.ALLOW_ACCESS, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', { + AllowExternalPrincipals: false, + Name: 'RAMSharee6e0e560e6f8', + Principals: ['arn:aws:organizations::123456789012:organization/o-70oi5564q1'], + ResourceArns: [{ 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Arn'] }], + PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationAllowAssociation'], + }); + }); + }); }); diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/cdk.out index 90bef2e09ad39..588d7b269d34f 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/cdk.out @@ -1 +1 @@ -{"version":"17.0.0"} \ No newline at end of file +{"version":"20.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/integ-servicecatalogappregistry-attribute-group.assets.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/integ-servicecatalogappregistry-attribute-group.assets.json index ad1dbecb5777a..f3f8d64d86ef0 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/integ-servicecatalogappregistry-attribute-group.assets.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/integ-servicecatalogappregistry-attribute-group.assets.json @@ -1,7 +1,7 @@ { - "version": "17.0.0", + "version": "20.0.0", "files": { - "c059ea8a1cd78cc14e1411059f2226cf0422fadb9bd8a4853596607856ab81d3": { + "3dece22dad73361a79cb380f2880362a20ffc5c0cc75ddc6707e26b5a88cf93f": { "source": { "path": "integ-servicecatalogappregistry-attribute-group.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "c059ea8a1cd78cc14e1411059f2226cf0422fadb9bd8a4853596607856ab81d3.json", + "objectKey": "3dece22dad73361a79cb380f2880362a20ffc5c0cc75ddc6707e26b5a88cf93f.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/integ-servicecatalogappregistry-attribute-group.template.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/integ-servicecatalogappregistry-attribute-group.template.json index 72050e4e3b28b..5588a96737393 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/integ-servicecatalogappregistry-attribute-group.template.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/integ-servicecatalogappregistry-attribute-group.template.json @@ -18,6 +18,102 @@ "Name": "myAttributeGroupTest", "Description": "my attribute group description" } + }, + "TestAttributeGroupRAMSharec67f7d80e5baA10EFB4E": { + "Type": "AWS::RAM::ResourceShare", + "Properties": { + "Name": "RAMSharec67f7d80e5ba", + "AllowExternalPrincipals": false, + "PermissionArns": [ + "arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryAttributeGroupReadOnly" + ], + "Principals": [ + { + "Fn::GetAtt": [ + "MyRoleF48FFE04", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "MySecondRoleB9F57405", + "Arn" + ] + } + ], + "ResourceArns": [ + { + "Fn::GetAtt": [ + "TestAttributeGroupB1CB284F", + "Arn" + ] + } + ] + } + }, + "MyRoleF48FFE04": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MySecondRoleB9F57405": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/manifest.json index eb2988c414171..f9c67b5df26ef 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "17.0.0", + "version": "20.0.0", "artifacts": { "Tree": { "type": "cdk:tree", @@ -20,6 +20,24 @@ "type": "aws:cdk:logicalId", "data": "TestAttributeGroupB1CB284F" } + ], + "/integ-servicecatalogappregistry-attribute-group/TestAttributeGroup/RAMSharec67f7d80e5ba": [ + { + "type": "aws:cdk:logicalId", + "data": "TestAttributeGroupRAMSharec67f7d80e5baA10EFB4E" + } + ], + "/integ-servicecatalogappregistry-attribute-group/MyRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyRoleF48FFE04" + } + ], + "/integ-servicecatalogappregistry-attribute-group/MySecondRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MySecondRoleB9F57405" + } ] }, "displayName": "integ-servicecatalogappregistry-attribute-group" diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/tree.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/tree.json index a4bd5b4ae312b..13fb0f632832a 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/tree.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.integ.snapshot/tree.json @@ -9,7 +9,7 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.0.9" + "version": "10.1.51" } }, "integ-servicecatalogappregistry-attribute-group": { @@ -46,12 +46,152 @@ "fqn": "@aws-cdk/aws-servicecatalogappregistry.CfnAttributeGroup", "version": "0.0.0" } + }, + "RAMSharec67f7d80e5ba": { + "id": "RAMSharec67f7d80e5ba", + "path": "integ-servicecatalogappregistry-attribute-group/TestAttributeGroup/RAMSharec67f7d80e5ba", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::RAM::ResourceShare", + "aws:cdk:cloudformation:props": { + "name": "RAMSharec67f7d80e5ba", + "allowExternalPrincipals": false, + "permissionArns": [ + "arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryAttributeGroupReadOnly" + ], + "principals": [ + { + "Fn::GetAtt": [ + "MyRoleF48FFE04", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "MySecondRoleB9F57405", + "Arn" + ] + } + ], + "resourceArns": [ + { + "Fn::GetAtt": [ + "TestAttributeGroupB1CB284F", + "Arn" + ] + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ram.CfnResourceShare", + "version": "0.0.0" + } } }, "constructInfo": { "fqn": "@aws-cdk/aws-servicecatalogappregistry.AttributeGroup", "version": "0.0.0" } + }, + "MyRole": { + "id": "MyRole", + "path": "integ-servicecatalogappregistry-attribute-group/MyRole", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-servicecatalogappregistry-attribute-group/MyRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "MySecondRole": { + "id": "MySecondRole", + "path": "integ-servicecatalogappregistry-attribute-group/MySecondRole", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-servicecatalogappregistry-attribute-group/MySecondRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } } }, "constructInfo": { diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.test.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.test.ts index b94ff8411afdd..5230071cd50f1 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.test.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.test.ts @@ -1,4 +1,5 @@ import { Template } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as appreg from '../lib'; @@ -174,4 +175,111 @@ describe('Attribute Group', () => { Attributes: {}, }); }); + + describe('Resource sharing of an attribute group', () => { + let attributeGroup: appreg.AttributeGroup; + + beforeEach(() => { + attributeGroup = new appreg.AttributeGroup(stack, 'MyAttributeGroup', { + attributeGroupName: 'MyAttributeGroup', + attributes: {}, + }); + }); + + test('fails for sharing attribute group without principals', () => { + expect(() => { + attributeGroup.shareAttributeGroup({}); + }).toThrow(/An entity must be provided for the share/); + }); + + test('share attribute group with an organization', () => { + attributeGroup.shareAttributeGroup({ + organizationArns: ['arn:aws:organizations::123456789012:organization/o-70oi5564q1'], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', { + AllowExternalPrincipals: false, + Name: 'RAMShare76d2681489c0', + Principals: ['arn:aws:organizations::123456789012:organization/o-70oi5564q1'], + ResourceArns: [{ 'Fn::GetAtt': ['MyAttributeGroup99099500', 'Arn'] }], + PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryAttributeGroupReadOnly'], + }); + }); + + test('share attribute group with an account', () => { + attributeGroup.shareAttributeGroup({ + accounts: ['123456789012'], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', { + AllowExternalPrincipals: false, + Name: 'RAMShare76d2681489c0', + Principals: ['123456789012'], + ResourceArns: [{ 'Fn::GetAtt': ['MyAttributeGroup99099500', 'Arn'] }], + PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryAttributeGroupReadOnly'], + }); + }); + + test('share attribute group with an IAM role', () => { + const myRole = iam.Role.fromRoleArn(stack, 'MyRole', 'arn:aws:iam::123456789012:role/myRole'); + + attributeGroup.shareAttributeGroup({ + roles: [myRole], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', { + AllowExternalPrincipals: false, + Name: 'RAMShare76d2681489c0', + Principals: ['arn:aws:iam::123456789012:role/myRole'], + ResourceArns: [{ 'Fn::GetAtt': ['MyAttributeGroup99099500', 'Arn'] }], + PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryAttributeGroupReadOnly'], + }); + }); + + test('share attribute group with an IAM user', () => { + const myUser = iam.User.fromUserArn(stack, 'MyUser', 'arn:aws:iam::123456789012:user/myUser'); + + attributeGroup.shareAttributeGroup({ + users: [myUser], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', { + AllowExternalPrincipals: false, + Name: 'RAMShare76d2681489c0', + Principals: ['arn:aws:iam::123456789012:user/myUser'], + ResourceArns: [{ 'Fn::GetAtt': ['MyAttributeGroup99099500', 'Arn'] }], + PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryAttributeGroupReadOnly'], + }); + }); + + test('share attribute group with organization, give explicit read only access to the attribute group', () => { + attributeGroup.shareAttributeGroup({ + organizationArns: ['arn:aws:organizations::123456789012:organization/o-70oi5564q1'], + sharePermission: appreg.SharePermission.READ_ONLY, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', { + AllowExternalPrincipals: false, + Name: 'RAMShare76d2681489c0', + Principals: ['arn:aws:organizations::123456789012:organization/o-70oi5564q1'], + ResourceArns: [{ 'Fn::GetAtt': ['MyAttributeGroup99099500', 'Arn'] }], + PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryAttributeGroupReadOnly'], + }); + }); + + test('share attribute group with organization, give access to mutate attribute groups', () => { + attributeGroup.shareAttributeGroup({ + organizationArns: ['arn:aws:organizations::123456789012:organization/o-70oi5564q1'], + sharePermission: appreg.SharePermission.ALLOW_ACCESS, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', { + AllowExternalPrincipals: false, + Name: 'RAMShare76d2681489c0', + Principals: ['arn:aws:organizations::123456789012:organization/o-70oi5564q1'], + ResourceArns: [{ 'Fn::GetAtt': ['MyAttributeGroup99099500', 'Arn'] }], + PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryAttributeGroupAllowAssociation'], + }); + }); + }); }); diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.ts index 809927cd66f58..d51cad051252c 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.ts @@ -1,3 +1,4 @@ +import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as appreg from '../lib'; @@ -32,6 +33,11 @@ const attributeGroup = new appreg.AttributeGroup(stack, 'TestAttributeGroup', { application.associateStack(stack); application.associateAttributeGroup(attributeGroup); +const myRole = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.AccountPrincipal(stack.account), +}); +application.shareApplication({ + roles: [myRole], +}); app.synth(); - diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.attribute-group.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.attribute-group.ts index c55983dc6f4fd..6d5ccb59b8ef2 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.attribute-group.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.attribute-group.ts @@ -1,10 +1,11 @@ +import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as appreg from '../lib'; const app = new cdk.App(); const stack = new cdk.Stack(app, 'integ-servicecatalogappregistry-attribute-group'); -new appreg.AttributeGroup(stack, 'TestAttributeGroup', { +const attributeGroup = new appreg.AttributeGroup(stack, 'TestAttributeGroup', { attributeGroupName: 'myAttributeGroupTest', description: 'my attribute group description', attributes: { @@ -20,5 +21,14 @@ new appreg.AttributeGroup(stack, 'TestAttributeGroup', { }, }, }); +const myRole = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.AccountPrincipal(stack.account), +}); +const mySecondRole = new iam.Role(stack, 'MySecondRole', { + assumedBy: new iam.AccountPrincipal(stack.account), +}); +attributeGroup.shareAttributeGroup({ + roles: [myRole, mySecondRole], +}); app.synth();