diff --git a/packages/aws-cdk-lib/aws-dynamodb/test/dynamodb.test.ts b/packages/aws-cdk-lib/aws-dynamodb/test/dynamodb.test.ts index b3b05fc391de5..add429387d673 100644 --- a/packages/aws-cdk-lib/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/aws-cdk-lib/aws-dynamodb/test/dynamodb.test.ts @@ -441,6 +441,59 @@ describe('L1 static factory methods', () => { }); }); +describe('L1 grant method', () => { + test('grant can be used to add permissions to a principal', () => { + // GIVEN + const stack = new Stack(); + const table = new CfnTable(stack, 'MyTable', { + keySchema: [{ attributeName: 'ID', keyType: 'HASH' }], + }); + const user = new iam.User(stack, 'user'); + + // WHEN + const grant = table.grant( + user, + ['dynamodb:GetRecords', 'dynamodb:GetShardIterator'], + { + 'StringEqualsIfExists': { + 'dynamodb:Select': 'SPECIFIC_ATTRIBUTES', + }, + }, + ); + + // THEN + grant.assertSuccess(); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': ['dynamodb:GetRecords', 'dynamodb:GetShardIterator'], + 'Effect': 'Allow', + 'Resource': { + 'Fn::GetAtt': [ + 'MyTable', + 'Arn', + ], + }, + 'Condition': { + 'StringEqualsIfExists': { + 'dynamodb:Select': 'SPECIFIC_ATTRIBUTES', + }, + }, + }, + ], + 'Version': '2012-10-17', + }, + 'PolicyName': 'userDefaultPolicy083DF682', + 'Users': [ + { + 'Ref': 'user2C2B57AE', + }, + ], + }); + }); +}); + testDeprecated('when specifying every property', () => { const stack = new Stack(); const stream = new kinesis.Stream(stack, 'MyStream'); diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/cdk.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/cdk.ts index 60074230f0cdc..4f082ecd88b1f 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/cdk.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/cdk.ts @@ -123,8 +123,14 @@ export class CdkCloudWatch extends ExternalModule { public readonly MetricOptions = Type.fromName(this, 'MetricOptions'); } +export class CdkIam extends ExternalModule { + public readonly Grant = $T(Type.fromName(this, 'Grant')); + public readonly IGrantable = $T(Type.fromName(this, 'IGrantable')); +} + export const CDK_CORE = new CdkCore('aws-cdk-lib'); export const CDK_CLOUDWATCH = new CdkCloudWatch('aws-cdk-lib/aws-cloudwatch'); +export const CDK_IAM = new CdkIam('aws-cdk-lib/aws-iam'); export const CONSTRUCTS = new Constructs(); function makeCallableExpr(scope: IScope, name: string) { diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts index 3e08ed49323df..960e677b9ee3d 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts @@ -6,8 +6,10 @@ import { Block, ClassType, code, + DocsSpec, DummyScope, - expr, Expression, + expr, + Expression, Initializer, InterfaceType, IScope, @@ -16,7 +18,8 @@ import { MemberVisibility, Module, ObjectLiteral, - Stability, Statement, + Stability, + Statement, stmt, StructType, SuperInitializer, @@ -24,18 +27,20 @@ import { TruthyOr, Type, TypeDeclarationStatement, - DocsSpec, } from '@cdklabs/typewriter'; -import { CDK_CORE, CONSTRUCTS } from './cdk'; +import { CDK_CORE, CDK_IAM, CONSTRUCTS } from './cdk'; import { CloudFormationMapping } from './cloudformation-mapping'; import { ResourceDecider, shouldBuildReferenceInterface } from './resource-decider'; import { TypeConverter } from './type-converter'; import { + attributePropertyName, cfnParserNameFromType, cfnProducerNameFromType, classNameFromResource, - cloudFormationDocLink, propertyNameFromCloudFormation, - propStructNameFromResource, referencePropertyName, + cloudFormationDocLink, + propertyNameFromCloudFormation, + propStructNameFromResource, + referencePropertyName, staticRequiredTransform, staticResourceTypeName, } from '../naming'; @@ -152,6 +157,7 @@ export class ResourceClass extends ClassType { this.makeFromCloudFormationFactory(); this.makeFromArnFactory(); this.makeFromNameFactory(); + this.makeGrant(); if (this.resource.cloudFormationTransform) { this.addProperty({ @@ -467,6 +473,54 @@ export class ResourceClass extends ClassType { ); } + private makeGrant() { + const cfnArnProperty = this.decider.findArnProperty(); + if (cfnArnProperty == null) { + return; + } + + if (!this.module.imports.some(imp => imp.module.fqn === CDK_IAM.fqn)) { + CDK_IAM.import(this.module, 'iam'); + } + + const grant = this.addMethod({ + name: 'grant', + docs: { + summary: 'Grant the given IGrantable permissions to perform the actions specified in actions on this resource.', + }, + returnType: CDK_IAM.Grant, + }); + + grant.addParameter({ + name: 'grantee', + type: CDK_IAM.IGrantable, + documentation: 'The principal to grant permissions to.', + }); + + grant.addParameter({ + name: 'actions', + type: Type.arrayOf(Type.STRING), + documentation: 'The actions to grant permissions for.', + }); + + grant.addParameter({ + name: 'conditions', + type: Type.mapOf(Type.mapOf(Type.ambient('unknown'))), + documentation: 'The conditions under which the actions should be allowed.', + optional: true, + }); + + grant.addBody( + stmt.ret(CDK_IAM.Grant.addToPrincipal(expr.object({ + grantee: expr.ident('grantee'), + actions: expr.ident('actions'), + resourceArns: expr.list([$this[attributePropertyName(cfnArnProperty)]]), + scope: $this, + conditions: expr.ident('conditions'), + }))), + ); + } + private makeConstructor() { // Ctor const init = this.addInitializer({