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

1# Copyright The Cloud Custodian Authors. 

2# SPDX-License-Identifier: Apache-2.0 

3 

4import itertools 

5from c7n_gcp.filters.iampolicy import IamPolicyFilter 

6 

7from c7n_gcp.actions import SetIamPolicy, MethodAction 

8from c7n_gcp.provider import resources 

9from c7n_gcp.query import QueryResourceManager, TypeInfo 

10 

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 

15 

16from googleapiclient.errors import HttpError 

17 

18 

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 

40 

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}) 

46 

47 

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 

57 

58 

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 

77 

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 

86 

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']} 

92 

93 

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 

114 

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}} 

122 

123 @staticmethod 

124 def get(client, resource_info): 

125 return client.execute_query( 

126 'get', {'projectId': resource_info['resourceName'].rsplit('/', 1)[-1]}) 

127 

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']} 

134 

135 

136Project.filter_registry.register('missing', Missing) 

137 

138 

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',) 

145 

146 def _verb_arguments(self, resource): 

147 verb_arguments = SetIamPolicy._verb_arguments(self, resource) 

148 verb_arguments['body'] = {} 

149 return verb_arguments 

150 

151 

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. 

157 

158 :example: 

159 

160 Find Projects that have not enabled OS Login for compute instances 

161 

162 .. code-block:: yaml 

163 

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 

173 

174 """ 

175 

176 key = 'c7n:projectComputeMeta' 

177 permissions = ('compute.projects.get',) 

178 schema = type_schema('compute-meta', rinherit=ValueFilter.schema) 

179 

180 def __call__(self, resource): 

181 if self.key in resource: 

182 return resource[self.key] 

183 

184 session = local_session(self.manager.session_factory) 

185 self.client = session.client('compute', 'v1', 'projects') 

186 

187 resource[self.key] = self.client.execute_command('get', {"project": resource['projectId']}) 

188 

189 return super().__call__(resource[self.key]) 

190 

191 

192@Project.action_registry.register('delete') 

193class ProjectDelete(MethodAction): 

194 """Delete a GCP Project 

195 

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 

201 

202 """ 

203 method_spec = {'op': 'delete'} 

204 attr_filter = ('lifecycleState', ('ACTIVE',)) 

205 schema = type_schema('delete') 

206 

207 def get_resource_params(self, model, resource): 

208 return {'projectId': resource['projectId']} 

209 

210 

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 

220 

221 

222class HierarchyAction(MethodAction): 

223 

224 def load_hierarchy(self, resources): 

225 parents = {} 

226 session = local_session(self.manager.session_factory) 

227 

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())) 

237 

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))} 

243 

244 def load_metadata(self): 

245 raise NotImplementedError() 

246 

247 def diff(self, resources): 

248 raise NotImplementedError() 

249 

250 def process(self, resources): 

251 if self.attr_filter: 

252 resources = self.filter_resources(resources) 

253 

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) 

260 

261 

262@Project.action_registry.register('propagate-labels') 

263class ProjectPropagateLabels(HierarchyAction): 

264 """Propagate labels from the organization hierarchy to a project. 

265 

266 folder-labels should resolve to a json data mapping of folder path 

267 to labels that should be applied to contained projects. 

268 

269 as a worked example assume the following resource hierarchy 

270 

271 :: 

272 

273 - /dev 

274 /network 

275 /project-a 

276 /ml 

277 /project-b 

278 

279 Given a folder-labels json with contents like 

280 

281 .. code-block:: json 

282 

283 {"dev": {"env": "dev", "owner": "dev"}, 

284 "dev/network": {"owner": "network"}, 

285 "dev/ml": {"owner": "ml"} 

286 

287 Running the following policy 

288 

289 .. code-block:: yaml 

290 

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 

305 

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 

308 

309 """ 

310 schema = type_schema( 

311 'propagate-labels', 

312 required=('folder-labels',), 

313 **{ 

314 'folder-labels': { 

315 '$ref': '#/definitions/filters_common/value_from'}}, 

316 ) 

317 

318 attr_filter = ('lifecycleState', ('ACTIVE',)) 

319 permissions = ('resourcemanager.folders.get', 

320 'resourcemanager.projects.update') 

321 method_spec = {'op': 'update'} 

322 

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() 

329 

330 def resolve_paths(self): 

331 self.folder_paths = {} 

332 

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'] 

339 

340 for fid in self.folder_ids: 

341 self.folder_paths[fid] = '/'.join(get_path_segments(fid)) 

342 

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, {})) 

350 

351 return hlabels 

352 

353 def diff(self, resources): 

354 model = self.manager.resource_type 

355 

356 for r in resources: 

357 hlabels = self.resolve_labels(r['projectId']) 

358 if not hlabels: 

359 continue 

360 

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 

368 

369 rlabels = dict(rlabels) 

370 rlabels.update(hlabels) 

371 

372 if delta: 

373 yield ('update', model.get_label_params(r, rlabels)) 

374 

375 

376@Organization.filter_registry.register('essential-contacts') 

377class OrgContactsFilter(ListItemFilter): 

378 """Filter Resources based on essential contacts configuration 

379 

380 .. code-block:: yaml 

381 

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 ) 

401 

402 annotate_items = True 

403 permissions = ("essentialcontacts.contacts.list",) 

404 

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 

413 

414@Organization.filter_registry.register('org-policy') 

415class OrgPoliciesFilter(ListItemFilter): 

416 """Filter Resources based on orgpolicy configuration 

417 

418 .. code-block:: yaml 

419 

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 ) 

434 

435 annotate_items = True 

436 permissions = ("orgpolicy.policy.get",) 

437 

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 

446 

447 

448@Project.filter_registry.register('access-approval') 

449class AccessApprovalFilter(ValueFilter): 

450 """Filter Resources based on access approval configuration 

451 

452 .. code-block:: yaml 

453 

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',) 

463 

464 def process(self, resources, event=None): 

465 return [r for r in resources 

466 if self.match(self.get_access_approval(r))] 

467 

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'] 

472 

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 

486 

487 return access_approval 

488 

489 

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',) 

496 

497 def _verb_arguments(self, resource): 

498 verb_arguments = SetIamPolicy._verb_arguments(self, resource) 

499 verb_arguments['body'] = {} 

500 return verb_arguments 

501 

502 

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',) 

509 

510 def _verb_arguments(self, resource): 

511 verb_arguments = SetIamPolicy._verb_arguments(self, resource) 

512 verb_arguments['body'] = {} 

513 return verb_arguments