166 lines
6.1 KiB
Python
166 lines
6.1 KiB
Python
# -*- 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
|