Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(iam): generate AccessKeys #18180

Merged
merged 8 commits into from Jan 11, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -67,7 +67,7 @@
]
}
},
"UserAccess": {
"UserAccessEC42ADF7": {
"Type": "AWS::IAM::AccessKey",
"Properties": {
"UserName": {
Expand Down Expand Up @@ -184,13 +184,13 @@
},
"TESTACCESSKEYID": {
"Value": {
"Ref": "UserAccess"
"Ref": "UserAccessEC42ADF7"
}
},
"TESTSECRETACCESSKEY": {
"Value": {
"Fn::GetAtt": [
"UserAccess",
"UserAccessEC42ADF7",
"SecretAccessKey"
]
}
Expand Down
Expand Up @@ -17,8 +17,8 @@ class ExampleComIntegration extends apigatewayv2.HttpRouteIntegration {
const app = new cdk.App();
const stack = new cdk.Stack(app, 'IntegApiGatewayV2Iam');
const user = new iam.User(stack, 'User');
const userAccessKey = new iam.CfnAccessKey(stack, 'UserAccess', {
userName: user.userName,
const userAccessKey = new iam.AccessKey(stack, 'UserAccess', {
user,
});

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

new cdk.CfnOutput(stack, 'TESTACCESSKEYID', {
value: userAccessKey.ref,
value: userAccessKey.accessKeyId,
});

new cdk.CfnOutput(stack, 'TESTSECRETACCESSKEY', {
value: userAccessKey.attrSecretAccessKey,
value: userAccessKey.secretAccessKey,
});

new cdk.CfnOutput(stack, 'TESTREGION', {
Expand Down
21 changes: 21 additions & 0 deletions packages/@aws-cdk/aws-iam/README.md
Expand Up @@ -457,6 +457,27 @@ const user = iam.User.fromUserAttributes(this, 'MyImportedUserByAttributes', {
});
```

### Access Keys

The ability for a user to make API calls via the CLI or an SDK is enabled by the user having an
access key pair. To create an access key:

```ts
const user = new iam.User(this, 'MyUser');
const accessKey = new iam.AccessKey(this, 'MyAccessKey', { user: user });
```

You can force CloudFormation to rotate the access key by providing a monotonically increasing `serial`
property. Simply provide a higher serial value than any number used previously:

```ts
const user = new iam.User(this, 'MyUser');
const accessKey = new iam.AccessKey(this, 'MyAccessKey', { user: user, serial: 1 });
```

An access key may only be associated with a single user and cannot be "moved" between users. Changing
the user associated with an access key replaces the access key (and its ID and secret value).

## Groups

An IAM user group is a collection of IAM users. User groups let you specify permissions for multiple users.
Expand Down
91 changes: 91 additions & 0 deletions packages/@aws-cdk/aws-iam/lib/access-key.ts
@@ -0,0 +1,91 @@
import { IResource, Resource } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnAccessKey } from './iam.generated';
import { IUser } from './user';

/**
* Valid statuses for an IAM Access Key.
*/
export enum AccessKeyStatus {
/**
* An active access key. An active key can be used to make API calls.
*/
ACTIVE = 'Active',

/**
* An inactive access key. An inactive key cannot be used to make API calls.
*/
INACTIVE = 'Inactive'
}

/**
* Represents an IAM Access Key.
*
* @see https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html
*/
export interface IAccessKey extends IResource {
/**
* The Access Key ID.
*
* @attribute
*/
readonly accessKeyId: string;

/**
* The Secret Access Key.
*
* @attribute
*/
readonly secretAccessKey: string;
}

/**
* Properties for defining an IAM access key.
*/
export interface AccessKeyProps {
/**
* A CloudFormation-specific value that signifies the access key should be
* replaced/rotated. This value can only be incremented. Incrementing this
* value will cause CloudFormation to replace the Access Key resource.
*
* @default - No serial value
*/
readonly serial?: number;

/**
* The status of the access key. An Active access key is allowed to be used
* to make API calls; An Inactive key cannot.
*
* @default - The access key is active
*/
readonly status?: AccessKeyStatus;

/**
* The IAM user this key will belong to.
*
* Changing this value will result in the access key being deleted and a new
* access key (with a different ID and secret value) being assigned to the new
* user.
*/
readonly user: IUser;
}

/**
* Define a new IAM Access Key.
*/
export class AccessKey extends Resource implements IAccessKey {
public readonly accessKeyId: string;
public readonly secretAccessKey: string;

constructor(scope: Construct, id: string, props: AccessKeyProps) {
super(scope, id);
const accessKey = new CfnAccessKey(this, 'Resource', {
userName: props.user.userName,
serial: props.serial,
status: props.status,
});

this.accessKeyId = accessKey.ref;
this.secretAccessKey = accessKey.attrSecretAccessKey;
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-iam/lib/index.ts
Expand Up @@ -13,6 +13,7 @@ export * from './unknown-principal';
export * from './oidc-provider';
export * from './permissions-boundary';
export * from './saml-provider';
export * from './access-key';

// AWS::IAM CloudFormation Resources:
export * from './iam.generated';
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-iam/package.json
Expand Up @@ -108,9 +108,11 @@
"awslint": {
"exclude": [
"from-signature:@aws-cdk/aws-iam.Role.fromRoleArn",
"from-method:@aws-cdk/aws-iam.AccessKey",
"construct-interface-extends-iconstruct:@aws-cdk/aws-iam.IManagedPolicy",
"props-physical-name:@aws-cdk/aws-iam.OpenIdConnectProviderProps",
"props-physical-name:@aws-cdk/aws-iam.SamlProviderProps",
"props-physical-name:@aws-cdk/aws-iam.AccessKeyProps",
"resource-interface-extends-resource:@aws-cdk/aws-iam.IManagedPolicy",
"docs-public-apis:@aws-cdk/aws-iam.IUser"
]
Expand Down
79 changes: 79 additions & 0 deletions packages/@aws-cdk/aws-iam/test/access-key.test.ts
@@ -0,0 +1,79 @@
import '@aws-cdk/assert-internal/jest';
import { App, Stack } from '@aws-cdk/core';
import { AccessKey, AccessKeyStatus, User } from '../lib';

describe('IAM Access keys', () => {
test('user name is identifed via reference', () => {
// GIVEN
const app = new App();
const stack = new Stack(app, 'MyStack');
const user = new User(stack, 'MyUser');

// WHEN
new AccessKey(stack, 'MyAccessKey', { user });

// THEN
expect(stack).toMatchTemplate({
Resources: {
MyUserDC45028B: {
Type: 'AWS::IAM::User',
},
MyAccessKeyF0FFBE2E: {
Type: 'AWS::IAM::AccessKey',
Properties: {
UserName: { Ref: 'MyUserDC45028B' },
},
},
},
});
});

test('active status is specified with correct capitalization', () => {
// GIVEN
const app = new App();
const stack = new Stack(app, 'MyStack');
const user = new User(stack, 'MyUser');

// WHEN
new AccessKey(stack, 'MyAccessKey', { user, status: AccessKeyStatus.ACTIVE });

// THEN
expect(stack).toHaveResourceLike('AWS::IAM::AccessKey', { Status: 'Active' });
});

test('inactive status is specified with correct capitalization', () => {
// GIVEN
const app = new App();
const stack = new Stack(app, 'MyStack');
const user = new User(stack, 'MyUser');

// WHEN
new AccessKey(stack, 'MyAccessKey', {
user,
status: AccessKeyStatus.INACTIVE,
});

// THEN
expect(stack).toHaveResourceLike('AWS::IAM::AccessKey', {
Status: 'Inactive',
});
});

test('access key secret ', () => {
// GIVEN
const app = new App();
const stack = new Stack(app, 'MyStack');
const user = new User(stack, 'MyUser');

// WHEN
const accessKey = new AccessKey(stack, 'MyAccessKey', {
user,
});

// THEN
expect(stack.resolve(accessKey.secretAccessKey)).toStrictEqual({
'Fn::GetAtt': ['MyAccessKeyF0FFBE2E', 'SecretAccessKey'],
});
});

});
22 changes: 22 additions & 0 deletions packages/@aws-cdk/aws-iam/test/integ.access-key.expected.json
@@ -0,0 +1,22 @@
{
"Resources": {
"TestUser6A619381": {
"Type": "AWS::IAM::User"
},
"TestAccessKey4BFC5CF5": {
"Type": "AWS::IAM::AccessKey",
"Properties": {
"UserName": {
"Ref": "TestUser6A619381"
}
}
}
},
"Outputs": {
"AccessKeyId": {
"Value": {
"Ref": "TestAccessKey4BFC5CF5"
}
}
}
}
12 changes: 12 additions & 0 deletions packages/@aws-cdk/aws-iam/test/integ.access-key.ts
@@ -0,0 +1,12 @@
import { App, CfnOutput, Stack } from '@aws-cdk/core';
import { AccessKey, User } from '../lib';

const app = new App();
const stack = new Stack(app, 'integ-iam-access-key-1');

const user = new User(stack, 'TestUser');
const accessKey = new AccessKey(stack, 'TestAccessKey', { user });

new CfnOutput(stack, 'AccessKeyId', { value: accessKey.accessKeyId });

app.synth();
6 changes: 3 additions & 3 deletions packages/@aws-cdk/aws-secretsmanager/lib/secret.ts
Expand Up @@ -205,8 +205,8 @@ export class SecretStringValueBeta1 {
* ```ts
* // Creates a new IAM user, access and secret keys, and stores the secret access key in a Secret.
* const user = new iam.User(this, 'User');
* const accessKey = new iam.CfnAccessKey(this, 'AccessKey', { userName: user.userName });
* const secretValue = secretsmanager.SecretStringValueBeta1.fromToken(accessKey.attrSecretAccessKey);
* const accessKey = new iam.AccessKey(this, 'AccessKey', { user });
* const secretValue = secretsmanager.SecretStringValueBeta1.fromToken(accessKey.secretAccessKey);
* new secretsmanager.Secret(this, 'Secret', {
* secretStringBeta1: secretValue,
* });
Expand All @@ -216,7 +216,7 @@ export class SecretStringValueBeta1 {
* const secretValue = secretsmanager.SecretStringValueBeta1.fromToken(JSON.stringify({
* username: user.userName,
* database: 'foo',
* password: accessKey.attrSecretAccessKey
* password: accessKey.secretAccessKey
* }));
*
* Note that the value being a Token does *not* guarantee safety. For example, a Lazy-evaluated string
Expand Down
Expand Up @@ -127,7 +127,7 @@
}
}
},
"AccessKey": {
"AccessKeyE6B25659": {
"Type": "AWS::IAM::AccessKey",
"Properties": {
"UserName": {
Expand All @@ -140,7 +140,7 @@
"Properties": {
"SecretString": {
"Fn::GetAtt": [
"AccessKey",
"AccessKeyE6B25659",
"SecretAccessKey"
]
}
Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts
Expand Up @@ -31,9 +31,9 @@ class SecretsManagerStack extends cdk.Stack {
});

// Secret with predefined value
const accessKey = new iam.CfnAccessKey(this, 'AccessKey', { userName: user.userName });
const accessKey = new iam.AccessKey(this, 'AccessKey', { user });
new secretsmanager.Secret(this, 'PredefinedSecret', {
secretStringBeta1: secretsmanager.SecretStringValueBeta1.fromToken(accessKey.attrSecretAccessKey),
secretStringBeta1: secretsmanager.SecretStringValueBeta1.fromToken(accessKey.secretAccessKey),
});
/// !hide
}
Expand Down