Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(iam): not possible to represent Principal: * #16843

Merged
merged 3 commits into from
Oct 7, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions packages/@aws-cdk/aws-iam/lib/policy-statement.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as cdk from '@aws-cdk/core';
import { AnyPrincipal } from '.';
import { Group } from './group';
import {
AccountPrincipal, AccountRootPrincipal, Anyone, ArnPrincipal, CanonicalUserPrincipal,
AccountPrincipal, AccountRootPrincipal, ArnPrincipal, CanonicalUserPrincipal,
FederatedPrincipal, IPrincipal, PrincipalBase, PrincipalPolicyFragment, ServicePrincipal, ServicePrincipalOpts,
} from './principals';
import { mergePrincipal } from './util';
import { LITERAL_STRING_KEY, mergePrincipal } from './util';

const ensureArrayOrUndefined = (field: any) => {
if (field === undefined) {
Expand Down Expand Up @@ -239,7 +240,7 @@ export class PolicyStatement {
* Adds all identities in all accounts ("*") to this policy statement
*/
public addAnyPrincipal() {
this.addPrincipals(new Anyone());
this.addPrincipals(new AnyPrincipal());
}

//
Expand Down Expand Up @@ -370,6 +371,11 @@ export class PolicyStatement {
function _normPrincipal(principal: { [key: string]: any[] }) {
const keys = Object.keys(principal);
if (keys.length === 0) { return undefined; }

if (LITERAL_STRING_KEY in principal) {
return principal[LITERAL_STRING_KEY][0];
}

const result: any = {};
for (const key of keys) {
const normVal = _norm(principal[key]);
Expand Down Expand Up @@ -600,9 +606,13 @@ class JsonPrincipal extends PrincipalBase {
constructor(json: any = { }) {
super();

// special case: if principal is a string, turn it into an "AWS" principal
// special case: if principal is a string, turn it into a "LiteralString" principal,
// so we render the exact same string back out.
if (typeof(json) === 'string') {
json = { AWS: json };
json = { [LITERAL_STRING_KEY]: [json] };
}
if (typeof(json) !== 'object') {
throw new Error(`JSON IAM principal should be an object, got ${JSON.stringify(json)}`);
}

this.policyFragment = {
Expand Down
40 changes: 38 additions & 2 deletions packages/@aws-cdk/aws-iam/lib/principals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Default, RegionInfo } from '@aws-cdk/region-info';
import { IOpenIdConnectProvider } from './oidc-provider';
import { Condition, Conditions, PolicyStatement } from './policy-statement';
import { ISamlProvider } from './saml-provider';
import { mergePrincipal } from './util';
import { LITERAL_STRING_KEY, mergePrincipal } from './util';

/**
* Any object that has an associated principal that a permission can be granted to
Expand Down Expand Up @@ -252,6 +252,15 @@ export class PrincipalWithConditions implements IPrincipal {
*
* This consists of the JSON used in the "Principal" field, and optionally a
* set of "Condition"s that need to be applied to the policy.
*
* Generally, a principal looks like:
*
* { '<TYPE>': ['ID', 'ID', ...] }
*
* And this is also the type of the field `principalJson`. However, there is a
* special type of principal that is just the string '*', which is treated
* differently by some services. To represent that principal, `principalJson`
* should contain `{ 'LiteralString': ['*'] }`.
*/
export class PrincipalPolicyFragment {
/**
Expand Down Expand Up @@ -545,7 +554,14 @@ export class AccountRootPrincipal extends AccountPrincipal {
}

/**
* A principal representing all identities in all accounts
* A principal representing all AWS identities in all accounts
*
* Some services behave differently when you specify `Principal: '*'`
* or `Principal: { AWS: "*" }` in their resource policy.
*
* `AnyPrincipal` renders to `Principal: { AWS: "*" }`. This is correct
* most of the time, but in cases where you need the other principal,
* use `StarPrincipal` instead.
*/
export class AnyPrincipal extends ArnPrincipal {
constructor() {
Expand All @@ -563,6 +579,26 @@ export class AnyPrincipal extends ArnPrincipal {
*/
export class Anyone extends AnyPrincipal { }

/**
* A principal that uses a literal '*' in the IAM JSON language
*
* Some services behave differently when you specify `Principal: "*"`
* or `Principal: { AWS: "*" }` in their resource policy.
*
* `StarPrincipal` renders to `Principal: *`. Most of the time, you
* should use `AnyPrincipal` instead.
*/
export class StarPrincipal extends PrincipalBase {
public readonly policyFragment: PrincipalPolicyFragment = {
principalJson: { [LITERAL_STRING_KEY]: ['*'] },
conditions: {},
};

public toString() {
return 'StarPrincipal()';
}
}

/**
* Represents a principal that has multiple types of principals. A composite principal cannot
* have conditions. i.e. multiple ServicePrincipals that form a composite principal
Expand Down
16 changes: 15 additions & 1 deletion packages/@aws-cdk/aws-iam/lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { IPolicy } from './policy';

const MAX_POLICY_NAME_LEN = 128;

export const LITERAL_STRING_KEY = 'LiteralString';

export function undefinedIfEmpty(f: () => string[]): string[] {
return Lazy.list({
produce: () => {
Expand Down Expand Up @@ -67,10 +69,18 @@ export class AttachedPolicies {

/**
* Merge two dictionaries that represent IAM principals
*
* Does an in-place merge.
*/
export function mergePrincipal(target: { [key: string]: string[] }, source: { [key: string]: string[] }) {
// If one represents a literal string, the other one must be empty
if ((LITERAL_STRING_KEY in source && !isEmptyObject(target)) ||
(LITERAL_STRING_KEY in target && !isEmptyObject(source))) {
throw new Error(`Cannot merge principals ${JSON.stringify(target)} and ${JSON.stringify(source)}; if one uses a literal principal string the other one must be empty`);
}

for (const key of Object.keys(source)) {
target[key] = target[key] || [];
target[key] = target[key] ?? [];

let value = source[key];
if (!Array.isArray(value)) {
Expand Down Expand Up @@ -123,4 +133,8 @@ export class UniqueStringSet implements IResolvable, IPostProcessor {
public toString(): string {
return Token.asString(this);
}
}

function isEmptyObject(x: { [key: string]: any }): boolean {
return Object.keys(x).length === 0;
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-iam/test/policy-document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,7 @@ describe('IAM policy document', () => {
});
}).toThrow(/Statement must be an array/);
});

});

test('adding another condition with the same operator does not delete the original', () => {
Expand Down
25 changes: 25 additions & 0 deletions packages/@aws-cdk/aws-iam/test/policy-statement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,32 @@ describe('IAM policy statement', () => {
const doc2 = PolicyDocument.fromJson(doc1.toJSON());

expect(stack.resolve(doc2)).toEqual(stack.resolve(doc1));
});

test('should not convert `Principal: *` to `Principal: { AWS: * }`', () => {
const stack = new Stack();
const s = PolicyStatement.fromJson({
Action: ['service:action1'],
Principal: '*',
Resource: '*',
});

const doc1 = new PolicyDocument();
doc1.addStatements(s);

const rendered = stack.resolve(doc1);

expect(rendered).toEqual({
Statement: [
{
Action: 'service:action1',
Effect: 'Allow',
Principal: '*',
Resource: '*',
},
],
Version: '2012-10-17',
});
});

test('parses a given notPrincipal', () => {
Expand Down
29 changes: 29 additions & 0 deletions packages/@aws-cdk/aws-iam/test/principals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,35 @@ test('SAML principal', () => {
});
});

test('StarPrincipal', () => {
// GIVEN
const stack = new Stack();

// WHEN
const pol = new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: ['service:action'],
resources: ['*'],
principals: [new iam.StarPrincipal()],
}),
],
});

// THEN
expect(stack.resolve(pol)).toEqual({
Statement: [
{
Action: 'service:action',
Effect: 'Allow',
Principal: '*',
Resource: '*',
},
],
Version: '2012-10-17',
});
});

test('PrincipalWithConditions.addCondition should work', () => {
// GIVEN
const stack = new Stack();
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-msk/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ export class Cluster extends ClusterBase {
new iam.PolicyStatement({
sid:
'Allow access through AWS Secrets Manager for all principals in the account that are authorized to use AWS Secrets Manager',
principals: [new iam.Anyone()],
principals: [new iam.AnyPrincipal()],
actions: [
'kms:Encrypt',
'kms:Decrypt',
Expand Down