diff --git a/moto/iam/models.py b/moto/iam/models.py
index 27ea31de2..14fba2ae7 100644
--- a/moto/iam/models.py
+++ b/moto/iam/models.py
@@ -1,10 +1,57 @@
from __future__ import unicode_literals
-
-from moto.core import BaseBackend
-from .exceptions import IAMNotFoundException, IAMConflictException, IAMReportNotPresentException
-from .utils import random_access_key, random_alphanumeric, random_resource_id
-from datetime import datetime
import base64
+from datetime import datetime
+
+import pytz
+from moto.core import BaseBackend
+
+from .exceptions import IAMNotFoundException, IAMConflictException, IAMReportNotPresentException
+from .utils import random_access_key, random_alphanumeric, random_resource_id, random_policy_id
+
+
+class Policy(object):
+
+ is_attachable = False
+
+ def __init__(self,
+ name,
+ default_version_id=None,
+ description=None,
+ document=None,
+ path=None):
+ self.document = document or {}
+ self.name = name
+
+ self.attachment_count = 0
+ self.description = description or ''
+ self.id = random_policy_id()
+ self.path = path or '/'
+ self.default_version_id = default_version_id or 'v1'
+
+ self.create_datetime = datetime.now(pytz.utc)
+ self.update_datetime = datetime.now(pytz.utc)
+
+ @property
+ def arn(self):
+ return 'arn:aws:iam::aws:policy{0}{1}'.format(self.path, self.name)
+
+
+class ManagedPolicy(Policy):
+ """Managed policy."""
+
+ is_attachable = True
+
+ def attach_to_role(self, role):
+ self.attachment_count += 1
+ role.managed_policies[self.name] = self
+
+
+class AWSManagedPolicy(ManagedPolicy):
+ """AWS-managed policy."""
+
+
+class InlinePolicy(Policy):
+ """TODO: is this needed?"""
class Role(object):
@@ -15,6 +62,7 @@ class Role(object):
self.assume_role_policy_document = assume_role_policy_document
self.path = path
self.policies = {}
+ self.managed_policies = {}
@classmethod
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
@@ -225,6 +273,115 @@ class User(object):
)
+# predefine AWS managed policies
+aws_managed_policies = [
+ AWSManagedPolicy(
+ 'AmazonElasticMapReduceRole',
+ default_version_id='v6',
+ path='/service-role/',
+ document={
+ "Version": "2012-10-17",
+ "Statement": [{
+ "Effect": "Allow",
+ "Resource": "*",
+ "Action": [
+ "ec2:AuthorizeSecurityGroupEgress",
+ "ec2:AuthorizeSecurityGroupIngress",
+ "ec2:CancelSpotInstanceRequests",
+ "ec2:CreateNetworkInterface",
+ "ec2:CreateSecurityGroup",
+ "ec2:CreateTags",
+ "ec2:DeleteNetworkInterface",
+ "ec2:DeleteSecurityGroup",
+ "ec2:DeleteTags",
+ "ec2:DescribeAvailabilityZones",
+ "ec2:DescribeAccountAttributes",
+ "ec2:DescribeDhcpOptions",
+ "ec2:DescribeInstanceStatus",
+ "ec2:DescribeInstances",
+ "ec2:DescribeKeyPairs",
+ "ec2:DescribeNetworkAcls",
+ "ec2:DescribeNetworkInterfaces",
+ "ec2:DescribePrefixLists",
+ "ec2:DescribeRouteTables",
+ "ec2:DescribeSecurityGroups",
+ "ec2:DescribeSpotInstanceRequests",
+ "ec2:DescribeSpotPriceHistory",
+ "ec2:DescribeSubnets",
+ "ec2:DescribeVpcAttribute",
+ "ec2:DescribeVpcEndpoints",
+ "ec2:DescribeVpcEndpointServices",
+ "ec2:DescribeVpcs",
+ "ec2:DetachNetworkInterface",
+ "ec2:ModifyImageAttribute",
+ "ec2:ModifyInstanceAttribute",
+ "ec2:RequestSpotInstances",
+ "ec2:RevokeSecurityGroupEgress",
+ "ec2:RunInstances",
+ "ec2:TerminateInstances",
+ "ec2:DeleteVolume",
+ "ec2:DescribeVolumeStatus",
+ "ec2:DescribeVolumes",
+ "ec2:DetachVolume",
+ "iam:GetRole",
+ "iam:GetRolePolicy",
+ "iam:ListInstanceProfiles",
+ "iam:ListRolePolicies",
+ "iam:PassRole",
+ "s3:CreateBucket",
+ "s3:Get*",
+ "s3:List*",
+ "sdb:BatchPutAttributes",
+ "sdb:Select",
+ "sqs:CreateQueue",
+ "sqs:Delete*",
+ "sqs:GetQueue*",
+ "sqs:PurgeQueue",
+ "sqs:ReceiveMessage"
+ ]
+ }]
+ }
+ ),
+ AWSManagedPolicy(
+ 'AmazonElasticMapReduceforEC2Role',
+ default_version_id='v2',
+ path='/service-role/',
+ document={
+ "Version": "2012-10-17",
+ "Statement": [{
+ "Effect": "Allow",
+ "Resource": "*",
+ "Action": [
+ "cloudwatch:*",
+ "dynamodb:*",
+ "ec2:Describe*",
+ "elasticmapreduce:Describe*",
+ "elasticmapreduce:ListBootstrapActions",
+ "elasticmapreduce:ListClusters",
+ "elasticmapreduce:ListInstanceGroups",
+ "elasticmapreduce:ListInstances",
+ "elasticmapreduce:ListSteps",
+ "kinesis:CreateStream",
+ "kinesis:DeleteStream",
+ "kinesis:DescribeStream",
+ "kinesis:GetRecords",
+ "kinesis:GetShardIterator",
+ "kinesis:MergeShards",
+ "kinesis:PutRecord",
+ "kinesis:SplitShard",
+ "rds:Describe*",
+ "s3:*",
+ "sdb:*",
+ "sns:*",
+ "sqs:*"
+ ]
+ }]
+ }
+ )
+]
+# TODO: add more predefined AWS managed policies
+
+
class IAMBackend(BaseBackend):
def __init__(self):
@@ -234,8 +391,71 @@ class IAMBackend(BaseBackend):
self.groups = {}
self.users = {}
self.credential_report = None
+ self.managed_policies = self._init_managed_policies()
super(IAMBackend, self).__init__()
+ def _init_managed_policies(self):
+ return dict((p.name, p) for p in aws_managed_policies)
+
+ def attach_role_policy(self, policy_arn, role_name):
+ arns = dict((p.arn, p) for p in self.managed_policies.values())
+ policy = arns[policy_arn]
+ policy.attach_to_role(self.get_role(role_name))
+
+ def create_policy(self, description, path, policy_document, policy_name):
+ policy = ManagedPolicy(
+ policy_name,
+ description=description,
+ document=policy_document,
+ path=path,
+ )
+ self.managed_policies[policy.name] = policy
+ return policy
+
+ def list_attached_role_policies(self, role_name, marker=None, max_items=100, path_prefix='/'):
+ policies = self.get_role(role_name).managed_policies.values()
+
+ if path_prefix:
+ policies = [p for p in policies if p.path.startswith(path_prefix)]
+
+ policies = sorted(policies)
+ start_idx = int(marker) if marker else 0
+
+ policies = policies[start_idx:start_idx + max_items]
+
+ if len(policies) < max_items:
+ marker = None
+ else:
+ marker = str(start_idx + max_items)
+
+ return policies, marker
+
+ def list_policies(self, marker, max_items, only_attached, path_prefix, scope):
+ policies = self.managed_policies.values()
+
+ if only_attached:
+ policies = [p for p in policies if p.attachment_count > 0]
+
+ if scope == 'AWS':
+ policies = [p for p in policies if isinstance(p, AWSManagedPolicy)]
+ elif scope == 'Local':
+ policies = [p for p in policies if not isinstance(p, AWSManagedPolicy)]
+
+ if path_prefix:
+ policies = [p for p in policies if p.path.startswith(path_prefix)]
+
+ policies = sorted(policies)
+ start_idx = int(marker) if marker else 0
+
+ policies = policies[start_idx:start_idx + max_items]
+
+ if len(policies) < max_items:
+ marker = None
+ else:
+ marker = str(start_idx + max_items)
+
+ return policies, marker
+
def create_role(self, role_name, assume_role_policy_document, path):
role_id = random_resource_id()
role = Role(role_id, role_name, assume_role_policy_document, path)
diff --git a/moto/iam/responses.py b/moto/iam/responses.py
index 8a237e60b..c638b861e 100644
--- a/moto/iam/responses.py
+++ b/moto/iam/responses.py
@@ -6,6 +6,41 @@ from .models import iam_backend
class IamResponse(BaseResponse):
+ def attach_role_policy(self):
+ policy_arn = self._get_param('PolicyArn')
+ role_name = self._get_param('RoleName')
+ iam_backend.attach_role_policy(policy_arn, role_name)
+ template = self.response_template(ATTACH_ROLE_POLICY_TEMPLATE)
+ return template.render()
+
+ def create_policy(self):
+ description = self._get_param('Description')
+ path = self._get_param('Path')
+ policy_document = self._get_param('PolicyDocument')
+ policy_name = self._get_param('PolicyName')
+ policy = iam_backend.create_policy(description, path, policy_document, policy_name)
+ template = self.response_template(CREATE_POLICY_TEMPLATE)
+ return template.render(policy=policy)
+
+ def list_attached_role_policies(self):
+ marker = self._get_param('Marker')
+ max_items = self._get_int_param('MaxItems', 100)
+ path_prefix = self._get_param('PathPrefix', '/')
+ role_name = self._get_param('RoleName')
+ policies, marker = iam_backend.list_attached_role_policies(role_name, marker=marker, max_items=max_items, path_prefix=path_prefix)
+ template = self.response_template(LIST_ATTACHED_ROLE_POLICIES_TEMPLATE)
+ return template.render(policies=policies, marker=marker)
+
+ def list_policies(self):
+ marker = self._get_param('Marker')
+ max_items = self._get_int_param('MaxItems', 100)
+ only_attached = self._get_bool_param('OnlyAttached', False)
+ path_prefix = self._get_param('PathPrefix', '/')
+ scope = self._get_param('Scope', 'All')
+ policies, marker = iam_backend.list_policies(marker, max_items, only_attached, path_prefix, scope)
+ template = self.response_template(LIST_POLICIES_TEMPLATE)
+ return template.render(policies=policies, marker=marker)
+
def create_role(self):
role_name = self._get_param('RoleName')
path = self._get_param('Path')
@@ -267,6 +302,81 @@ class IamResponse(BaseResponse):
template = self.response_template(CREDENTIAL_REPORT)
return template.render(report=report)
+
+ATTACH_ROLE_POLICY_TEMPLATE = """
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+"""
+
+CREATE_POLICY_TEMPLATE = """
+
+
+ {{ policy.arn }}
+ {{ policy.attachment_count }}
+ {{ policy.create_datetime.isoformat() }}
+ {{ policy.default_version_id }}
+ {{ policy.path }}
+ {{ policy.id }}
+ {{ policy.name }}
+ {{ policy.update_datetime.isoformat() }}
+
+
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+"""
+
+LIST_ATTACHED_ROLE_POLICIES_TEMPLATE = """
+
+ {% if marker is none %}
+ false
+ {% else %}
+ true
+ {{ marker }}
+ {% endif %}
+
+ {% for policy in policies %}
+
+ {{ policy.name }}
+ {{ policy.arn }}
+
+ {% endfor %}
+
+
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+"""
+
+LIST_POLICIES_TEMPLATE = """
+
+ {% if marker is none %}
+ false
+ {% else %}
+ true
+ {{ marker }}
+ {% endif %}
+
+ {% for policy in policies %}
+
+ {{ policy.arn }}
+ {{ policy.attachment_count }}
+ {{ policy.create_datetime.isoformat() }}
+ {{ policy.default_version_id }}
+ {{ policy.path }}
+ {{ policy.id }}
+ {{ policy.name }}
+ {{ policy.update_datetime.isoformat() }}
+
+ {% endfor %}
+
+
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+"""
+
GENERIC_EMPTY_TEMPLATE = """<{{ name }}Response>
7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
diff --git a/moto/iam/utils.py b/moto/iam/utils.py
index 14038b5fc..1fae85a6c 100644
--- a/moto/iam/utils.py
+++ b/moto/iam/utils.py
@@ -25,3 +25,10 @@ def random_access_key():
string.ascii_uppercase + string.digits
)) for _ in range(16)
)
+
+
+def random_policy_id():
+ return 'A' + ''.join(
+ random.choice(string.ascii_uppercase + string.digits)
+ for _ in range(20)
+ )
diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py
index 0bbe6142a..cd7fdd03b 100644
--- a/tests/test_iam/test_iam.py
+++ b/tests/test_iam/test_iam.py
@@ -1,14 +1,17 @@
from __future__ import unicode_literals
+import base64
+
import boto
import boto3
import sure # noqa
-
-from nose.tools import assert_raises, assert_equals, assert_not_equals
from boto.exception import BotoServerError
-import base64
from moto import mock_iam
+from moto.iam.models import aws_managed_policies
+from nose.tools import assert_raises, assert_equals, assert_not_equals
from nose.tools import raises
+from tests.helpers import requires_boto_gte
+
@mock_iam()
def test_get_all_server_certs():
@@ -272,3 +275,41 @@ def test_get_credential_report():
result = conn.get_credential_report()
report = base64.b64decode(result['get_credential_report_response']['get_credential_report_result']['content'].encode('ascii')).decode('ascii')
report.should.match(r'.*my-user.*')
+
+
+@requires_boto_gte('2.39')
+@mock_iam()
+def test_managed_policy():
+ conn = boto.connect_iam()
+
+ conn.create_policy(policy_name='UserManagedPolicy',
+ policy_document={'mypolicy': 'test'},
+ path='/mypolicy/',
+ description='my user managed policy')
+
+ aws_policies = conn.list_policies(scope='AWS')['list_policies_response']['list_policies_result']['policies']
+ set(p.name for p in aws_managed_policies).should.equal(set(p['policy_name'] for p in aws_policies))
+
+ user_policies = conn.list_policies(scope='Local')['list_policies_response']['list_policies_result']['policies']
+ set(['UserManagedPolicy']).should.equal(set(p['policy_name'] for p in user_policies))
+
+ all_policies = conn.list_policies()['list_policies_response']['list_policies_result']['policies']
+ set(p['policy_name'] for p in aws_policies + user_policies).should.equal(set(p['policy_name'] for p in all_policies))
+
+ role_name = 'my-role'
+ conn.create_role(role_name, assume_role_policy_document={'policy': 'test'}, path="my-path")
+ for policy_name in ['AmazonElasticMapReduceRole',
+ 'AmazonElasticMapReduceforEC2Role']:
+ policy_arn = 'arn:aws:iam::aws:policy/service-role/' + policy_name
+ conn.attach_role_policy(policy_arn, role_name)
+
+ rows = conn.list_policies(only_attached=True)['list_policies_response']['list_policies_result']['policies']
+ rows.should.have.length_of(2)
+ for x in rows:
+ int(x['attachment_count']).should.be.greater_than(0)
+
+ # boto has not implemented this end point but accessible this way
+ resp = conn.get_response('ListAttachedRolePolicies',
+ {'RoleName': role_name},
+ list_marker='AttachedPolicies')
+ resp['list_attached_role_policies_response']['list_attached_role_policies_result']['attached_policies'].should.have.length_of(2)