Skip to content

Commit cefdfd3

Browse files
authored
feat(elasticsearch): Decouple setting access policies from domain constructor (#15876)
Currently when creating an elasticsearch domain the access policies must be set in the constructor. This makes it impossible to create access policies that reference the domain. ### Use Cases See: https://aws.amazon.com/premiumsupport/knowledge-center/kinesis-firehose-cross-account-streaming/ ### Proposed Solution This PR extracts the access policy setting to a helper method which can be used if the `accessPolicies` and `useUnsignedBasicAuth` props are not set. The helper will error if access policies are already set to prevent creating duplicate custom resources. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 0e514de commit cefdfd3

16 files changed

+491
-134
lines changed

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

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,62 @@ const domain = new es.Domain(this, 'Domain', {
233233
const masterUserPassword = domain.masterUserPassword;
234234
```
235235

236+
## Custom access policies
236237

238+
If the domain requires custom access control it can be configured either as a
239+
constructor property, or later by means of a helper method.
240+
241+
For simple permissions the `accessPolicies` constructor may be sufficient:
242+
243+
```ts
244+
const domain = new es.Domain(this, 'Domain', {
245+
version: es.ElasticsearchVersion.V7_1,
246+
accessPolicies: [
247+
new iam.PolicyStatement({
248+
actions: ['es:*ESHttpPost', 'es:ESHttpPut*'],
249+
effect: iam.Effect.ALLOW,
250+
principals: [new iam.AccountPrincipal('123456789012')],
251+
resources: ['*'],
252+
}),
253+
]
254+
});
255+
```
256+
257+
For more complex use-cases, for example, to set the domain up to receive data from a
258+
[cross-account Kinesis Firehose](https://aws.amazon.com/premiumsupport/knowledge-center/kinesis-firehose-cross-account-streaming/) the `addAccessPolicies` helper method
259+
allows for policies that include the explicit domain ARN.
260+
261+
```ts
262+
const domain = new es.Domain(this, 'Domain', {
263+
version: es.ElasticsearchVersion.V7_1,
264+
});
265+
266+
domain.addAccessPolicies(
267+
new iam.PolicyStatement({
268+
actions: ['es:ESHttpPost', 'es:ESHttpPut'],
269+
effect: iam.Effect.ALLOW,
270+
principals: [new iam.AccountPrincipal('123456789012')],
271+
resources: [domain.domainArn, `${domain.domainArn}/*`],
272+
}),
273+
new iam.PolicyStatement({
274+
actions: ['es:ESHttpGet'],
275+
effect: iam.Effect.ALLOW,
276+
principals: [new iam.AccountPrincipal('123456789012')],
277+
resources: [
278+
`${domain.domainArn}/_all/_settings`,
279+
`${domain.domainArn}/_cluster/stats`,
280+
`${domain.domainArn}/index-name*/_mapping/type-name`,
281+
`${domain.domainArn}/roletest*/_mapping/roletest`,
282+
`${domain.domainArn}/_nodes`,
283+
`${domain.domainArn}/_nodes/stats`,
284+
`${domain.domainArn}/_nodes/*/stats`,
285+
`${domain.domainArn}/_stats`,
286+
`${domain.domainArn}/index-name*/_stats`,
287+
`${domain.domainArn}/roletest*/_stat`,
288+
],
289+
}),
290+
);
291+
```
237292

238293
## Audit logs
239294

@@ -400,7 +455,7 @@ Make the following modifications to your CDK application to migrate to the `@aws
400455
Follow these steps to migrate your application without data loss:
401456

402457
- Ensure that the [removal policy](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.RemovalPolicy.html) on your domains are set to `RemovalPolicy.RETAIN`. This is the default for the domain construct, so nothing is required unless you have specifically set the removal policy to some other value.
403-
- Remove the domain resource from your CloudFormation stacks by manually modifying the synthesized templates used to create the CloudFormation stacks. This may also involve modifying or deleting dependent resources, such as the custom resources that CDK creates to manage the domain's access policy or any other resource you have connected to the domain. You will need to search for references to each domain's logical ID to determine which other resources refer to it and replace or delete those references. Do not remove resources that are dependencies of the domain or you will have to recreate or import them before importing the domain. After modification, deploy the stacks through the AWS Management Console or using the AWS CLI.
458+
- Remove the domain resource from your CloudFormation stacks by manually modifying the synthesized templates used to create the CloudFormation stacks. This may also involve modifying or deleting dependent resources, such as the custom resources that CDK creates to manage the domain's access policy or any other resource you have connected to the domain. You will need to search for references to each domain's logical ID to determine which other resources refer to it and replace or delete those references. Do not remove resources that are dependencies of the domain or you will have to recreate or import them before importing the domain. After modification, deploy the stacks through the AWS Management Console or using the AWS CLI.
404459
- Migrate your CDK application to use the new `@aws-cdk/aws-opensearchservice` module by applying the necessary modifications listed above. Synthesize your application and obtain the resulting stack templates.
405460
- Copy just the definition of the domain from the "migrated" templates to the corresponding "stripped" templates that you deployed above. [Import](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import-existing-stack.html) the orphaned domains into your CloudFormation stacks using these templates.
406461
- Synthesize and deploy your CDK application to reconfigure/recreate the modified dependent resources. The CloudFormation stacks should now contain the same resources as existed prior to migration.

packages/@aws-cdk/aws-elasticsearch/lib/domain.ts

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,6 +1264,9 @@ export class Domain extends DomainBase implements IDomain, ec2.IConnectable {
12641264

12651265
private readonly domain: CfnDomain;
12661266

1267+
private accessPolicy?: ElasticsearchAccessPolicy
1268+
private encryptionAtRestOptions?: EncryptionAtRestOptions
1269+
12671270
private readonly _connections: ec2.Connections | undefined;
12681271

12691272
constructor(scope: Construct, id: string, props: DomainProps) {
@@ -1720,33 +1723,12 @@ export class Domain extends DomainBase implements IDomain, ec2.IConnectable {
17201723
});
17211724
}
17221725

1723-
const accessPolicyStatements: iam.PolicyStatement[] | undefined = unsignedBasicAuthEnabled
1724-
? (props.accessPolicies ?? []).concat(unsignedAccessPolicy)
1725-
: props.accessPolicies;
1726-
1727-
if (accessPolicyStatements != null) {
1728-
const accessPolicy = new ElasticsearchAccessPolicy(this, 'ESAccessPolicy', {
1729-
domainName: this.domainName,
1730-
domainArn: this.domainArn,
1731-
accessPolicies: accessPolicyStatements,
1732-
});
1733-
1734-
if (props.encryptionAtRest?.kmsKey) {
1735-
1736-
// https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/encryption-at-rest.html
1737-
1738-
// these permissions are documented as required during domain creation.
1739-
// while not strictly documented for updates as well, it stands to reason that an update
1740-
// operation might require these in case the cluster uses a kms key.
1741-
// empircal evidence shows this is indeed required: https://github.com/aws/aws-cdk/issues/11412
1742-
accessPolicy.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({
1743-
actions: ['kms:List*', 'kms:Describe*', 'kms:CreateGrant'],
1744-
resources: [props.encryptionAtRest.kmsKey.keyArn],
1745-
effect: iam.Effect.ALLOW,
1746-
}));
1747-
}
1748-
1749-
accessPolicy.node.addDependency(this.domain);
1726+
this.encryptionAtRestOptions = props.encryptionAtRest;
1727+
if (props.accessPolicies) {
1728+
this.addAccessPolicies(...props.accessPolicies);
1729+
}
1730+
if (unsignedBasicAuthEnabled) {
1731+
this.addAccessPolicies(unsignedAccessPolicy);
17501732
}
17511733
}
17521734

@@ -1760,6 +1742,38 @@ export class Domain extends DomainBase implements IDomain, ec2.IConnectable {
17601742
}
17611743
return this._connections;
17621744
}
1745+
1746+
/**
1747+
* Add policy statements to the domain access policy
1748+
*/
1749+
public addAccessPolicies(...accessPolicyStatements: iam.PolicyStatement[]) {
1750+
if (accessPolicyStatements.length > 0) {
1751+
if (!this.accessPolicy) {
1752+
// Only create the custom resource after there are statements to set.
1753+
this.accessPolicy = new ElasticsearchAccessPolicy(this, 'ESAccessPolicy', {
1754+
domainName: this.domainName,
1755+
domainArn: this.domainArn,
1756+
accessPolicies: accessPolicyStatements,
1757+
});
1758+
1759+
if (this.encryptionAtRestOptions?.kmsKey) {
1760+
// https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/encryption-at-rest.html
1761+
1762+
// these permissions are documented as required during domain creation.
1763+
// while not strictly documented for updates as well, it stands to reason that an update
1764+
// operation might require these in case the cluster uses a kms key.
1765+
// empircal evidence shows this is indeed required: https://github.com/aws/aws-cdk/issues/11412
1766+
this.accessPolicy.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({
1767+
actions: ['kms:List*', 'kms:Describe*', 'kms:CreateGrant'],
1768+
resources: [this.encryptionAtRestOptions.kmsKey.keyArn],
1769+
effect: iam.Effect.ALLOW,
1770+
}));
1771+
}
1772+
} else {
1773+
this.accessPolicy.addAccessPolicies(...accessPolicyStatements);
1774+
}
1775+
}
1776+
}
17631777
}
17641778

17651779
/**

packages/@aws-cdk/aws-elasticsearch/lib/elasticsearch-access-policy.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import * as iam from '@aws-cdk/aws-iam';
2+
import * as cdk from '@aws-cdk/core';
23
import * as cr from '@aws-cdk/custom-resources';
3-
4-
// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
5-
// eslint-disable-next-line no-duplicate-imports, import/order
6-
import { Construct } from '@aws-cdk/core';
4+
import { Construct } from 'constructs';
75

86
/**
97
* Construction properties for ElasticsearchAccessPolicy
@@ -29,25 +27,39 @@ export interface ElasticsearchAccessPolicyProps {
2927
* Creates LogGroup resource policies.
3028
*/
3129
export class ElasticsearchAccessPolicy extends cr.AwsCustomResource {
32-
constructor(scope: Construct, id: string, props: ElasticsearchAccessPolicyProps) {
33-
const policyDocument = new iam.PolicyDocument({
34-
statements: props.accessPolicies,
35-
});
3630

31+
private accessPolicyStatements: iam.PolicyStatement[] = [];
32+
33+
constructor(scope: Construct, id: string, props: ElasticsearchAccessPolicyProps) {
3734
super(scope, id, {
3835
resourceType: 'Custom::ElasticsearchAccessPolicy',
3936
onUpdate: {
4037
action: 'updateElasticsearchDomainConfig',
4138
service: 'ES',
4239
parameters: {
4340
DomainName: props.domainName,
44-
AccessPolicies: JSON.stringify(policyDocument.toJSON()),
41+
AccessPolicies: cdk.Lazy.string({
42+
produce: () => JSON.stringify(
43+
new iam.PolicyDocument({
44+
statements: this.accessPolicyStatements,
45+
}).toJSON(),
46+
),
47+
}),
4548
},
4649
// this is needed to limit the response body, otherwise it exceeds the CFN 4k limit
4750
outputPaths: ['DomainConfig.ElasticsearchClusterConfig.AccessPolicies'],
4851
physicalResourceId: cr.PhysicalResourceId.of(`${props.domainName}AccessPolicy`),
4952
},
5053
policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ resources: [props.domainArn] }),
5154
});
55+
56+
this.addAccessPolicies(...props.accessPolicies);
57+
}
58+
59+
/**
60+
* Add policy statements to the domain access policy
61+
*/
62+
public addAccessPolicies(...accessPolicyStatements: iam.PolicyStatement[]) {
63+
this.accessPolicyStatements.push(...accessPolicyStatements);
5264
}
5365
}

packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,67 @@ test('can enable version upgrade update policy', () => {
200200
});
201201
});
202202

203+
test('can set a self-referencing custom policy', () => {
204+
const domain = new Domain(stack, 'Domain', {
205+
version: ElasticsearchVersion.V7_1,
206+
});
207+
208+
domain.addAccessPolicies(
209+
new iam.PolicyStatement({
210+
actions: ['es:ESHttpPost', 'es:ESHttpPut'],
211+
effect: iam.Effect.ALLOW,
212+
principals: [new iam.AccountPrincipal('5678')],
213+
resources: [domain.domainArn, `${domain.domainArn}/*`],
214+
}),
215+
);
216+
217+
const expectedPolicy = {
218+
'Fn::Join': [
219+
'',
220+
[
221+
'{"action":"updateElasticsearchDomainConfig","service":"ES","parameters":{"DomainName":"',
222+
{
223+
Ref: 'Domain66AC69E0',
224+
},
225+
'","AccessPolicies":"{\\"Statement\\":[{\\"Action\\":[\\"es:ESHttpPost\\",\\"es:ESHttpPut\\"],\\"Effect\\":\\"Allow\\",\\"Principal\\":{\\"AWS\\":\\"arn:',
226+
{
227+
Ref: 'AWS::Partition',
228+
},
229+
':iam::5678:root\\"},\\"Resource\\":[\\"',
230+
{
231+
'Fn::GetAtt': [
232+
'Domain66AC69E0',
233+
'Arn',
234+
],
235+
},
236+
'\\",\\"',
237+
{
238+
'Fn::GetAtt': [
239+
'Domain66AC69E0',
240+
'Arn',
241+
],
242+
},
243+
'/*\\"]}],\\"Version\\":\\"2012-10-17\\"}"},"outputPaths":["DomainConfig.ElasticsearchClusterConfig.AccessPolicies"],"physicalResourceId":{"id":"',
244+
{
245+
Ref: 'Domain66AC69E0',
246+
},
247+
'AccessPolicy"}}',
248+
],
249+
],
250+
};
251+
Template.fromStack(stack).hasResourceProperties('Custom::ElasticsearchAccessPolicy', {
252+
ServiceToken: {
253+
'Fn::GetAtt': [
254+
'AWS679f53fac002430cb0da5b7982bd22872D164C4C',
255+
'Arn',
256+
],
257+
},
258+
Create: expectedPolicy,
259+
Update: expectedPolicy,
260+
});
261+
});
262+
263+
203264
describe('UltraWarm instances', () => {
204265

205266
test('can enable UltraWarm instances', () => {

packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,55 @@ test('minimal example renders correctly', () => {
5555
}),
5656
});
5757
});
58+
59+
test('support access policy added inline and later', () => {
60+
const elasticsearchAccessPolicy = new ElasticsearchAccessPolicy(stack, 'ElasticsearchAccessPolicy', {
61+
domainName: 'TestDomain',
62+
domainArn: 'test:arn',
63+
accessPolicies: [
64+
new iam.PolicyStatement({
65+
effect: iam.Effect.ALLOW,
66+
actions: ['es:ESHttp*'],
67+
principals: [new iam.AnyPrincipal()],
68+
resources: ['test:arn'],
69+
}),
70+
],
71+
});
72+
elasticsearchAccessPolicy.addAccessPolicies(
73+
new iam.PolicyStatement({
74+
effect: iam.Effect.ALLOW,
75+
actions: ['*'],
76+
principals: [new iam.AnyPrincipal()],
77+
resources: ['test:arn'],
78+
}),
79+
);
80+
81+
Template.fromStack(stack).hasResourceProperties('Custom::ElasticsearchAccessPolicy', {
82+
ServiceToken: {
83+
'Fn::GetAtt': [
84+
'AWS679f53fac002430cb0da5b7982bd22872D164C4C',
85+
'Arn',
86+
],
87+
},
88+
Create: JSON.stringify({
89+
action: 'updateElasticsearchDomainConfig',
90+
service: 'ES',
91+
parameters: {
92+
DomainName: 'TestDomain',
93+
AccessPolicies: '{"Statement":[{"Action":"es:ESHttp*","Effect":"Allow","Principal":{"AWS":"*"},"Resource":"test:arn"},{"Action":"*","Effect":"Allow","Principal":{"AWS":"*"},"Resource":"test:arn"}],"Version":"2012-10-17"}',
94+
},
95+
outputPaths: ['DomainConfig.ElasticsearchClusterConfig.AccessPolicies'],
96+
physicalResourceId: { id: 'TestDomainAccessPolicy' },
97+
}),
98+
Update: JSON.stringify({
99+
action: 'updateElasticsearchDomainConfig',
100+
service: 'ES',
101+
parameters: {
102+
DomainName: 'TestDomain',
103+
AccessPolicies: '{"Statement":[{"Action":"es:ESHttp*","Effect":"Allow","Principal":{"AWS":"*"},"Resource":"test:arn"},{"Action":"*","Effect":"Allow","Principal":{"AWS":"*"},"Resource":"test:arn"}],"Version":"2012-10-17"}',
104+
},
105+
outputPaths: ['DomainConfig.ElasticsearchClusterConfig.AccessPolicies'],
106+
physicalResourceId: { id: 'TestDomainAccessPolicy' },
107+
}),
108+
});
109+
});

packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.expected.json

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,7 @@
222222
"Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2"
223223
}
224224
]
225-
},
226-
"DependsOn": [
227-
"Domain66AC69E0"
228-
]
225+
}
229226
},
230227
"DomainESAccessPolicy89986F33": {
231228
"Type": "Custom::ElasticsearchAccessPolicy",
@@ -287,8 +284,7 @@
287284
"InstallLatestAwsSdk": true
288285
},
289286
"DependsOn": [
290-
"DomainESAccessPolicyCustomResourcePolicy9747FC42",
291-
"Domain66AC69E0"
287+
"DomainESAccessPolicyCustomResourcePolicy9747FC42"
292288
],
293289
"UpdateReplacePolicy": "Delete",
294290
"DeletionPolicy": "Delete"

0 commit comments

Comments
 (0)