diff --git a/packages/aws-cdk-lib/aws-rds/lib/instance.ts b/packages/aws-cdk-lib/aws-rds/lib/instance.ts index ebf5a4218f7d9..cf2e81e0747b5 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/instance.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/instance.ts @@ -47,6 +47,14 @@ export interface IDatabaseInstance extends IResource, ec2.IConnectable, secretsm */ readonly dbInstanceEndpointPort: string; + /** + * The AWS Region-unique, immutable identifier for the DB instance. + * This identifier is found in AWS CloudTrail log entries whenever the AWS KMS key for the DB instance is accessed. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbinstance.html#aws-resource-rds-dbinstance-return-values + */ + readonly instanceResourceId?: string; + /** * The instance endpoint. */ @@ -66,10 +74,11 @@ export interface IDatabaseInstance extends IResource, ec2.IConnectable, secretsm /** * Grant the given identity connection access to the database. - * **Note**: this method does not currently work, see https://github.com/aws/aws-cdk/issues/11851 for details. - * @see https://github.com/aws/aws-cdk/issues/11851 + * + * @param grantee the Principal to grant the permissions to + * @param dbUser the name of the database user to allow connecting as to the db instance */ - grantConnect(grantee: iam.IGrantable): iam.Grant; + grantConnect(grantee: iam.IGrantable, dbUser?: string): iam.Grant; /** * Defines a CloudWatch event rule which triggers for instance events. Use @@ -97,6 +106,14 @@ export interface DatabaseInstanceAttributes { */ readonly port: number; + /** + * The AWS Region-unique, immutable identifier for the DB instance. + * This identifier is found in AWS CloudTrail log entries whenever the AWS KMS key for the DB instance is accessed. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbinstance.html#aws-resource-rds-dbinstance-return-values + */ + readonly instanceResourceId?: string; + /** * The security groups of the instance. */ @@ -130,6 +147,7 @@ export abstract class DatabaseInstanceBase extends Resource implements IDatabase public readonly instanceEndpoint = new Endpoint(attrs.instanceEndpointAddress, attrs.port); public readonly engine = attrs.engine; protected enableIamAuthentication = true; + public readonly instanceResourceId = attrs.instanceResourceId; } return new Import(scope, id); @@ -138,6 +156,7 @@ export abstract class DatabaseInstanceBase extends Resource implements IDatabase public abstract readonly instanceIdentifier: string; public abstract readonly dbInstanceEndpointAddress: string; public abstract readonly dbInstanceEndpointPort: string; + public abstract readonly instanceResourceId?: string; public abstract readonly instanceEndpoint: Endpoint; // only required because of JSII bug: https://github.com/aws/jsii/issues/2040 public abstract readonly engine?: IInstanceEngine; @@ -158,16 +177,33 @@ export abstract class DatabaseInstanceBase extends Resource implements IDatabase }); } - public grantConnect(grantee: iam.IGrantable): iam.Grant { + public grantConnect(grantee: iam.IGrantable, dbUser?: string): iam.Grant { if (this.enableIamAuthentication === false) { throw new Error('Cannot grant connect when IAM authentication is disabled'); } + if (!this.instanceResourceId) { + throw new Error('For imported Database Instances, instanceResourceId is required to grantConnect()'); + } + + if (!dbUser) { + throw new Error('For imported Database Instances, the dbUser is required to grantConnect()'); + } + this.enableIamAuthentication = true; return iam.Grant.addToPrincipal({ grantee, actions: ['rds-db:connect'], - resourceArns: [this.instanceArn], + resourceArns: [ + // The ARN of an IAM policy for IAM database access is not the same as the instance ARN, so we cannot use `this.instanceArn`. + // See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.IAMPolicy.html + Stack.of(this).formatArn({ + arnFormat: ArnFormat.COLON_RESOURCE_NAME, + service: 'rds-db', + resource: 'dbuser', + resourceName: [this.instanceResourceId, dbUser].join('/'), + }), + ], }); } @@ -1029,6 +1065,26 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa target: this, }); } + + /** + * Grant the given identity connection access to the database. + * + * @param grantee the Principal to grant the permissions to + * @param dbUser the name of the database user to allow connecting as to the db instance + * + * @default the default user, obtained from the Secret + */ + public grantConnect(grantee: iam.IGrantable, dbUser?: string): iam.Grant { + if (!dbUser) { + if (!this.secret) { + throw new Error('A secret or dbUser is required to grantConnect()'); + } + + dbUser = this.secret.secretValueFromJson('username').toString(); + } + + return super.grantConnect(grantee, dbUser); + } } /** @@ -1074,6 +1130,7 @@ export class DatabaseInstance extends DatabaseInstanceSource implements IDatabas public readonly instanceIdentifier: string; public readonly dbInstanceEndpointAddress: string; public readonly dbInstanceEndpointPort: string; + public readonly instanceResourceId?: string; public readonly instanceEndpoint: Endpoint; public readonly secret?: secretsmanager.ISecret; @@ -1095,6 +1152,7 @@ export class DatabaseInstance extends DatabaseInstanceSource implements IDatabas this.instanceIdentifier = this.getResourceNameAttribute(instance.ref); this.dbInstanceEndpointAddress = instance.attrEndpointAddress; this.dbInstanceEndpointPort = instance.attrEndpointPort; + this.instanceResourceId = instance.attrDbiResourceId; // create a number token that represents the port of the instance const portAttribute = Token.asNumber(instance.attrEndpointPort); @@ -1141,6 +1199,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme public readonly instanceIdentifier: string; public readonly dbInstanceEndpointAddress: string; public readonly dbInstanceEndpointPort: string; + public readonly instanceResourceId?: string; public readonly instanceEndpoint: Endpoint; public readonly secret?: secretsmanager.ISecret; @@ -1172,6 +1231,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme this.instanceIdentifier = instance.ref; this.dbInstanceEndpointAddress = instance.attrEndpointAddress; this.dbInstanceEndpointPort = instance.attrEndpointPort; + this.instanceResourceId = instance.attrDbiResourceId; // create a number token that represents the port of the instance const portAttribute = Token.asNumber(instance.attrEndpointPort); @@ -1229,6 +1289,7 @@ export class DatabaseInstanceReadReplica extends DatabaseInstanceNew implements public readonly instanceIdentifier: string; public readonly dbInstanceEndpointAddress: string; public readonly dbInstanceEndpointPort: string; + public readonly instanceResourceId?: string; public readonly instanceEndpoint: Endpoint; public readonly engine?: IInstanceEngine = undefined; protected readonly instanceType: ec2.InstanceType; @@ -1260,6 +1321,7 @@ export class DatabaseInstanceReadReplica extends DatabaseInstanceNew implements this.instanceIdentifier = instance.ref; this.dbInstanceEndpointAddress = instance.attrEndpointAddress; this.dbInstanceEndpointPort = instance.attrEndpointPort; + this.instanceResourceId = instance.attrDbInstanceArn; // create a number token that represents the port of the instance const portAttribute = Token.asNumber(instance.attrEndpointPort); diff --git a/packages/aws-cdk-lib/aws-rds/test/instance.test.ts b/packages/aws-cdk-lib/aws-rds/test/instance.test.ts index f171e8f03618a..ac227e874c77c 100644 --- a/packages/aws-cdk-lib/aws-rds/test/instance.test.ts +++ b/packages/aws-cdk-lib/aws-rds/test/instance.test.ts @@ -1112,7 +1112,86 @@ describe('instance', () => { Effect: 'Allow', Action: 'rds-db:connect', Resource: { - 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':rds:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':db:', { Ref: 'InstanceC1063A87' }]], + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':rds-db:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':dbuser:', + { + 'Fn::GetAtt': [ + 'InstanceC1063A87', + 'DbiResourceId', + ], + }, + '/{{resolve:secretsmanager:', + { + Ref: 'InstanceSecretAttachment83BEE581', + }, + ':SecretString:username::}}', + ], + ], + }, + }], + Version: '2012-10-17', + }, + }); + }); + + test('createGrant - creates IAM policy and enables IAM auth for a specific user', () => { + const instance = new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), + vpc, + }); + const role = new Role(stack, 'DBRole', { + assumedBy: new AccountPrincipal(stack.account), + }); + instance.grantConnect(role, 'my-user'); + + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', { + EnableIAMDatabaseAuthentication: true, + }); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Effect: 'Allow', + Action: 'rds-db:connect', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':rds-db:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':dbuser:', + { + 'Fn::GetAtt': [ + 'InstanceC1063A87', + 'DbiResourceId', + ], + }, + '/my-user', + ], + ], }, }], Version: '2012-10-17',