Skip to content

Commit

Permalink
feat(iam): CompositePrincipal and allow multiple principal types (#1377)
Browse files Browse the repository at this point in the history
Relax constraint on IAM policy statement principals such
that multiple principal types can be used in a statement.

Also, the `CompositePrincipal` class can be use to construct
`PolicyPrincipal`s that consist of multiple principal types (without
conditions)

Backfill missing addXxxPrincipal methods.

Deprecate (soft) `Anyone` in favor of `AnyPrincipal`.

Fixes #1201
  • Loading branch information
Elad Ben-Israel authored Dec 18, 2018
1 parent 3d07e48 commit b942ae5
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 40 deletions.
54 changes: 54 additions & 0 deletions packages/@aws-cdk/aws-iam/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,60 @@ an `ExternalId` works like this:

[supplying an external ID](test/example.external-id.lit.ts)

### IAM Principals

When defining policy statements as part of an AssumeRole policy or as part of a
resource policy, statements would usually refer to a specific IAM principal
under `Principal`.

IAM principals are modeled as classes that derive from the `iam.PolicyPrincipal`
abstract class. Principal objects include principal type (string) and value
(array of string), optional set of conditions and the action that this principal
requires when it is used in an assume role policy document.

To add a principal to a policy statement you can either use the abstract
`statement.addPrincipal`, one of the concrete `addXxxPrincipal` methods:

* `addAwsPrincipal`, `addArnPrincipal` or `new ArnPrincipal(arn)` for `{ "AWS": arn }`
* `addAwsAccountPrincipal` or `new AccountPrincipal(accountId)` for `{ "AWS": account-arn }`
* `addServicePrincipal` or `new ServicePrincipal(service)` for `{ "Service": service }`
* `addAccountRootPrincipal` or `new AccountRootPrincipal()` for `{ "AWS": { "Ref: "AWS::AccountId" } }`
* `addCanonicalUserPrincipal` or `new CanonicalUserPrincipal(id)` for `{ "CanonicalUser": id }`
* `addFederatedPrincipal` or `new FederatedPrincipal(federated, conditions, assumeAction)` for
`{ "Federated": arn }` and a set of optional conditions and the assume role action to use.
* `addAnyPrincipal` or `new AnyPrincipal` for `{ "AWS": "*" }`

If multiple principals are added to the policy statement, they will be merged together:

```ts
const statement = new PolicyStatement();
statement.addServicePrincipal('cloudwatch.amazonaws.com');
statement.addServicePrincipal('ec2.amazonaws.com');
statement.addAwsPrincipal('arn:aws:boom:boom');
```

Will result in:

```json
{
"Principal": {
"Service": [ "cloudwatch.amazonaws.com", "ec2.amazonaws.com" ],
"AWS": "arn:aws:boom:boom"
}
}
```

The `CompositePrincipal` class can also be used to define complex principals, for example:

```ts
const role = new iam.Role(this, 'MyRole', {
assumedBy: new iam.CompositePrincipal(
new iam.ServicePrincipal('ec2.amazonawas.com'),
new iam.AccountPrincipal('1818188181818187272')
)
});
```

### Features

* Policy name uniqueness is enforced. If two policies by the same name are attached to the same
Expand Down
122 changes: 93 additions & 29 deletions packages/@aws-cdk/aws-iam/lib/policy-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export abstract class PolicyPrincipal {
/**
* When this Principal is used in an AssumeRole policy, the action to use.
*/
public readonly assumeRoleAction: string = 'sts:AssumeRole';
public assumeRoleAction: string = 'sts:AssumeRole';

/**
* Return the policy fragment that identifies this principal in a Policy.
Expand All @@ -65,8 +65,8 @@ export abstract class PolicyPrincipal {
*/
export class PrincipalPolicyFragment {
constructor(
public readonly principalJson: { [key: string]: any },
public readonly conditions: {[key: string]: any} = {}) {
public readonly principalJson: { [key: string]: string[] },
public readonly conditions: { [key: string]: any } = { }) {
}
}

Expand All @@ -76,7 +76,7 @@ export class ArnPrincipal extends PolicyPrincipal {
}

public policyFragment(): PrincipalPolicyFragment {
return new PrincipalPolicyFragment({ AWS: this.arn });
return new PrincipalPolicyFragment({ AWS: [ this.arn ] });
}
}

Expand All @@ -95,7 +95,7 @@ export class ServicePrincipal extends PolicyPrincipal {
}

public policyFragment(): PrincipalPolicyFragment {
return new PrincipalPolicyFragment({ Service: this.service });
return new PrincipalPolicyFragment({ Service: [ this.service ] });
}
}

Expand All @@ -113,25 +113,25 @@ export class ServicePrincipal extends PolicyPrincipal {
*
*/
export class CanonicalUserPrincipal extends PolicyPrincipal {
constructor(public readonly canonicalUserId: any) {
constructor(public readonly canonicalUserId: string) {
super();
}

public policyFragment(): PrincipalPolicyFragment {
return new PrincipalPolicyFragment({ CanonicalUser: this.canonicalUserId });
return new PrincipalPolicyFragment({ CanonicalUser: [ this.canonicalUserId ] });
}
}

export class FederatedPrincipal extends PolicyPrincipal {
constructor(
public readonly federated: any,
public readonly federated: string,
public readonly conditions: {[key: string]: any},
public readonly assumeRoleAction: string = 'sts:AssumeRole') {
public assumeRoleAction: string = 'sts:AssumeRole') {
super();
}

public policyFragment(): PrincipalPolicyFragment {
return new PrincipalPolicyFragment({ Federated: this.federated }, this.conditions);
return new PrincipalPolicyFragment({ Federated: [ this.federated ] }, this.conditions);
}
}

Expand All @@ -144,12 +144,60 @@ export class AccountRootPrincipal extends AccountPrincipal {
/**
* A principal representing all identities in all accounts
*/
export class Anyone extends ArnPrincipal {
export class AnyPrincipal extends ArnPrincipal {
constructor() {
super('*');
}
}

/**
* A principal representing all identities in all accounts
* @deprecated use `AnyPrincipal`
*/
export class Anyone extends AnyPrincipal { }

export class CompositePrincipal extends PolicyPrincipal {
private readonly principals = new Array<PolicyPrincipal>();

constructor(principal: PolicyPrincipal, ...additionalPrincipals: PolicyPrincipal[]) {
super();
this.assumeRoleAction = principal.assumeRoleAction;
this.addPrincipals(principal);
this.addPrincipals(...additionalPrincipals);
}

public addPrincipals(...principals: PolicyPrincipal[]): this {
for (const p of principals) {
if (p.assumeRoleAction !== this.assumeRoleAction) {
throw new Error(
`Cannot add multiple principals with different "assumeRoleAction". ` +
`Expecting "${this.assumeRoleAction}", got "${p.assumeRoleAction}"`);
}

const fragment = p.policyFragment();
if (fragment.conditions && Object.keys(fragment.conditions).length > 0) {
throw new Error(
`Components of a CompositePrincipal must not have conditions. ` +
`Tried to add the following fragment: ${JSON.stringify(fragment)}`);
}

this.principals.push(p);
}

return this;
}

public policyFragment(): PrincipalPolicyFragment {
const principalJson: { [key: string]: string[] } = { };

for (const p of this.principals) {
mergePrincipal(principalJson, p.policyFragment().principalJson);
}

return new PrincipalPolicyFragment(principalJson);
}
}

/**
* Represents a statement in an IAM policy document.
*/
Expand Down Expand Up @@ -191,44 +239,45 @@ export class PolicyStatement extends Token {
return Object.keys(this.principal).length > 0;
}

public addPrincipal(principal: PolicyPrincipal): PolicyStatement {
public addPrincipal(principal: PolicyPrincipal): this {
const fragment = principal.policyFragment();
for (const key of Object.keys(fragment.principalJson)) {
if (Object.keys(this.principal).length > 0 && !(key in this.principal)) {
throw new Error(`Attempted to add principal key ${key} in principal of type ${Object.keys(this.principal)[0]}`);
}
this.principal[key] = this.principal[key] || [];
const value = fragment.principalJson[key];
if (Array.isArray(value)) {
this.principal[key].push(...value);
} else {
this.principal[key].push(value);
}
}
mergePrincipal(this.principal, fragment.principalJson);
this.addConditions(fragment.conditions);
return this;
}

public addAwsPrincipal(arn: string): PolicyStatement {
public addAwsPrincipal(arn: string): this {
return this.addPrincipal(new ArnPrincipal(arn));
}

public addAwsAccountPrincipal(accountId: string): PolicyStatement {
public addArnPrincipal(arn: string): this {
return this.addAwsPrincipal(arn);
}

public addAwsAccountPrincipal(accountId: string): this {
return this.addPrincipal(new AccountPrincipal(accountId));
}

public addServicePrincipal(service: string): PolicyStatement {
public addServicePrincipal(service: string): this {
return this.addPrincipal(new ServicePrincipal(service));
}

public addFederatedPrincipal(federated: any, conditions: {[key: string]: any}): PolicyStatement {
public addFederatedPrincipal(federated: any, conditions: {[key: string]: any}): this {
return this.addPrincipal(new FederatedPrincipal(federated, conditions));
}

public addAccountRootPrincipal(): PolicyStatement {
public addAccountRootPrincipal(): this {
return this.addPrincipal(new AccountRootPrincipal());
}

public addCanonicalUserPrincipal(canonicalUserId: string): this {
return this.addPrincipal(new CanonicalUserPrincipal(canonicalUserId));
}

public addAnyPrincipal(): this {
return this.addPrincipal(new Anyone());
}

//
// Resources
//
Expand Down Expand Up @@ -386,3 +435,18 @@ export enum PolicyStatementEffect {
Allow = 'Allow',
Deny = 'Deny',
}

function mergePrincipal(target: { [key: string]: string[] }, source: { [key: string]: string[] }) {
for (const key of Object.keys(source)) {
target[key] = target[key] || [];

const value = source[key];
if (!Array.isArray(value)) {
throw new Error(`Principal value must be an array (it will be normalized later): ${value}`);
}

target[key].push(...value);
}

return target;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"Resources": {
"RoleWithCompositePrincipal17538C7F": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com",
"AWS": "*"
}
}
],
"Version": "2012-10-17"
}
}
}
}
}
21 changes: 21 additions & 0 deletions packages/@aws-cdk/aws-iam/test/integ.composite-principal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import cdk = require('@aws-cdk/cdk');
import iam = require('../lib');

class TestStack extends cdk.Stack {
constructor(parent: cdk.App, id: string) {
super(parent, id);

new iam.Role(this, 'RoleWithCompositePrincipal', {
assumedBy: new iam.CompositePrincipal(
new iam.ServicePrincipal('ec2.amazonaws.com'),
new iam.AnyPrincipal()
)
});
}
}

const app = new cdk.App();

new TestStack(app, 'iam-integ-composite-principal');

app.run();
Loading

0 comments on commit b942ae5

Please sign in to comment.