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): CompositePrincipal and allow multiple principal types #1377

Merged
merged 6 commits into from
Dec 18, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
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