Repository: google/cloud-forensics-utils

Issue:
Add projects.getIamPolicy to GCP cloudresourcemanager module
Will enable listing IAM bindings on GCP projects. API reference https://cloud.google.com/resource-manager/reference/rest/v3/projects/getIamPolicy

Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/google/cloud-forensics-utils/pull/427?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) Report
> :exclamation: No coverage uploaded for pull request base (`main@c5f4d5e`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#section-missing-base-commit).
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/google/cloud-forensics-utils/pull/427/graphs/tree.svg?width=650&height=150&src=pr&token=zZkPno3ah0&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google)](https://codecov.io/gh/google/cloud-forensics-utils/pull/427?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google)

```diff
@@           Coverage Diff           @@
##             main     #427   +/-   ##
=======================================
  Coverage        ?   59.15%           
=======================================
  Files           ?       49           
  Lines           ?     4010           
  Branches        ?        0           
=======================================
  Hits            ?     2372           
  Misses          ?     1638           
  Partials        ?        0           
```

| Flag | Coverage Δ | |
|---|---|---|
| nosetests | `59.15% <0.00%> (?)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#carryforward-flags-in-the-pull-request-comment) to find out more.


------

[Continue to review full report at Codecov](https://codecov.io/gh/google/cloud-forensics-utils/pull/427?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/google/cloud-forensics-utils/pull/427?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google). Last update [c5f4d5e...b25ac5c](https://codecov.io/gh/google/cloud-forensics-utils/pull/427?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google).


Patch:
diff --git a/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py b/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py
index 94016d2..d138df4 100644
--- a/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py
+++ b/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py
@@ -136,3 +136,31 @@ class GoogleCloudResourceManager:
     response = common.ExecuteRequest(resource_client, 'delete', request)[0]
     logger.info("Resource {0:s} was set for deletion.".format(name))
     return response
+
+  def GetIamPolicy(self, name: str) -> Dict[str, Any]:
+    """Get IAM policy bindings for a resource.
+
+    Args:
+      name (str): a resource identifier in the format
+        resource_type/resource_number e.g. projects/123456789012 where
+        project_type is one of projects, folders or organizations.
+
+    Returns:
+      Dict[str, Any]: The policy bindings for the resource.
+
+    Raises:
+      TypeError: if an invalid resource type is provided.
+    """
+    resource_type = name.split('/')[0]
+    if resource_type not in self.RESOURCE_TYPES:
+      raise TypeError('Invalid resource type "{0:s}", resource must be one of '
+          '"projects", "folders" or "organizations" provided in the format '
+          '"resource_type/resource_number".'.format(name))
+    service = self.GrmApi()
+    resource_client = getattr(service, resource_type)()
+    request = {'resource': name}
+    # Safe to unpack
+    response = common.ExecuteRequest(
+        resource_client, 'getIamPolicy', request)[0]
+
+    return response


Test patch:
diff --git a/tests/providers/gcp/gcp_mocks.py b/tests/providers/gcp/gcp_mocks.py
index 96fa871..056f016 100644
--- a/tests/providers/gcp/gcp_mocks.py
+++ b/tests/providers/gcp/gcp_mocks.py
@@ -955,3 +955,23 @@ MOCK_BIGQUERY_JOBS = {
 		"user_email": "fake-user-email@test.com"
 	}]
 }
+
+MOCK_IAM_POLICY = {
+  "version": 1,
+  "etag": "bm90X2V0YWc=",
+  "bindings": [
+    {
+      "role": "roles/cloudbuild.builds.builder",
+      "members": [
+        "serviceAccount:012345678901@cloudbuild.gserviceaccount.com"
+      ]
+    },
+    {
+      "role": "roles/owner",
+      "members": [
+        "serviceAccount:fake_sa@fake-project.iam.gserviceaccount.com",
+        "user:fakeaccount@fakedomain.com"
+      ]
+    }
+  ]
+}
diff --git a/tests/providers/gcp/internal/test_cloudresourcemanager.py b/tests/providers/gcp/internal/test_cloudresourcemanager.py
index e005b60..7c3fefe 100644
--- a/tests/providers/gcp/internal/test_cloudresourcemanager.py
+++ b/tests/providers/gcp/internal/test_cloudresourcemanager.py
@@ -99,3 +99,34 @@ class GoogleCloudResourceManagerTest(unittest.TestCase):
             'updateTime': '2020-01-01T00:00:00.000Z'
           }
         ])
+
+  @typing.no_type_check
+  @mock.patch('libcloudforensics.providers.gcp.internal.common.ExecuteRequest')
+  @mock.patch('libcloudforensics.providers.gcp.internal.cloudresourcemanager.GoogleCloudResourceManager.GrmApi')
+  def testGetIamPolicy(self, mock_grm_api, mock_execute_request):
+    """Validates the GetIamPolicy function"""
+    mock_execute_request.return_value = [gcp_mocks.MOCK_IAM_POLICY]
+    mock_resource_client = mock_grm_api.return_value.projects.return_value
+    response = gcp_mocks.FAKE_CLOUD_RESOURCE_MANAGER.GetIamPolicy(
+        'projects/000000000000')
+    mock_execute_request.assert_called_with(mock_resource_client,
+        'getIamPolicy', {'resource': 'projects/000000000000'})
+    self.assertEqual(response, {
+        "version": 1,
+        "etag": "bm90X2V0YWc=",
+        "bindings": [
+          {
+            "role": "roles/cloudbuild.builds.builder",
+            "members": [
+              "serviceAccount:012345678901@cloudbuild.gserviceaccount.com"
+            ]
+          },
+          {
+            "role": "roles/owner",
+            "members": [
+              "serviceAccount:fake_sa@fake-project.iam.gserviceaccount.com",
+              "user:fakeaccount@fakedomain.com"
+            ]
+          }
+        ]
+      })