diff --git a/packages/@aws-cdk/aws-logs/README.md b/packages/@aws-cdk/aws-logs/README.md index 1fcd323ca621c..631bc382365e0 100644 --- a/packages/@aws-cdk/aws-logs/README.md +++ b/packages/@aws-cdk/aws-logs/README.md @@ -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 @@ -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 @@ -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 diff --git a/packages/@aws-cdk/aws-logs/lib/log-group.ts b/packages/@aws-cdk/aws-logs/lib/log-group.ts index 4c74dbf02f3ee..c701f4b5e4c9f 100644 --- a/packages/@aws-cdk/aws-logs/lib/log-group.ts +++ b/packages/@aws-cdk/aws-logs/lib/log-group.ts @@ -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 * @@ -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 * @@ -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, }); } @@ -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 }; + } } /** diff --git a/packages/@aws-cdk/aws-logs/lib/policy.ts b/packages/@aws-cdk/aws-logs/lib/policy.ts new file mode 100644 index 0000000000000..974f517d48b25 --- /dev/null +++ b/packages/@aws-cdk/aws-logs/lib/policy.ts @@ -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); + } + } +} diff --git a/packages/@aws-cdk/aws-logs/test/loggroup.test.ts b/packages/@aws-cdk/aws-logs/test/loggroup.test.ts index 72b3c390a2f96..d7fa5cb600b76 100644 --- a/packages/@aws-cdk/aws-logs/test/loggroup.test.ts +++ b/packages/@aws-cdk/aws-logs/test/loggroup.test.ts @@ -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();