Skip to content

Commit f01cb67

Browse files
Fryyyyyramo-j
andauthored
CloudResourceManager OrgPolicy (#523)
* Bump version * OrgPolicy inside CRM * Bump version again * Show warning * Satisfy type checking * Satisfy linter * More linty typy * Update libcloudforensics/providers/gcp/internal/cloudresourcemanager.py Co-authored-by: Ramo <ramo_j@protonmail.com> --------- Co-authored-by: Ramo <ramo_j@protonmail.com>
1 parent 8b9ebbd commit f01cb67

5 files changed

Lines changed: 234 additions & 3 deletions

File tree

libcloudforensics/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@
1616

1717
# Since moving to poetry, ensure the version number tracked in pyproject.toml is
1818
# also updated
19-
__version__ = '20241205'
19+
__version__ = '20241207'

libcloudforensics/providers/gcp/internal/cloudresourcemanager.py

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
"""Google Cloud Resource Manager functionality."""
16-
from typing import TYPE_CHECKING, Dict, List, Any
16+
from typing import TYPE_CHECKING, Dict, List, Any, Optional
1717
from googleapiclient import errors as google_api_errors
1818

1919
from libcloudforensics import logging_utils
@@ -164,3 +164,149 @@ def GetIamPolicy(self, name: str) -> Dict[str, Any]:
164164
resource_client, 'getIamPolicy', request)[0]
165165

166166
return response
167+
168+
def GetOrgPolicy(self, resource: str, constraint: str) -> Dict[str, Any]:
169+
"""Gets a particular Org Policy on a resource.
170+
171+
Args:
172+
resource (str): a resource identifier in the format
173+
resource_type/resource_number e.g. projects/123456789012 where
174+
project_type is one of projects, folders or organizations.
175+
constraint (str): the name of the constraint to get.
176+
177+
Returns:
178+
Dict[str, Any]: The Org Policy details.
179+
See https://cloud.google.com/resource-manager/reference/rest/v1/Policy
180+
181+
Raises:
182+
TypeError: if an invalid resource type is provided.
183+
"""
184+
resource_type = resource.split('/')[0]
185+
if resource_type not in self.RESOURCE_TYPES:
186+
raise TypeError('Invalid resource type "{0:s}", resource must be one of '
187+
'"projects", "folders" or "organizations" provided in the format '
188+
'"resource_type/resource_number".'.format(resource))
189+
190+
if not constraint.startswith('constraints/'):
191+
constraint = 'constraints/' + constraint
192+
193+
# Override API version, since this doesn't exist in v2 or v3
194+
self.RESOURCE_MANAGER_API_VERSION = 'v1' # pylint: disable=invalid-name
195+
service = self.GrmApi()
196+
resource_client = getattr(service, resource_type)()
197+
response: Dict[str, Any] = resource_client.getOrgPolicy(
198+
resource=resource, body={'constraint': constraint}
199+
).execute()
200+
return response
201+
202+
def ListOrgPolicy(self, resource: str) -> Dict[str, Any]:
203+
"""Lists all Org Policies on a resource.
204+
205+
Args:
206+
resource (str): a resource identifier in the format
207+
resource_type/resource_number e.g. projects/123456789012 where
208+
project_type is one of projects, folders or organizations.
209+
210+
Returns:
211+
Dict[str, Any]: The Org Policy details.
212+
See https://cloud.google.com/resource-manager/reference/rest/v1/Policy
213+
214+
Raises:
215+
TypeError: if an invalid resource type is provided.
216+
"""
217+
resource_type = resource.split('/')[0]
218+
if resource_type not in self.RESOURCE_TYPES:
219+
raise TypeError('Invalid resource type "{0:s}", resource must be one of '
220+
'"projects", "folders" or "organizations" provided in the format '
221+
'"resource_type/resource_number".'.format(resource))
222+
223+
# Override API version, since this doesn't exist in v2 or v3
224+
self.RESOURCE_MANAGER_API_VERSION = 'v1'
225+
service = self.GrmApi()
226+
resource_client = getattr(service, resource_type)()
227+
response: Dict[str, Any] = resource_client.listOrgPolicies(
228+
resource=resource).execute()
229+
return response
230+
231+
def SetOrgPolicy(
232+
self, resource: str, policy: Dict[str, Any],
233+
etag: Optional[str] = None) -> Dict[str, Any]:
234+
"""Updates the specified Policy on the resource.
235+
Creates a new Policy for that Constraint on the resource if one does
236+
not exist.
237+
238+
239+
Args:
240+
resource (str): a resource identifier in the format
241+
resource_type/resource_number e.g. projects/123456789012 where
242+
project_type is one of projects, folders or organizations.
243+
policy (dict): The policy to create, as per
244+
https://cloud.google.com/resource-manager/reference/rest/v1/Policy
245+
etag (str): The current version, for concurrency control.
246+
Not supplying an etag on the request Policy results in an unconditional
247+
write of the Policy.
248+
249+
Returns:
250+
Dict[str, Any]: The Org Policy that was created.
251+
https://cloud.google.com/resource-manager/reference/rest/v1/Policy
252+
253+
Raises:
254+
TypeError: if an invalid resource type is provided.
255+
"""
256+
resource_type = resource.split('/')[0]
257+
if resource_type not in self.RESOURCE_TYPES:
258+
raise TypeError('Invalid resource type "{0:s}", resource must be one of '
259+
'"projects", "folders" or "organizations" provided in the format '
260+
'"resource_type/resource_number".'.format(resource))
261+
262+
# Override API version, since this doesn't exist in v2 or v3
263+
self.RESOURCE_MANAGER_API_VERSION = 'v1'
264+
service = self.GrmApi()
265+
resource_client = getattr(service, resource_type)()
266+
body = {'policy': policy}
267+
if etag:
268+
body['policy']['etag'] = etag
269+
response: Dict[str, Any] = resource_client.setOrgPolicy(resource=resource,
270+
body=body).execute()
271+
return response
272+
273+
def DeleteOrgPolicy(
274+
self, resource: str, constraint: str, etag: Optional[str] = None) -> bool:
275+
"""Removes a particular Org Policy on a resource.
276+
277+
Args:
278+
resource (str): a resource identifier in the format
279+
resource_type/resource_number e.g. projects/123456789012 where
280+
project_type is one of projects, folders or organizations.
281+
constraint (str): the name of the constraint to get.
282+
etag (str): The current version, for concurrency control.
283+
Not sending an etag will cause the Policy to be cleared blindly.
284+
285+
Returns:
286+
bool: True if successful, False otherwise.
287+
288+
Raises:
289+
TypeError: if an invalid resource type is provided.
290+
"""
291+
resource_type = resource.split('/')[0]
292+
if resource_type not in self.RESOURCE_TYPES:
293+
raise TypeError('Invalid resource type "{0:s}", resource must be one of '
294+
'"projects", "folders" or "organizations" provided in the format '
295+
'"resource_type/resource_number".'.format(resource))
296+
297+
if not constraint.startswith('constraints/'):
298+
constraint = 'constraints/' + constraint
299+
300+
# Override API version, since this doesn't exist in v2 or v3
301+
self.RESOURCE_MANAGER_API_VERSION = 'v1'
302+
service = self.GrmApi()
303+
resource_client = getattr(service, resource_type)()
304+
body = {'constraint': constraint}
305+
if etag:
306+
body['etag'] = etag
307+
response: Dict[str, Any] = resource_client.clearOrgPolicy(
308+
resource=resource, body=body).execute()
309+
if not response:
310+
return True
311+
logger.warning("Unable to delete Org Policy: {0}".format(response))
312+
return False

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "libcloudforensics"
3-
version = "20241205"
3+
version = "20241207"
44
description = "libcloudforensics is a set of tools to help acquire forensic evidence from Cloud platforms."
55
authors = ["cloud-forensics-utils development team <cloud-forensics-utils-dev@googlegroups.com>"]
66
license = "Apache-2.0"

tests/providers/gcp/gcp_mocks.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,3 +1090,15 @@
10901090
}
10911091
]
10921092
}
1093+
1094+
MOCK_ORG_POLICY = {
1095+
'constraint': 'constraints/testpolicy',
1096+
'etag': 'abcdefghijk='
1097+
}
1098+
1099+
MOCK_ORG_POLICIES = {
1100+
'policies': [
1101+
{'constraint': 'constraints/compute.requireShieldedVm', 'etag': 'abcdefghijk', 'updateTime': '2024-12-02T03:38:34.276794Z', 'booleanPolicy': {}},
1102+
{'constraint': 'constraints/compute.storageResourceUseRestrictions', 'etag': 'abcdefghijk', 'updateTime': '2024-12-06T02:01:04.737315Z', 'listPolicy': {'allValues': 'ALLOW'}},
1103+
]
1104+
}

tests/providers/gcp/internal/test_cloudresourcemanager.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,76 @@ def testGetIamPolicy(self, mock_grm_api, mock_execute_request):
130130
}
131131
]
132132
})
133+
134+
@typing.no_type_check
135+
@mock.patch('libcloudforensics.providers.gcp.internal.cloudresourcemanager.GoogleCloudResourceManager.GrmApi')
136+
def testGetOrgPolicy(self, mock_grm_api):
137+
"""Validates the GetOrgPolicy function"""
138+
api_get_org_policy = mock_grm_api.return_value.projects.return_value.getOrgPolicy
139+
api_get_org_policy.return_value.execute.return_value = gcp_mocks.MOCK_ORG_POLICY
140+
response = gcp_mocks.FAKE_CLOUD_RESOURCE_MANAGER.GetOrgPolicy(
141+
'projects/000000000000', 'fake-policy')
142+
api_get_org_policy.assert_called_with(
143+
resource='projects/000000000000',
144+
body={'constraint': 'constraints/fake-policy'})
145+
self.assertEqual(response, {
146+
'constraint': 'constraints/testpolicy',
147+
'etag': 'abcdefghijk='
148+
})
149+
150+
151+
@typing.no_type_check
152+
@mock.patch('libcloudforensics.providers.gcp.internal.cloudresourcemanager.GoogleCloudResourceManager.GrmApi')
153+
def testListOrgPolicy(self, mock_grm_api):
154+
"""Validates the ListOrgPolicy function"""
155+
api_list_org_policy = mock_grm_api.return_value.projects.return_value.listOrgPolicies
156+
api_list_org_policy.return_value.execute.return_value = gcp_mocks.MOCK_ORG_POLICIES
157+
response = gcp_mocks.FAKE_CLOUD_RESOURCE_MANAGER.ListOrgPolicy(
158+
'projects/000000000000'
159+
)
160+
api_list_org_policy.assert_called_with(
161+
resource='projects/000000000000')
162+
self.assertEqual(len(response.get('policies', [])), 2)
163+
164+
@typing.no_type_check
165+
@mock.patch('libcloudforensics.providers.gcp.internal.cloudresourcemanager.GoogleCloudResourceManager.GrmApi')
166+
def testSetOrgPolicy(self, mock_grm_api):
167+
"""Validates the SetOrgPolicy function"""
168+
api_set_org_policy = mock_grm_api.return_value.projects.return_value.setOrgPolicy
169+
api_set_org_policy.return_value.execute.return_value = gcp_mocks.MOCK_ORG_POLICY
170+
gcp_mocks.FAKE_CLOUD_RESOURCE_MANAGER.SetOrgPolicy(
171+
'projects/000000000000',
172+
{
173+
'constraint': 'constraints/compute.storageResourceUseRestrictions',
174+
'listPolicy': {
175+
'inheritFromParent': False, 'allValues': 'ALLOW'
176+
}
177+
},
178+
'abc123')
179+
api_set_org_policy.assert_called_with(
180+
resource='projects/000000000000',
181+
body={
182+
'policy': {
183+
'constraint': 'constraints/compute.storageResourceUseRestrictions',
184+
'listPolicy': {
185+
'inheritFromParent': False, 'allValues': 'ALLOW'
186+
},
187+
'etag': 'abc123'
188+
}
189+
})
190+
191+
@typing.no_type_check
192+
@mock.patch('libcloudforensics.providers.gcp.internal.cloudresourcemanager.GoogleCloudResourceManager.GrmApi')
193+
def testDeleteOrgPolicy(self, mock_grm_api):
194+
"""Validates the DeleteOrgPolicy function"""
195+
api_delete_org_policy = mock_grm_api.return_value.projects.return_value.clearOrgPolicy
196+
api_delete_org_policy.return_value.execute.return_value = True
197+
gcp_mocks.FAKE_CLOUD_RESOURCE_MANAGER.DeleteOrgPolicy(
198+
'projects/000000000000',
199+
'fake-policy',
200+
'abc123'
201+
)
202+
api_delete_org_policy.assert_called_with(
203+
resource='projects/000000000000',
204+
body={'constraint': 'constraints/fake-policy', 'etag': 'abc123'}
205+
)

0 commit comments

Comments
 (0)