Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/c7n_gcp/resources/resourcemanager.py: 52%
256 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
1# Copyright The Cloud Custodian Authors.
2# SPDX-License-Identifier: Apache-2.0
4import itertools
5from c7n_gcp.filters.iampolicy import IamPolicyFilter
7from c7n_gcp.actions import SetIamPolicy, MethodAction
8from c7n_gcp.provider import resources
9from c7n_gcp.query import QueryResourceManager, TypeInfo
11from c7n.resolver import ValuesFrom
12from c7n.utils import type_schema, local_session
13from c7n.filters.core import ValueFilter, ListItemFilter
14from c7n.filters.missing import Missing
16from googleapiclient.errors import HttpError
19@resources.register('organization')
20class Organization(QueryResourceManager):
21 """GCP resource: https://cloud.google.com/resource-manager/reference/rest/v1/organizations
22 """
23 class resource_type(TypeInfo):
24 service = 'cloudresourcemanager'
25 version = 'v1'
26 component = 'organizations'
27 scope = 'global'
28 enum_spec = ('search', 'organizations[]', {'body': {}})
29 id = 'name'
30 name = 'displayName'
31 default_report_fields = [
32 "name", "displayName", "creationTime", "lifecycleState"]
33 asset_type = "cloudresourcemanager.googleapis.com/Organization"
34 scc_type = "google.cloud.resourcemanager.Organization"
35 perm_service = 'resourcemanager'
36 permissions = ('resourcemanager.organizations.get',)
37 urn_component = "organization"
38 urn_id_segments = (-1,) # Just use the last segment of the id in the URN
39 urn_has_project = False
41 @staticmethod
42 def get(client, resource_info):
43 org = resource_info['resourceName'].rsplit('/', 1)[-1]
44 return client.execute_query(
45 'get', {'name': "organizations/" + org})
48@Organization.action_registry.register('set-iam-policy')
49class OrganizationSetIamPolicy(SetIamPolicy):
50 """
51 Overrides the base implementation to process Organization resources correctly.
52 """
53 def _verb_arguments(self, resource):
54 verb_arguments = SetIamPolicy._verb_arguments(self, resource)
55 verb_arguments['body'] = {}
56 return verb_arguments
59@resources.register('folder')
60class Folder(QueryResourceManager):
61 """GCP resource: https://cloud.google.com/resource-manager/reference/rest/v1/folders
62 """
63 class resource_type(TypeInfo):
64 service = 'cloudresourcemanager'
65 version = 'v2'
66 component = 'folders'
67 scope = 'global'
68 enum_spec = ('list', 'folders', None)
69 name = id = 'name'
70 default_report_fields = [
71 "name", "displayName", "lifecycleState", "createTime", "parent"]
72 asset_type = "cloudresourcemanager.googleapis.com/Folder"
73 perm_service = 'resourcemanager'
74 urn_component = "folder"
75 urn_id_segments = (-1,) # Just use the last segment of the id in the URN
76 urn_has_project = False
78 def get_resources(self, resource_ids):
79 client = self.get_client()
80 results = []
81 for rid in resource_ids:
82 if not rid.startswith('folders/'):
83 rid = 'folders/%s' % rid
84 results.append(client.execute_query('get', {'name': rid}))
85 return results
87 def get_resource_query(self):
88 if 'query' in self.data:
89 for child in self.data.get('query'):
90 if 'parent' in child:
91 return {'parent': child['parent']}
94@resources.register('project')
95class Project(QueryResourceManager):
96 """GCP resource: https://cloud.google.com/compute/docs/reference/rest/v1/projects
97 """
98 class resource_type(TypeInfo):
99 service = 'cloudresourcemanager'
100 version = 'v1'
101 component = 'projects'
102 scope = 'global'
103 enum_spec = ('list', 'projects', None)
104 name = id = 'projectId'
105 default_report_fields = [
106 "name", "lifecycleState", "createTime", "parent.id"]
107 asset_type = "cloudresourcemanager.googleapis.com/Project"
108 scc_type = "google.cloud.resourcemanager.Project"
109 perm_service = 'resourcemanager'
110 labels = True
111 labels_op = 'update'
112 urn_component = "project"
113 urn_has_project = False
115 @staticmethod
116 def get_label_params(resource, labels):
117 return {'projectId': resource['projectId'],
118 'body': {
119 'name': resource['name'],
120 'parent': resource['parent'],
121 'labels': labels}}
123 @staticmethod
124 def get(client, resource_info):
125 return client.execute_query(
126 'get', {'projectId': resource_info['resourceName'].rsplit('/', 1)[-1]})
128 def get_resource_query(self):
129 # https://cloud.google.com/resource-manager/reference/rest/v1/projects/list
130 if 'query' in self.data:
131 for child in self.data.get('query'):
132 if 'filter' in child:
133 return {'filter': child['filter']}
136Project.filter_registry.register('missing', Missing)
139@Project.filter_registry.register('iam-policy')
140class ProjectIamPolicyFilter(IamPolicyFilter):
141 """
142 Overrides the base implementation to process Project resources correctly.
143 """
144 permissions = ('resourcemanager.projects.getIamPolicy',)
146 def _verb_arguments(self, resource):
147 verb_arguments = SetIamPolicy._verb_arguments(self, resource)
148 verb_arguments['body'] = {}
149 return verb_arguments
152@Project.filter_registry.register('compute-meta')
153class ProjectComputeMetaFilter(ValueFilter):
154 """
155 Allows filtering on project-level compute metadata including common instance metadata
156 and quotas.
158 :example:
160 Find Projects that have not enabled OS Login for compute instances
162 .. code-block:: yaml
164 policies:
165 - name: project-compute-os-login-not-enabled
166 resource: gcp.project
167 filters:
168 - type: compute-meta
169 key: "commonInstanceMetadata.items[?key==`enable-oslogin`].value | [0]"
170 op: ne
171 value_type: normalize
172 value: true
174 """
176 key = 'c7n:projectComputeMeta'
177 permissions = ('compute.projects.get',)
178 schema = type_schema('compute-meta', rinherit=ValueFilter.schema)
180 def __call__(self, resource):
181 if self.key in resource:
182 return resource[self.key]
184 session = local_session(self.manager.session_factory)
185 self.client = session.client('compute', 'v1', 'projects')
187 resource[self.key] = self.client.execute_command('get', {"project": resource['projectId']})
189 return super().__call__(resource[self.key])
192@Project.action_registry.register('delete')
193class ProjectDelete(MethodAction):
194 """Delete a GCP Project
196 Note this will also schedule deletion of assets contained within
197 the project. The project will not be accessible, and assets
198 contained within the project may continue to accrue costs within
199 a 30 day period. For details see
200 https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects
202 """
203 method_spec = {'op': 'delete'}
204 attr_filter = ('lifecycleState', ('ACTIVE',))
205 schema = type_schema('delete')
207 def get_resource_params(self, model, resource):
208 return {'projectId': resource['projectId']}
211@Project.action_registry.register('set-iam-policy')
212class ProjectSetIamPolicy(SetIamPolicy):
213 """
214 Overrides the base implementation to process Project resources correctly.
215 """
216 def _verb_arguments(self, resource):
217 verb_arguments = SetIamPolicy._verb_arguments(self, resource)
218 verb_arguments['body'] = {}
219 return verb_arguments
222class HierarchyAction(MethodAction):
224 def load_hierarchy(self, resources):
225 parents = {}
226 session = local_session(self.manager.session_factory)
228 for r in resources:
229 client = self.get_client(session, self.manager.resource_type)
230 ancestors = client.execute_command(
231 'getAncestry', {'projectId': r['projectId']}).get('ancestor')
232 parents[r['projectId']] = [
233 a['resourceId']['id'] for a in ancestors
234 if a['resourceId']['type'] == 'folder']
235 self.parents = parents
236 self.folder_ids = set(itertools.chain(*self.parents.values()))
238 def load_folders(self):
239 folder_manager = self.manager.get_resource_manager('gcp.folder')
240 self.folders = {
241 f['name'].split('/', 1)[-1]: f for f in
242 folder_manager.get_resources(list(self.folder_ids))}
244 def load_metadata(self):
245 raise NotImplementedError()
247 def diff(self, resources):
248 raise NotImplementedError()
250 def process(self, resources):
251 if self.attr_filter:
252 resources = self.filter_resources(resources)
254 self.load_hierarchy(resources)
255 self.load_metadata()
256 op_set = self.diff(resources)
257 client = self.manager.get_client()
258 for op in op_set:
259 self.invoke_api(client, *op)
262@Project.action_registry.register('propagate-labels')
263class ProjectPropagateLabels(HierarchyAction):
264 """Propagate labels from the organization hierarchy to a project.
266 folder-labels should resolve to a json data mapping of folder path
267 to labels that should be applied to contained projects.
269 as a worked example assume the following resource hierarchy
271 ::
273 - /dev
274 /network
275 /project-a
276 /ml
277 /project-b
279 Given a folder-labels json with contents like
281 .. code-block:: json
283 {"dev": {"env": "dev", "owner": "dev"},
284 "dev/network": {"owner": "network"},
285 "dev/ml": {"owner": "ml"}
287 Running the following policy
289 .. code-block:: yaml
291 policies:
292 - name: tag-projects
293 resource: gcp.project
294 # use a server side filter to only look at projects
295 # under the /dev folder the id for the dev folder needs
296 # to be manually resolved outside of the policy.
297 query:
298 - filter: "parent.id:389734459211 parent.type:folder"
299 filters:
300 - "tag:owner": absent
301 actions:
302 - type: propagate-labels
303 folder-labels:
304 url: file://folder-labels.json
306 Will result in project-a being tagged with owner: network and env: dev
307 and project-b being tagged with owner: ml and env: dev
309 """
310 schema = type_schema(
311 'propagate-labels',
312 required=('folder-labels',),
313 **{
314 'folder-labels': {
315 '$ref': '#/definitions/filters_common/value_from'}},
316 )
318 attr_filter = ('lifecycleState', ('ACTIVE',))
319 permissions = ('resourcemanager.folders.get',
320 'resourcemanager.projects.update')
321 method_spec = {'op': 'update'}
323 def load_metadata(self):
324 """Load hierarchy tags"""
325 self.resolver = ValuesFrom(self.data['folder-labels'], self.manager)
326 self.labels = self.resolver.get_values()
327 self.load_folders()
328 self.resolve_paths()
330 def resolve_paths(self):
331 self.folder_paths = {}
333 def get_path_segments(fid):
334 p = self.folders[fid]['parent']
335 if p.startswith('folder'):
336 for s in get_path_segments(p.split('/')[-1]):
337 yield s
338 yield self.folders[fid]['displayName']
340 for fid in self.folder_ids:
341 self.folder_paths[fid] = '/'.join(get_path_segments(fid))
343 def resolve_labels(self, project_id):
344 hlabels = {}
345 parents = self.parents[project_id]
346 for p in reversed(parents):
347 pkeys = [p, self.folder_paths[p], 'folders/%s' % p]
348 for pk in pkeys:
349 hlabels.update(self.labels.get(pk, {}))
351 return hlabels
353 def diff(self, resources):
354 model = self.manager.resource_type
356 for r in resources:
357 hlabels = self.resolve_labels(r['projectId'])
358 if not hlabels:
359 continue
361 delta = False
362 rlabels = r.get('labels', {})
363 for k, v in hlabels.items():
364 if k not in rlabels or rlabels[k] != v:
365 delta = True
366 if not delta:
367 continue
369 rlabels = dict(rlabels)
370 rlabels.update(hlabels)
372 if delta:
373 yield ('update', model.get_label_params(r, rlabels))
376@Organization.filter_registry.register('essential-contacts')
377class OrgContactsFilter(ListItemFilter):
378 """Filter Resources based on essential contacts configuration
380 .. code-block:: yaml
382 - name: org-essential-contacts
383 resource: gcp.organization
384 filters:
385 - type: essential-contacts
386 count: 2
387 count_op: gte
388 attrs:
389 - validationState: VALID
390 - type: value
391 key: notificationCategorySubscriptions
392 value: TECHNICAL
393 op: contains
394 """
395 schema = type_schema(
396 'essential-contacts',
397 attrs={'$ref': '#/definitions/filters_common/list_item_attrs'},
398 count={'type': 'number'},
399 count_op={'$ref': '#/definitions/filters_common/comparison_operators'}
400 )
402 annotate_items = True
403 permissions = ("essentialcontacts.contacts.list",)
405 def get_item_values(self, resource):
406 session = local_session(self.manager.session_factory)
407 client = session.client("essentialcontacts", "v1", "organizations.contacts")
408 pages = client.execute_paged_query('list', {'parent': resource['name'], 'pageSize': 100})
409 contacts = []
410 for page in pages:
411 contacts.extend(page.get('contacts', []))
412 return contacts
414@Organization.filter_registry.register('org-policy')
415class OrgPoliciesFilter(ListItemFilter):
416 """Filter Resources based on orgpolicy configuration
418 .. code-block:: yaml
420 - name: org-policy
421 resource: gcp.organization
422 filters:
423 - type: org-policy
424 attrs:
425 - type: value
426 key: constraint
427 value: constraints/iam.allowedPolicyMemberDomains
428 op: contains
429 """
430 schema = type_schema(
431 'org-policy',
432 attrs={'$ref': '#/definitions/filters_common/list_item_attrs'}
433 )
435 annotate_items = True
436 permissions = ("orgpolicy.policy.get",)
438 def get_item_values(self, resource):
439 session = local_session(self.manager.session_factory)
440 client = session.client("cloudresourcemanager", "v1", "organizations")
441 pages = client.execute_paged_query('listOrgPolicies', { 'resource': resource['name'] })
442 policies = []
443 for page in pages:
444 policies.extend(page.get('policies', []))
445 return policies
448@Project.filter_registry.register('access-approval')
449class AccessApprovalFilter(ValueFilter):
450 """Filter Resources based on access approval configuration
452 .. code-block:: yaml
454 - name: project-access-approval
455 resource: gcp.project
456 filters:
457 - type: access-approval
458 key: enrolledServices.cloudProduct
459 value: "all"
460 """
461 schema = type_schema('access-approval', rinherit=ValueFilter.schema)
462 permissions = ('accessapproval.settings.get',)
464 def process(self, resources, event=None):
465 return [r for r in resources
466 if self.match(self.get_access_approval(r))]
468 def get_access_approval(self, resource):
469 session = local_session(self.manager.session_factory)
470 client = session.client("accessapproval", "v1", "projects")
471 project = resource['projectId']
473 try:
474 access_approval = client.execute_command(
475 'getAccessApprovalSettings',
476 {'name': f"projects/{project}/accessApprovalSettings"},)
477 except HttpError as ex:
478 if (ex.status_code == 400
479 and ex.reason == "Precondition check failed.") \
480 or (ex.status_code == 404):
481 # For above exceptions, it implies that access approval is
482 # not enabled, so we return an empty setting.
483 access_approval = {}
484 else:
485 raise ex
487 return access_approval
490@Organization.filter_registry.register('iam-policy')
491class OrganizationIamPolicyFilter(IamPolicyFilter):
492 """
493 Overrides the base implementation to process Organization resources correctly.
494 """
495 permissions = ('resourcemanager.organizations.getIamPolicy',)
497 def _verb_arguments(self, resource):
498 verb_arguments = SetIamPolicy._verb_arguments(self, resource)
499 verb_arguments['body'] = {}
500 return verb_arguments
503@Folder.filter_registry.register('iam-policy')
504class FolderIamPolicyFilter(IamPolicyFilter):
505 """
506 Overrides the base implementation to process Folder resources correctly.
507 """
508 permissions = ('resourcemanager.folders.getIamPolicy',)
510 def _verb_arguments(self, resource):
511 verb_arguments = SetIamPolicy._verb_arguments(self, resource)
512 verb_arguments['body'] = {}
513 return verb_arguments