Repository: cloud-custodian/cloud-custodian

Issue:
GCP - new serviceAccountsKeys resource
support for service account keys by adding a new resource under iam. 

https://cloud.google.com/iam/docs/reference/rest/v1/projects.serviceAccounts.keys


Discussion:

Resolving patch:
diff --git a/tools/c7n_gcp/c7n_gcp/resources/iam.py b/tools/c7n_gcp/c7n_gcp/resources/iam.py
--- a/tools/c7n_gcp/c7n_gcp/resources/iam.py
+++ b/tools/c7n_gcp/c7n_gcp/resources/iam.py
@@ -1,8 +1,12 @@
 # Copyright The Cloud Custodian Authors.
 # SPDX-License-Identifier: Apache-2.0
+import re
+
+from c7n.utils import type_schema
 
 from c7n_gcp.provider import resources
-from c7n_gcp.query import QueryResourceManager, TypeInfo
+from c7n_gcp.query import QueryResourceManager, TypeInfo, ChildResourceManager, ChildTypeInfo
+from c7n_gcp.actions import MethodAction
 
 
 @resources.register('project-role')
@@ -56,6 +60,66 @@ def get(client, resource_info):
                         resource_info['email_id'])})
 
 
+@resources.register('service-account-key')
+class ServiceAccountKey(ChildResourceManager):
+    """GCP Resource
+    https://cloud.google.com/iam/docs/reference/rest/v1/projects.serviceAccounts.keys
+    """
+    def _get_parent_resource_info(self, child_instance):
+        project_id, sa = re.match(
+            'projects/(.*?)/serviceAccounts/(.*?)/keys/.*',
+            child_instance['name']).groups()
+        return {'project_id': project_id,
+                'email_id': sa}
+
+    def get_resource_query(self):
+        """Does nothing as self does not need query values unlike its parent
+        which receives them with the use_child_query flag."""
+        pass
+
+    class resource_type(ChildTypeInfo):
+        service = 'iam'
+        version = 'v1'
+        component = 'projects.serviceAccounts.keys'
+        enum_spec = ('list', 'keys[]', [])
+        scope = None
+        scope_key = 'name'
+        name = id = 'name'
+        default_report_fields = ['name', 'privateKeyType', 'keyAlgorithm',
+          'validAfterTime', 'validBeforeTime', 'keyOrigin', 'keyType']
+        parent_spec = {
+            'resource': 'service-account',
+            'child_enum_params': [
+                ('name', 'name')
+            ],
+            'use_child_query': True
+        }
+        asset_type = "iam.googleapis.com/ServiceAccountKey"
+        scc_type = "google.iam.ServiceAccountKey"
+        permissions = ("iam.serviceAccounts.list",)
+
+        @staticmethod
+        def get(client, resource_info):
+            project, sa, key = re.match(
+                '.*?/projects/(.*?)/serviceAccounts/(.*?)/keys/(.*)',
+                resource_info['resourceName']).groups()
+            return client.execute_query(
+                'get', {
+                    'name': 'projects/{}/serviceAccounts/{}/keys/{}'.format(
+                        project, sa, key)})
+
+
+@ServiceAccountKey.action_registry.register('delete')
+class DeleteServiceAccountKey(MethodAction):
+
+    schema = type_schema('delete')
+    method_spec = {'op': 'delete'}
+    permissions = ("iam.serviceAccountKeys.delete",)
+
+    def get_resource_params(self, m, r):
+        return {'name': r['name']}
+
+
 @resources.register('iam-role')
 class Role(QueryResourceManager):
     """GCP Organization Role
diff --git a/tools/c7n_gcp/c7n_gcp/resources/resource_map.py b/tools/c7n_gcp/c7n_gcp/resources/resource_map.py
--- a/tools/c7n_gcp/c7n_gcp/resources/resource_map.py
+++ b/tools/c7n_gcp/c7n_gcp/resources/resource_map.py
@@ -78,6 +78,7 @@
     "gcp.router": "c7n_gcp.resources.network.Router",
     "gcp.service": "c7n_gcp.resources.service.Service",
     "gcp.service-account": "c7n_gcp.resources.iam.ServiceAccount",
+    "gcp.service-account-key": "c7n_gcp.resources.iam.ServiceAccountKey",
     "gcp.snapshot": "c7n_gcp.resources.compute.Snapshot",
     "gcp.sourcerepo": "c7n_gcp.resources.source.SourceRepository",
     "gcp.spanner-database-instance": "c7n_gcp.resources.spanner.SpannerDatabaseInstance",


Test patch:
diff --git a/tools/c7n_gcp/tests/data/flights/iam-delete-service-account-key/delete-v1-projects-cloud-custodian-serviceAccounts-test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com-keys-2_1.json b/tools/c7n_gcp/tests/data/flights/iam-delete-service-account-key/delete-v1-projects-cloud-custodian-serviceAccounts-test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com-keys-2_1.json
new file mode 100644
--- /dev/null
+++ b/tools/c7n_gcp/tests/data/flights/iam-delete-service-account-key/delete-v1-projects-cloud-custodian-serviceAccounts-test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com-keys-2_1.json
@@ -0,0 +1,18 @@
+{
+  "headers": {
+    "content-type": "application/json; charset=UTF-8",
+    "vary": "Origin, X-Origin, Referer",
+    "date": "Wed, 31 Mar 2021 15:25:30 GMT",
+    "server": "ESF",
+    "cache-control": "private",
+    "x-xss-protection": "0",
+    "x-frame-options": "SAMEORIGIN",
+    "x-content-type-options": "nosniff",
+    "alt-svc": "h3-29=\":443\"; ma=2592000,h3-T051=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"",
+    "transfer-encoding": "chunked",
+    "status": "200",
+    "content-length": "3",
+    "-content-encoding": "gzip"
+  },
+  "body": {}
+}
\ No newline at end of file
diff --git a/tools/c7n_gcp/tests/data/flights/iam-delete-service-account-key/get-v1-projects-cloud-custodian-serviceAccounts-test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com-keys-2_1.json b/tools/c7n_gcp/tests/data/flights/iam-delete-service-account-key/get-v1-projects-cloud-custodian-serviceAccounts-test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com-keys-2_1.json
new file mode 100644
--- /dev/null
+++ b/tools/c7n_gcp/tests/data/flights/iam-delete-service-account-key/get-v1-projects-cloud-custodian-serviceAccounts-test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com-keys-2_1.json
@@ -0,0 +1,24 @@
+{
+  "headers": {
+    "vary": "Origin, X-Origin, Referer",
+    "content-type": "application/json; charset=UTF-8",
+    "date": "Wed, 31 Mar 2021 15:25:31 GMT",
+    "server": "ESF",
+    "cache-control": "private",
+    "x-xss-protection": "0",
+    "x-frame-options": "SAMEORIGIN",
+    "x-content-type-options": "nosniff",
+    "alt-svc": "h3-29=\":443\"; ma=2592000,h3-T051=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"",
+    "transfer-encoding": "chunked",
+    "status": "404",
+    "content-length": "159",
+    "-content-encoding": "gzip"
+  },
+  "body": {
+    "error": {
+      "code": 404,
+      "message": "Service account key 2 does not exist.",
+      "status": "NOT_FOUND"
+    }
+  }
+}
\ No newline at end of file
diff --git a/tools/c7n_gcp/tests/data/flights/iam-delete-service-account-key/get-v1-projects-cloud-custodian-serviceAccounts-test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com-keys_1.json b/tools/c7n_gcp/tests/data/flights/iam-delete-service-account-key/get-v1-projects-cloud-custodian-serviceAccounts-test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com-keys_1.json
new file mode 100644
--- /dev/null
+++ b/tools/c7n_gcp/tests/data/flights/iam-delete-service-account-key/get-v1-projects-cloud-custodian-serviceAccounts-test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com-keys_1.json
@@ -0,0 +1,30 @@
+{
+  "headers": {
+    "content-type": "application/json; charset=UTF-8",
+    "vary": "Origin, X-Origin, Referer",
+    "date": "Wed, 31 Mar 2021 13:42:48 GMT",
+    "server": "ESF",
+    "cache-control": "private",
+    "x-xss-protection": "0",
+    "x-frame-options": "SAMEORIGIN",
+    "x-content-type-options": "nosniff",
+    "alt-svc": "h3-29=\":443\"; ma=2592000,h3-T051=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"",
+    "transfer-encoding": "chunked",
+    "status": "200",
+    "content-length": "813",
+    "-content-encoding": "gzip",
+    "content-location": "https://iam.googleapis.com/v1/projects/cloud-custodian/serviceAccounts/test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com/keys?alt=json"
+  },
+  "body": {
+    "keys": [
+      {
+        "name": "projects/cloud-custodian/serviceAccounts/test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com/keys/2",
+        "validAfterTime": "2021-03-31T13:27:15Z",
+        "validBeforeTime": "9999-12-31T23:59:59Z",
+        "keyAlgorithm": "KEY_ALG_RSA_2048",
+        "keyOrigin": "GOOGLE_PROVIDED",
+        "keyType": "USER_MANAGED"
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tools/c7n_gcp/tests/data/flights/iam-delete-service-account-key/get-v1-projects-cloud-custodian-serviceAccounts_1.json b/tools/c7n_gcp/tests/data/flights/iam-delete-service-account-key/get-v1-projects-cloud-custodian-serviceAccounts_1.json
new file mode 100644
--- /dev/null
+++ b/tools/c7n_gcp/tests/data/flights/iam-delete-service-account-key/get-v1-projects-cloud-custodian-serviceAccounts_1.json
@@ -0,0 +1,30 @@
+{
+  "headers": {
+    "content-type": "application/json; charset=UTF-8",
+    "vary": "Origin, X-Origin, Referer",
+    "date": "Wed, 31 Mar 2021 13:42:41 GMT",
+    "server": "ESF",
+    "cache-control": "private",
+    "x-xss-protection": "0",
+    "x-frame-options": "SAMEORIGIN",
+    "x-content-type-options": "nosniff",
+    "alt-svc": "h3-29=\":443\"; ma=2592000,h3-T051=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"",
+    "transfer-encoding": "chunked",
+    "status": "200",
+    "content-length": "1876",
+    "-content-encoding": "gzip",
+    "content-location": "https://iam.googleapis.com/v1/projects/cloud-custodian/serviceAccounts?alt=json"
+  },
+  "body": {
+    "accounts": [
+      {
+        "name": "projects/cloud-custodian/serviceAccounts/test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com",
+        "projectId": "cloud-custodian",
+        "email": "test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com",
+        "displayName": "test-cutodian-scc",
+        "etag": "MDEwMjE5MjA=",
+        "description": "for testing custodian scc"
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tools/c7n_gcp/tests/data/flights/iam-service-account-key-query/get-v1-projects-cloud-custodian-serviceAccounts-test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com-keys_1.json b/tools/c7n_gcp/tests/data/flights/iam-service-account-key-query/get-v1-projects-cloud-custodian-serviceAccounts-test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com-keys_1.json
new file mode 100644
--- /dev/null
+++ b/tools/c7n_gcp/tests/data/flights/iam-service-account-key-query/get-v1-projects-cloud-custodian-serviceAccounts-test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com-keys_1.json
@@ -0,0 +1,38 @@
+{
+  "headers": {
+    "content-type": "application/json; charset=UTF-8",
+    "vary": "Origin, X-Origin, Referer",
+    "date": "Wed, 31 Mar 2021 13:42:48 GMT",
+    "server": "ESF",
+    "cache-control": "private",
+    "x-xss-protection": "0",
+    "x-frame-options": "SAMEORIGIN",
+    "x-content-type-options": "nosniff",
+    "alt-svc": "h3-29=\":443\"; ma=2592000,h3-T051=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"",
+    "transfer-encoding": "chunked",
+    "status": "200",
+    "content-length": "813",
+    "-content-encoding": "gzip",
+    "content-location": "https://iam.googleapis.com/v1/projects/cloud-custodian/serviceAccounts/test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com/keys?alt=json"
+  },
+  "body": {
+    "keys": [
+      {
+        "name": "projects/cloud-custodian/serviceAccounts/test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com/keys/1",
+        "validAfterTime": "2021-03-23T14:07:01Z",
+        "validBeforeTime": "2021-04-09T14:07:01Z",
+        "keyAlgorithm": "KEY_ALG_RSA_2048",
+        "keyOrigin": "GOOGLE_PROVIDED",
+        "keyType": "SYSTEM_MANAGED"
+      },
+      {
+        "name": "projects/cloud-custodian/serviceAccounts/test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com/keys/2",
+        "validAfterTime": "2021-03-31T13:27:15Z",
+        "validBeforeTime": "9999-12-31T23:59:59Z",
+        "keyAlgorithm": "KEY_ALG_RSA_2048",
+        "keyOrigin": "GOOGLE_PROVIDED",
+        "keyType": "USER_MANAGED"
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tools/c7n_gcp/tests/data/flights/iam-service-account-key-query/get-v1-projects-cloud-custodian-serviceAccounts_1.json b/tools/c7n_gcp/tests/data/flights/iam-service-account-key-query/get-v1-projects-cloud-custodian-serviceAccounts_1.json
new file mode 100644
--- /dev/null
+++ b/tools/c7n_gcp/tests/data/flights/iam-service-account-key-query/get-v1-projects-cloud-custodian-serviceAccounts_1.json
@@ -0,0 +1,30 @@
+{
+  "headers": {
+    "content-type": "application/json; charset=UTF-8",
+    "vary": "Origin, X-Origin, Referer",
+    "date": "Wed, 31 Mar 2021 13:42:41 GMT",
+    "server": "ESF",
+    "cache-control": "private",
+    "x-xss-protection": "0",
+    "x-frame-options": "SAMEORIGIN",
+    "x-content-type-options": "nosniff",
+    "alt-svc": "h3-29=\":443\"; ma=2592000,h3-T051=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"",
+    "transfer-encoding": "chunked",
+    "status": "200",
+    "content-length": "1876",
+    "-content-encoding": "gzip",
+    "content-location": "https://iam.googleapis.com/v1/projects/cloud-custodian/serviceAccounts?alt=json"
+  },
+  "body": {
+    "accounts": [
+      {
+        "name": "projects/cloud-custodian/serviceAccounts/test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com",
+        "projectId": "cloud-custodian",
+        "email": "test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com",
+        "displayName": "test-cutodian-scc",
+        "etag": "MDEwMjE5MjA=",
+        "description": "for testing custodian scc"
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tools/c7n_gcp/tests/data/flights/iam-service-account-key/get-v1-projects-cloud-custodian-serviceAccounts-111111111111111-keys-2222_1.json b/tools/c7n_gcp/tests/data/flights/iam-service-account-key/get-v1-projects-cloud-custodian-serviceAccounts-111111111111111-keys-2222_1.json
new file mode 100644
--- /dev/null
+++ b/tools/c7n_gcp/tests/data/flights/iam-service-account-key/get-v1-projects-cloud-custodian-serviceAccounts-111111111111111-keys-2222_1.json
@@ -0,0 +1,26 @@
+{
+  "headers": {
+    "content-type": "application/json; charset=UTF-8",
+    "vary": "Origin, X-Origin, Referer",
+    "date": "Wed, 31 Mar 2021 13:35:44 GMT",
+    "server": "ESF",
+    "cache-control": "private",
+    "x-xss-protection": "0",
+    "x-frame-options": "SAMEORIGIN",
+    "x-content-type-options": "nosniff",
+    "alt-svc": "h3-29=\":443\"; ma=2592000,h3-T051=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"",
+    "transfer-encoding": "chunked",
+    "status": "200",
+    "content-length": "363",
+    "-content-encoding": "gzip",
+    "content-location": "https://iam.googleapis.com/v1/projects/cloud-custodian/serviceAccounts/111111111111111/keys/2222?alt=json"
+  },
+  "body": {
+    "name": "projects/cloud-custodian/serviceAccounts/test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com/keys/2222",
+    "validAfterTime": "2021-03-31T13:27:15Z",
+    "validBeforeTime": "9999-12-31T23:59:59Z",
+    "keyAlgorithm": "KEY_ALG_RSA_2048",
+    "keyOrigin": "GOOGLE_PROVIDED",
+    "keyType": "USER_MANAGED"
+  }
+}
\ No newline at end of file
diff --git a/tools/c7n_gcp/tests/data/flights/iam-service-account-key/get-v1-projects-cloud-custodian-serviceAccounts-test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com_1.json b/tools/c7n_gcp/tests/data/flights/iam-service-account-key/get-v1-projects-cloud-custodian-serviceAccounts-test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com_1.json
new file mode 100644
--- /dev/null
+++ b/tools/c7n_gcp/tests/data/flights/iam-service-account-key/get-v1-projects-cloud-custodian-serviceAccounts-test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com_1.json
@@ -0,0 +1,28 @@
+{
+  "headers": {
+    "content-type": "application/json; charset=UTF-8",
+    "vary": "Origin, X-Origin, Referer",
+    "date": "Wed, 31 Mar 2021 13:35:45 GMT",
+    "server": "ESF",
+    "cache-control": "private",
+    "x-xss-protection": "0",
+    "x-frame-options": "SAMEORIGIN",
+    "x-content-type-options": "nosniff",
+    "alt-svc": "h3-29=\":443\"; ma=2592000,h3-T051=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"",
+    "transfer-encoding": "chunked",
+    "status": "200",
+    "content-length": "439",
+    "-content-encoding": "gzip",
+    "content-location": "https://iam.googleapis.com/v1/projects/cloud-custodian/serviceAccounts/test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com?alt=json"
+  },
+  "body": {
+    "name": "projects/cloud-custodian/serviceAccounts/test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com",
+    "projectId": "cloud-custodian",
+    "uniqueId": "111111111111111",
+    "email": "test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com",
+    "displayName": "test-cutodian-scc",
+    "etag": "MDEwMjE5MjA=",
+    "description": "for testing custodian scc",
+    "oauth2ClientId": "111111111111111"
+  }
+}
\ No newline at end of file
diff --git a/tools/c7n_gcp/tests/test_gcp_iam.py b/tools/c7n_gcp/tests/test_gcp_iam.py
--- a/tools/c7n_gcp/tests/test_gcp_iam.py
+++ b/tools/c7n_gcp/tests/test_gcp_iam.py
@@ -1,7 +1,10 @@
 # Copyright The Cloud Custodian Authors.
 # SPDX-License-Identifier: Apache-2.0
 
+import time
+
 from gcp_common import BaseTest, event_data
+from googleapiclient.errors import HttpError
 
 
 class ProjectRoleTest(BaseTest):
@@ -40,6 +43,59 @@ def test_get(self):
         self.assertEqual(resource['displayName'], 'devtest')
 
 
+class ServiceAccountKeyTest(BaseTest):
+
+    def test_service_account_key_query(self):
+        project_id = "cloud-custodian"
+
+        session_factory = self.replay_flight_data(
+            'iam-service-account-key-query', project_id)
+
+        policy = self.load_policy(
+            {
+                'name': 'iam-service-account-key-query',
+                'resource': 'gcp.service-account-key'
+            },
+            session_factory=session_factory)
+
+        resources = policy.run()
+        self.assertEqual(len(resources), 2)
+        self.assertEqual(resources[0]["keyType"], "SYSTEM_MANAGED")
+        self.assertEqual(resources[1]["keyType"], "USER_MANAGED")
+
+    def test_get_service_account_key(self):
+        factory = self.replay_flight_data('iam-service-account-key')
+        p = self.load_policy({
+            'name': 'sa-key-get',
+            'resource': 'gcp.service-account-key'},
+            session_factory=factory)
+        resource = p.resource_manager.get_resource(
+            {'resourceName': '//iam.googleapis.com/projects/cloud-custodian/'
+            'serviceAccounts/111111111111111/keys/2222'})
+        self.assertEqual(resource['keyType'], 'USER_MANAGED')
+        self.assertEqual(resource["c7n:service-account"]["email"],
+        "test-cutodian-scc@cloud-custodian.iam.gserviceaccount.com")
+
+    def test_delete_service_account_key(self):
+        factory = self.replay_flight_data('iam-delete-service-account-key')
+        p = self.load_policy({
+            'name': 'sa-key-delete',
+            'resource': 'gcp.service-account-key',
+            'actions': ['delete']},
+            session_factory=factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        if self.recording:
+            time.sleep(1)
+        client = p.resource_manager.get_client()
+        try:
+            result = client.execute_query(
+                'get', {'name': resources[0]["name"]})
+            self.fail('found deleted service account key: %s' % result)
+        except HttpError as e:
+            self.assertTrue("does not exist" in str(e))
+
+
 class IAMRoleTest(BaseTest):
 
     def test_iam_role_query(self):