Skip to content
This repository has been archived by the owner on Feb 24, 2022. It is now read-only.

Commit

Permalink
feat(iam): add arbitrary conditions to existing principals (aws#7015)
Browse files Browse the repository at this point in the history
Closes aws#5855 

Adds a `PrincipalWithConditions` wrapper that allows conditions to be added to
any principal.

Behaviour is consistent with the way that `PolicyStatement.addCondition`
and `.addConditions` currently work - most notably in that adding an operator
that is already present will merge their objects, but adding a condition to
an operator/key combination that is already set will overwrite the existing
value (rather than merge the values into an array).

BREAKING CHANGES: every place an IAM Condition was expected it is now typed as `{[key: string]: any}`, instead of plain `any`. You were always supposed to pass a map/dictionary in these locations, but the type system didn't enforce it. It now does. This will not impact correct programs, but may cause compiler errors in programs that were incorrect.
  • Loading branch information
zakwalters authored and horsmand committed Apr 8, 2020
1 parent af520bb commit 4d769d1
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 20 deletions.
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

0 comments on commit 4d769d1

Please sign in to comment.