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

feat(iam): policy document from json #6486

Merged
merged 28 commits into from
Mar 11, 2020
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a13af7b
initial unit tests added
mbonig Feb 27, 2020
50a3635
filling out tests and Document/Statement
mbonig Feb 27, 2020
f52cf41
updating README with example usage
mbonig Feb 27, 2020
2e09290
refactoring for readability
mbonig Feb 27, 2020
97a63b3
fixing casing on the .fromJson in the readme
mbonig Feb 27, 2020
56e4708
adding kitchen sink test for a little more coverage.
mbonig Feb 27, 2020
b146cdf
Merge branch 'feat/policy-document-parsing' of github.com:mbonig/aws-…
mbonig Feb 27, 2020
e31ea46
adding additional tests
mbonig Feb 27, 2020
5652383
Merge branch 'master' into feat/policy-document-parsing
mbonig Feb 27, 2020
fca4707
adding test to explicitly check for the Statement being an array
mbonig Feb 28, 2020
95e8b7a
Merge branch 'feat/policy-document-parsing' of github.com:mbonig/aws-…
mbonig Feb 28, 2020
9536101
Merge branch 'master' into feat/policy-document-parsing
mbonig Feb 29, 2020
0c4da49
better failure order
mbonig Mar 2, 2020
98927a2
Merge branch 'feat/policy-document-parsing' of github.com:mbonig/aws-…
mbonig Mar 2, 2020
00001d3
reworking field logic
mbonig Mar 2, 2020
347c63e
pulled ensure function to module level
mbonig Mar 2, 2020
e9b6b58
Merge branch 'master' into feat/policy-document-parsing
mbonig Mar 4, 2020
92ae4bb
Merge branch 'master' into feat/policy-document-parsing
Mar 4, 2020
4393c81
Merge branch 'master' into feat/policy-document-parsing
mbonig Mar 5, 2020
89127a1
simplify principal parsing
Mar 10, 2020
1ba33b6
Merge pull request #1 from eladb/benisrae/policy-document-parsing-pro…
mbonig Mar 10, 2020
f4c3781
Merge branch 'master' into feat/policy-document-parsing
mbonig Mar 10, 2020
756c618
adding notPrinciapl test
mbonig Mar 10, 2020
76e0272
fixing test name
mbonig Mar 10, 2020
15d6242
Merge branch 'master' into feat/policy-document-parsing
mbonig Mar 11, 2020
8c530ef
Merge branch 'master' into feat/policy-document-parsing
mergify[bot] Mar 11, 2020
ffc2fcd
Merge branch 'master' into feat/policy-document-parsing
mergify[bot] Mar 11, 2020
c0378a2
Merge branch 'master' into feat/policy-document-parsing
mergify[bot] Mar 11, 2020
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
40 changes: 40 additions & 0 deletions packages/@aws-cdk/aws-iam/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,46 @@ const role = new iam.Role(this, 'MyRole', {
});
```

### Parsing JSON Policy Documents

The `PolicyDocument.fromJson` and `PolicyStatement.fromJson` static methods can be used to parse JSON objects. For example:

```ts
const policyDocument = {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "FirstStatement",
"Effect": "Allow",
"Action": ["iam:ChangePassword"],
"Resource": "*"
},
{
"Sid": "SecondStatement",
"Effect": "Allow",
"Action": "s3:ListAllMyBuckets",
"Resource": "*"
},
{
"Sid": "ThirdStatement",
"Effect": "Allow",
"Action": [
"s3:List*",
"s3:Get*"
],
"Resource": [
"arn:aws:s3:::confidential-data",
"arn:aws:s3:::confidential-data/*"
],
"Condition": {"Bool": {"aws:MultiFactorAuthPresent": "true"}}
}
]
};

const newPolicyDocument = PolicyDocument.fromJson(policyDocument);

```

### Features

* Policy name uniqueness is enforced. If two policies by the same name are attached to the same
Expand Down
16 changes: 16 additions & 0 deletions packages/@aws-cdk/aws-iam/lib/policy-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ export interface PolicyDocumentProps {
* A PolicyDocument is a collection of statements
*/
export class PolicyDocument implements cdk.IResolvable {

/**
* Creates a new PolicyDocument based on the object provided.
* This will accept an object created from the `.toJSON()` call
* @param obj the PolicyDocument in object form.
*/
public static fromJson(obj: any): PolicyDocument {
const newPolicyDocument = new PolicyDocument();
const statement = obj.Statement ?? [];
if (statement && !Array.isArray(statement)) {
throw new Error('Statement must be an array');
}
newPolicyDocument.addStatements(...obj.Statement.map((s: any) => PolicyStatement.fromJson(s)));
return newPolicyDocument;
}

public readonly creationStack: string[];
private readonly statements = new Array<PolicyStatement>();
private readonly autoAssignSids: boolean;
Expand Down
46 changes: 46 additions & 0 deletions packages/@aws-cdk/aws-iam/lib/policy-statement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,56 @@ import { AccountPrincipal, AccountRootPrincipal, Anyone, ArnPrincipal, Canonical
FederatedPrincipal, IPrincipal, ServicePrincipal, ServicePrincipalOpts } from './principals';
import { mergePrincipal } from './util';

const ensureArrayOrUndefined = (field: any) => {
if (field === undefined) {
return undefined;
}
if (typeof (field) !== "string" && !Array.isArray(field)) {
throw new Error("Fields must be either a string or an array of strings");
}
if (Array.isArray(field) && !!field.find((f: any) => typeof (f) !== "string")) {
throw new Error("Fields must be either a string or an array of strings");
}
return Array.isArray(field) ? field : [field];
};

/**
* Represents a statement in an IAM policy document.
*/
export class PolicyStatement {

/**
* Creates a new PolicyStatement based on the object provided.
* This will accept an object created from the `.toJSON()` call
* @param obj the PolicyStatement in object form.
*/
public static fromJson(obj: any) {
const statement = new PolicyStatement({
actions: ensureArrayOrUndefined(obj.Action),
resources: ensureArrayOrUndefined(obj.Resource),
conditions: obj.Condition,
effect: obj.Effect,
notActions: ensureArrayOrUndefined(obj.NotAction),
notResources: ensureArrayOrUndefined(obj.NotResource)
});

statement.sid = obj.Sid;

// Since the principals are a more complex object, not just a string or an array of strings,
// then just passing them through on the constructor doesn't work.
mbonig marked this conversation as resolved.
Show resolved Hide resolved
if (obj.Principal) {
/* tslint:disable:no-unused-expression */
obj.Principal === "*" && statement.addAnyPrincipal();
obj.Principal.AWS && statement.addArnPrincipal(obj.Principal.AWS);
obj.Principal.CanonicalUser && statement.addCanonicalUserPrincipal(obj.Principal.CanonicalUser);
obj.Principal.Federated && statement.addFederatedPrincipal(obj.Principal.Federated, {});
obj.Principal.Service && statement.addServicePrincipal(obj.Principal.Service.replace(/.amazonaws.com/i, ''));
/* tslint:enable:no-unused-expression */
}

return statement;

}
/**
* Statement ID for this statement
*/
Expand Down
10 changes: 10 additions & 0 deletions packages/@aws-cdk/aws-iam/test/policy-document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,4 +572,14 @@ describe('IAM polocy document', () => {

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

describe('fromJson', () => {
test("throws error when Statement isn't an array", () => {
expect(() => {
PolicyDocument.fromJson({
Statement: 'asdf'
});
}).toThrow(/Statement must be an array/);
});
});
});
257 changes: 257 additions & 0 deletions packages/@aws-cdk/aws-iam/test/policy-statement.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import '@aws-cdk/assert/jest';
import { Stack } from '@aws-cdk/core';
import { PolicyDocument, PolicyStatement } from '../lib';

describe('IAM policy statement', () => {

describe('from JSON', () => {
test('parses with no principal', () => {
// given
const stack = new Stack();

const s = new PolicyStatement();
s.addActions('service:action1', 'service:action2');
s.addAllResources();
s.addCondition('key', { equals: 'value' });

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

// when
const doc2 = PolicyDocument.fromJson(doc1.toJSON());

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

test('parses a given arnPrincipal', () => {
const stack = new Stack();

const s = new PolicyStatement();
s.addActions('service:action1', 'service:action2');
s.addAllResources();
s.addArnPrincipal('somearn');
s.addCondition('key', { equals: 'value' });

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

const doc2 = PolicyDocument.fromJson(doc1.toJSON());

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

});

test('parses a given anyPrincipal', () => {
const stack = new Stack();

const s = new PolicyStatement();
s.addActions('service:action1', 'service:action2');
s.addAllResources();
s.addAnyPrincipal();
s.addCondition('key', { equals: 'value' });

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

const doc2 = PolicyDocument.fromJson(doc1.toJSON());

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

});

test('parses an awsAccountPrincipal', () => {
const stack = new Stack();

const s = new PolicyStatement();
s.addActions('service:action1', 'service:action2');
s.addAllResources();
s.addAwsAccountPrincipal('someaccountid');
s.addCondition('key', { equals: 'value' });

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

const doc2 = PolicyDocument.fromJson(doc1.toJSON());

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

});

test('parses a given canonicalUserPrincipal', () => {
const stack = new Stack();

const s = new PolicyStatement();
s.addActions('service:action1', 'service:action2');
s.addAllResources();
s.addCanonicalUserPrincipal('someconnonicaluser');
s.addCondition('key', { equals: 'value' });

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

const doc2 = PolicyDocument.fromJson(doc1.toJSON());

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

});

test('parses a given federatedPrincipal', () => {
const stack = new Stack();

const s = new PolicyStatement();
s.addActions('service:action1', 'service:action2');
s.addAllResources();
s.addFederatedPrincipal('federated', {});
s.addCondition('key', { equals: 'value' });

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

const doc2 = PolicyDocument.fromJson(doc1.toJSON());

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

});

test('parses a given servicePrincipal', () => {
const stack = new Stack();

const s = new PolicyStatement();
s.addActions('service:action1', 'service:action2');
s.addAllResources();
s.addServicePrincipal('serviceprincipal', { conditions: { one: "two" }, region: 'us-west-2' });
s.addCondition('key', { equals: 'value' });

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

const doc2 = PolicyDocument.fromJson(doc1.toJSON());

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

});

test('parses with notAction', () => {
const stack = new Stack();

const s = new PolicyStatement();
s.addNotActions('service:action3');
s.addAllResources();

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

const doc2 = PolicyDocument.fromJson(doc1.toJSON());

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

});

test('parses with notActions', () => {
const stack = new Stack();

const s = new PolicyStatement();
s.addNotActions('service:action3', 'service:action4');
s.addAllResources();

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

const doc2 = PolicyDocument.fromJson(doc1.toJSON());

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

});

test('parses with notResource', () => {
const stack = new Stack();

const s = new PolicyStatement();
s.addActions('service:action3', 'service:action4');
s.addNotResources('resource1');

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

const doc2 = PolicyDocument.fromJson(doc1.toJSON());

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

});

test('parses with notResources', () => {
const stack = new Stack();

const s = new PolicyStatement();
s.addActions('service:action3', 'service:action4');
s.addNotResources('resource1', 'resource2');

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

const doc2 = PolicyDocument.fromJson(doc1.toJSON());

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

});

test('the kitchen sink', () => {
const stack = new Stack();

/* tslint:disable */
const policyDocument = {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "FirstStatement",
"Effect": "Allow",
"Action": "iam:ChangePassword",
"Resource": "*"
},
{
"Sid": "SecondStatement",
"Effect": "Allow",
"Action": "s3:ListAllMyBuckets",
"Resource": "*"
},
{
"Sid": "ThirdStatement",
"Effect": "Allow",
"Action": [
"s3:List*",
"s3:Get*"
],
"Resource": [
"arn:aws:s3:::confidential-data",
"arn:aws:s3:::confidential-data/*"
],
"Condition": {"Bool": {"aws:MultiFactorAuthPresent": "true"}}
}
]
};
/* tslint:enable */

const doc = PolicyDocument.fromJson(policyDocument);

expect(stack.resolve(doc)).toEqual(policyDocument);
});

test('throws error with field data being object', () => {
expect(() => {
PolicyStatement.fromJson({
Action: {}
});
}).toThrow(/Fields must be either a string or an array of strings/);
});

test('throws error with field data being object', () => {
expect(() => {
PolicyStatement.fromJson({
Action: [{}]
});
}).toThrow(/Fields must be either a string or an array of strings/);
});
});

});