Repository: cloud-custodian/cloud-custodian

Issue:
Add value filter op logic to iam-role.filters.has-specific-managed-policy filter
### Describe the feature

I want to find all roles which have a managed policy attached with a name which ends in "FullAccess" to find all the roles which have managed FullAccess policies attached. Currently the has-specific-managed-policy filter only accepts the exact policy name via string and I would like the ability to use different op: with it such as regex, contains, in etc.

This would allow pulling back the attached policies to the roles and filtering them by a naming pattern

### Extra information or context

There is no workaround at this time.


Discussion:

Resolving patch:
diff --git a/c7n/resources/ec2.py b/c7n/resources/ec2.py
--- a/c7n/resources/ec2.py
+++ b/c7n/resources/ec2.py
@@ -2391,16 +2391,62 @@ class HasSpecificManagedPolicy(SpecificIamProfileManagedPolicy):
        a specific managed IAM policy. If an EC2 instance does not have a profile or the profile
        does not contain an IAM role, then it will be treated as not having the policy.
 
-    :Example:
+    :example:
 
     .. code-block:: yaml
 
         policies:
           - name: ec2-instance-has-admin-policy
-            resource: ec2
+            resource: aws.ec2
             filters:
               - type: has-specific-managed-policy
                 value: admin-policy
+
+    :example:
+
+    Check for EC2 instances with instance profile roles that have an
+    attached policy matching a given list:
+
+    .. code-block:: yaml
+
+        policies:
+          - name: ec2-instance-with-selected-policies
+            resource: aws.ec2
+            filters:
+              - type: has-specific-managed-policy
+                op: in
+                value:
+                  - AmazonS3FullAccess
+                  - AWSOrganizationsFullAccess
+
+    :example:
+
+    Check for EC2 instances with instance profile roles that have
+    attached policy names matching a pattern:
+
+    .. code-block:: yaml
+
+        policies:
+          - name: ec2-instance-with-full-access-policies
+            resource: aws.ec2
+            filters:
+              - type: has-specific-managed-policy
+                op: glob
+                value: "*FullAccess"
+
+    Check for EC2 instances with instance profile roles that have
+    attached policy ARNs matching a pattern:
+
+    .. code-block:: yaml
+
+        policies:
+          - name: ec2-instance-with-aws-full-access-policies
+            resource: aws.ec2
+            filters:
+              - type: has-specific-managed-policy
+                key: PolicyArn
+                op: regex
+                value: "arn:aws:iam::aws:policy/.*FullAccess"
     """
 
     permissions = (
@@ -2425,7 +2471,11 @@ def process(self, resources, event=None):
             if not profile:
                 continue
 
-            if self.has_managed_policy(client, profile):
+            self.get_managed_policies(client, [profile])
+
+            matched_keys = [k for k in profile[self.annotation_key] if self.match(k)]
+            self.merge_annotation(profile, self.matched_annotation_key, matched_keys)
+            if matched_keys:
                 results.append(r)
 
         return results
diff --git a/c7n/resources/iam.py b/c7n/resources/iam.py
--- a/c7n/resources/iam.py
+++ b/c7n/resources/iam.py
@@ -985,35 +985,105 @@ def process(self, resources, event=None):
 
 
 @Role.filter_registry.register('has-specific-managed-policy')
-class SpecificIamRoleManagedPolicy(Filter):
-    """Filter IAM roles that has a specific policy attached
-
-    For example, if the user wants to check all roles with 'admin-policy':
+class SpecificIamRoleManagedPolicy(ValueFilter):
+    """Find IAM roles that have a specific policy attached
 
     :example:
 
+    Check for roles with 'admin-policy' attached:
+
     .. code-block:: yaml
 
         policies:
           - name: iam-roles-have-admin
-            resource: iam-role
+            resource: aws.iam-role
             filters:
               - type: has-specific-managed-policy
                 value: admin-policy
+
+    :example:
+
+    Check for roles with an attached policy matching
+    a given list:
+
+    .. code-block:: yaml
+
+        policies:
+          - name: iam-roles-with-selected-policies
+            resource: aws.iam-role
+            filters:
+              - type: has-specific-managed-policy
+                op: in
+                value:
+                  - AmazonS3FullAccess
+                  - AWSOrganizationsFullAccess
+
+    :example:
+
+    Check for roles with attached policy names matching a pattern:
+
+    .. code-block:: yaml
+
+        policies:
+          - name: iam-roles-with-full-access-policies
+            resource: aws.iam-role
+            filters:
+              - type: has-specific-managed-policy
+                op: glob
+                value: "*FullAccess"
+
+    Check for roles with attached policy ARNs matching a pattern:
+
+    .. code-block:: yaml
+
+        policies:
+          - name: iam-roles-with-aws-full-access-policies
+            resource: aws.iam-role
+            filters:
+              - type: has-specific-managed-policy
+                key: PolicyArn
+                op: regex
+                value: "arn:aws:iam::aws:policy/.*FullAccess"
     """
 
-    schema = type_schema('has-specific-managed-policy', value={'type': 'string'})
+    schema = type_schema('has-specific-managed-policy', rinherit=ValueFilter.schema)
     permissions = ('iam:ListAttachedRolePolicies',)
+    annotation_key = 'c7n:AttachedPolicies'
+    matched_annotation_key = 'c7n:MatchedPolicies'
+    schema_alias = False
 
-    def _managed_policies(self, client, resource):
-        return [r['PolicyName'] for r in client.list_attached_role_policies(
-            RoleName=resource['RoleName'])['AttachedPolicies']]
+    def __init__(self, data, manager=None):
+        # Preserve backward compatibility
+        if 'key' not in data:
+            data['key'] = 'PolicyName'
+        super(SpecificIamRoleManagedPolicy, self).__init__(data, manager)
+
+    def get_managed_policies(self, client, role_set):
+        for role in role_set:
+            role[self.annotation_key] = [
+                role_policy
+                for role_policy
+                in client.list_attached_role_policies(
+                    RoleName=role['RoleName'])['AttachedPolicies']
+            ]
 
     def process(self, resources, event=None):
-        c = local_session(self.manager.session_factory).client('iam')
-        if self.data.get('value'):
-            return [r for r in resources if self.data.get('value') in self._managed_policies(c, r)]
-        return []
+        client = local_session(self.manager.session_factory).client('iam')
+        with self.executor_factory(max_workers=2) as w:
+            augment_set = [r for r in resources if self.annotation_key not in r]
+            self.log.debug(
+                "Querying %d roles' attached policies" % len(augment_set))
+            list(w.map(
+                functools.partial(self.get_managed_policies, client),
+                chunks(augment_set, 50)))
+
+        matched = []
+        for r in resources:
+            matched_keys = [k for k in r[self.annotation_key] if self.match(k)]
+            self.merge_annotation(r, self.matched_annotation_key, matched_keys)
+            if matched_keys:
+                matched.append(r)
+        return matched
 
 
 @Role.filter_registry.register('no-specific-managed-policy')
@@ -2817,38 +2887,109 @@ class resource_type(TypeInfo):
 
 
 @InstanceProfile.filter_registry.register('has-specific-managed-policy')
-class SpecificIamProfileManagedPolicy(Filter):
+class SpecificIamProfileManagedPolicy(ValueFilter):
     """Filter an IAM instance profile that contains an IAM role that has a specific managed IAM
        policy. If an IAM instance profile does not contain an IAM role, then it will be treated
        as not having the policy.
 
-    :Example:
+    :example:
+
+    Check for instance profile roles with 'admin-policy' attached:
 
     .. code-block:: yaml
 
         policies:
-          - name: iam-profile-has-admin-policy
-            resource: iam-profile
+          - name: iam-profiles-have-admin
+            resource: aws.iam-profile
             filters:
               - type: has-specific-managed-policy
                 value: admin-policy
+
+    :example:
+
+    Check for instance profile roles with an attached policy matching
+    a given list:
+
+    .. code-block:: yaml
+
+        policies:
+          - name: iam-profiles-with-selected-policies
+            resource: aws.iam-profile
+            filters:
+              - type: has-specific-managed-policy
+                value:
+                  - AmazonS3FullAccess
+                  - AWSOrganizationsFullAccess
+
+    :example:
+
+    Check for instance profile roles with attached policy names matching
+    a pattern:
+
+    .. code-block:: yaml
+
+        policies:
+          - name: iam-profiles-with-full-access-policies
+            resource: aws.iam-profile
+            filters:
+              - type: has-specific-managed-policy
+                op: glob
+                value: "*FullAccess"
+
+    Check for instance profile roles with attached policy ARNs matching
+    a pattern:
+
+    .. code-block:: yaml
+
+        policies:
+          - name: iam-profiles-with-aws-full-access-policies
+            resource: aws.iam-profile
+            filters:
+              - type: has-specific-managed-policy
+                key: PolicyArn
+                op: regex
+                value: "arn:aws:iam::aws:policy/.*FullAccess"
     """
 
-    schema = type_schema('has-specific-managed-policy', value={'type': 'string'})
+    schema = type_schema('has-specific-managed-policy', rinherit=ValueFilter.schema)
     permissions = ('iam:ListAttachedRolePolicies',)
+    annotation_key = 'c7n:AttachedPolicies'
+    matched_annotation_key = 'c7n:MatchedPolicies'
+    schema_alias = False
 
-    def _managed_policies(self, client, role_name):
-        attached_policies = client.list_attached_role_policies(
-            RoleName=role_name)['AttachedPolicies']
-        return [p['PolicyName'] for p in attached_policies]
-
-    def has_managed_policy(self, client, resource):
-        for role in resource.get('Roles', []):
-            managed_policies = self._managed_policies(client, role.get('RoleName'))
-            if self.data.get('value') in managed_policies:
-                return True
-        return False
+    def __init__(self, data, manager=None):
+        # Preserve backward compatibility
+        if 'key' not in data:
+            data['key'] = 'PolicyName'
+        super(SpecificIamProfileManagedPolicy, self).__init__(data, manager)
+
+    # This code assumes a single IAM role in the profile and the annotation
+    # is added to the profile resource, not the role
+    def get_managed_policies(self, client, prof_set):
+        for prof in prof_set:
+            prof[self.annotation_key] = []
+            for role in prof.get('Roles', []):
+                prof[self.annotation_key] = [
+                    role_policy
+                    for role_policy
+                    in client.list_attached_role_policies(
+                        RoleName=role['RoleName'])['AttachedPolicies']
+                ]
 
     def process(self, resources, event=None):
-        c = local_session(self.manager.session_factory).client('iam')
-        return [r for r in resources if self.has_managed_policy(c, r)]
+        client = local_session(self.manager.session_factory).client('iam')
+        with self.executor_factory(max_workers=2) as w:
+            augment_set = [r for r in resources if self.annotation_key not in r]
+            self.log.debug(
+                "Querying %d roles' attached policies" % len(augment_set))
+            list(w.map(
+                functools.partial(self.get_managed_policies, client),
+                chunks(augment_set, 50)))
+
+        matched = []
+        for r in resources:
+            matched_keys = [k for k in r[self.annotation_key] if self.match(k)]
+            self.merge_annotation(r, self.matched_annotation_key, matched_keys)
+            if matched_keys:
+                matched.append(r)
+        return matched


Test patch: