Repository: aws-cloudformation/cfn-lint

Issue:
E1029 - False positive with IAM policy Redshift condition keys
*cfn-lint version: (`cfn-lint --version`)*
cfn-lint 0.24.1

*Description of issue.*
${redshift:DbUser} triggers:
`E1029:Found an embedded parameter outside of an "Fn::Sub"`

I'm defining a policy document similar to the one below.  cfn-lint returns a E1029 on each line where the ${redshift:DbUser} condition key is used. Source: [https://docs.aws.amazon.com/redshift/latest/mgmt/redshift-iam-access-control-identity-based.html#redshift-policy-resources.getclustercredentials-resources](url)

Sample:
```json
{
"Version": "2012-10-17",
  "Statement": [
    {
     "Sid": "GetClusterCredsStatement",
      "Effect": "Allow",
      "Action": [
        "redshift:GetClusterCredentials"
      ],
      "Resource": [
        "arn:aws:redshift:us-west-2:123456789012:dbuser:examplecluster/${redshift:DbUser}",
        "arn:aws:redshift:us-west-2:123456789012:dbname:examplecluster/testdb",
        "arn:aws:redshift:us-west-2:123456789012:dbgroup:examplecluster/common_group"
      ],
        "Condition": {
            "StringEquals": {
           "aws:userid":"AIDIODR4TAW7CSEXAMPLE:${redshift:DbUser}@yourdomain.com"
                            }
                      }
    },
    {
      "Sid": "CreateClusterUserStatement",
      "Effect": "Allow",
      "Action": [
        "redshift:CreateClusterUser"
      ],
      "Resource": [
        "arn:aws:redshift:us-west-2:123456789012:dbuser:examplecluster/${redshift:DbUser}"
      ],
      "Condition": {
        "StringEquals": {
          "aws:userid":"AIDIODR4TAW7CSEXAMPLE:${redshift:DbUser}@yourdomain.com"
        }
      }
    },
    {
      "Sid": "RedshiftJoinGroupStatement",
      "Effect": "Allow",
      "Action": [
        "redshift:JoinGroup"
      ],
      "Resource": [
        "arn:aws:redshift:us-west-2:123456789012:dbgroup:examplecluster/common_group"
      ]
    }
  ]
}
```
Resource string from actual template:
`{ "Fn::Join" : [ "", [ "arn:aws:redshift:us-west-2:", { "Ref" : "AWS::AccountId" }, ":dbuser:", { "Fn::FindInMap" : [ "Environment", { "Ref" : "EnvironmentType" }, "RSClusterName" ] }, "/${redshift:DBUser}" ] ] }`


Discussion:

Resolving patch:
diff --git a/src/cfnlint/rules/functions/SubNeeded.py b/src/cfnlint/rules/functions/SubNeeded.py
--- a/src/cfnlint/rules/functions/SubNeeded.py
+++ b/src/cfnlint/rules/functions/SubNeeded.py
@@ -18,6 +18,7 @@
 from cfnlint.rules import CloudFormationLintRule
 from cfnlint.rules import RuleMatch
 
+
 class SubNeeded(CloudFormationLintRule):
     """Check if a substitution string exists without a substitution function"""
     id = 'E1029'
@@ -28,7 +29,8 @@ class SubNeeded(CloudFormationLintRule):
 
     # Free-form text properties to exclude from this rule
     # content is part of AWS::CloudFormation::Init
-    excludes = ['UserData', 'ZipFile', 'Condition', 'AWS::CloudFormation::Init', 'CloudWatchAlarmDefinition', 'TopicRulePayload']
+    excludes = ['UserData', 'ZipFile', 'Condition', 'AWS::CloudFormation::Init',
+                'CloudWatchAlarmDefinition', 'TopicRulePayload']
     api_excludes = ['Uri', 'Body']
 
     # IAM Policy has special variables that don't require !Sub, Check for these
@@ -37,13 +39,25 @@ class SubNeeded(CloudFormationLintRule):
     # https://docs.aws.amazon.com/iot/latest/developerguide/thing-policy-variables.html
     # https://docs.aws.amazon.com/transfer/latest/userguide/users.html#users-policies-scope-down
     # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html
-    resource_excludes = ['${aws:CurrentTime}', '${aws:EpochTime}', '${aws:TokenIssueTime}', '${aws:principaltype}',
-                         '${aws:SecureTransport}', '${aws:SourceIp}', '${aws:UserAgent}', '${aws:userid}',
+    resource_excludes = ['${aws:CurrentTime}', '${aws:EpochTime}',
+                         '${aws:TokenIssueTime}', '${aws:principaltype}',
+                         '${aws:SecureTransport}', '${aws:SourceIp}',
+                         '${aws:UserAgent}', '${aws:userid}',
                          '${aws:username}', '${ec2:SourceInstanceARN}',
-                         '${iot:Connection.Thing.ThingName}', '${iot:Connection.Thing.ThingTypeName}',
-                         '${iot:Connection.Thing.IsAttached}', '${iot:ClientId}', '${transfer:HomeBucket}',
-                         '${transfer:HomeDirectory}', '${transfer:HomeFolder}', '${transfer:UserName}',
-                         '${cognito-identity.amazonaws.com:aud}', '${cognito-identity.amazonaws.com:sub}', '${cognito-identity.amazonaws.com:amr}']
+                         '${iot:Connection.Thing.ThingName}',
+                         '${iot:Connection.Thing.ThingTypeName}',
+                         '${iot:Connection.Thing.IsAttached}',
+                         '${iot:ClientId}', '${transfer:HomeBucket}',
+                         '${transfer:HomeDirectory}', '${transfer:HomeFolder}',
+                         '${transfer:UserName}', '${redshift:DbUser}',
+                         '${cognito-identity.amazonaws.com:aud}',
+                         '${cognito-identity.amazonaws.com:sub}',
+                         '${cognito-identity.amazonaws.com:amr}']
+
+    # https://docs.aws.amazon.com/redshift/latest/mgmt/redshift-iam-access-control-identity-based.html
+    condition_excludes = [
+        '${redshift:DbUser}',
+    ]
 
     def _match_values(self, searchRegex, cfnelem, path):
         """Recursively search for values matching the searchRegex"""
@@ -96,13 +110,15 @@ def match(self, cfn):
 
         # We want to search all of the paths to check if each one contains an 'Fn::Sub'
         for parameter_string_path in parameter_string_paths:
-
             # Exxclude the special IAM variables
             variable = parameter_string_path[-1]
 
             if 'Resource' in parameter_string_path:
                 if variable in self.resource_excludes:
                     continue
+            if 'Condition' in parameter_string_path:
+                if variable in self.condition_excludes:
+                    continue
 
             # Exclude literals (https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html)
             if variable.startswith('${!'):
@@ -121,7 +137,8 @@ def match(self, cfn):
             if not found_sub:
                 # Remove the last item (the variable) to prevent multiple errors on 1 line errors
                 path = parameter_string_path[:-1]
-                message = 'Found an embedded parameter outside of an "Fn::Sub" at {}'.format('/'.join(map(str, path)))
+                message = 'Found an embedded parameter outside of an "Fn::Sub" at {}'.format(
+                    '/'.join(map(str, path)))
                 matches.append(RuleMatch(path, message))
 
         return matches


Test patch:
diff --git a/test/fixtures/templates/good/functions/sub_needed.yaml b/test/fixtures/templates/good/functions/sub_needed.yaml
--- a/test/fixtures/templates/good/functions/sub_needed.yaml
+++ b/test/fixtures/templates/good/functions/sub_needed.yaml
@@ -18,6 +18,60 @@ Resources:
           Action:
             - "iam:UploadSSHPublicKey"
           Resource: "arn:aws:iam::*:user/${aws:username}"
+  myPolicy2:
+    Type: AWS::IAM::Policy
+    Properties:
+      Roles:
+      - testRole
+      PolicyName: test2
+      PolicyDocument:
+        {
+          "Version": "2012-10-17",
+            "Statement": [
+              {
+              "Sid": "GetClusterCredsStatement",
+                "Effect": "Allow",
+                "Action": [
+                  "redshift:GetClusterCredentials"
+                ],
+                "Resource": [
+                  "arn:aws:redshift:us-west-2:123456789012:dbuser:examplecluster/${redshift:DbUser}",
+                  "arn:aws:redshift:us-west-2:123456789012:dbname:examplecluster/testdb",
+                  "arn:aws:redshift:us-west-2:123456789012:dbgroup:examplecluster/common_group"
+                ],
+                  "Condition": {
+                      "StringEquals": {
+                    "aws:userid":"AIDIODR4TAW7CSEXAMPLE:${redshift:DbUser}@yourdomain.com"
+                                      }
+                                }
+              },
+              {
+                "Sid": "CreateClusterUserStatement",
+                "Effect": "Allow",
+                "Action": [
+                  "redshift:CreateClusterUser"
+                ],
+                "Resource": [
+                  "arn:aws:redshift:us-west-2:123456789012:dbuser:examplecluster/${redshift:DbUser}"
+                ],
+                "Condition": {
+                  "StringEquals": {
+                    "aws:userid":"AIDIODR4TAW7CSEXAMPLE:${redshift:DbUser}@yourdomain.com"
+                  }
+                }
+              },
+              {
+                "Sid": "RedshiftJoinGroupStatement",
+                "Effect": "Allow",
+                "Action": [
+                  "redshift:JoinGroup"
+                ],
+                "Resource": [
+                  "arn:aws:redshift:us-west-2:123456789012:dbgroup:examplecluster/common_group"
+                ]
+              }
+            ]
+          }
   Key:
     Type: AWS::ApiGateway::ApiKey
     Properties: