Repository: getmoto/moto

Issue:
Support for IAM Policies on CloudFormation
It seems that Moto's CloudFormation service does not support IAM::ManagedPolicy or IAM::Policy.

When I execute a create_stack operation on a CloudFormation template that creates and exports an IAM::ManagedPolicy, I see the following warnings & errors:

Warnings:
----------
```
/moto/cloudformation/parsing.py:295: UserWarning: Tried to parse AWS::IAM::ManagedPolicy but it's not supported by moto's CloudFormation implementation resource_type
```

Errors:
------
Can be reproduced by including the IAM::ManagedPolicy in the `Outputs` section as follows:
```
SampleManagedPolicyOutput:
    Description: some description
    Value: !Ref SampleManagedPolicy
```

```
moto/cloudformation/parsing.py", line 402, in parse_output output.value = clean_json(output_json["Value"], resources_map)
KeyError: 'Value'
```

Are there plans on implementing support for IAM: Policy or IAM: Managed Policy with Moto's CloudFormation service?



Discussion:
Thanks for raising this @apranav19 - marking this as an enhancement.

I'm not aware of any plans, but PR's are always welcome!

Resolving patch:
diff --git a/moto/iam/models.py b/moto/iam/models.py
--- a/moto/iam/models.py
+++ b/moto/iam/models.py
@@ -251,7 +251,7 @@ def created_iso_8601(self):
         return iso_8601_datetime_with_milliseconds(self.create_date)
 
 
-class ManagedPolicy(Policy):
+class ManagedPolicy(Policy, CloudFormationModel):
     """Managed policy."""
 
     is_attachable = True
@@ -310,6 +310,47 @@ def to_config_dict(self):
             "supplementaryConfiguration": {},
         }
 
+    @staticmethod
+    def cloudformation_name_type():
+        return None  # Resource never gets named after by template PolicyName!
+
+    @staticmethod
+    def cloudformation_type():
+        return "AWS::IAM::ManagedPolicy"
+
+    @classmethod
+    def create_from_cloudformation_json(
+        cls, resource_physical_name, cloudformation_json, region_name
+    ):
+        properties = cloudformation_json.get("Properties", {})
+        policy_document = json.dumps(properties.get("PolicyDocument"))
+        name = properties.get("ManagedPolicyName", resource_physical_name)
+        description = properties.get("Description")
+        path = properties.get("Path")
+        group_names = properties.get("Groups", [])
+        user_names = properties.get("Users", [])
+        role_names = properties.get("Roles", [])
+
+        policy = iam_backend.create_policy(
+            description=description,
+            path=path,
+            policy_document=policy_document,
+            policy_name=name,
+        )
+        for group_name in group_names:
+            iam_backend.attach_group_policy(
+                group_name=group_name, policy_arn=policy.arn
+            )
+        for user_name in user_names:
+            iam_backend.attach_user_policy(user_name=user_name, policy_arn=policy.arn)
+        for role_name in role_names:
+            iam_backend.attach_role_policy(role_name=role_name, policy_arn=policy.arn)
+        return policy
+
+    @property
+    def physical_resource_id(self):
+        return self.arn
+
 
 class AWSManagedPolicy(ManagedPolicy):
     """AWS-managed policy."""
diff --git a/moto/iam/responses.py b/moto/iam/responses.py
--- a/moto/iam/responses.py
+++ b/moto/iam/responses.py
@@ -124,7 +124,7 @@ def list_entities_for_policy(self):
         entity_groups = []
         entity_users = []
 
-        if entity == "User":
+        if not entity or entity == "User":
             users = iam_backend.list_users(path_prefix, marker, max_items)
             if users:
                 for user in users:
@@ -132,7 +132,7 @@ def list_entities_for_policy(self):
                         if p == policy_arn:
                             entity_users.append(user.name)
 
-        elif entity == "Role":
+        if not entity or entity == "Role":
             roles, _ = iam_backend.list_roles(path_prefix, marker, max_items)
             if roles:
                 for role in roles:
@@ -140,7 +140,7 @@ def list_entities_for_policy(self):
                         if p == policy_arn:
                             entity_roles.append(role.name)
 
-        elif entity == "Group":
+        if not entity or entity == "Group":
             groups = iam_backend.list_groups()
             if groups:
                 for group in groups:
@@ -148,7 +148,7 @@ def list_entities_for_policy(self):
                         if p == policy_arn:
                             entity_groups.append(group.name)
 
-        elif entity == "LocalManagedPolicy" or entity == "AWSManagedPolicy":
+        if entity == "LocalManagedPolicy" or entity == "AWSManagedPolicy":
             users = iam_backend.list_users(path_prefix, marker, max_items)
             if users:
                 for user in users:


Test patch:
diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py
--- a/tests/test_iam/test_iam.py
+++ b/tests/test_iam/test_iam.py
@@ -2370,18 +2370,24 @@ def test_list_entities_for_policy():
         EntityFilter="Role",
     )
     assert response["PolicyRoles"] == [{"RoleName": "my-role"}]
+    response["PolicyGroups"].should.equal([])
+    response["PolicyUsers"].should.equal([])
 
     response = conn.list_entities_for_policy(
         PolicyArn="arn:aws:iam::{}:policy/testPolicy".format(ACCOUNT_ID),
         EntityFilter="User",
     )
     assert response["PolicyUsers"] == [{"UserName": "testUser"}]
+    response["PolicyGroups"].should.equal([])
+    response["PolicyRoles"].should.equal([])
 
     response = conn.list_entities_for_policy(
         PolicyArn="arn:aws:iam::{}:policy/testPolicy".format(ACCOUNT_ID),
         EntityFilter="Group",
     )
     assert response["PolicyGroups"] == [{"GroupName": "testGroup"}]
+    response["PolicyRoles"].should.equal([])
+    response["PolicyUsers"].should.equal([])
 
     response = conn.list_entities_for_policy(
         PolicyArn="arn:aws:iam::{}:policy/testPolicy".format(ACCOUNT_ID),
@@ -2391,6 +2397,14 @@ def test_list_entities_for_policy():
     assert response["PolicyUsers"] == [{"UserName": "testUser"}]
     assert response["PolicyRoles"] == [{"RoleName": "my-role"}]
 
+    # Return everything when no entity is specified
+    response = conn.list_entities_for_policy(
+        PolicyArn="arn:aws:iam::{}:policy/testPolicy".format(ACCOUNT_ID)
+    )
+    response["PolicyGroups"].should.equal([{"GroupName": "testGroup"}])
+    response["PolicyUsers"].should.equal([{"UserName": "testUser"}])
+    response["PolicyRoles"].should.equal([{"RoleName": "my-role"}])
+
 
 @mock_iam()
 def test_create_role_no_path():
diff --git a/tests/test_iam/test_iam_cloudformation.py b/tests/test_iam/test_iam_cloudformation.py
--- a/tests/test_iam/test_iam_cloudformation.py
+++ b/tests/test_iam/test_iam_cloudformation.py
@@ -6,6 +6,7 @@
 from botocore.exceptions import ClientError
 
 from moto import mock_iam, mock_cloudformation, mock_s3, mock_sts
+from moto.core import ACCOUNT_ID
 
 # AWS::IAM::User Tests
 @mock_iam
@@ -281,6 +282,245 @@ def test_iam_cloudformation_user_get_attr():
     output_user_arn.should.equal(user_description["Arn"])
 
 
+# AWS::IAM::ManagedPolicy Tests
+@mock_iam
+@mock_cloudformation
+def test_iam_cloudformation_create_managed_policy():
+    iam_client = boto3.client("iam", region_name="us-east-1")
+    cf_client = boto3.client("cloudformation", region_name="us-east-1")
+    stack_name = "MyStack"
+
+    template = """
+Resources:
+  ThePolicy:
+    Type: AWS::IAM::ManagedPolicy
+    Properties:
+      PolicyDocument:
+        Version: '2012-10-17'
+        Statement:
+        - Effect: Allow
+          Action: s3:*
+          Resource: '*'
+""".strip()
+
+    cf_client.create_stack(StackName=stack_name, TemplateBody=template)
+
+    provisioned_resource = cf_client.list_stack_resources(StackName=stack_name)[
+        "StackResourceSummaries"
+    ][0]
+    logical_resource_id = provisioned_resource["LogicalResourceId"]
+    logical_resource_id.should.equal("ThePolicy")
+
+    policy_arn = provisioned_resource["PhysicalResourceId"]
+    policy_arn.should.match(
+        "arn:aws:iam::{}:policy/MyStack-ThePolicy-[A-Z0-9]+".format(ACCOUNT_ID)
+    )
+    expected_name = policy_arn.split("/")[1]
+
+    response = iam_client.list_entities_for_policy(PolicyArn=policy_arn)
+    response.should.have.key("PolicyGroups").equal([])
+    response.should.have.key("PolicyUsers").equal([])
+    response.should.have.key("PolicyRoles").equal([])
+
+    policy = iam_client.get_policy(PolicyArn=policy_arn)["Policy"]
+    policy.should.have.key("Arn").equal(policy_arn)
+    policy.should.have.key("PolicyName").equal(expected_name)
+    policy.should.have.key("Description").equal("")
+    policy.should.have.key("Path").equal("/")
+
+
+@mock_iam
+@mock_cloudformation
+def test_iam_cloudformation_create_managed_policy_with_additional_properties():
+    iam_client = boto3.client("iam", region_name="us-east-1")
+    cf_client = boto3.client("cloudformation", region_name="us-east-1")
+    stack_name = "MyStack"
+    name = "FancyManagedPolicy"
+    desc = "Custom managed policy with name"
+
+    template = """
+Resources:
+  ThePolicy:
+    Type: AWS::IAM::ManagedPolicy
+    Properties:
+      Description: {0}
+      Path: /
+      ManagedPolicyName: {1}
+      PolicyDocument:
+        Version: '2012-10-17'
+        Statement:
+        - Effect: Allow
+          Action: s3:*
+          Resource: '*'
+""".strip().format(
+        desc, name
+    )
+
+    cf_client.create_stack(StackName=stack_name, TemplateBody=template)
+
+    provisioned_resource = cf_client.list_stack_resources(StackName=stack_name)[
+        "StackResourceSummaries"
+    ][0]
+    logical_resource_id = provisioned_resource["LogicalResourceId"]
+    logical_resource_id.should.equal("ThePolicy")
+
+    policy_arn = provisioned_resource["PhysicalResourceId"]
+    policy_arn.should.equal("arn:aws:iam::{}:policy/{}".format(ACCOUNT_ID, name))
+
+    policy = iam_client.get_policy(PolicyArn=policy_arn)["Policy"]
+    policy.should.have.key("Arn").equal(policy_arn)
+    policy.should.have.key("Path").equal("/")
+    policy.should.have.key("Description").equal(desc)
+    policy.should.have.key("PolicyName").equal(name)
+
+
+@mock_iam
+@mock_cloudformation
+def test_iam_cloudformation_create_managed_policy_attached_to_a_group():
+    iam_client = boto3.client("iam", region_name="us-east-1")
+    group_name = "MyGroup"
+    iam_client.create_group(GroupName=group_name)
+
+    cf_client = boto3.client("cloudformation", region_name="us-east-1")
+    stack_name = "MyStack"
+    desc = "Custom managed policy"
+
+    template = """
+Resources:
+  ThePolicy:
+    Type: AWS::IAM::ManagedPolicy
+    Properties:
+      Description: {0}
+      Path: /
+      PolicyDocument:
+        Version: '2012-10-17'
+        Statement:
+        - Effect: Allow
+          Action: s3:*
+          Resource: '*'
+      Groups:
+        - {1}
+""".strip().format(
+        desc, group_name
+    )
+
+    cf_client.create_stack(StackName=stack_name, TemplateBody=template)
+
+    provisioned_resource = cf_client.list_stack_resources(StackName=stack_name)[
+        "StackResourceSummaries"
+    ][0]
+    logical_resource_id = provisioned_resource["LogicalResourceId"]
+    logical_resource_id.should.equal("ThePolicy")
+
+    policy_arn = provisioned_resource["PhysicalResourceId"]
+    policy_arn.should.match(
+        "rn:aws:iam::{}:policy/MyStack-ThePolicy-[A-Z0-9]+".format(ACCOUNT_ID)
+    )
+
+    response = iam_client.list_entities_for_policy(PolicyArn=policy_arn)
+    response.should.have.key("PolicyGroups").equal([{"GroupName": group_name}])
+    response.should.have.key("PolicyUsers").equal([])
+    response.should.have.key("PolicyRoles").equal([])
+
+
+@mock_iam
+@mock_cloudformation
+def test_iam_cloudformation_create_managed_policy_attached_to_a_user():
+    iam_client = boto3.client("iam", region_name="us-east-1")
+    user_name = "MyUser"
+    iam_client.create_user(UserName=user_name)
+
+    cf_client = boto3.client("cloudformation", region_name="us-east-1")
+    stack_name = "MyStack"
+    desc = "Custom managed policy"
+
+    template = """
+Resources:
+  ThePolicy:
+    Type: AWS::IAM::ManagedPolicy
+    Properties:
+      Description: {0}
+      Path: /
+      PolicyDocument:
+        Version: '2012-10-17'
+        Statement:
+        - Effect: Allow
+          Action: s3:*
+          Resource: '*'
+      Users:
+        - {1}
+""".strip().format(
+        desc, user_name
+    )
+
+    cf_client.create_stack(StackName=stack_name, TemplateBody=template)
+
+    provisioned_resource = cf_client.list_stack_resources(StackName=stack_name)[
+        "StackResourceSummaries"
+    ][0]
+    logical_resource_id = provisioned_resource["LogicalResourceId"]
+    logical_resource_id.should.equal("ThePolicy")
+
+    policy_arn = provisioned_resource["PhysicalResourceId"]
+    policy_arn.should.match(
+        "rn:aws:iam::{}:policy/MyStack-ThePolicy-[A-Z0-9]+".format(ACCOUNT_ID)
+    )
+
+    response = iam_client.list_entities_for_policy(PolicyArn=policy_arn)
+    response.should.have.key("PolicyGroups").equal([])
+    response.should.have.key("PolicyUsers").equal([{"UserName": user_name}])
+    response.should.have.key("PolicyRoles").equal([])
+
+
+@mock_iam
+@mock_cloudformation
+def test_iam_cloudformation_create_managed_policy_attached_to_a_role():
+    iam_client = boto3.client("iam", region_name="us-east-1")
+    role_name = "MyRole"
+    iam_client.create_role(RoleName=role_name, AssumeRolePolicyDocument="some policy")
+
+    cf_client = boto3.client("cloudformation", region_name="us-east-1")
+    stack_name = "MyStack"
+    desc = "Custom managed policy"
+
+    template = """
+Resources:
+  ThePolicy:
+    Type: AWS::IAM::ManagedPolicy
+    Properties:
+      Description: {0}
+      Path: /
+      PolicyDocument:
+        Version: '2012-10-17'
+        Statement:
+        - Effect: Allow
+          Action: s3:*
+          Resource: '*'
+      Roles:
+        - {1}
+""".strip().format(
+        desc, role_name
+    )
+
+    cf_client.create_stack(StackName=stack_name, TemplateBody=template)
+
+    provisioned_resource = cf_client.list_stack_resources(StackName=stack_name)[
+        "StackResourceSummaries"
+    ][0]
+    logical_resource_id = provisioned_resource["LogicalResourceId"]
+    logical_resource_id.should.equal("ThePolicy")
+
+    policy_arn = provisioned_resource["PhysicalResourceId"]
+    policy_arn.should.match(
+        "rn:aws:iam::{}:policy/MyStack-ThePolicy-[A-Z0-9]+".format(ACCOUNT_ID)
+    )
+
+    response = iam_client.list_entities_for_policy(PolicyArn=policy_arn)
+    response.should.have.key("PolicyGroups").equal([])
+    response.should.have.key("PolicyUsers").equal([])
+    response.should.have.key("PolicyRoles").equal([{"RoleName": role_name}])
+
+
 # AWS::IAM::Policy Tests
 @mock_s3
 @mock_iam