Skip to content

Commit 227ea09

Browse files
authored
fix(rds): Correct ARN in IAM policy for IAM database access (#25141)
The [IAM policy for IAM database access](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.IAMPolicy.html) takes the form of: ``` arn:aws:rds-db:region:account-id:dbuser:DbiResourceId/db-user-name ``` Following aws-cloudformation/cloudformation-coverage-roadmap#105, this change updates the ARN used in `grantConnect` to this format. Additionally, update the signature of `grantConnect` to take an optional `dbUser`, which is defaulted to the master username of the database, obtained via the available `Secret`. This signature change also matches `grantConnect` in `DatabaseProxy`. See #12416. Closes #11851. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 5a836cb commit 227ea09

File tree

2 files changed

+147
-6
lines changed

2 files changed

+147
-6
lines changed

packages/aws-cdk-lib/aws-rds/lib/instance.ts

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ export interface IDatabaseInstance extends IResource, ec2.IConnectable, secretsm
4747
*/
4848
readonly dbInstanceEndpointPort: string;
4949

50+
/**
51+
* The AWS Region-unique, immutable identifier for the DB instance.
52+
* This identifier is found in AWS CloudTrail log entries whenever the AWS KMS key for the DB instance is accessed.
53+
*
54+
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbinstance.html#aws-resource-rds-dbinstance-return-values
55+
*/
56+
readonly instanceResourceId?: string;
57+
5058
/**
5159
* The instance endpoint.
5260
*/
@@ -66,10 +74,11 @@ export interface IDatabaseInstance extends IResource, ec2.IConnectable, secretsm
6674

6775
/**
6876
* Grant the given identity connection access to the database.
69-
* **Note**: this method does not currently work, see https://github.com/aws/aws-cdk/issues/11851 for details.
70-
* @see https://github.com/aws/aws-cdk/issues/11851
77+
*
78+
* @param grantee the Principal to grant the permissions to
79+
* @param dbUser the name of the database user to allow connecting as to the db instance
7180
*/
72-
grantConnect(grantee: iam.IGrantable): iam.Grant;
81+
grantConnect(grantee: iam.IGrantable, dbUser?: string): iam.Grant;
7382

7483
/**
7584
* Defines a CloudWatch event rule which triggers for instance events. Use
@@ -97,6 +106,14 @@ export interface DatabaseInstanceAttributes {
97106
*/
98107
readonly port: number;
99108

109+
/**
110+
* The AWS Region-unique, immutable identifier for the DB instance.
111+
* This identifier is found in AWS CloudTrail log entries whenever the AWS KMS key for the DB instance is accessed.
112+
*
113+
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbinstance.html#aws-resource-rds-dbinstance-return-values
114+
*/
115+
readonly instanceResourceId?: string;
116+
100117
/**
101118
* The security groups of the instance.
102119
*/
@@ -130,6 +147,7 @@ export abstract class DatabaseInstanceBase extends Resource implements IDatabase
130147
public readonly instanceEndpoint = new Endpoint(attrs.instanceEndpointAddress, attrs.port);
131148
public readonly engine = attrs.engine;
132149
protected enableIamAuthentication = true;
150+
public readonly instanceResourceId = attrs.instanceResourceId;
133151
}
134152

135153
return new Import(scope, id);
@@ -138,6 +156,7 @@ export abstract class DatabaseInstanceBase extends Resource implements IDatabase
138156
public abstract readonly instanceIdentifier: string;
139157
public abstract readonly dbInstanceEndpointAddress: string;
140158
public abstract readonly dbInstanceEndpointPort: string;
159+
public abstract readonly instanceResourceId?: string;
141160
public abstract readonly instanceEndpoint: Endpoint;
142161
// only required because of JSII bug: https://github.com/aws/jsii/issues/2040
143162
public abstract readonly engine?: IInstanceEngine;
@@ -158,16 +177,33 @@ export abstract class DatabaseInstanceBase extends Resource implements IDatabase
158177
});
159178
}
160179

161-
public grantConnect(grantee: iam.IGrantable): iam.Grant {
180+
public grantConnect(grantee: iam.IGrantable, dbUser?: string): iam.Grant {
162181
if (this.enableIamAuthentication === false) {
163182
throw new Error('Cannot grant connect when IAM authentication is disabled');
164183
}
165184

185+
if (!this.instanceResourceId) {
186+
throw new Error('For imported Database Instances, instanceResourceId is required to grantConnect()');
187+
}
188+
189+
if (!dbUser) {
190+
throw new Error('For imported Database Instances, the dbUser is required to grantConnect()');
191+
}
192+
166193
this.enableIamAuthentication = true;
167194
return iam.Grant.addToPrincipal({
168195
grantee,
169196
actions: ['rds-db:connect'],
170-
resourceArns: [this.instanceArn],
197+
resourceArns: [
198+
// The ARN of an IAM policy for IAM database access is not the same as the instance ARN, so we cannot use `this.instanceArn`.
199+
// See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.IAMPolicy.html
200+
Stack.of(this).formatArn({
201+
arnFormat: ArnFormat.COLON_RESOURCE_NAME,
202+
service: 'rds-db',
203+
resource: 'dbuser',
204+
resourceName: [this.instanceResourceId, dbUser].join('/'),
205+
}),
206+
],
171207
});
172208
}
173209

@@ -1029,6 +1065,26 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa
10291065
target: this,
10301066
});
10311067
}
1068+
1069+
/**
1070+
* Grant the given identity connection access to the database.
1071+
*
1072+
* @param grantee the Principal to grant the permissions to
1073+
* @param dbUser the name of the database user to allow connecting as to the db instance
1074+
*
1075+
* @default the default user, obtained from the Secret
1076+
*/
1077+
public grantConnect(grantee: iam.IGrantable, dbUser?: string): iam.Grant {
1078+
if (!dbUser) {
1079+
if (!this.secret) {
1080+
throw new Error('A secret or dbUser is required to grantConnect()');
1081+
}
1082+
1083+
dbUser = this.secret.secretValueFromJson('username').toString();
1084+
}
1085+
1086+
return super.grantConnect(grantee, dbUser);
1087+
}
10321088
}
10331089

10341090
/**
@@ -1074,6 +1130,7 @@ export class DatabaseInstance extends DatabaseInstanceSource implements IDatabas
10741130
public readonly instanceIdentifier: string;
10751131
public readonly dbInstanceEndpointAddress: string;
10761132
public readonly dbInstanceEndpointPort: string;
1133+
public readonly instanceResourceId?: string;
10771134
public readonly instanceEndpoint: Endpoint;
10781135
public readonly secret?: secretsmanager.ISecret;
10791136

@@ -1095,6 +1152,7 @@ export class DatabaseInstance extends DatabaseInstanceSource implements IDatabas
10951152
this.instanceIdentifier = this.getResourceNameAttribute(instance.ref);
10961153
this.dbInstanceEndpointAddress = instance.attrEndpointAddress;
10971154
this.dbInstanceEndpointPort = instance.attrEndpointPort;
1155+
this.instanceResourceId = instance.attrDbiResourceId;
10981156

10991157
// create a number token that represents the port of the instance
11001158
const portAttribute = Token.asNumber(instance.attrEndpointPort);
@@ -1141,6 +1199,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme
11411199
public readonly instanceIdentifier: string;
11421200
public readonly dbInstanceEndpointAddress: string;
11431201
public readonly dbInstanceEndpointPort: string;
1202+
public readonly instanceResourceId?: string;
11441203
public readonly instanceEndpoint: Endpoint;
11451204
public readonly secret?: secretsmanager.ISecret;
11461205

@@ -1172,6 +1231,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme
11721231
this.instanceIdentifier = instance.ref;
11731232
this.dbInstanceEndpointAddress = instance.attrEndpointAddress;
11741233
this.dbInstanceEndpointPort = instance.attrEndpointPort;
1234+
this.instanceResourceId = instance.attrDbiResourceId;
11751235

11761236
// create a number token that represents the port of the instance
11771237
const portAttribute = Token.asNumber(instance.attrEndpointPort);
@@ -1229,6 +1289,7 @@ export class DatabaseInstanceReadReplica extends DatabaseInstanceNew implements
12291289
public readonly instanceIdentifier: string;
12301290
public readonly dbInstanceEndpointAddress: string;
12311291
public readonly dbInstanceEndpointPort: string;
1292+
public readonly instanceResourceId?: string;
12321293
public readonly instanceEndpoint: Endpoint;
12331294
public readonly engine?: IInstanceEngine = undefined;
12341295
protected readonly instanceType: ec2.InstanceType;
@@ -1260,6 +1321,7 @@ export class DatabaseInstanceReadReplica extends DatabaseInstanceNew implements
12601321
this.instanceIdentifier = instance.ref;
12611322
this.dbInstanceEndpointAddress = instance.attrEndpointAddress;
12621323
this.dbInstanceEndpointPort = instance.attrEndpointPort;
1324+
this.instanceResourceId = instance.attrDbInstanceArn;
12631325

12641326
// create a number token that represents the port of the instance
12651327
const portAttribute = Token.asNumber(instance.attrEndpointPort);

packages/aws-cdk-lib/aws-rds/test/instance.test.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1112,7 +1112,86 @@ describe('instance', () => {
11121112
Effect: 'Allow',
11131113
Action: 'rds-db:connect',
11141114
Resource: {
1115-
'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':rds:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':db:', { Ref: 'InstanceC1063A87' }]],
1115+
'Fn::Join': [
1116+
'',
1117+
[
1118+
'arn:',
1119+
{
1120+
Ref: 'AWS::Partition',
1121+
},
1122+
':rds-db:',
1123+
{
1124+
Ref: 'AWS::Region',
1125+
},
1126+
':',
1127+
{
1128+
Ref: 'AWS::AccountId',
1129+
},
1130+
':dbuser:',
1131+
{
1132+
'Fn::GetAtt': [
1133+
'InstanceC1063A87',
1134+
'DbiResourceId',
1135+
],
1136+
},
1137+
'/{{resolve:secretsmanager:',
1138+
{
1139+
Ref: 'InstanceSecretAttachment83BEE581',
1140+
},
1141+
':SecretString:username::}}',
1142+
],
1143+
],
1144+
},
1145+
}],
1146+
Version: '2012-10-17',
1147+
},
1148+
});
1149+
});
1150+
1151+
test('createGrant - creates IAM policy and enables IAM auth for a specific user', () => {
1152+
const instance = new rds.DatabaseInstance(stack, 'Instance', {
1153+
engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }),
1154+
vpc,
1155+
});
1156+
const role = new Role(stack, 'DBRole', {
1157+
assumedBy: new AccountPrincipal(stack.account),
1158+
});
1159+
instance.grantConnect(role, 'my-user');
1160+
1161+
Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBInstance', {
1162+
EnableIAMDatabaseAuthentication: true,
1163+
});
1164+
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
1165+
PolicyDocument: {
1166+
Statement: [{
1167+
Effect: 'Allow',
1168+
Action: 'rds-db:connect',
1169+
Resource: {
1170+
'Fn::Join': [
1171+
'',
1172+
[
1173+
'arn:',
1174+
{
1175+
Ref: 'AWS::Partition',
1176+
},
1177+
':rds-db:',
1178+
{
1179+
Ref: 'AWS::Region',
1180+
},
1181+
':',
1182+
{
1183+
Ref: 'AWS::AccountId',
1184+
},
1185+
':dbuser:',
1186+
{
1187+
'Fn::GetAtt': [
1188+
'InstanceC1063A87',
1189+
'DbiResourceId',
1190+
],
1191+
},
1192+
'/my-user',
1193+
],
1194+
],
11161195
},
11171196
}],
11181197
Version: '2012-10-17',

0 commit comments

Comments
 (0)