# -*- coding: utf-8 -*- # # Copyright 2017 Google LLC. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Utilities necessary to augment images statuses with org policy. AugmentImagesStatus function in this module call OrgPolicy and augment images status if the policy requires it. """ from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import copy from apitools.base.py import exceptions as apitools_exceptions from googlecloudsdk.api_lib.resource_manager import org_policies from googlecloudsdk.core import log from googlecloudsdk.core import resources def AugmentImagesStatus(resource_registry, project_id, images): """Sets images status to 'BLOCKED_BY_POLICY' as specified by OrgPolicy. Get OrgPolicy for the project and set images status to BLOCKED_BY_POLICY if the policy exists and blocks the image. If no policy exists, all images are allowed. NOTE: This function sends requests to OrgPolicy API. Args: resource_registry: resources.Registry, Resource registry project_id: str, Project in which image will be used images: Iterable[Dict], The images (in dict form) to set the status on Yields: Images (in dict form) with status set to BLOCKED_BY_POLICY as specified by OrgPolicy. Raises: exceptions.GetPolicyError if OrgPolicy call failed or returned malformed data. """ # Iterate through generator and convert to list up-front: we'll need to copy # it # Copy all images since we'll be mutating them images = copy.deepcopy(list(images)) # If the OrgPolicy call fails, it populates errors_collected rather than # raising an exception. Checking the OrgPolicy client-side is non-critical, # so we don't want a failure here to block output. Instead, we yield all # images to the user and then raise exception (if errors are present). # This way we both return data and errors. errors_collected = [] policy = _GetPolicyNoThrow(project_id, errors_collected) if policy is not None: # Else GetEffectiveOrgPolicy call failed. We don't report any image as # BLOCKED_BY_POLICY in such case. for image in images: if image['status'] != 'READY': yield image elif _IsAllowed(resource_registry, resource_registry.Parse(image['selfLink']).project, policy, errors_collected): yield image else: image['status'] = 'BLOCKED_BY_POLICY' yield image else: for image in images: yield image # At this point all images were returned, but we may have some errors # collected which should be forwarded to the user as well. OrgPolicy call is # considered non-critical for the command. If API call failed or returned # malformed data, list command will continue to work (without information from # OrgPolicy). Error will be visible only if --verbosity=info or greater for error in errors_collected or []: log.info(error) def _GetPolicy(project_id): """Get effective org policy of given project.""" messages = org_policies.OrgPoliciesMessages() request = messages.CloudresourcemanagerProjectsGetEffectiveOrgPolicyRequest( projectsId=project_id, getEffectiveOrgPolicyRequest=messages.GetEffectiveOrgPolicyRequest( constraint=org_policies.FormatConstraint( 'compute.trustedImageProjects'))) client = org_policies.OrgPoliciesClient() response = client.projects.GetEffectiveOrgPolicy(request) # There are several possible policy types; the only policy type that applies # to 'compute.trustedImageProjects' is listPolicy, so we can assume that's # what the caller is interested in. return response.listPolicy def _GetPolicyNoThrow(project_id, errors_to_propagate): """Call GetPolicy and handle possible errors from backend.""" try: return _GetPolicy(project_id) except apitools_exceptions.HttpError as e: # We were unable to get OrgPolicy. This is an attempt to degrade gcloud # functionality gracefully. We turn stack unwind into object and store it # in output parameter for later processing (displaying). errors_to_propagate.append(e) return None def _IsAllowed(resource_registry, project_id, policy, errors_to_propagate): """Decides if project is allowed within policy.""" # Computational complexity is O(len(allowed_values) + len(denied_values)) if policy.allValues is policy.AllValuesValueValuesEnum.ALLOW: return True elif policy.allValues is policy.AllValuesValueValuesEnum.DENY: return False is_allowed = False # policy.allowedValues is a repeated field, it is never None if not policy.allowedValues: is_allowed = True try: for project_record in policy.allowedValues: resource_registry.ParseRelativeName(project_record, 'compute.projects') except resources.InvalidResourceException as e: # If policy is malformed we want to consider all projects as allowed # (graceful degradation). Error will be reported to the user separately errors_to_propagate.append(e) is_allowed = True else: if (resource_registry.Parse( project_id, collection='compute.projects').RelativeName() in policy.allowedValues): is_allowed = True is_denied = False try: for project_record in policy.deniedValues: resource_registry.ParseRelativeName(project_record, 'compute.projects') except resources.InvalidResourceException as e: is_denied = False errors_to_propagate.append(e) else: if (resource_registry.Parse( project_id, collection='compute.projects').RelativeName() in policy.deniedValues): is_denied = True return is_allowed and not is_denied