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): add arbitrary conditions to existing principals #7015

Merged
merged 20 commits into from
Apr 6, 2020
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
5 changes: 5 additions & 0 deletions allowed-breaking-changes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,8 @@ change-return-type:@aws-cdk/aws-lambda-destinations.LambdaDestination.bind
change-return-type:@aws-cdk/aws-lambda-destinations.SnsDestination.bind
change-return-type:@aws-cdk/aws-lambda-destinations.SqsDestination.bind
removed:@aws-cdk/cdk-assets-schema.DockerImageDestination.imageUri
incompatible-argument:@aws-cdk/aws-iam.FederatedPrincipal.<initializer>
incompatible-argument:@aws-cdk/aws-iam.PolicyStatement.addCondition
incompatible-argument:@aws-cdk/aws-iam.PolicyStatement.addConditions
incompatible-argument:@aws-cdk/aws-iam.PolicyStatement.addFederatedPrincipal
incompatible-argument:@aws-cdk/aws-iam.PrincipalPolicyFragment.<initializer>
10 changes: 10 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,16 @@ const role = new iam.Role(this, 'MyRole', {
});
```

The `PrincipalWithConditions` class can be used to add conditions to a
principal, especially those that don't take a `conditions` parameter in their
constructor. The `principal.withConditions()` method can be used to create a
`PrincipalWithConditions` from an existing principal, for example:

```ts
const principal = new iam.AccountPrincipal('123456789000')
.withConditions({ StringEquals: { foo: "baz" } });
```

### Parsing JSON Policy Documents

The `PolicyDocument.fromJson` and `PolicyStatement.fromJson` static methods can be used to parse JSON objects. For example:
Expand Down
26 changes: 22 additions & 4 deletions packages/@aws-cdk/aws-iam/lib/policy-statement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export class PolicyStatement {
this.addPrincipals(new ServicePrincipal(service, opts));
}

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

Expand Down Expand Up @@ -200,15 +200,15 @@ export class PolicyStatement {
/**
* Add a condition to the Policy
*/
public addCondition(key: string, value: any) {
public addCondition(key: string, value: Condition) {
const existingValue = this.condition[key];
this.condition[key] = existingValue ? { ...existingValue, ...value } : value;
}

/**
* Add multiple conditions to the Policy
*/
public addConditions(conditions: {[key: string]: any}) {
public addConditions(conditions: Conditions) {
Object.keys(conditions).map(key => {
this.addCondition(key, conditions[key]);
});
Expand Down Expand Up @@ -303,6 +303,24 @@ export enum Effect {
DENY = 'Deny',
}

/**
* Condition for when an IAM policy is in effect. Maps from the keys in a request's context to
* a string value or array of string values. See the Conditions interface for more details.
*/
export type Condition = Record<string, any>;

/**
* Conditions for when an IAM Policy is in effect, specified in the following structure:
*
* `{ "Operator": { "keyInRequestContext": "value" } }`
*
* The value can be either a single string value or an array of string values.
*
* For more information, including which operators are supported, see [the IAM
* documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html).
*/
export type Conditions = Record<string, Condition>;

/**
* Interface for creating a policy statement
*/
Expand Down Expand Up @@ -398,7 +416,7 @@ class JsonPrincipal extends PrincipalBase {

this.policyFragment = {
principalJson: json,
conditions: []
conditions: {}
};
}
}
130 changes: 123 additions & 7 deletions packages/@aws-cdk/aws-iam/lib/principals.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as cdk from '@aws-cdk/core';
import { Default, RegionInfo } from '@aws-cdk/region-info';
import { PolicyStatement } from './policy-statement';
import { Condition, Conditions, PolicyStatement } from './policy-statement';
import { mergePrincipal } from './util';

/**
Expand Down Expand Up @@ -78,10 +78,108 @@ export abstract class PrincipalBase implements IPrincipal {
return JSON.stringify(this.policyFragment.principalJson);
}

/**
* JSON-ify the principal
*
* Used when JSON.stringify() is called
*/
public toJSON() {
// Have to implement toJSON() because the default will lead to infinite recursion.
return this.policyFragment.principalJson;
}

/**
* Returns a new PrincipalWithConditions using this principal as the base, with the
* passed conditions added.
*
* When there is a value for the same operator and key in both the principal and the
* conditions parameter, the value from the conditions parameter will be used.
*
* @returns a new PrincipalWithConditions object.
*/
public withConditions(conditions: Conditions): IPrincipal {
return new PrincipalWithConditions(this, conditions);
}
}

/**
* An IAM principal with additional conditions specifying when the policy is in effect.
*
* For more information about conditions, see:
* https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html
*/
export class PrincipalWithConditions implements IPrincipal {
public readonly grantPrincipal: IPrincipal = this;
public readonly assumeRoleAction: string = this.principal.assumeRoleAction;
private additionalConditions: Conditions;

constructor(
private readonly principal: IPrincipal,
conditions: Conditions,
) {
this.additionalConditions = conditions;
}

/**
* Add a condition to the principal
*/
public addCondition(key: string, value: Condition) {
const existingValue = this.conditions[key];
this.conditions[key] = existingValue ? { ...existingValue, ...value } : value;
}

/**
* Adds multiple conditions to the principal
*
* Values from the conditions parameter will overwrite existing values with the same operator
* and key.
*/
public addConditions(conditions: Conditions) {
Object.entries(conditions).forEach(([key, value]) => {
this.addCondition(key, value);
});
}

/**
* The conditions under which the policy is in effect.
* See [the IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html).
*/
public get conditions() {
return this.mergeConditions(this.principal.policyFragment.conditions, this.additionalConditions);
}

public get policyFragment(): PrincipalPolicyFragment {
return new PrincipalPolicyFragment(this.principal.policyFragment.principalJson, this.conditions);
}

public addToPolicy(statement: PolicyStatement): boolean {
return this.principal.addToPolicy(statement);
}

public toString() {
return this.principal.toString();
}

/**
* JSON-ify the principal
*
* Used when JSON.stringify() is called
*/
public toJSON() {
// Have to implement toJSON() because the default will lead to infinite recursion.
return this.policyFragment.principalJson;
}

private mergeConditions(principalConditions: Conditions, additionalConditions: Conditions): Conditions {
const mergedConditions: Conditions = {};
Object.entries(principalConditions).forEach(([operator, condition]) => {
mergedConditions[operator] = condition;
});
Object.entries(additionalConditions).forEach(([operator, condition]) => {
mergedConditions[operator] = { ...mergedConditions[operator], ...condition };
});
return mergedConditions;
}
}

/**
Expand All @@ -93,7 +191,11 @@ export abstract class PrincipalBase implements IPrincipal {
export class PrincipalPolicyFragment {
constructor(
public readonly principalJson: { [key: string]: string[] },
public readonly conditions: { [key: string]: any } = { }) {
/**
* The conditions under which the policy is in effect.
* See [the IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html).
*/
public readonly conditions: Conditions = {}) {
}
}

Expand All @@ -103,7 +205,7 @@ export class ArnPrincipal extends PrincipalBase {
}

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

public toString() {
Expand Down Expand Up @@ -200,7 +302,7 @@ export class CanonicalUserPrincipal extends PrincipalBase {
}

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

public toString() {
Expand All @@ -213,15 +315,19 @@ export class FederatedPrincipal extends PrincipalBase {

constructor(
public readonly federated: string,
public readonly conditions: {[key: string]: any},
/**
* The conditions under which the policy is in effect.
* See [the IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html).
*/
public readonly conditions: Conditions,
assumeRoleAction: string = 'sts:AssumeRole') {
super();

this.assumeRoleAction = assumeRoleAction;
}

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

public toString() {
Expand Down Expand Up @@ -293,7 +399,7 @@ export class CompositePrincipal extends PrincipalBase {
}

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

for (const p of this.principals) {
mergePrincipal(principalJson, p.policyFragment.principalJson);
Expand Down Expand Up @@ -324,6 +430,11 @@ class StackDependentToken implements cdk.IResolvable {
return cdk.Token.asString(this);
}

/**
* JSON-ify the token
*
* Used when JSON.stringify() is called
*/
public toJSON() {
return '<unresolved-token>';
}
Expand All @@ -349,6 +460,11 @@ class ServicePrincipalToken implements cdk.IResolvable {
});
}

/**
* JSON-ify the token
*
* Used when JSON.stringify() is called
*/
public toJSON() {
return `<${this.service}>`;
}
Expand Down
3 changes: 0 additions & 3 deletions packages/@aws-cdk/aws-iam/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@
"docs-public-apis:@aws-cdk/aws-iam.CompositePrincipal",
"docs-public-apis:@aws-cdk/aws-iam.CompositePrincipal.addPrincipals",
"docs-public-apis:@aws-cdk/aws-iam.FederatedPrincipal",
"docs-public-apis:@aws-cdk/aws-iam.FederatedPrincipal.conditions",
"docs-public-apis:@aws-cdk/aws-iam.FederatedPrincipal.federated",
"docs-public-apis:@aws-cdk/aws-iam.Group",
"docs-public-apis:@aws-cdk/aws-iam.LazyRole.roleId",
Expand All @@ -146,8 +145,6 @@
"docs-public-apis:@aws-cdk/aws-iam.PolicyStatement.addResources",
"docs-public-apis:@aws-cdk/aws-iam.PolicyStatement.toStatementJson",
"docs-public-apis:@aws-cdk/aws-iam.PolicyStatement.toString",
"docs-public-apis:@aws-cdk/aws-iam.PrincipalBase.toJSON",
"docs-public-apis:@aws-cdk/aws-iam.PrincipalPolicyFragment.conditions",
"docs-public-apis:@aws-cdk/aws-iam.PrincipalPolicyFragment.principalJson",
"docs-public-apis:@aws-cdk/aws-iam.ServicePrincipal.service",
"props-default-doc:@aws-cdk/aws-iam.GrantOnPrincipalOptions.scope",
Expand Down
Loading