Skip to content

Commit beb5706

Browse files
authored
feat(iam): generate AccessKeys (#18180)
This adds an L2 resource for creating IAM access keys. Instructions for creating access keys are added to the README near the information on creating users. Tests are added (including an integration test) and locations elsewhere in the CDK where `CfnAccessKey` was used have been updated to leverage the new L2 construct (which required changes in the `secretsmanager` and `apigatewayv2-authorizers` packages). Excludes were added for two `awslint` rules. Access Keys don't support specifying physical names, so having such a property is impossible. Additionally, since the primary value of an `AWS::IAM::AccessKey` is to gain access to the `SecretAccessKey` value, a `fromXXX` static method doesn't seem to make a lot of sense (because ideally you'd just pull that from a Secret anyway if it was required in the app). I looked into integrating with `secretsmanager.Secret` as part of this PR; however, at this time it's currently experimental to support strings via tokens and the experimental resource's documentation isn't available so it seemed suboptimal to do that integration. Resolves: #8432 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent c9909c2 commit beb5706

File tree

13 files changed

+252
-22
lines changed

13 files changed

+252
-22
lines changed

packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.iam.expected.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
]
6868
}
6969
},
70-
"UserAccess": {
70+
"UserAccessEC42ADF7": {
7171
"Type": "AWS::IAM::AccessKey",
7272
"Properties": {
7373
"UserName": {
@@ -184,13 +184,13 @@
184184
},
185185
"TESTACCESSKEYID": {
186186
"Value": {
187-
"Ref": "UserAccess"
187+
"Ref": "UserAccessEC42ADF7"
188188
}
189189
},
190190
"TESTSECRETACCESSKEY": {
191191
"Value": {
192192
"Fn::GetAtt": [
193-
"UserAccess",
193+
"UserAccessEC42ADF7",
194194
"SecretAccessKey"
195195
]
196196
}

packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.iam.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ class ExampleComIntegration extends apigatewayv2.HttpRouteIntegration {
1717
const app = new cdk.App();
1818
const stack = new cdk.Stack(app, 'IntegApiGatewayV2Iam');
1919
const user = new iam.User(stack, 'User');
20-
const userAccessKey = new iam.CfnAccessKey(stack, 'UserAccess', {
21-
userName: user.userName,
20+
const userAccessKey = new iam.AccessKey(stack, 'UserAccess', {
21+
user,
2222
});
2323

2424
const httpApi = new apigatewayv2.HttpApi(stack, 'HttpApi', {
@@ -44,11 +44,11 @@ new cdk.CfnOutput(stack, 'API', {
4444
});
4545

4646
new cdk.CfnOutput(stack, 'TESTACCESSKEYID', {
47-
value: userAccessKey.ref,
47+
value: userAccessKey.accessKeyId,
4848
});
4949

5050
new cdk.CfnOutput(stack, 'TESTSECRETACCESSKEY', {
51-
value: userAccessKey.attrSecretAccessKey,
51+
value: userAccessKey.secretAccessKey.toString(),
5252
});
5353

5454
new cdk.CfnOutput(stack, 'TESTREGION', {

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

+21
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,27 @@ const user = iam.User.fromUserAttributes(this, 'MyImportedUserByAttributes', {
457457
});
458458
```
459459

460+
### Access Keys
461+
462+
The ability for a user to make API calls via the CLI or an SDK is enabled by the user having an
463+
access key pair. To create an access key:
464+
465+
```ts
466+
const user = new iam.User(this, 'MyUser');
467+
const accessKey = new iam.AccessKey(this, 'MyAccessKey', { user: user });
468+
```
469+
470+
You can force CloudFormation to rotate the access key by providing a monotonically increasing `serial`
471+
property. Simply provide a higher serial value than any number used previously:
472+
473+
```ts
474+
const user = new iam.User(this, 'MyUser');
475+
const accessKey = new iam.AccessKey(this, 'MyAccessKey', { user: user, serial: 1 });
476+
```
477+
478+
An access key may only be associated with a single user and cannot be "moved" between users. Changing
479+
the user associated with an access key replaces the access key (and its ID and secret value).
480+
460481
## Groups
461482

462483
An IAM user group is a collection of IAM users. User groups let you specify permissions for multiple users.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { IResource, Resource, SecretValue } from '@aws-cdk/core';
2+
import { Construct } from 'constructs';
3+
import { CfnAccessKey } from './iam.generated';
4+
import { IUser } from './user';
5+
6+
/**
7+
* Valid statuses for an IAM Access Key.
8+
*/
9+
export enum AccessKeyStatus {
10+
/**
11+
* An active access key. An active key can be used to make API calls.
12+
*/
13+
ACTIVE = 'Active',
14+
15+
/**
16+
* An inactive access key. An inactive key cannot be used to make API calls.
17+
*/
18+
INACTIVE = 'Inactive'
19+
}
20+
21+
/**
22+
* Represents an IAM Access Key.
23+
*
24+
* @see https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html
25+
*/
26+
export interface IAccessKey extends IResource {
27+
/**
28+
* The Access Key ID.
29+
*
30+
* @attribute
31+
*/
32+
readonly accessKeyId: string;
33+
34+
/**
35+
* The Secret Access Key.
36+
*
37+
* @attribute
38+
*/
39+
readonly secretAccessKey: SecretValue;
40+
}
41+
42+
/**
43+
* Properties for defining an IAM access key.
44+
*/
45+
export interface AccessKeyProps {
46+
/**
47+
* A CloudFormation-specific value that signifies the access key should be
48+
* replaced/rotated. This value can only be incremented. Incrementing this
49+
* value will cause CloudFormation to replace the Access Key resource.
50+
*
51+
* @default - No serial value
52+
*/
53+
readonly serial?: number;
54+
55+
/**
56+
* The status of the access key. An Active access key is allowed to be used
57+
* to make API calls; An Inactive key cannot.
58+
*
59+
* @default - The access key is active
60+
*/
61+
readonly status?: AccessKeyStatus;
62+
63+
/**
64+
* The IAM user this key will belong to.
65+
*
66+
* Changing this value will result in the access key being deleted and a new
67+
* access key (with a different ID and secret value) being assigned to the new
68+
* user.
69+
*/
70+
readonly user: IUser;
71+
}
72+
73+
/**
74+
* Define a new IAM Access Key.
75+
*/
76+
export class AccessKey extends Resource implements IAccessKey {
77+
public readonly accessKeyId: string;
78+
public readonly secretAccessKey: SecretValue;
79+
80+
constructor(scope: Construct, id: string, props: AccessKeyProps) {
81+
super(scope, id);
82+
const accessKey = new CfnAccessKey(this, 'Resource', {
83+
userName: props.user.userName,
84+
serial: props.serial,
85+
status: props.status,
86+
});
87+
88+
this.accessKeyId = accessKey.ref;
89+
90+
// Not actually 'plainText', but until we have a more apt constructor
91+
this.secretAccessKey = SecretValue.plainText(accessKey.attrSecretAccessKey);
92+
}
93+
}

packages/@aws-cdk/aws-iam/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export * from './unknown-principal';
1313
export * from './oidc-provider';
1414
export * from './permissions-boundary';
1515
export * from './saml-provider';
16+
export * from './access-key';
1617

1718
// AWS::IAM CloudFormation Resources:
1819
export * from './iam.generated';

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

+2
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,11 @@
108108
"awslint": {
109109
"exclude": [
110110
"from-signature:@aws-cdk/aws-iam.Role.fromRoleArn",
111+
"from-method:@aws-cdk/aws-iam.AccessKey",
111112
"construct-interface-extends-iconstruct:@aws-cdk/aws-iam.IManagedPolicy",
112113
"props-physical-name:@aws-cdk/aws-iam.OpenIdConnectProviderProps",
113114
"props-physical-name:@aws-cdk/aws-iam.SamlProviderProps",
115+
"props-physical-name:@aws-cdk/aws-iam.AccessKeyProps",
114116
"resource-interface-extends-resource:@aws-cdk/aws-iam.IManagedPolicy",
115117
"docs-public-apis:@aws-cdk/aws-iam.IUser"
116118
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import '@aws-cdk/assert-internal/jest';
2+
import { App, Stack } from '@aws-cdk/core';
3+
import { AccessKey, AccessKeyStatus, User } from '../lib';
4+
5+
describe('IAM Access keys', () => {
6+
test('user name is identifed via reference', () => {
7+
// GIVEN
8+
const app = new App();
9+
const stack = new Stack(app, 'MyStack');
10+
const user = new User(stack, 'MyUser');
11+
12+
// WHEN
13+
new AccessKey(stack, 'MyAccessKey', { user });
14+
15+
// THEN
16+
expect(stack).toMatchTemplate({
17+
Resources: {
18+
MyUserDC45028B: {
19+
Type: 'AWS::IAM::User',
20+
},
21+
MyAccessKeyF0FFBE2E: {
22+
Type: 'AWS::IAM::AccessKey',
23+
Properties: {
24+
UserName: { Ref: 'MyUserDC45028B' },
25+
},
26+
},
27+
},
28+
});
29+
});
30+
31+
test('active status is specified with correct capitalization', () => {
32+
// GIVEN
33+
const app = new App();
34+
const stack = new Stack(app, 'MyStack');
35+
const user = new User(stack, 'MyUser');
36+
37+
// WHEN
38+
new AccessKey(stack, 'MyAccessKey', { user, status: AccessKeyStatus.ACTIVE });
39+
40+
// THEN
41+
expect(stack).toHaveResourceLike('AWS::IAM::AccessKey', { Status: 'Active' });
42+
});
43+
44+
test('inactive status is specified with correct capitalization', () => {
45+
// GIVEN
46+
const app = new App();
47+
const stack = new Stack(app, 'MyStack');
48+
const user = new User(stack, 'MyUser');
49+
50+
// WHEN
51+
new AccessKey(stack, 'MyAccessKey', {
52+
user,
53+
status: AccessKeyStatus.INACTIVE,
54+
});
55+
56+
// THEN
57+
expect(stack).toHaveResourceLike('AWS::IAM::AccessKey', {
58+
Status: 'Inactive',
59+
});
60+
});
61+
62+
test('access key secret ', () => {
63+
// GIVEN
64+
const app = new App();
65+
const stack = new Stack(app, 'MyStack');
66+
const user = new User(stack, 'MyUser');
67+
68+
// WHEN
69+
const accessKey = new AccessKey(stack, 'MyAccessKey', {
70+
user,
71+
});
72+
73+
// THEN
74+
expect(stack.resolve(accessKey.secretAccessKey)).toStrictEqual({
75+
'Fn::GetAtt': ['MyAccessKeyF0FFBE2E', 'SecretAccessKey'],
76+
});
77+
});
78+
79+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"Resources": {
3+
"TestUser6A619381": {
4+
"Type": "AWS::IAM::User"
5+
},
6+
"TestAccessKey4BFC5CF5": {
7+
"Type": "AWS::IAM::AccessKey",
8+
"Properties": {
9+
"UserName": {
10+
"Ref": "TestUser6A619381"
11+
}
12+
}
13+
}
14+
},
15+
"Outputs": {
16+
"AccessKeyId": {
17+
"Value": {
18+
"Ref": "TestAccessKey4BFC5CF5"
19+
}
20+
}
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { App, CfnOutput, Stack } from '@aws-cdk/core';
2+
import { AccessKey, User } from '../lib';
3+
4+
const app = new App();
5+
const stack = new Stack(app, 'integ-iam-access-key-1');
6+
7+
const user = new User(stack, 'TestUser');
8+
const accessKey = new AccessKey(stack, 'TestAccessKey', { user });
9+
10+
new CfnOutput(stack, 'AccessKeyId', { value: accessKey.accessKeyId });
11+
12+
app.synth();

packages/@aws-cdk/aws-secretsmanager/lib/secret.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,8 @@ export class SecretStringValueBeta1 {
205205
* ```ts
206206
* // Creates a new IAM user, access and secret keys, and stores the secret access key in a Secret.
207207
* const user = new iam.User(this, 'User');
208-
* const accessKey = new iam.CfnAccessKey(this, 'AccessKey', { userName: user.userName });
209-
* const secretValue = secretsmanager.SecretStringValueBeta1.fromToken(accessKey.attrSecretAccessKey);
208+
* const accessKey = new iam.AccessKey(this, 'AccessKey', { user });
209+
* const secretValue = secretsmanager.SecretStringValueBeta1.fromToken(accessKey.secretAccessKey.toString());
210210
* new secretsmanager.Secret(this, 'Secret', {
211211
* secretStringBeta1: secretValue,
212212
* });
@@ -216,7 +216,7 @@ export class SecretStringValueBeta1 {
216216
* const secretValue = secretsmanager.SecretStringValueBeta1.fromToken(JSON.stringify({
217217
* username: user.userName,
218218
* database: 'foo',
219-
* password: accessKey.attrSecretAccessKey
219+
* password: accessKey.secretAccessKey.toString(),
220220
* }));
221221
*
222222
* Note that the value being a Token does *not* guarantee safety. For example, a Lazy-evaluated string

packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.expected.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@
127127
}
128128
}
129129
},
130-
"AccessKey": {
130+
"AccessKeyE6B25659": {
131131
"Type": "AWS::IAM::AccessKey",
132132
"Properties": {
133133
"UserName": {
@@ -140,7 +140,7 @@
140140
"Properties": {
141141
"SecretString": {
142142
"Fn::GetAtt": [
143-
"AccessKey",
143+
"AccessKeyE6B25659",
144144
"SecretAccessKey"
145145
]
146146
}

packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ class SecretsManagerStack extends cdk.Stack {
3131
});
3232

3333
// Secret with predefined value
34-
const accessKey = new iam.CfnAccessKey(this, 'AccessKey', { userName: user.userName });
34+
const accessKey = new iam.AccessKey(this, 'AccessKey', { user });
3535
new secretsmanager.Secret(this, 'PredefinedSecret', {
36-
secretStringBeta1: secretsmanager.SecretStringValueBeta1.fromToken(accessKey.attrSecretAccessKey),
36+
secretStringBeta1: secretsmanager.SecretStringValueBeta1.fromToken(accessKey.secretAccessKey.toString()),
3737
});
3838
/// !hide
3939
}

0 commit comments

Comments
 (0)