Skip to content

Commit 1fdf122

Browse files
authored
fix(logs): Faulty Resource Policy Generated (#19640)
Closes #17544. Cloudwatch logs resource policies do not accept ARNs of any kind as principals. This PR adds logic to convert any ARN principals to account ID strings for resource policies and provides methods to do so if needed in other modules, even if those ARNs are encoded as tokens (for example, if using an imported value to retrieve an ARN principal). Shout-out to @rix0rrr for coauthoring this. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent ce2b91b commit 1fdf122

File tree

5 files changed

+102
-10
lines changed

5 files changed

+102
-10
lines changed

packages/@aws-cdk/aws-iam/lib/policy-statement.ts

+32
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ export class PolicyStatement {
7070
private readonly condition: { [key: string]: any } = { };
7171
private principalConditionsJson?: string;
7272

73+
// Hold on to those principals
74+
private readonly _principals = new Array<IPrincipal>();
75+
7376
constructor(props: PolicyStatementProps = {}) {
7477
// Validate actions
7578
for (const action of [...props.actions || [], ...props.notActions || []]) {
@@ -145,6 +148,7 @@ export class PolicyStatement {
145148
* @param principals IAM principals that will be added
146149
*/
147150
public addPrincipals(...principals: IPrincipal[]) {
151+
this._principals.push(...principals);
148152
if (Object.keys(principals).length > 0 && Object.keys(this.notPrincipal).length > 0) {
149153
throw new Error('Cannot add \'Principals\' to policy statement if \'NotPrincipals\' have been added');
150154
}
@@ -156,6 +160,15 @@ export class PolicyStatement {
156160
}
157161
}
158162

163+
/**
164+
* Expose principals to allow their ARNs to be replaced by account ID strings
165+
* in policy statements for resources policies that don't allow full account ARNs,
166+
* such as AWS::Logs::ResourcePolicy.
167+
*/
168+
public get principals(): IPrincipal[] {
169+
return [...this._principals];
170+
}
171+
159172
/**
160173
* Specify principals that is not allowed or denied access to the "NotPrincipal" section of
161174
* a policy statement.
@@ -319,6 +332,25 @@ export class PolicyStatement {
319332
this.addCondition('StringEquals', { 'sts:ExternalId': accountId });
320333
}
321334

335+
/**
336+
* Create a new `PolicyStatement` with the same exact properties
337+
* as this one, except for the overrides
338+
*/
339+
public copy(overrides: PolicyStatementProps = {}) {
340+
return new PolicyStatement({
341+
sid: overrides.sid ?? this.sid,
342+
effect: overrides.effect ?? this.effect,
343+
actions: overrides.actions ?? this.action,
344+
notActions: overrides.notActions ?? this.notAction,
345+
346+
principals: overrides.principals,
347+
notPrincipals: overrides.notPrincipals,
348+
349+
resources: overrides.resources ?? this.resource,
350+
notResources: overrides.notResources ?? this.notResource,
351+
});
352+
}
353+
322354
/**
323355
* JSON-ify the policy statement
324356
*

packages/@aws-cdk/aws-lambda/test/singleton-lambda.test.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -172,17 +172,17 @@ describe('singleton lambda', () => {
172172

173173
// WHEN
174174
const invokeResult = singleton.grantInvoke(new iam.ServicePrincipal('events.amazonaws.com'));
175-
const statement = stack.resolve(invokeResult.resourceStatement);
175+
const statement = stack.resolve(invokeResult.resourceStatement?.toJSON());
176176

177177
// THEN
178178
Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', {
179179
Action: 'lambda:InvokeFunction',
180180
Principal: 'events.amazonaws.com',
181181
});
182-
expect(statement.action).toEqual(['lambda:InvokeFunction']);
183-
expect(statement.principal).toEqual({ Service: ['events.amazonaws.com'] });
184-
expect(statement.effect).toEqual('Allow');
185-
expect(statement.resource).toEqual([
182+
expect(statement.Action).toEqual('lambda:InvokeFunction');
183+
expect(statement.Principal).toEqual({ Service: 'events.amazonaws.com' });
184+
expect(statement.Effect).toEqual('Allow');
185+
expect(statement.Resource).toEqual([
186186
{ 'Fn::GetAtt': ['SingletonLambda84c0de93353f42179b0b45b6c993251a840BCC38', 'Arn'] },
187187
{ 'Fn::Join': ['', [{ 'Fn::GetAtt': ['SingletonLambda84c0de93353f42179b0b45b6c993251a840BCC38', 'Arn'] }, ':*']] },
188188
]);

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

+4
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ const logGroup = new logs.LogGroup(this, 'LogGroup');
6969
logGroup.grantWrite(new iam.ServicePrincipal('es.amazonaws.com'));
7070
```
7171

72+
Be aware that any ARNs or tokenized values passed to the resource policy will be converted into AWS Account IDs.
73+
This is because CloudWatch Logs Resource Policies do not accept ARNs as principals, but they do accept
74+
Account ID strings. Non-ARN principals, like Service principals or Any princpals, are accepted by CloudWatch.
75+
7276
## Encrypting Log Groups
7377

7478
By default, log group data is always encrypted in CloudWatch Logs. You have the

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

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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 { ArnFormat, RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core';
4+
import { Arn, ArnFormat, 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';
@@ -194,15 +194,39 @@ abstract class LogGroupBase extends Resource implements ILogGroup {
194194
/**
195195
* Adds a statement to the resource policy associated with this log group.
196196
* A resource policy will be automatically created upon the first call to `addToResourcePolicy`.
197+
*
198+
* Any ARN Principals inside of the statement will be converted into AWS Account ID strings
199+
* because CloudWatch Logs Resource Policies do not accept ARN principals.
200+
*
197201
* @param statement The policy statement to add
198202
*/
199203
public addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult {
200204
if (!this.policy) {
201205
this.policy = new ResourcePolicy(this, 'Policy');
202206
}
203-
this.policy.document.addStatements(statement);
207+
this.policy.document.addStatements(statement.copy({
208+
principals: statement.principals.map(p => this.convertArnPrincpalToAccountId(p)),
209+
}));
204210
return { statementAdded: true, policyDependable: this.policy };
205211
}
212+
213+
private convertArnPrincpalToAccountId(principal: iam.IPrincipal) {
214+
if (principal.principalAccount) {
215+
// we use ArnPrincipal here because the constructor inserts the argument
216+
// into the template without mutating it, which means that there is no
217+
// ARN created by this call.
218+
return new iam.ArnPrincipal(principal.principalAccount);
219+
}
220+
221+
if (principal instanceof iam.ArnPrincipal) {
222+
const parsedArn = Arn.split(principal.arn, ArnFormat.SLASH_RESOURCE_NAME);
223+
if (parsedArn.account) {
224+
return new iam.ArnPrincipal(parsedArn.account);
225+
}
226+
}
227+
228+
return principal;
229+
}
206230
}
207231

208232
/**

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

+35-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Template } from '@aws-cdk/assertions';
22
import * as iam from '@aws-cdk/aws-iam';
33
import * as kms from '@aws-cdk/aws-kms';
4-
import { CfnParameter, RemovalPolicy, Stack } from '@aws-cdk/core';
4+
import { CfnParameter, Fn, RemovalPolicy, Stack } from '@aws-cdk/core';
55
import { LogGroup, RetentionDays } from '../lib';
66

77
describe('log group', () => {
@@ -364,7 +364,7 @@ describe('log group', () => {
364364
});
365365
});
366366

367-
test('can add a policy to the log group', () => {
367+
test('when added to log groups, IAM users are converted into account IDs in the resource policy', () => {
368368
// GIVEN
369369
const stack = new Stack();
370370
const lg = new LogGroup(stack, 'LogGroup');
@@ -378,11 +378,43 @@ describe('log group', () => {
378378

379379
// THEN
380380
Template.fromStack(stack).hasResourceProperties('AWS::Logs::ResourcePolicy', {
381-
PolicyDocument: '{"Statement":[{"Action":"logs:PutLogEvents","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::123456789012:user/user-name"},"Resource":"*"}],"Version":"2012-10-17"}',
381+
PolicyDocument: '{"Statement":[{"Action":"logs:PutLogEvents","Effect":"Allow","Principal":{"AWS":"123456789012"},"Resource":"*"}],"Version":"2012-10-17"}',
382382
PolicyName: 'LogGroupPolicy643B329C',
383383
});
384384
});
385385

386+
test('imported values are treated as if they are ARNs and converted to account IDs via CFN pseudo parameters', () => {
387+
// GIVEN
388+
const stack = new Stack();
389+
const lg = new LogGroup(stack, 'LogGroup');
390+
391+
// WHEN
392+
lg.addToResourcePolicy(new iam.PolicyStatement({
393+
resources: ['*'],
394+
actions: ['logs:PutLogEvents'],
395+
principals: [iam.Role.fromRoleArn(stack, 'Role', Fn.importValue('SomeRole'))],
396+
}));
397+
398+
// THEN
399+
Template.fromStack(stack).hasResourceProperties('AWS::Logs::ResourcePolicy', {
400+
PolicyDocument: {
401+
'Fn::Join': [
402+
'',
403+
[
404+
'{\"Statement\":[{\"Action\":\"logs:PutLogEvents\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"',
405+
{
406+
'Fn::Select': [
407+
4,
408+
{ 'Fn::Split': [':', { 'Fn::ImportValue': 'SomeRole' }] },
409+
],
410+
},
411+
'\"},\"Resource\":\"*\"}],\"Version\":\"2012-10-17\"}',
412+
],
413+
],
414+
},
415+
});
416+
});
417+
386418
test('correctly returns physical name of the log group', () => {
387419
// GIVEN
388420
const stack = new Stack();

0 commit comments

Comments
 (0)