# -*- coding: utf-8 -*- # # Copyright 2018 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. """API library for VPC Service Controls Service Perimeters.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals from apitools.base.py import list_pager from googlecloudsdk.api_lib.accesscontextmanager import util from googlecloudsdk.api_lib.util import waiter from googlecloudsdk.core import log from googlecloudsdk.core import resources as core_resources import six def _SetIfNotNone(field_name, field_value, obj, update_mask): """Sets specified field to the provided value and adds it to update mask. Args: field_name: The name of the field to set the value of. field_value: The value to set the field to. If it is None, the field will NOT be set. obj: The object on which the value is to be set. update_mask: The update mask to add this field to. Returns: True if the field was set and False otherwise. """ if field_value is not None: setattr(obj, field_name, field_value) update_mask.append(field_name) return True return False def _CreateServicePerimeterConfig(messages, mask_prefix, resources, restricted_services, levels, vpc_allowed_services, enable_vpc_accessible_services, vpc_yaml_flag_used, vpc_accessible_services_config=None, ingress_policies=None, egress_policies=None): """Returns a ServicePerimeterConfig and its update mask.""" config = messages.ServicePerimeterConfig() mask = [] _SetIfNotNone('resources', resources, config, mask) _SetIfNotNone('restrictedServices', restricted_services, config, mask) _SetIfNotNone('ingressPolicies', ingress_policies, config, mask) _SetIfNotNone('egressPolicies', egress_policies, config, mask) if levels is not None: mask.append('accessLevels') level_names = [] for l in levels: # If the caller supplies the levels as strings already, use them directly. if isinstance(l, six.string_types): level_names.append(l) else: # Otherwise, the caller needs to supply resource objects for Access # Levels, and we extract the level name from those. level_names.append(l.RelativeName()) config.accessLevels = level_names if vpc_yaml_flag_used: mask.append('vpcAccessibleServices') config.vpcAccessibleServices = vpc_accessible_services_config elif ( enable_vpc_accessible_services is not None or vpc_allowed_services is not None ): service_filter = messages.VpcAccessibleServices() service_filter_mask = [] _SetIfNotNone('allowedServices', vpc_allowed_services, service_filter, service_filter_mask) _SetIfNotNone('enableRestriction', enable_vpc_accessible_services, service_filter, service_filter_mask) config.vpcAccessibleServices = service_filter mask.extend(['vpcAccessibleServices.' + m for m in service_filter_mask]) if not mask: return None, [] return config, ['{}.{}'.format(mask_prefix, item) for item in mask] class Client(object): """High-level API client for VPC Service Controls Service Perimeters.""" def __init__(self, client=None, messages=None, version='v1'): self.client = client or util.GetClient(version=version) self.messages = messages or self.client.MESSAGES_MODULE def Get(self, zone_ref): return self.client.accessPolicies_servicePerimeters.Get( self.messages .AccesscontextmanagerAccessPoliciesServicePerimetersGetRequest( name=zone_ref.RelativeName())) def List(self, policy_ref, limit=None): req = self.messages.AccesscontextmanagerAccessPoliciesServicePerimetersListRequest( parent=policy_ref.RelativeName()) return list_pager.YieldFromList( self.client.accessPolicies_servicePerimeters, req, limit=limit, batch_size_attribute='pageSize', batch_size=None, field='servicePerimeters') def Commit(self, policy_ref, etag): commit_req = self.messages.CommitServicePerimetersRequest(etag=etag) req = self.messages.AccesscontextmanagerAccessPoliciesServicePerimetersCommitRequest( parent=policy_ref.RelativeName(), commitServicePerimetersRequest=commit_req) operation = self.client.accessPolicies_servicePerimeters.Commit(req) poller = waiter.CloudOperationPollerNoResources(self.client.operations) operation_ref = core_resources.REGISTRY.Parse( operation.name, collection='accesscontextmanager.operations') return waiter.WaitFor( poller, operation_ref, 'Waiting for COMMIT operation [{}]'.format(operation_ref.Name())) def _ApplyPatch(self, perimeter_ref, perimeter, update_mask): """Applies a PATCH to the provided Service Perimeter.""" m = self.messages update_mask = sorted(update_mask) # For ease-of-testing request_type = ( m.AccesscontextmanagerAccessPoliciesServicePerimetersPatchRequest) request = request_type( servicePerimeter=perimeter, name=perimeter_ref.RelativeName(), updateMask=','.join(update_mask), ) operation = self.client.accessPolicies_servicePerimeters.Patch(request) poller = util.OperationPoller(self.client.accessPolicies_servicePerimeters, self.client.operations, perimeter_ref) operation_ref = core_resources.REGISTRY.Parse( operation.name, collection='accesscontextmanager.operations') return waiter.WaitFor( poller, operation_ref, 'Waiting for PATCH operation [{}]'.format(operation_ref.Name())) def Patch( self, perimeter_ref, description=None, title=None, perimeter_type=None, resources=None, restricted_services=None, levels=None, vpc_allowed_services=None, enable_vpc_accessible_services=None, vpc_yaml_flag_used=False, vpc_accessible_services_config=None, ingress_policies=None, egress_policies=None, etag=None, ): """Patch a service perimeter. Args: perimeter_ref: resources.Resource, reference to the perimeter to patch description: str, description of the zone or None if not updating title: str, title of the zone or None if not updating perimeter_type: PerimeterTypeValueValuesEnum type enum value for the level or None if not updating resources: list of str, the names of resources (for now, just 'projects/...') in the zone or None if not updating. restricted_services: list of str, the names of services ('example.googleapis.com') that *are* restricted by the access zone or None if not updating. levels: list of Resource, the access levels (in the same policy) that must be satisfied for calls into this zone or None if not updating. vpc_allowed_services: list of str, the names of services ('example.googleapis.com') that *are* allowed to be made within the access zone, or None if not updating. enable_vpc_accessible_services: bool, whether to restrict the set of APIs callable within the access zone, or None if not updating. vpc_yaml_flag_used: bool, whether the vpc yaml flag was used. vpc_accessible_services_config: VpcAccessibleServices, or None if not updating. ingress_policies: list of IngressPolicy, or None if not updating. egress_policies: list of EgressPolicy, or None if not updating. etag: str, the optional etag for the version of the Perimeter that this operation is to be performed on. Returns: ServicePerimeter, the updated Service Perimeter. """ m = self.messages perimeter = m.ServicePerimeter() update_mask = [] _SetIfNotNone('title', title, perimeter, update_mask) _SetIfNotNone('description', description, perimeter, update_mask) _SetIfNotNone('perimeterType', perimeter_type, perimeter, update_mask) _SetIfNotNone('etag', etag, perimeter, update_mask) config, config_mask_additions = _CreateServicePerimeterConfig( messages=m, mask_prefix='status', resources=resources, restricted_services=restricted_services, levels=levels, vpc_allowed_services=vpc_allowed_services, enable_vpc_accessible_services=enable_vpc_accessible_services, vpc_yaml_flag_used=vpc_yaml_flag_used, vpc_accessible_services_config=vpc_accessible_services_config, ingress_policies=ingress_policies, egress_policies=egress_policies) perimeter.status = config update_mask.extend(config_mask_additions) # No update mask implies no fields were actually edited, so this is a no-op. if not update_mask: log.warning( 'The update specified results in an identical resource. Skipping request.' ) return perimeter return self._ApplyPatch(perimeter_ref, perimeter, update_mask) def PatchDryRunConfig( self, perimeter_ref, description=None, title=None, perimeter_type=None, resources=None, restricted_services=None, levels=None, vpc_allowed_services=None, enable_vpc_accessible_services=None, vpc_yaml_flag_used=False, vpc_accessible_services_config=None, ingress_policies=None, egress_policies=None, etag=None, ): """Patch the dry-run config (spec) for a Service Perimeter. Args: perimeter_ref: resources.Resource, reference to the perimeter to patch description: str, description of the zone or None if not updating title: str, title of the zone or None if not updating perimeter_type: PerimeterTypeValueValuesEnum type enum value for the level or None if not updating resources: list of str, the names of resources (for now, just 'projects/...') in the zone or None if not updating. restricted_services: list of str, the names of services ('example.googleapis.com') that *are* restricted by the access zone or None if not updating. levels: list of Resource, the access levels (in the same policy) that must be satisfied for calls into this zone or None if not updating. vpc_allowed_services: list of str, the names of services ('example.googleapis.com') that *are* allowed to be made within the access zone, or None if not updating. enable_vpc_accessible_services: bool, whether to restrict the set of APIs callable within the access zone, or None if not updating. vpc_yaml_flag_used: bool, whether the vpc yaml flag was used. vpc_accessible_services_config: VpcAccessibleServices, or None if not updating. ingress_policies: list of IngressPolicy, or None if not updating. egress_policies: list of EgressPolicy, or None if not updating. etag: str, the optional etag for the version of the Perimeter that this operation is to be performed on. Returns: ServicePerimeter, the updated Service Perimeter. """ m = self.messages perimeter = m.ServicePerimeter() update_mask = [] if _SetIfNotNone('title', title, perimeter, update_mask): perimeter.name = perimeter_ref.RelativeName() # Necessary for upsert. update_mask.append('name') _SetIfNotNone('description', description, perimeter, update_mask) _SetIfNotNone('perimeterType', perimeter_type, perimeter, update_mask) _SetIfNotNone('etag', etag, perimeter, update_mask) config, config_mask_additions = _CreateServicePerimeterConfig( messages=m, mask_prefix='spec', resources=resources, restricted_services=restricted_services, levels=levels, vpc_allowed_services=vpc_allowed_services, enable_vpc_accessible_services=enable_vpc_accessible_services, vpc_yaml_flag_used=vpc_yaml_flag_used, vpc_accessible_services_config=vpc_accessible_services_config, ingress_policies=ingress_policies, egress_policies=egress_policies) perimeter.spec = config update_mask.extend(config_mask_additions) perimeter.useExplicitDryRunSpec = True update_mask.append('useExplicitDryRunSpec') return self._ApplyPatch(perimeter_ref, perimeter, update_mask) def EnforceDryRunConfig(self, perimeter_ref): """Promotes a Service Perimeter's dry-run config to enforcement config. Args: perimeter_ref: resources.Resource, reference to the perimeter to patch Returns: ServicePerimeter, the updated Service Perimeter. """ original_perimeter = self.Get(perimeter_ref) m = self.messages perimeter = m.ServicePerimeter() update_mask = ['status', 'spec', 'useExplicitDryRunSpec'] perimeter.status = original_perimeter.spec perimeter.spec = None perimeter.useExplicitDryRunSpec = False return self._ApplyPatch(perimeter_ref, perimeter, update_mask) def UnsetSpec(self, perimeter_ref, use_explicit_dry_run_spec): """Unsets the spec for a Service Perimeter. Args: perimeter_ref: resources.Resource, reference to the perimeter to patch. use_explicit_dry_run_spec: The value to use for the perimeter field of the same name. Returns: ServicePerimeter, the updated Service Perimeter. """ perimeter = self.messages.ServicePerimeter() perimeter.useExplicitDryRunSpec = use_explicit_dry_run_spec perimeter.spec = None update_mask = ['spec', 'useExplicitDryRunSpec'] return self._ApplyPatch(perimeter_ref, perimeter, update_mask)