Repository: cloud-custodian/cloud-custodian

Issue:
azure - 'scope' filter includes everything for the Resource Group
Searching for the role assignments at RG scope returns all role assignments that have smaller scope (e.g. VM, Storage)


Discussion:
whats the proposed solution? to only query role assignments valid at the root scope of the rg?
Yeah, if some SP has access to the Virtual Machine resource only, it should not be marked for Resource Group scope.

Resolving patch:
diff --git a/tools/c7n_azure/c7n_azure/resources/access_control.py b/tools/c7n_azure/c7n_azure/resources/access_control.py
--- a/tools/c7n_azure/c7n_azure/resources/access_control.py
+++ b/tools/c7n_azure/c7n_azure/resources/access_control.py
@@ -153,21 +153,6 @@ def get_resources(self, query):
         return [r.serialize(True) for r in resources]
 
 
-def is_scope(scope, scope_type):
-    if not isinstance(scope, six.string_types):
-        return False
-
-    regex = ""
-    if scope_type == "subscription":
-        regex = r"^\/subscriptions\/[^\/]+$"
-    elif scope_type == "resource-group":
-        regex = r"^\/subscriptions\/([^\/]+)\/resourceGroups\/.*$"
-    else:
-        return False
-
-    return bool(re.match(regex, scope, flags=re.IGNORECASE))
-
-
 @RoleAssignment.filter_registry.register('role')
 class RoleFilter(RelatedResourceFilter):
     """Filters role assignments based on role definitions
@@ -284,13 +269,35 @@ class ScopeFilter(Filter):
 
     """
 
+    SUBSCRIPTION_SCOPE = 'subscription'
+    RG_SCOPE = 'resource-group'
+    MG_SCOPE = 'management-group'
+
     schema = type_schema(
         'scope',
-        value={'type': 'string', 'enum': ['subscription', 'resource-group']})
+        value={'type': 'string', 'enum': [SUBSCRIPTION_SCOPE, RG_SCOPE, MG_SCOPE]})
 
     def process(self, data, event=None):
         scope_value = self.data.get('value', '')
-        return [d for d in data if is_scope(d["properties"]["scope"], scope_value)]
+        return [d for d in data if self.is_scope(d["properties"]["scope"], scope_value)]
+
+    def is_scope(self, scope, scope_type):
+        if not isinstance(scope, six.string_types):
+            return False
+
+        regex = ""
+        if scope_type == self.SUBSCRIPTION_SCOPE:
+            regex = r"^\/subscriptions\/[^\/]+$"
+        elif scope_type == self.RG_SCOPE:
+            regex = r"^\/subscriptions\/([^\/]+)\/resourceGroups\/[^\/]+$"
+        elif scope_type == self.MG_SCOPE:
+            regex = r"^\/providers\/Microsoft\.Management\/managementGroups/[^\/]+$"
+        else:
+            return False
+
+        match = re.match(regex, scope, flags=re.IGNORECASE)
+
+        return bool(match)
 
 
 @RoleAssignment.action_registry.register('delete')


Test patch:
diff --git a/tools/c7n_azure/tests/test_access_control.py b/tools/c7n_azure/tests/test_access_control.py
--- a/tools/c7n_azure/tests/test_access_control.py
+++ b/tools/c7n_azure/tests/test_access_control.py
@@ -14,8 +14,8 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
 from azure_common import BaseTest, arm_template
-from c7n_azure.resources.access_control import is_scope
-from mock import patch
+from c7n_azure.resources.access_control import ScopeFilter
+from mock import patch, MagicMock
 
 
 class AccessControlTest(BaseTest):
@@ -101,22 +101,49 @@ def test_find_definition_by_name(self):
         definitions = p.run()
         self.assertEqual(len(definitions), 1)
 
-    def test_is_scope(self):
+    def test_scope_filter_subscription(self):
         sub_scope = "/subscriptions/111-111-1111"
         resource_group_scope = sub_scope + "/resourceGroups/foo"
+        management_group_scope = "/providers/Microsoft.Management/managementGroups/foo"
 
-        # Subscription scope tests
-        self.assertTrue(is_scope(sub_scope, "subscription"))
-        self.assertFalse(is_scope(resource_group_scope, "subscription"))
-        self.assertFalse(is_scope("subscriptions", "subscription"))
-        self.assertFalse(is_scope("/subscription", "subscription"))
-        self.assertFalse(is_scope("/foo/bar", "subscription"))
-
-        # Resource group scope test
-        self.assertTrue(is_scope(resource_group_scope, "resource-group"))
-        self.assertFalse(is_scope(sub_scope, "resource-group"))
-        self.assertFalse(is_scope("/subscriptions/resourceGroups", "resource-group"))
-        self.assertFalse(is_scope("/subscriptions/resourceGroups/", "resource-group"))
-        self.assertFalse(is_scope("/subscriptions/resourceGroup/", "resource-group"))
-        self.assertFalse(is_scope("/subscription/resourceGroups/foo", "resource-group"))
-        self.assertFalse(is_scope("/foo/bar/xyz", "resource-group"))
+        scope_filter = ScopeFilter(MagicMock())
+
+        sub_type = 'subscription'
+        self.assertTrue(scope_filter.is_scope(sub_scope, sub_type))
+        self.assertFalse(scope_filter.is_scope(resource_group_scope, sub_type))
+        self.assertFalse(scope_filter.is_scope(management_group_scope, sub_type))
+        self.assertFalse(scope_filter.is_scope("subscriptions", sub_type))
+        self.assertFalse(scope_filter.is_scope("/subscription", sub_type))
+        self.assertFalse(scope_filter.is_scope("/foo/bar", sub_type))
+
+    def test_scope_filter_resource_group(self):
+        sub_scope = "/subscriptions/111-111-1111"
+        resource_group_scope = sub_scope + "/resourceGroups/foo"
+        management_group_scope = "/providers/Microsoft.Management/managementGroups/foo"
+
+        scope_filter = ScopeFilter(MagicMock())
+
+        rg_type = 'resource-group'
+
+        self.assertTrue(scope_filter.is_scope(resource_group_scope, rg_type))
+        self.assertFalse(scope_filter.is_scope(sub_scope, rg_type))
+        self.assertFalse(scope_filter.is_scope(management_group_scope, rg_type))
+        self.assertFalse(scope_filter.is_scope("/subscriptions/resourceGroups", rg_type))
+        self.assertFalse(scope_filter.is_scope("/subscriptions/resourceGroups/", rg_type))
+        self.assertFalse(scope_filter.is_scope("/subscriptions/resourceGroup/", rg_type))
+        self.assertFalse(scope_filter.is_scope("/subscription/resourceGroups/foo", rg_type))
+        self.assertFalse(scope_filter.is_scope("/foo/bar/xyz", rg_type))
+        self.assertFalse(scope_filter.is_scope(resource_group_scope + "/vm/bar", rg_type))
+
+    def test_scope_filter_management_group(self):
+        sub_scope = "/subscriptions/111-111-1111"
+        resource_group_scope = sub_scope + "/resourceGroups/foo"
+        management_group_scope = "/providers/Microsoft.Management/managementGroups/foo"
+
+        scope_filter = ScopeFilter(MagicMock())
+
+        mg_type = 'management-group'
+
+        self.assertTrue(scope_filter.is_scope(management_group_scope, mg_type))
+        self.assertFalse(scope_filter.is_scope(resource_group_scope, mg_type))
+        self.assertFalse(scope_filter.is_scope(sub_scope, mg_type))