Skip to content

Commit

Permalink
feat(kms): support fromLookup in KMS key to get key by alias name (#1…
Browse files Browse the repository at this point in the history
…5652)

Add method `fromLookup` in KMS key which provides the option to get a KMS key including its key id by an alias name.
In some cases, aliases can't be used because access to the underlying key id is necessary. In this case, the `fromLookup` method can be used.

The following packages were changed:
- @aws-cdk/aws-kms: introduce new `fromLookup` method
- @aws-cdk/cx-api: new KeyContextResponse
- @aws-cdk/cloud-assembly-schema: new ContextProvider KEY_PROVIDER and KeyContextQuery
- aws-cdk: implementation of key ContextProvider

Closes #8822 

-----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
jumic authored Sep 3, 2021
1 parent 2023004 commit 34a57ed
Show file tree
Hide file tree
Showing 16 changed files with 481 additions and 7 deletions.
34 changes: 33 additions & 1 deletion packages/@aws-cdk/aws-kms/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ pass the construct to the other stack:

## Importing existing keys

### Import key by ARN

To use a KMS key that is not defined in this CDK app, but is created through other means, use
`Key.fromKeyArn(parent, name, ref)`:

Expand All @@ -72,10 +74,12 @@ const myKeyImported = kms.Key.fromKeyArn(this, 'MyImportedKey', 'arn:aws:...');
myKeyImported.addAlias('alias/foo');
```

Note that a call to `.addToPolicy(statement)` on `myKeyImported` will not have
Note that a call to `.addToResourcePolicy(statement)` on `myKeyImported` will not have
an affect on the key's policy because it is not owned by your stack. The call
will be a no-op.

### Import key by alias

If a Key has an associated Alias, the Alias can be imported by name and used in place
of the Key as a reference. A common scenario for this is in referencing AWS managed keys.

Expand All @@ -91,6 +95,34 @@ Note that calls to `addToResourcePolicy` and `grant*` methods on `myKeyAlias` wi
no-ops, and `addAlias` and `aliasTargetKey` will fail, as the imported alias does not
have a reference to the underlying KMS Key.

### Lookup key by alias

If you can't use a KMS key imported by alias (e.g. because you need access to the key id), you can lookup the key with `Key.fromLookup()`.

In general, the preferred method would be to use `Alias.fromAliasName()` which returns an `IAlias` object which extends `IKey`. However, some services need to have access to the underlying key id. In this case, `Key.fromLookup()` allows to lookup the key id.

The result of the `Key.fromLookup()` operation will be written to a file
called `cdk.context.json`. You must commit this file to source control so
that the lookup values are available in non-privileged environments such
as CI build steps, and to ensure your template builds are repeatable.

Here's how `Key.fromLookup()` can be used:

```ts
const myKeyLookup = kms.Key.fromLookup(this, 'MyKeyLookup', {
aliasName: 'alias/KeyAlias'
});

const role = new iam.Role(this, 'MyRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
});
myKeyLookup.grantEncryptDecrypt(role);
```

Note that a call to `.addToResourcePolicy(statement)` on `myKeyLookup` will not have
an affect on the key's policy because it is not owned by your stack. The call
will be a no-op.

## Key Policies

Controlling access and usage of KMS Keys requires the use of key policies (resource-based policies attached to the key);
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-kms/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './key';
export * from './key-lookup';
export * from './alias';
export * from './via-service-principal';

Expand Down
9 changes: 9 additions & 0 deletions packages/@aws-cdk/aws-kms/lib/key-lookup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Properties for looking up an existing Key.
*/
export interface KeyLookupOptions {
/**
* The alias name of the Key
*/
readonly aliasName: string;
}
58 changes: 57 additions & 1 deletion packages/@aws-cdk/aws-kms/lib/key.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import * as iam from '@aws-cdk/aws-iam';
import { FeatureFlags, IResource, Lazy, RemovalPolicy, Resource, Stack, Duration } from '@aws-cdk/core';
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import { FeatureFlags, IResource, Lazy, RemovalPolicy, Resource, Stack, Duration, Token, ContextProvider, Arn } from '@aws-cdk/core';
import * as cxapi from '@aws-cdk/cx-api';
import { IConstruct, Construct } from 'constructs';
import { Alias } from './alias';
import { KeyLookupOptions } from './key-lookup';
import { CfnKey } from './kms.generated';
import * as perms from './private/perms';

Expand Down Expand Up @@ -534,6 +536,60 @@ export class Key extends KeyBase {
}(cfnKey, id);
}

/**
* Import an existing Key by querying the AWS environment this stack is deployed to.
*
* This function only needs to be used to use Keys not defined in your CDK
* application. If you are looking to share a Key between stacks, you can
* pass the `Key` object between stacks and use it as normal. In addition,
* it's not necessary to use this method if an interface accepts an `IKey`.
* In this case, `Alias.fromAliasName()` can be used which returns an alias
* that extends `IKey`.
*
* Calling this method will lead to a lookup when the CDK CLI is executed.
* You can therefore not use any values that will only be available at
* CloudFormation execution time (i.e., Tokens).
*
* The Key information will be cached in `cdk.context.json` and the same Key
* will be used on future runs. To refresh the lookup, you will have to
* evict the value from the cache using the `cdk context` command. See
* https://docs.aws.amazon.com/cdk/latest/guide/context.html for more information.
*/
public static fromLookup(scope: Construct, id: string, options: KeyLookupOptions): IKey {
class Import extends KeyBase {
public readonly keyArn: string;
public readonly keyId: string;
protected readonly policy?: iam.PolicyDocument | undefined = undefined;
// defaulting true: if we are importing the key the key policy is
// undefined and impossible to change here; this means updating identity
// policies is really the only option
protected readonly trustAccountIdentities: boolean = true;

constructor(keyId: string, keyArn: string) {
super(scope, id);

this.keyId = keyId;
this.keyArn = keyArn;
}
}
if (Token.isUnresolved(options.aliasName)) {
throw new Error('All arguments to Key.fromLookup() must be concrete (no Tokens)');
}

const attributes: cxapi.KeyContextResponse = ContextProvider.getValue(scope, {
provider: cxschema.ContextProvider.KEY_PROVIDER,
props: {
aliasName: options.aliasName,
} as cxschema.KeyContextQuery,
dummyValue: {
keyId: '1234abcd-12ab-34cd-56ef-1234567890ab',
},
}).value;

return new Import(attributes.keyId,
Arn.format({ resource: 'key', service: 'kms', resourceName: attributes.keyId }, Stack.of(scope)));
}

public readonly keyArn: string;
public readonly keyId: string;
protected readonly policy?: iam.PolicyDocument;
Expand Down
9 changes: 6 additions & 3 deletions packages/@aws-cdk/aws-kms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,20 +78,23 @@
"cdk-integ-tools": "0.0.0",
"cfn2ts": "0.0.0",
"pkglint": "0.0.0",
"@aws-cdk/assert-internal": "0.0.0"
"@aws-cdk/assert-internal": "0.0.0",
"@aws-cdk/cloud-assembly-schema": "0.0.0"
},
"dependencies": {
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/core": "0.0.0",
"@aws-cdk/cx-api": "0.0.0",
"constructs": "^3.3.69"
"constructs": "^3.3.69",
"@aws-cdk/cloud-assembly-schema": "0.0.0"
},
"homepage": "https://github.com/aws/aws-cdk",
"peerDependencies": {
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/core": "0.0.0",
"@aws-cdk/cx-api": "0.0.0",
"constructs": "^3.3.69"
"constructs": "^3.3.69",
"@aws-cdk/cloud-assembly-schema": "0.0.0"
},
"engines": {
"node": ">= 10.13.0 <13 || >=13.7.0"
Expand Down
70 changes: 70 additions & 0 deletions packages/@aws-cdk/aws-kms/test/key.from-lookup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import { ContextProvider, GetContextValueOptions, GetContextValueResult, Lazy, Stack } from '@aws-cdk/core';
import * as cxapi from '@aws-cdk/cx-api';
import { Construct } from 'constructs';
import { Key } from '../lib';
import '@aws-cdk/assert-internal/jest';

test('requires concrete values', () => {
expect(() => {
// GIVEN
const stack = new Stack();

Key.fromLookup(stack, 'Key', {
aliasName: Lazy.string({ produce: () => 'some-id' }),
});
}).toThrow('All arguments to Key.fromLookup() must be concrete (no Tokens)');
});

test('return correct key', () => {
const previous = mockKeyContextProviderWith({
keyId: '12345678-1234-1234-1234-123456789012',
}, options => {
expect(options.aliasName).toEqual('alias/foo');
});

const stack = new Stack(undefined, undefined, { env: { region: 'us-east-1', account: '123456789012' } });
const key = Key.fromLookup(stack, 'Key', {
aliasName: 'alias/foo',
});

expect(key.keyId).toEqual('12345678-1234-1234-1234-123456789012');
expect(stack.resolve(key.keyArn)).toEqual({
'Fn::Join': ['', [
'arn:',
{ Ref: 'AWS::Partition' },
':kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012',
]],
});

restoreContextProvider(previous);
});

interface MockKeyContextResponse {
readonly keyId: string;
}

function mockKeyContextProviderWith(
response: MockKeyContextResponse,
paramValidator?: (options: cxschema.KeyContextQuery) => void) {
const previous = ContextProvider.getValue;
ContextProvider.getValue = (_scope: Construct, options: GetContextValueOptions) => {
// do some basic sanity checks
expect(options.provider).toEqual(cxschema.ContextProvider.KEY_PROVIDER);

if (paramValidator) {
paramValidator(options.props as any);
}

return {
value: {
...response,
} as cxapi.KeyContextResponse,
};
};
return previous;
}

function restoreContextProvider(previous: (scope: Construct, options: GetContextValueOptions) => GetContextValueResult): void {
ContextProvider.getValue = previous;
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export enum ContextProvider {
* Security group provider
*/
SECURITY_GROUP_PROVIDER = 'security-group',

/**
* KMS Key Provider
*/
KEY_PROVIDER = 'key-provider',
}

/**
Expand Down Expand Up @@ -415,6 +420,34 @@ export interface SecurityGroupContextQuery {
readonly securityGroupId: string;
}

/**
* Query input for looking up a KMS Key
*/
export interface KeyContextQuery {
/**
* Query account
*/
readonly account: string;

/**
* Query region
*/
readonly region: string;

/**
* The ARN of the role that should be used to look up the missing values
*
* @default - None
*/
readonly lookupRoleArn?: string;

/**
* Alias name used to search the Key
*/
readonly aliasName: string;

}

export type ContextQueryProperties = AmiContextQuery
| AvailabilityZonesContextQuery
| HostedZoneContextQuery
Expand All @@ -423,4 +456,5 @@ export type ContextQueryProperties = AmiContextQuery
| EndpointServiceAvailabilityZonesContextQuery
| LoadBalancerContextQuery
| LoadBalancerListenerContextQuery
| SecurityGroupContextQuery;
| SecurityGroupContextQuery
| KeyContextQuery;
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,9 @@
},
{
"$ref": "#/definitions/SecurityGroupContextQuery"
},
{
"$ref": "#/definitions/KeyContextQuery"
}
]
}
Expand All @@ -437,6 +440,7 @@
"availability-zones",
"endpoint-service-availability-zones",
"hosted-zone",
"key-provider",
"load-balancer",
"load-balancer-listener",
"security-group",
Expand Down Expand Up @@ -767,6 +771,33 @@
"securityGroupId"
]
},
"KeyContextQuery": {
"description": "Query input for looking up a KMS Key",
"type": "object",
"properties": {
"account": {
"description": "Query account",
"type": "string"
},
"region": {
"description": "Query region",
"type": "string"
},
"lookupRoleArn": {
"description": "The ARN of the role that should be used to look up the missing values (Default - None)",
"type": "string"
},
"aliasName": {
"description": "Alias name used to search the Key",
"type": "string"
}
},
"required": [
"account",
"aliasName",
"region"
]
},
"RuntimeInfo": {
"description": "Information about the application's runtime components.",
"type": "object",
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":"13.0.0"}
{"version":"14.0.0"}
11 changes: 11 additions & 0 deletions packages/@aws-cdk/cx-api/lib/context/key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Properties of a discovered key
*/
export interface KeyContextResponse {

/**
* Id of the key
*/
readonly keyId: string;

}
1 change: 1 addition & 0 deletions packages/@aws-cdk/cx-api/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './context/load-balancer';
export * from './context/availability-zones';
export * from './context/endpoint-service-availability-zones';
export * from './context/security-group';
export * from './context/key';
export * from './cloud-artifact';
export * from './artifacts/asset-manifest-artifact';
export * from './artifacts/cloudformation-artifact';
Expand Down
5 changes: 5 additions & 0 deletions packages/aws-cdk/lib/api/aws-auth/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface ISDK {
ecr(): AWS.ECR;
elbv2(): AWS.ELBv2;
secretsManager(): AWS.SecretsManager;
kms(): AWS.KMS;
}

/**
Expand Down Expand Up @@ -123,6 +124,10 @@ export class SDK implements ISDK {
return this.wrapServiceErrorHandling(new AWS.SecretsManager(this.config));
}

public kms(): AWS.KMS {
return this.wrapServiceErrorHandling(new AWS.KMS(this.config));
}

public async currentAccount(): Promise<Account> {
// Get/refresh if necessary before we can access `accessKeyId`
await this.forceCredentialRetrieval();
Expand Down
Loading

0 comments on commit 34a57ed

Please sign in to comment.