Repository: aws-cloudformation/cfn-lint

Issue:
iam policy statement can only contain one colon
*cfn-lint version: (`cfn-lint --version`)*

*Description of issue.*

```
  IamRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "iamrole"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: "sts:AssumeRole"
      Policies:
        -
          PolicyName: "iampolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action: 
                  - "cloudwatch:*"
                  - "ec2:ec2:CreateNetworkInterface"←wrong
                  - "ec2:DescribeNetworkInterfaces"
                  - "ec2:DeleteNetworkInterface"
                Resource: "*"
```

The following error occurs when deploying using this template.

```
Actions/Condition can contain only one colon. (Service: AmazonIdentityManagement; Status Code: 400; Error Code: MalformedPolicyDocument; Request ID: 751c17bd-199d-453a-a08f-40f8c5b251a9)
```

It may be difficult to check all of iam's policies, but it may be possible to detect grammatical errors, such as the absence of multiple colons.



Discussion:

Resolving patch:
diff --git a/src/cfnlint/rules/resources/iam/Permissions.py b/src/cfnlint/rules/resources/iam/Permissions.py
--- a/src/cfnlint/rules/resources/iam/Permissions.py
+++ b/src/cfnlint/rules/resources/iam/Permissions.py
@@ -14,8 +14,9 @@
   OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 """
+import json
 import six
-
+from cfnlint.helpers import convert_dict
 from cfnlint import CloudFormationLintRule
 from cfnlint import RuleMatch
 
@@ -31,12 +32,25 @@ class Permissions(CloudFormationLintRule):
     tags = ['properties', 'iam', 'permissions']
     experimental = True
 
-    IAM_PERMISSION_RESOURCE_TYPES = [
-        'AWS::IAM::ManagedPolicy',
-        'AWS::IAM::Policy',
-        'AWS::SNS::TopicPolicy',
-        'AWS::SQS::QueuePolicy'
-    ]
+    IAM_PERMISSION_RESOURCE_TYPES = {
+        'AWS::SNS::TopicPolicy': 'PolicyDocument',
+        'AWS::S3::BucketPolicy': 'PolicyDocument',
+        'AWS::KMS::Key': 'KeyPolicy',
+        'AWS::SQS::QueuePolicy': 'PolicyDocument',
+        'AWS::Elasticsearch::Domain': 'AccessPolicies',
+        'AWS::IAM::Group': 'Policies',
+        'AWS::IAM::ManagedPolicy': 'PolicyDocument',
+        'AWS::IAM::Policy': 'PolicyDocument',
+        'AWS::IAM::Role': 'Policies',
+        'AWS::IAM::User': 'Policies',
+    }
+
+    def __init__(self):
+        """Init"""
+        super(Permissions, self).__init__()
+        self.service_map = self.load_service_map()
+        for resource_type in self.IAM_PERMISSION_RESOURCE_TYPES:
+            self.resource_property_types.append(resource_type)
 
     def load_service_map(self):
         """
@@ -59,50 +73,87 @@ def load_service_map(self):
 
         return policy_service_map
 
-    def check_permissions(self, action, path, service_map):
+    def check_policy_document(self, value, path, start_mark, end_mark):
+        """Check policy document"""
+        matches = []
+
+        if isinstance(value, six.string_types):
+            try:
+                value = convert_dict(json.loads(value), start_mark, end_mark)
+            except Exception as ex:  # pylint: disable=W0703,W0612
+                message = 'IAM Policy Documents need to be JSON'
+                matches.append(RuleMatch(path[:], message))
+                return matches
+
+        if not isinstance(value, dict):
+            return matches
+
+        for p_vs, p_p in value.items_safe(path[:], (dict)):
+            statements = p_vs.get('Statement')
+            if statements:
+                if isinstance(statements, dict):
+                    statements = [statements]
+                if isinstance(statements, list):
+                    for index, statement in enumerate(statements):
+                        actions = []
+
+                        if isinstance(statement, dict):
+                            actions.extend(self.get_actions(statement))
+
+                        elif isinstance(statement, list):
+                            for effective_permission in statement:
+                                actions.extend(self.get_actions(effective_permission))
+
+                        for action in actions:
+                            matches.extend(self.check_permissions(action, p_p + ['Statement', index]))
+
+        return matches
+
+    def check_permissions(self, action, path):
         """
         Check if permission is valid
         """
         matches = []
-
         if action == '*':
             return matches
 
-        service, permission = action.split(':')
+        service, permission = action.split(':', 1)
         # Get lowercase so we can check case insenstive. Keep the original values for the message
         service_value = service.lower()
         permission_value = permission.lower()
 
-        if service_value in service_map:
-            if permission_value.endswith('*'):
+        if service_value in self.service_map:
+            if permission_value == '*':
+                pass
+            elif permission_value.endswith('*'):
                 wilcarded_permission = permission_value.split('*')[0]
-                if not any(wilcarded_permission in action for action in service_map[service_value]):
+                if not any(wilcarded_permission in action for action in self.service_map[service_value]):
                     message = 'Invalid permission "{}" for "{}" found in permissions'
                     matches.append(
                         RuleMatch(
                             path,
-                            message.format(permission, service, '/'.join(path))))
+                            message.format(permission, service)))
 
             elif permission_value.startswith('*'):
                 wilcarded_permission = permission_value.split('*')[1]
-                if not any(wilcarded_permission in action for action in service_map[service_value]):
+                if not any(wilcarded_permission in action for action in self.service_map[service_value]):
                     message = 'Invalid permission "{}" for "{}" found in permissions'
                     matches.append(
                         RuleMatch(
                             path,
-                            message.format(permission, service, '/'.join(path))))
-            elif permission_value not in service_map[service_value]:
+                            message.format(permission, service)))
+            elif permission_value not in self.service_map[service_value]:
                 message = 'Invalid permission "{}" for "{}" found in permissions'
                 matches.append(
                     RuleMatch(
                         path,
-                        message.format(permission, service, '/'.join(path))))
+                        message.format(permission, service)))
         else:
             message = 'Invalid service "{}" found in permissions'
             matches.append(
                 RuleMatch(
                     path,
-                    message.format(service, '/'.join(path))))
+                    message.format(service)))
 
         return matches
 
@@ -125,29 +176,28 @@ def get_actions(self, effective_permissions):
 
         return actions
 
-    def match(self, cfn):
-        """Check IAM Permissions"""
+    def match_resource_properties(self, properties, resourcetype, path, cfn):
+        """Check CloudFormation Properties"""
         matches = []
 
-        statements = []
-        service_map = self.load_service_map()
-        for resource_type in self.IAM_PERMISSION_RESOURCE_TYPES:
-            statements.extend(cfn.get_resource_properties([resource_type, 'PolicyDocument', 'Statement']))
-
-        for statement in statements:
-            path = statement['Path']
-            effective_permissions = statement['Value']
-
-            actions = []
-
-            if isinstance(effective_permissions, dict):
-                actions.extend(self.get_actions(effective_permissions))
-
-            elif isinstance(effective_permissions, list):
-                for effective_permission in effective_permissions:
-                    actions.extend(self.get_actions(effective_permission))
-
-            for action in actions:
-                matches.extend(self.check_permissions(action, path, service_map))
+        key = self.IAM_PERMISSION_RESOURCE_TYPES.get(resourcetype)
+        for key, value in properties.items():
+            if key == 'Policies' and isinstance(value, list):
+                for index, policy in enumerate(properties.get(key, [])):
+                    matches.extend(
+                        cfn.check_value(
+                            obj=policy, key='PolicyDocument',
+                            path=path[:] + ['Policies', index],
+                            check_value=self.check_policy_document,
+                            start_mark=key.start_mark, end_mark=key.end_mark,
+                        ))
+            elif key in ['KeyPolicy', 'PolicyDocument', 'RepositoryPolicyText', 'AccessPolicies']:
+                matches.extend(
+                    cfn.check_value(
+                        obj=properties, key=key,
+                        path=path[:],
+                        check_value=self.check_policy_document,
+                        start_mark=key.start_mark, end_mark=key.end_mark,
+                    ))
 
         return matches


Test patch:
diff --git a/test/fixtures/results/quickstart/nist_iam.json b/test/fixtures/results/quickstart/nist_iam.json
--- a/test/fixtures/results/quickstart/nist_iam.json
+++ b/test/fixtures/results/quickstart/nist_iam.json
@@ -1,15 +1,8 @@
 [
     {
-        "Rule": {
-            "Id": "W3005",
-            "Description": "Check if DependsOn is specified if not needed. A Ref and GetAtt implicitly results in DependsOn behaviour.",
-            "ShortDescription": "Check obsolete DependsOn configuration for Resources"
-        },
+        "Filename": "test/fixtures/templates/quickstart/nist_iam.yaml",
+        "Level": "Warning",
         "Location": {
-            "Start": {
-                "ColumnNumber": 5,
-                "LineNumber": 56
-            },
             "End": {
                 "ColumnNumber": 14,
                 "LineNumber": 56
@@ -18,23 +11,24 @@
                 "Resources",
                 "rIAMAdminProfile",
                 "DependsOn"
-            ]
+            ],
+            "Start": {
+                "ColumnNumber": 5,
+                "LineNumber": 56
+            }
         },
-        "Level": "Warning",
-        "Message": "Obsolete DependsOn on resource (rIAMAdminRole), dependency already enforced by \"!Ref\" at Resources/rIAMAdminProfile/Properties/Roles/0/Ref/rIAMAdminRole",
-        "Filename": "test/templates/quickstart/nist_iam.yaml"
-    },
-    {
+        "Message": "Obsolete DependsOn on resource (rIAMAdminRole), dependency already enforced by a \"Ref\" at Resources/rIAMAdminProfile/Properties/Roles/0/Ref/rIAMAdminRole",
         "Rule": {
+            "Description": "Check if DependsOn is specified if not needed. A Ref or a Fn::GetAtt already is an implicit dependency.",
             "Id": "W3005",
-            "Description": "Check if DependsOn is specified if not needed. A Ref and GetAtt implicitly results in DependsOn behaviour.",
-            "ShortDescription": "Check obsolete DependsOn configuration for Resources"
-        },
+            "ShortDescription": "Check obsolete DependsOn configuration for Resources",
+            "Source": "https://aws.amazon.com/blogs/devops/optimize-aws-cloudformation-templates/"
+        }
+    },
+    {
+        "Filename": "test/fixtures/templates/quickstart/nist_iam.yaml",
+        "Level": "Warning",
         "Location": {
-            "Start": {
-                "ColumnNumber": 5,
-                "LineNumber": 136
-            },
             "End": {
                 "ColumnNumber": 14,
                 "LineNumber": 136
@@ -43,87 +37,99 @@
                 "Resources",
                 "rInstanceOpsProfile",
                 "DependsOn"
-            ]
+            ],
+            "Start": {
+                "ColumnNumber": 5,
+                "LineNumber": 136
+            }
         },
-        "Level": "Warning",
-        "Message": "Obsolete DependsOn on resource (rInstanceOpsRole), dependency already enforced by \"!Ref\" at Resources/rInstanceOpsProfile/Properties/Roles/0/Ref/rInstanceOpsRole",
-        "Filename": "test/templates/quickstart/nist_iam.yaml"
-    },
-    {
+        "Message": "Obsolete DependsOn on resource (rInstanceOpsRole), dependency already enforced by a \"Ref\" at Resources/rInstanceOpsProfile/Properties/Roles/0/Ref/rInstanceOpsRole",
         "Rule": {
+            "Description": "Check if DependsOn is specified if not needed. A Ref or a Fn::GetAtt already is an implicit dependency.",
             "Id": "W3005",
-            "Description": "Check if DependsOn is specified if not needed. A Ref and GetAtt implicitly results in DependsOn behaviour.",
-            "ShortDescription": "Check obsolete DependsOn configuration for Resources"
-        },
+            "ShortDescription": "Check obsolete DependsOn configuration for Resources",
+            "Source": "https://aws.amazon.com/blogs/devops/optimize-aws-cloudformation-templates/"
+        }
+    },
+    {
+        "Filename": "test/fixtures/templates/quickstart/nist_iam.yaml",
+        "Level": "Warning",
         "Location": {
-            "Start": {
-                "ColumnNumber": 5,
-                "LineNumber": 235
-            },
             "End": {
-                "ColumnNumber": 14,
-                "LineNumber": 235
+                "ColumnNumber": 9,
+                "LineNumber": 227
             },
             "Path": [
                 "Resources",
-                "rReadOnlyAdminProfile",
-                "DependsOn"
-            ]
+                "rReadOnlyAdminPolicy",
+                "Properties",
+                "PolicyDocument",
+                "Statement",
+                0
+            ],
+            "Start": {
+                "ColumnNumber": 11,
+                "LineNumber": 164
+            }
         },
-        "Level": "Warning",
-        "Message": "Obsolete DependsOn on resource (rReadOnlyAdminRole), dependency already enforced by \"!Ref\" at Resources/rReadOnlyAdminProfile/Properties/Roles/0/Ref/rReadOnlyAdminRole",
-        "Filename": "test/templates/quickstart/nist_iam.yaml"
+        "Message": "Invalid permission \"Get*\" for \"appstream\" found in permissions",
+        "Rule": {
+            "Description": "Check for valid IAM Permissions",
+            "Id": "W3037",
+            "ShortDescription": "Check IAM Permission configuration",
+            "Source": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_action.html"
+        }
     },
     {
-        "Rule": {
-            "Id": "W3005",
-            "Description": "Check if DependsOn is specified if not needed. A Ref and GetAtt implicitly results in DependsOn behaviour.",
-            "ShortDescription": "Check obsolete DependsOn configuration for Resources"
-        },
+        "Filename": "test/fixtures/templates/quickstart/nist_iam.yaml",
+        "Level": "Warning",
         "Location": {
-            "Start": {
-                "ColumnNumber": 5,
-                "LineNumber": 311
-            },
             "End": {
                 "ColumnNumber": 14,
-                "LineNumber": 311
+                "LineNumber": 235
             },
             "Path": [
                 "Resources",
-                "rSysAdminProfile",
+                "rReadOnlyAdminProfile",
                 "DependsOn"
-            ]
+            ],
+            "Start": {
+                "ColumnNumber": 5,
+                "LineNumber": 235
+            }
         },
-        "Level": "Warning",
-        "Message": "Obsolete DependsOn on resource (rSysAdminRole), dependency already enforced by \"!Ref\" at Resources/rSysAdminProfile/Properties/Roles/0/Ref/rSysAdminRole",
-        "Filename": "test/templates/quickstart/nist_iam.yaml"
+        "Message": "Obsolete DependsOn on resource (rReadOnlyAdminRole), dependency already enforced by a \"Ref\" at Resources/rReadOnlyAdminProfile/Properties/Roles/0/Ref/rReadOnlyAdminRole",
+        "Rule": {
+            "Description": "Check if DependsOn is specified if not needed. A Ref or a Fn::GetAtt already is an implicit dependency.",
+            "Id": "W3005",
+            "ShortDescription": "Check obsolete DependsOn configuration for Resources",
+            "Source": "https://aws.amazon.com/blogs/devops/optimize-aws-cloudformation-templates/"
+        }
     },
     {
-        "Rule": {
-            "Id": "W3037",
-            "Description": "Check for valid IAM Permissions",
-            "ShortDescription": "Check IAM Permission configuration"
-        },
+        "Filename": "test/fixtures/templates/quickstart/nist_iam.yaml",
+        "Level": "Warning",
         "Location": {
-            "Start": {
-                "ColumnNumber": 9,
-                "LineNumber": 163
-            },
             "End": {
-                "ColumnNumber": 9,
-                "LineNumber": 163
+                "ColumnNumber": 14,
+                "LineNumber": 311
             },
             "Path": [
                 "Resources",
-                "rReadOnlyAdminPolicy",
-                "Properties",
-                "PolicyDocument",
-                "Statement"
-            ]
+                "rSysAdminProfile",
+                "DependsOn"
+            ],
+            "Start": {
+                "ColumnNumber": 5,
+                "LineNumber": 311
+            }
         },
-        "Level": "Warning",
-        "Message": "Invalid permission \"Get*\" for \"appstream\" found in permissions",
-        "Filename": "test/templates/quickstart/nist_iam.yaml"
+        "Message": "Obsolete DependsOn on resource (rSysAdminRole), dependency already enforced by a \"Ref\" at Resources/rSysAdminProfile/Properties/Roles/0/Ref/rSysAdminRole",
+        "Rule": {
+            "Description": "Check if DependsOn is specified if not needed. A Ref or a Fn::GetAtt already is an implicit dependency.",
+            "Id": "W3005",
+            "ShortDescription": "Check obsolete DependsOn configuration for Resources",
+            "Source": "https://aws.amazon.com/blogs/devops/optimize-aws-cloudformation-templates/"
+        }
     }
 ]