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): customer managed policies #3578

Merged
merged 12 commits into from
Aug 9, 2019
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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we really need a way to add comments here

"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