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)