Skip to content

Commit

Permalink
feat(iam): customer managed policies (#3578)
Browse files Browse the repository at this point in the history
* Support creation of customer managed policies

* Make ManagedPolicy changes pass build

* Add tests and make policy stuff work with users, groups and roles

* fix call to generatePolicyName from ManagedPolicy after merge from master

* Fix implementation of ManagedPolicy and add integ test

* Revert test.role.ts to its original form

* Remove unnecessary Lazy.stringValue

* Allow adding parameters to previously unused constructor

* Fix policy naming

* Fix tests
  • Loading branch information
IainCole authored and mergify[bot] committed Aug 9, 2019
1 parent ba2a4df commit 4681d01
Show file tree
Hide file tree
Showing 10 changed files with 807 additions and 8 deletions.
2 changes: 2 additions & 0 deletions allowed-breaking-changes.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
change-return-type:@aws-cdk/core.Fn.getAtt
new-argument:@aws-cdk/aws-iam.ManagedPolicy.<initializer>
new-argument:@aws-cdk/aws-iam.ManagedPolicy.<initializer>
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-iam/lib/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export class Group extends GroupBase {
* @param policy The managed policy to attach.
*/
public addManagedPolicy(policy: IManagedPolicy) {
if (this.managedPolicies.find(mp => mp === policy)) { return; }
this.managedPolicies.push(policy);
}
}
218 changes: 213 additions & 5 deletions packages/@aws-cdk/aws-iam/lib/managed-policy.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,111 @@
import { IResolveContext, Lazy, Stack } from '@aws-cdk/core';
import { Construct, IResolveContext, Lazy, Resource, Stack} from '@aws-cdk/core';
import { IGroup } from './group';
import { CfnManagedPolicy } from './iam.generated';
import { PolicyDocument } from './policy-document';
import { PolicyStatement } from './policy-statement';
import { IRole } from './role';
import { IUser } from './user';
import { undefinedIfEmpty } from './util';

/**
* A managed policy
*/
export interface IManagedPolicy {
/**
* The ARN of the managed policy
* @attribute
*/
readonly managedPolicyArn: string;
}

export interface ManagedPolicyProps {
/**
* The name of the managed policy. If you specify multiple policies for an entity,
* specify unique names. For example, if you specify a list of policies for
* an IAM role, each policy must have a unique name.
*
* @default - A name is automatically generated.
*/
readonly managedPolicyName?: string;

/**
* A description of the managed policy. Typically used to store information about the
* permissions defined in the policy. For example, "Grants access to production DynamoDB tables."
* The policy description is immutable. After a value is assigned, it cannot be changed.
*
* @default - empty
*/
readonly description?: string;

/**
* The path for the policy. This parameter allows (through its regex pattern) a string of characters
* consisting of either a forward slash (/) by itself or a string that must begin and end with forward slashes.
* In addition, it can contain any ASCII character from the ! (\u0021) through the DEL character (\u007F),
* including most punctuation characters, digits, and upper and lowercased letters.
*
* For more information about paths, see IAM Identifiers in the IAM User Guide.
*
* @default - "/"
*/
readonly path?: string;

/**
* Users to attach this policy to.
* You can also use `attachToUser(user)` to attach this policy to a user.
*
* @default - No users.
*/
readonly users?: IUser[];

/**
* Roles to attach this policy to.
* You can also use `attachToRole(role)` to attach this policy to a role.
*
* @default - No roles.
*/
readonly roles?: IRole[];

/**
* Groups to attach this policy to.
* You can also use `attachToGroup(group)` to attach this policy to a group.
*
* @default - No groups.
*/
readonly groups?: IGroup[];

/**
* Initial set of permissions to add to this policy document.
* You can also use `addPermission(statement)` to add permissions later.
*
* @default - No statements.
*/
readonly statements?: PolicyStatement[];
}

/**
* Managed policy
*
* This class is an incomplete placeholder class, and exists only to get access
* to AWS Managed policies.
*/
export class ManagedPolicy {
export class ManagedPolicy extends Resource implements IManagedPolicy {
/**
* Construct a customer managed policy from the managedPolicyName
*
* For this managed policy, you only need to know the name to be able to use it.
*
*/
public static fromManagedPolicyName(scope: Construct, id: string, managedPolicyName: string): IManagedPolicy {
class Import extends Resource implements IManagedPolicy {
public readonly managedPolicyArn = Stack.of(scope).formatArn({
service: "iam",
region: "", // no region for managed policy
account: Stack.of(scope).account, // Can this be something the user specifies?
resource: "policy",
resourceName: managedPolicyName
});
}
return new Import(scope, id);
}

/**
* Construct a managed policy from one of the policies that AWS manages
*
Expand All @@ -43,6 +132,125 @@ export class ManagedPolicy {
return new AwsManagedPolicy();
}

protected constructor() {
/**
* Returns the ARN of this managed policy.
*
* @attribute
*/
public readonly managedPolicyArn: string;

/**
* The policy document.
*/
public readonly document = new PolicyDocument();

/**
* The name of this policy.
*
* @attribute
*/
public readonly managedPolicyName: string;

/**
* The description of this policy.
*
* @attribute
*/
public readonly description: string;

/**
* The path of this policy.
*
* @attribute
*/
public readonly path: string;

private readonly roles = new Array<IRole>();
private readonly users = new Array<IUser>();
private readonly groups = new Array<IGroup>();

constructor(scope: Construct, id: string, props: ManagedPolicyProps = {}) {
super(scope, id, {
physicalName: props.managedPolicyName
});

this.description = props.description || '';
this.path = props.path || '/';

const resource = new CfnManagedPolicy(this, 'Resource', {
policyDocument: this.document,
managedPolicyName: this.physicalName,
description: this.description,
path: this.path,
roles: undefinedIfEmpty(() => this.roles.map(r => r.roleName)),
users: undefinedIfEmpty(() => this.users.map(u => u.userName)),
groups: undefinedIfEmpty(() => this.groups.map(g => g.groupName)),
});

if (props.users) {
props.users.forEach(u => this.attachToUser(u));
}

if (props.groups) {
props.groups.forEach(g => this.attachToGroup(g));
}

if (props.roles) {
props.roles.forEach(r => this.attachToRole(r));
}

if (props.statements) {
props.statements.forEach(p => this.addStatements(p));
}

this.managedPolicyName = this.getResourceNameAttribute(resource.ref);
this.managedPolicyArn = this.getResourceArnAttribute(resource.ref, {
region: '', // IAM is global in each partition
service: 'iam',
resource: 'role',
resourceName: this.physicalName,
});
}

/**
* Adds a statement to the policy document.
*/
public addStatements(...statement: PolicyStatement[]) {
this.document.addStatements(...statement);
}

/**
* Attaches this policy to a user.
*/
public attachToUser(user: IUser) {
if (this.users.find(u => u === user)) { return; }
this.users.push(user);
}

/**
* Attaches this policy to a role.
*/
public attachToRole(role: IRole) {
if (this.roles.find(r => r === role)) { return; }
this.roles.push(role);
}

/**
* Attaches this policy to a group.
*/
public attachToGroup(group: IGroup) {
if (this.groups.find(g => g === group)) { return; }
this.groups.push(group);
}

protected validate(): string[] {
const result = new Array<string>();

// validate that the policy document is not empty
if (this.document.isEmpty) {
result.push('Managed Policy is empty. You must add statements to the policy');
}

return result;
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-iam/lib/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ export class Role extends Resource implements IRole {
* @param policy The the managed policy to attach.
*/
public addManagedPolicy(policy: IManagedPolicy) {
if (this.managedPolicies.find(mp => mp === policy)) { return; }
this.managedPolicies.push(policy);
}

Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-iam/lib/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export class User extends Resource implements IIdentity {
* @param policy The managed policy to attach.
*/
public addManagedPolicy(policy: IManagedPolicy) {
if (this.managedPolicies.find(mp => mp === policy)) { return; }
this.managedPolicies.push(policy);
}

Expand Down
6 changes: 6 additions & 0 deletions packages/@aws-cdk/aws-iam/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,11 @@
"engines": {
"node": ">= 8.10.0"
},
"awslint": {
"exclude": [
"construct-interface-extends-iconstruct:@aws-cdk/aws-iam.IManagedPolicy",
"resource-interface-extends-resource:@aws-cdk/aws-iam.IManagedPolicy"
]
},
"stability": "stable"
}
66 changes: 66 additions & 0 deletions packages/@aws-cdk/aws-iam/test/integ.managed-policy.expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"Resources": {
"MyUserDC45028B": {
"Type": "AWS::IAM::User",
"Properties": {
"ManagedPolicyArns": [
{
"Ref": "TwoManagedPolicy7E701864"
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/SecurityAudit"
]
]
}
]
}
},
"OneManagedPolicy77F9185F": {
"Type": "AWS::IAM::ManagedPolicy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "sqs:SendMessage",
"Effect": "Allow",
"Resource": "*"
}
],
"Version": "2012-10-17"
},
"Description": "My Policy",
"ManagedPolicyName": "Default",
"Path": "/some/path/",
"Users": [
{
"Ref": "MyUserDC45028B"
}
]
}
},
"TwoManagedPolicy7E701864": {
"Type": "AWS::IAM::ManagedPolicy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "lambda:InvokeFunction",
"Effect": "Allow",
"Resource": "*"
}
],
"Version": "2012-10-17"
},
"Description": "",
"Path": "/"
}
}
}
}
26 changes: 26 additions & 0 deletions packages/@aws-cdk/aws-iam/test/integ.managed-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { App, Stack } from "@aws-cdk/core";
import { ManagedPolicy, PolicyStatement } from "../lib";
import { User } from "../lib/user";

const app = new App();

const stack = new Stack(app, 'aws-cdk-iam-managed-policy');

const user = new User(stack, 'MyUser');

const policy = new ManagedPolicy(stack, 'OneManagedPolicy', {
managedPolicyName: 'Default',
description: 'My Policy',
path: '/some/path/',
});
policy.addStatements(new PolicyStatement({ resources: ['*'], actions: ['sqs:SendMessage'] }));
policy.attachToUser(user);

const policy2 = new ManagedPolicy(stack, 'TwoManagedPolicy');
policy2.addStatements(new PolicyStatement({ resources: ['*'], actions: ['lambda:InvokeFunction'] }));
user.addManagedPolicy(policy2);

const policy3 = ManagedPolicy.fromAwsManagedPolicyName('SecurityAudit');
user.addManagedPolicy(policy3);

app.synth();
Loading

0 comments on commit 4681d01

Please sign in to comment.