Repository: aws-cloudformation/cfn-lint

Issue:
AWS::ECR::Repository RepositoryPolicyText doesn't need 'Resource'
*cfn-lint version: (`cfn-lint --version`)* 0.7.1

*Description of issue.*

When upgrading to 0.7.1 my template with a valid policy adapted from [step 3k of this tutorial](https://docs.aws.amazon.com/codebuild/latest/userguide/sample-ecr.html) triggers "E2507 IAM Policy statement missing Resource or NotResource". This kind of policy apparently doesn't need a `Resource` key, so this shouldn't happen.


Discussion:

Resolving patch:
diff --git a/src/cfnlint/rules/resources/iam/Policy.py b/src/cfnlint/rules/resources/iam/Policy.py
--- a/src/cfnlint/rules/resources/iam/Policy.py
+++ b/src/cfnlint/rules/resources/iam/Policy.py
@@ -30,6 +30,9 @@ class Policy(CloudFormationLintRule):
 
     def __init__(self):
         """Init"""
+        self.resource_exceptions = {
+            'AWS::ECR::Repository': 'RepositoryPolicyText',
+        }
         self.resources_and_keys = {
             'AWS::SNS::TopicPolicy': 'PolicyDocument',
             'AWS::S3::BucketPolicy': 'PolicyDocument',
@@ -50,7 +53,7 @@ def __init__(self):
         for resource_type in self.idp_and_keys:
             self.resource_property_types.append(resource_type)
 
-    def check_policy_document(self, value, path, cfn, is_identity_policy):
+    def check_policy_document(self, value, path, cfn, is_identity_policy, resource_exceptions):
         """Check policy document"""
         matches = []
 
@@ -84,7 +87,7 @@ def check_policy_document(self, value, path, cfn, is_identity_policy):
                     for statement in statements:
                         matches.extend(
                             self._check_policy_statement(
-                                statement['Path'], statement['Value'], is_identity_policy
+                                statement['Path'], statement['Value'], is_identity_policy, resource_exceptions
                             )
                         )
                 else:
@@ -93,7 +96,7 @@ def check_policy_document(self, value, path, cfn, is_identity_policy):
                         RuleMatch(path[:] + [parent_key], message))
         return matches
 
-    def _check_policy_statement(self, branch, statement, is_identity_policy):
+    def _check_policy_statement(self, branch, statement, is_identity_policy, resource_exceptions):
         """Check statements"""
         matches = []
         statement_valid_keys = [
@@ -137,10 +140,11 @@ def _check_policy_statement(self, branch, statement, is_identity_policy):
                 message = 'IAM Resource Policy statement should have Principal or NotPrincipal'
                 matches.append(
                     RuleMatch(branch[:] + ['Principal'], message))
-        if 'Resource' not in statement and 'NotResource' not in statement:
-            message = 'IAM Policy statement missing Resource or NotResource'
-            matches.append(
-                RuleMatch(branch[:], message))
+        if not resource_exceptions:
+            if 'Resource' not in statement and 'NotResource' not in statement:
+                message = 'IAM Policy statement missing Resource or NotResource'
+                matches.append(
+                    RuleMatch(branch[:], message))
 
         return(matches)
 
@@ -162,6 +166,10 @@ def match_resource_properties(self, properties, resourcetype, path, cfn):
             # Key isn't defined return nothing
             return matches
 
+        resource_exceptions = False
+        if key == self.resource_exceptions.get(resourcetype):
+            resource_exceptions = True
+
         if key == 'Policies':
             for index, policy in enumerate(properties.get(key, [])):
                 matches.extend(
@@ -170,7 +178,8 @@ def match_resource_properties(self, properties, resourcetype, path, cfn):
                         path=path[:] + ['Policies', index],
                         check_value=self.check_policy_document,
                         cfn=cfn,
-                        is_identity_policy=is_identity_policy
+                        is_identity_policy=is_identity_policy,
+                        resource_exceptions=resource_exceptions,
                     ))
         else:
             matches.extend(
@@ -179,7 +188,8 @@ def match_resource_properties(self, properties, resourcetype, path, cfn):
                     path=path[:],
                     check_value=self.check_policy_document,
                     cfn=cfn,
-                    is_identity_policy=is_identity_policy
+                    is_identity_policy=is_identity_policy,
+                    resource_exceptions=resource_exceptions,
                 ))
 
         return matches


Test patch:
diff --git a/test/fixtures/templates/bad/resources/iam/resource_policy.yaml b/test/fixtures/templates/bad/resources/iam/resource_policy.yaml
--- a/test/fixtures/templates/bad/resources/iam/resource_policy.yaml
+++ b/test/fixtures/templates/bad/resources/iam/resource_policy.yaml
@@ -11,7 +11,6 @@ Resources:
             Action:
               - "s3:GetObject"
             Effect: "Allow"
-            Resource: arn:aws:s3:::myExampleBucket/*
             Condition:
               StringLike:
                 aws:Referer:
diff --git a/test/fixtures/templates/good/resources/iam/resource_policy.yaml b/test/fixtures/templates/good/resources/iam/resource_policy.yaml
--- a/test/fixtures/templates/good/resources/iam/resource_policy.yaml
+++ b/test/fixtures/templates/good/resources/iam/resource_policy.yaml
@@ -4,6 +4,27 @@ Parameters:
   myExampleBucket:
     Type: String
 Resources:
+  Ecr:
+    Type: AWS::ECR::Repository
+    Properties:
+      RepositoryPolicyText:
+        {
+          "Version": "2012-10-17",
+          "Statement": [
+            {
+              "Sid": "CodeBuildAccess",
+              "Effect": "Allow",
+              "Principal": {
+                "Service": "codebuild.amazonaws.com"
+              },
+              "Action": [
+                "ecr:GetDownloadUrlForLayer",
+                "ecr:BatchGetImage",
+                "ecr:BatchCheckLayerAvailability"
+              ]
+            }
+          ]
+        }
   SampleBucketPolicy:
     Type: AWS::S3::BucketPolicy
     Properties:
diff --git a/test/rules/resources/iam/test_iam_policy.py b/test/rules/resources/iam/test_iam_policy.py
--- a/test/rules/resources/iam/test_iam_policy.py
+++ b/test/rules/resources/iam/test_iam_policy.py
@@ -39,4 +39,4 @@ def test_file_negative(self):
 
     def test_file_resource_negative(self):
         """Test failure"""
-        self.helper_file_negative('fixtures/templates/bad/resources/iam/resource_policy.yaml', 3)
+        self.helper_file_negative('fixtures/templates/bad/resources/iam/resource_policy.yaml', 4)