Skip to content

Commit e9a461d

Browse files
authored
feat(logs): add support for cloudwatch logs resource policy (#17015)
CloudFormation now supports [Cloudwatch logs Resource policies](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-resourcepolicy.html) This PR adds L2 support for it. And now its possible to grant access to service principals as follows. Previously this was throwing an error - see #5343 ```ts const eventsTargetLogs = new logs.LogGroup(this, 'EventsTargetLogGroup'); eventsTargetLogs.grantWrite(new iam.ServicePrincipal('events.amazonaws.com')).assertSuccess(); ``` In future, following custom resource implementation of `LogGroupResourcePolicy` could be replaced. https://github.com/aws/aws-cdk/blob/83b8df8c390a27e10bf362f49babfb24ee425506/packages/@aws-cdk/aws-elasticsearch/lib/log-group-resource-policy.ts#L25 https://github.com/aws/aws-cdk/blob/a872e672f8990fc3879413e5d797533d3916e1fd/packages/@aws-cdk/aws-events-targets/lib/log-group-resource-policy.ts#L26 https://github.com/aws/aws-cdk/blob/a872e672f8990fc3879413e5d797533d3916e1fd/packages/@aws-cdk/aws-events-targets/lib/log-group-resource-policy.ts#L26 closes #5343 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent db63fba commit e9a461d

File tree

4 files changed

+157
-6
lines changed

4 files changed

+157
-6
lines changed

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

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,44 @@ By default, the log group will be created in the same region as the stack. The `
4848
log groups in other regions. This is typically useful when controlling retention for log groups auto-created by global services that
4949
publish their log group to a specific region, such as AWS Chatbot creating a log group in `us-east-1`.
5050

51+
## Resource Policy
52+
53+
CloudWatch Resource Policies allow other AWS services or IAM Principals to put log events into the log groups.
54+
A resource policy is automatically created when `addToResourcePolicy` is called on the LogGroup for the first time.
55+
56+
`ResourcePolicy` can also be created manually.
57+
58+
```ts
59+
const logGroup = new LogGroup(this, 'LogGroup');
60+
const resourcePolicy = new ResourcePolicy(this, 'ResourcePolicy');
61+
resourcePolicy.document.addStatements(new iam.PolicyStatement({
62+
actions: ['logs:CreateLogStream', 'logs:PutLogEvents'],
63+
principals: [new iam.ServicePrincipal('es.amazonaws.com')],
64+
resources: [logGroup.logGroupArn],
65+
}));
66+
```
67+
68+
Or more conveniently, write permissions to the log group can be granted as follows which gives same result as in the above example.
69+
70+
```ts
71+
const logGroup = new LogGroup(this, 'LogGroup');
72+
logGroup.grantWrite(iam.ServicePrincipal('es.amazonaws.com'));
73+
```
74+
75+
Optionally name and policy statements can also be passed on `ResourcePolicy` construction.
76+
77+
```ts
78+
const policyStatement = new new iam.PolicyStatement({
79+
resources: ["*"],
80+
actions: ['logs:PutLogEvents'],
81+
principals: [new iam.ArnPrincipal('arn:aws:iam::123456789012:user/user-name')],
82+
});
83+
const resourcePolicy = new ResourcePolicy(this, 'ResourcePolicy', {
84+
policyName: 'myResourcePolicy',
85+
policyStatements: [policyStatement],
86+
});
87+
```
88+
5189
## Encrypting Log Groups
5290

5391
By default, log group data is always encrypted in CloudWatch Logs. You have the
@@ -182,7 +220,6 @@ line.
182220
all of the terms in any of the groups (specified as arrays) matches. This is
183221
an OR match.
184222

185-
186223
Examples:
187224

188225
```ts
@@ -231,7 +268,6 @@ and then descending into it, such as `$.field` or `$.list[0].field`.
231268
given JSON patterns match. This makes an OR combination of the given
232269
patterns.
233270

234-
235271
Example:
236272

237273
```ts

packages/@aws-cdk/aws-logs/lib/log-group.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
22
import * as iam from '@aws-cdk/aws-iam';
33
import * as kms from '@aws-cdk/aws-kms';
4-
import { IResource, RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core';
4+
import { RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core';
55
import { Construct } from 'constructs';
66
import { LogStream } from './log-stream';
77
import { CfnLogGroup } from './logs.generated';
88
import { MetricFilter } from './metric-filter';
99
import { FilterPattern, IFilterPattern } from './pattern';
10+
import { ResourcePolicy } from './policy';
1011
import { ILogSubscriptionDestination, SubscriptionFilter } from './subscription-filter';
1112

12-
export interface ILogGroup extends IResource {
13+
export interface ILogGroup extends iam.IResourceWithPolicy {
1314
/**
1415
* The ARN of this log group, with ':*' appended
1516
*
@@ -93,6 +94,9 @@ abstract class LogGroupBase extends Resource implements ILogGroup {
9394
*/
9495
public abstract readonly logGroupName: string;
9596

97+
98+
private policy?: ResourcePolicy;
99+
96100
/**
97101
* Create a new Log Stream for this Log Group
98102
*
@@ -169,13 +173,13 @@ abstract class LogGroupBase extends Resource implements ILogGroup {
169173
* Give the indicated permissions on this log group and all streams
170174
*/
171175
public grant(grantee: iam.IGrantable, ...actions: string[]) {
172-
return iam.Grant.addToPrincipal({
176+
return iam.Grant.addToPrincipalOrResource({
173177
grantee,
174178
actions,
175179
// A LogGroup ARN out of CloudFormation already includes a ':*' at the end to include the log streams under the group.
176180
// See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-loggroup.html#w2ab1c21c10c63c43c11
177181
resourceArns: [this.logGroupArn],
178-
scope: this,
182+
resource: this,
179183
});
180184
}
181185

@@ -186,6 +190,19 @@ abstract class LogGroupBase extends Resource implements ILogGroup {
186190
public logGroupPhysicalName(): string {
187191
return this.physicalName;
188192
}
193+
194+
/**
195+
* Adds a statement to the resource policy associated with this log group.
196+
* A resource policy will be automatically created upon the first call to `addToResourcePolicy`.
197+
* @param statement The policy statement to add
198+
*/
199+
public addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult {
200+
if (!this.policy) {
201+
this.policy = new ResourcePolicy(this, 'Policy');
202+
}
203+
this.policy.document.addStatements(statement);
204+
return { statementAdded: true, policyDependable: this.policy };
205+
}
189206
}
190207

191208
/**
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { PolicyDocument, PolicyStatement } from '@aws-cdk/aws-iam';
2+
import { Resource, Lazy, Names } from '@aws-cdk/core';
3+
import { Construct } from 'constructs';
4+
import { CfnResourcePolicy } from './logs.generated';
5+
6+
/**
7+
* Properties to define Cloudwatch log group resource policy
8+
*/
9+
export interface ResourcePolicyProps {
10+
/**
11+
* Name of the log group resource policy
12+
* @default - Uses a unique id based on the construct path
13+
*/
14+
readonly policyName?: string;
15+
16+
/**
17+
* Initial statements to add to the resource policy
18+
*
19+
* @default - No statements
20+
*/
21+
readonly policyStatements?: PolicyStatement[];
22+
}
23+
24+
/**
25+
* Creates Cloudwatch log group resource policies
26+
*/
27+
export class ResourcePolicy extends Resource {
28+
/**
29+
* The IAM policy document for this resource policy.
30+
*/
31+
public readonly document = new PolicyDocument();
32+
33+
constructor(scope: Construct, id: string, props?: ResourcePolicyProps) {
34+
super(scope, id);
35+
new CfnResourcePolicy(this, 'Resource', {
36+
policyName: Lazy.string({
37+
produce: () => props?.policyName ?? Names.uniqueId(this),
38+
}),
39+
policyDocument: Lazy.string({
40+
produce: () => JSON.stringify(this.document),
41+
}),
42+
});
43+
if (props?.policyStatements) {
44+
this.document.addStatements(...props.policyStatements);
45+
}
46+
}
47+
}

packages/@aws-cdk/aws-logs/test/loggroup.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,57 @@ describe('log group', () => {
335335

336336
});
337337

338+
test('grant to service principal', () => {
339+
// GIVEN
340+
const stack = new Stack();
341+
const lg = new LogGroup(stack, 'LogGroup');
342+
const sp = new iam.ServicePrincipal('es.amazonaws.com');
343+
344+
// WHEN
345+
lg.grantWrite(sp);
346+
347+
// THEN
348+
expect(stack).toHaveResource('AWS::Logs::ResourcePolicy', {
349+
PolicyDocument: {
350+
'Fn::Join': [
351+
'',
352+
[
353+
'{"Statement":[{"Action":["logs:CreateLogStream","logs:PutLogEvents"],"Effect":"Allow","Principal":{"Service":"es.amazonaws.com"},"Resource":"',
354+
{
355+
'Fn::GetAtt': [
356+
'LogGroupF5B46931',
357+
'Arn',
358+
],
359+
},
360+
'"}],"Version":"2012-10-17"}',
361+
],
362+
],
363+
},
364+
PolicyName: 'LogGroupPolicy643B329C',
365+
});
366+
367+
});
368+
369+
370+
test('can add a policy to the log group', () => {
371+
// GIVEN
372+
const stack = new Stack();
373+
const lg = new LogGroup(stack, 'LogGroup');
374+
375+
// WHEN
376+
lg.addToResourcePolicy(new iam.PolicyStatement({
377+
resources: ['*'],
378+
actions: ['logs:PutLogEvents'],
379+
principals: [new iam.ArnPrincipal('arn:aws:iam::123456789012:user/user-name')],
380+
}));
381+
382+
// THEN
383+
expect(stack).toHaveResource('AWS::Logs::ResourcePolicy', {
384+
PolicyDocument: '{"Statement":[{"Action":"logs:PutLogEvents","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::123456789012:user/user-name"},"Resource":"*"}],"Version":"2012-10-17"}',
385+
PolicyName: 'LogGroupPolicy643B329C',
386+
});
387+
});
388+
338389
test('correctly returns physical name of the log group', () => {
339390
// GIVEN
340391
const stack = new Stack();

0 commit comments

Comments
 (0)