Skip to content

Commit

Permalink
feat(logs): add support for cloudwatch logs resource policy (#17015)
Browse files Browse the repository at this point in the history
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*
  • Loading branch information
nom3ad authored Nov 1, 2021
1 parent db63fba commit e9a461d
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 6 deletions.
40 changes: 38 additions & 2 deletions packages/@aws-cdk/aws-logs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,44 @@ By default, the log group will be created in the same region as the stack. The `
log groups in other regions. This is typically useful when controlling retention for log groups auto-created by global services that
publish their log group to a specific region, such as AWS Chatbot creating a log group in `us-east-1`.

## Resource Policy

CloudWatch Resource Policies allow other AWS services or IAM Principals to put log events into the log groups.
A resource policy is automatically created when `addToResourcePolicy` is called on the LogGroup for the first time.

`ResourcePolicy` can also be created manually.

```ts
const logGroup = new LogGroup(this, 'LogGroup');
const resourcePolicy = new ResourcePolicy(this, 'ResourcePolicy');
resourcePolicy.document.addStatements(new iam.PolicyStatement({
actions: ['logs:CreateLogStream', 'logs:PutLogEvents'],
principals: [new iam.ServicePrincipal('es.amazonaws.com')],
resources: [logGroup.logGroupArn],
}));
```

Or more conveniently, write permissions to the log group can be granted as follows which gives same result as in the above example.

```ts
const logGroup = new LogGroup(this, 'LogGroup');
logGroup.grantWrite(iam.ServicePrincipal('es.amazonaws.com'));
```

Optionally name and policy statements can also be passed on `ResourcePolicy` construction.

```ts
const policyStatement = new new iam.PolicyStatement({
resources: ["*"],
actions: ['logs:PutLogEvents'],
principals: [new iam.ArnPrincipal('arn:aws:iam::123456789012:user/user-name')],
});
const resourcePolicy = new ResourcePolicy(this, 'ResourcePolicy', {
policyName: 'myResourcePolicy',
policyStatements: [policyStatement],
});
```

## Encrypting Log Groups

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


Examples:

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


Example:

```ts
Expand Down
25 changes: 21 additions & 4 deletions packages/@aws-cdk/aws-logs/lib/log-group.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import { IResource, RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core';
import { RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { LogStream } from './log-stream';
import { CfnLogGroup } from './logs.generated';
import { MetricFilter } from './metric-filter';
import { FilterPattern, IFilterPattern } from './pattern';
import { ResourcePolicy } from './policy';
import { ILogSubscriptionDestination, SubscriptionFilter } from './subscription-filter';

export interface ILogGroup extends IResource {
export interface ILogGroup extends iam.IResourceWithPolicy {
/**
* The ARN of this log group, with ':*' appended
*
Expand Down Expand Up @@ -93,6 +94,9 @@ abstract class LogGroupBase extends Resource implements ILogGroup {
*/
public abstract readonly logGroupName: string;


private policy?: ResourcePolicy;

/**
* Create a new Log Stream for this Log Group
*
Expand Down Expand Up @@ -169,13 +173,13 @@ abstract class LogGroupBase extends Resource implements ILogGroup {
* Give the indicated permissions on this log group and all streams
*/
public grant(grantee: iam.IGrantable, ...actions: string[]) {
return iam.Grant.addToPrincipal({
return iam.Grant.addToPrincipalOrResource({
grantee,
actions,
// A LogGroup ARN out of CloudFormation already includes a ':*' at the end to include the log streams under the group.
// See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-loggroup.html#w2ab1c21c10c63c43c11
resourceArns: [this.logGroupArn],
scope: this,
resource: this,
});
}

Expand All @@ -186,6 +190,19 @@ abstract class LogGroupBase extends Resource implements ILogGroup {
public logGroupPhysicalName(): string {
return this.physicalName;
}

/**
* Adds a statement to the resource policy associated with this log group.
* A resource policy will be automatically created upon the first call to `addToResourcePolicy`.
* @param statement The policy statement to add
*/
public addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult {
if (!this.policy) {
this.policy = new ResourcePolicy(this, 'Policy');
}
this.policy.document.addStatements(statement);
return { statementAdded: true, policyDependable: this.policy };
}
}

/**
Expand Down
47 changes: 47 additions & 0 deletions packages/@aws-cdk/aws-logs/lib/policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { PolicyDocument, PolicyStatement } from '@aws-cdk/aws-iam';
import { Resource, Lazy, Names } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnResourcePolicy } from './logs.generated';

/**
* Properties to define Cloudwatch log group resource policy
*/
export interface ResourcePolicyProps {
/**
* Name of the log group resource policy
* @default - Uses a unique id based on the construct path
*/
readonly policyName?: string;

/**
* Initial statements to add to the resource policy
*
* @default - No statements
*/
readonly policyStatements?: PolicyStatement[];
}

/**
* Creates Cloudwatch log group resource policies
*/
export class ResourcePolicy extends Resource {
/**
* The IAM policy document for this resource policy.
*/
public readonly document = new PolicyDocument();

constructor(scope: Construct, id: string, props?: ResourcePolicyProps) {
super(scope, id);
new CfnResourcePolicy(this, 'Resource', {
policyName: Lazy.string({
produce: () => props?.policyName ?? Names.uniqueId(this),
}),
policyDocument: Lazy.string({
produce: () => JSON.stringify(this.document),
}),
});
if (props?.policyStatements) {
this.document.addStatements(...props.policyStatements);
}
}
}
51 changes: 51 additions & 0 deletions packages/@aws-cdk/aws-logs/test/loggroup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,57 @@ describe('log group', () => {

});

test('grant to service principal', () => {
// GIVEN
const stack = new Stack();
const lg = new LogGroup(stack, 'LogGroup');
const sp = new iam.ServicePrincipal('es.amazonaws.com');

// WHEN
lg.grantWrite(sp);

// THEN
expect(stack).toHaveResource('AWS::Logs::ResourcePolicy', {
PolicyDocument: {
'Fn::Join': [
'',
[
'{"Statement":[{"Action":["logs:CreateLogStream","logs:PutLogEvents"],"Effect":"Allow","Principal":{"Service":"es.amazonaws.com"},"Resource":"',
{
'Fn::GetAtt': [
'LogGroupF5B46931',
'Arn',
],
},
'"}],"Version":"2012-10-17"}',
],
],
},
PolicyName: 'LogGroupPolicy643B329C',
});

});


test('can add a policy to the log group', () => {
// GIVEN
const stack = new Stack();
const lg = new LogGroup(stack, 'LogGroup');

// WHEN
lg.addToResourcePolicy(new iam.PolicyStatement({
resources: ['*'],
actions: ['logs:PutLogEvents'],
principals: [new iam.ArnPrincipal('arn:aws:iam::123456789012:user/user-name')],
}));

// THEN
expect(stack).toHaveResource('AWS::Logs::ResourcePolicy', {
PolicyDocument: '{"Statement":[{"Action":"logs:PutLogEvents","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::123456789012:user/user-name"},"Resource":"*"}],"Version":"2012-10-17"}',
PolicyName: 'LogGroupPolicy643B329C',
});
});

test('correctly returns physical name of the log group', () => {
// GIVEN
const stack = new Stack();
Expand Down

0 comments on commit e9a461d

Please sign in to comment.