# -*- coding: utf-8 -*- # # Copyright 2019 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. """Util for iap.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import abc from apitools.base.py import encoding from apitools.base.py import list_pager from googlecloudsdk.api_lib.app import appengine_api_client from googlecloudsdk.api_lib.app import operations_util from googlecloudsdk.api_lib.cloudresourcemanager import projects_api from googlecloudsdk.api_lib.compute import base_classes from googlecloudsdk.api_lib.util import apis from googlecloudsdk.calliope import base from googlecloudsdk.calliope import exceptions as calliope_exceptions from googlecloudsdk.command_lib.iam import iam_util from googlecloudsdk.command_lib.projects import util as projects_util from googlecloudsdk.core import log from googlecloudsdk.core import resources from googlecloudsdk.core import yaml import six IAP_API = 'iap' APPENGINE_APPS_COLLECTION = 'appengine.apps' COMPUTE_BACKEND_SERVICES_COLLECTION = 'compute.backendServices' COMPUTE_REGION_BACKEND_SERVICES_COLLECTION = 'compute.regionBackendServices' PROJECTS_COLLECTION = 'iap.projects' IAP_WEB_COLLECTION = 'iap.projects.iap_web' IAP_WEB_SERVICES_COLLECTION = 'iap.projects.iap_web.services' IAP_WEB_SERVICES_VERSIONS_COLLECTION = 'iap.projects.iap_web.services.versions' IAP_TCP_DESTGROUP_COLLECTION = 'iap.projects.iap_tunnel.locations.destGroups' IAP_TCP_LOCATIONS_COLLECTION = 'iap.projects.iap_tunnel.locations' def _ApiVersion(release_track): del release_track return 'v1' def _GetRegistry(api_version): # Override the default API map version so we can increment API versions on a # API interface basis. registry = resources.REGISTRY.Clone() registry.RegisterApiByName(IAP_API, api_version) return registry def _GetProject(project_id): return projects_api.Get(projects_util.ParseProject(project_id)) class IapIamResource(six.with_metaclass(abc.ABCMeta, object)): """Base class for IAP IAM resources.""" def __init__(self, release_track, project): """Base Constructor for an IAP IAM resource. Args: release_track: base.ReleaseTrack, release track of command. project: Project of the IAP IAM resource """ self.release_track = release_track self.api_version = _ApiVersion(release_track) self.client = apis.GetClientInstance(IAP_API, self.api_version) self.registry = _GetRegistry(self.api_version) self.project = project @property def messages(self): return self.client.MESSAGES_MODULE @property def service(self): return getattr(self.client, self.api_version) @abc.abstractmethod def _Name(self): """Human-readable name of the resource.""" pass @abc.abstractmethod def _Parse(self): """Parses the IAP IAM resource from the arguments.""" pass def _GetIamPolicy(self, resource_ref): request = self.messages.IapGetIamPolicyRequest( resource=resource_ref.RelativeName(), getIamPolicyRequest=self.messages.GetIamPolicyRequest( options=self.messages.GetPolicyOptions( requestedPolicyVersion= iam_util.MAX_LIBRARY_IAM_SUPPORTED_VERSION))) return self.service.GetIamPolicy(request) def GetIamPolicy(self): """Get IAM policy for an IAP IAM resource.""" resource_ref = self._Parse() return self._GetIamPolicy(resource_ref) def _SetIamPolicy(self, resource_ref, policy): policy.version = iam_util.MAX_LIBRARY_IAM_SUPPORTED_VERSION request = self.messages.IapSetIamPolicyRequest( resource=resource_ref.RelativeName(), setIamPolicyRequest=self.messages.SetIamPolicyRequest(policy=policy) ) response = self.service.SetIamPolicy(request) iam_util.LogSetIamPolicy(resource_ref.RelativeName(), self._Name()) return response def SetIamPolicy(self, policy_file): """Set the IAM policy for an IAP IAM resource.""" policy = iam_util.ParsePolicyFile(policy_file, self.messages.Policy) resource_ref = self._Parse() return self._SetIamPolicy(resource_ref, policy) def AddIamPolicyBinding(self, member, role, condition): """Add IAM policy binding to an IAP IAM resource.""" resource_ref = self._Parse() policy = self._GetIamPolicy(resource_ref) iam_util.AddBindingToIamPolicyWithCondition( self.messages.Binding, self.messages.Expr, policy, member, role, condition) self._SetIamPolicy(resource_ref, policy) def RemoveIamPolicyBinding(self, member, role, condition, all_conditions): """Remove IAM policy binding from an IAP IAM resource.""" resource_ref = self._Parse() policy = self._GetIamPolicy(resource_ref) iam_util.RemoveBindingFromIamPolicyWithCondition( policy, member, role, condition, all_conditions=all_conditions) self._SetIamPolicy(resource_ref, policy) class IAPWeb(IapIamResource): """IAP IAM project resource. """ def _Name(self): return 'project' def _Parse(self): project = _GetProject(self.project) return self.registry.Parse( None, params={ 'projectsId': '{}/iap_web'.format(project.projectNumber), }, collection=PROJECTS_COLLECTION) def _AppEngineAppId(app_id): return 'appengine-{}'.format(app_id) def _GetApplication(project): """Returns the application, given a project.""" api_client = appengine_api_client.AppengineApiClient.GetApiClient() application = resources.REGISTRY.Parse( None, params={ 'appsId': project, }, collection=APPENGINE_APPS_COLLECTION) request = api_client.messages.AppengineAppsGetRequest( name=application.RelativeName()) return api_client.client.apps.Get(request) class AppEngineApplication(IapIamResource): """IAP IAM App Engine application resource. """ def _Name(self): return 'App Engine application' def _Parse(self): project = _GetProject(self.project) return self.registry.Parse( None, params={ 'project': project.projectNumber, 'iapWebId': _AppEngineAppId(project.projectId), }, collection=IAP_WEB_COLLECTION) def _SetAppEngineApplicationIap(self, enabled, oauth2_client_id=None, oauth2_client_secret=None): application = _GetApplication(self.project) api_client = appengine_api_client.AppengineApiClient.GetApiClient() iap_kwargs = _MakeIAPKwargs(False, application.iap, enabled, oauth2_client_id, oauth2_client_secret) application_update = api_client.messages.Application( iap=api_client.messages.IdentityAwareProxy(**iap_kwargs)) application = resources.REGISTRY.Parse( self.project, collection=APPENGINE_APPS_COLLECTION) update_request = api_client.messages.AppengineAppsPatchRequest( name=application.RelativeName(), application=application_update, updateMask='iap,') operation = api_client.client.apps.Patch(update_request) return operations_util.WaitForOperation(api_client.client.apps_operations, operation) def Enable(self, oauth2_client_id, oauth2_client_secret): """Enable IAP on an App Engine Application.""" return self._SetAppEngineApplicationIap(True, oauth2_client_id, oauth2_client_secret) def Disable(self): """Disable IAP on an App Engine Application.""" return self._SetAppEngineApplicationIap(False) class AppEngineService(IapIamResource): """IAP IAM App Engine service resource. """ def __init__(self, release_track, project, service_id): super(AppEngineService, self).__init__(release_track, project) self.service_id = service_id def _Name(self): return 'App Engine application service' def _Parse(self): project = _GetProject(self.project) return self.registry.Parse( None, params={ 'project': project.projectNumber, 'iapWebId': _AppEngineAppId(project.projectId), 'serviceId': self.service_id, }, collection=IAP_WEB_SERVICES_COLLECTION) class AppEngineServiceVersion(IapIamResource): """IAP IAM App Engine service version resource. """ def __init__(self, release_track, project, service_id, version_id): super(AppEngineServiceVersion, self).__init__(release_track, project) self.service_id = service_id self.version_id = version_id def _Name(self): return 'App Engine application service version' def _Parse(self): project = _GetProject(self.project) return self.registry.Parse( None, params={ 'project': project.projectNumber, 'iapWebId': _AppEngineAppId(project.projectId), 'serviceId': self.service_id, 'versionId': self.version_id, }, collection=IAP_WEB_SERVICES_VERSIONS_COLLECTION) BACKEND_SERVICES = 'compute' class BackendServices(IapIamResource): """IAP IAM backend services resource. """ def __init__(self, release_track, project, region_id): super(BackendServices, self).__init__(release_track, project) self.region_id = region_id def _Name(self): return 'backend services' def _IapWebId(self): if self.region_id: return '%s-%s' % (BACKEND_SERVICES, self.region_id) else: return BACKEND_SERVICES def _Parse(self): project = _GetProject(self.project) iap_web_id = self._IapWebId() return self.registry.Parse( None, params={ 'project': project.projectNumber, 'iapWebId': iap_web_id, }, collection=IAP_WEB_COLLECTION) class BackendService(IapIamResource): """IAP IAM backend service resource. """ def __init__(self, release_track, project, region_id, service_id): super(BackendService, self).__init__(release_track, project) self.region_id = region_id self.service_id = service_id def _Name(self): return 'backend service' def _IapWebId(self): if self.region_id: return '%s-%s' % (BACKEND_SERVICES, self.region_id) else: return BACKEND_SERVICES def _Parse(self): project = _GetProject(self.project) iap_web_id = self._IapWebId() return self.registry.Parse( None, params={ 'project': project.projectNumber, 'iapWebId': iap_web_id, 'serviceId': self.service_id, }, collection=IAP_WEB_SERVICES_COLLECTION) def _SetBackendServiceIap(self, enabled, oauth2_client_id=None, oauth2_client_secret=None): holder = base_classes.ComputeApiHolder(base.ReleaseTrack.GA) client = holder.client def MakeRequest(method, request): if self.region_id: return ( holder.client.apitools_client.regionBackendServices, method, request, ) else: return holder.client.apitools_client.backendServices, method, request if self.region_id: backend_service = holder.resources.Parse( self.service_id, params={ 'project': self.project, 'region': self.region_id, }, collection=COMPUTE_REGION_BACKEND_SERVICES_COLLECTION, ) get_request = client.messages.ComputeRegionBackendServicesGetRequest( project=backend_service.project, region=backend_service.region, backendService=backend_service.Name(), ) else: backend_service = holder.resources.Parse( self.service_id, params={ 'project': self.project, }, collection=COMPUTE_BACKEND_SERVICES_COLLECTION, ) get_request = client.messages.ComputeBackendServicesGetRequest( project=backend_service.project, backendService=backend_service.Name() ) objects = client.MakeRequests([MakeRequest('Get', get_request)]) if (enabled and objects[0].protocol is not client.messages.BackendService.ProtocolValueValuesEnum.HTTPS): log.warning('IAP has been enabled for a backend service that does not ' 'use HTTPS. Data sent from the Load Balancer to your VM will ' 'not be encrypted.') iap_kwargs = _MakeIAPKwargs(True, objects[0].iap, enabled, oauth2_client_id, oauth2_client_secret) replacement = encoding.CopyProtoMessage(objects[0]) replacement.iap = client.messages.BackendServiceIAP(**iap_kwargs) if self.region_id: update_request = client.messages.ComputeRegionBackendServicesPatchRequest( project=backend_service.project, region=backend_service.region, backendService=backend_service.Name(), backendServiceResource=replacement, ) else: update_request = client.messages.ComputeBackendServicesPatchRequest( project=backend_service.project, backendService=backend_service.Name(), backendServiceResource=replacement) return client.MakeRequests([MakeRequest('Patch', update_request)]) def Enable(self, oauth2_client_id, oauth2_client_secret): """Enable IAP on a backend service.""" return self._SetBackendServiceIap(True, oauth2_client_id, oauth2_client_secret) def Disable(self): """Disable IAP on a backend service.""" return self._SetBackendServiceIap(False) FORWARDING_RULE = 'forwarding_rule' class ForwardingRules(IapIamResource): """IAP IAM forwarding rules resource. """ def __init__(self, release_track, project, region_id): super(ForwardingRules, self).__init__(release_track, project) self.region_id = region_id def _Name(self): return 'forwarding rules' def _IapWebId(self): if self.region_id: return '%s-%s' % (FORWARDING_RULE, self.region_id) else: return FORWARDING_RULE def _Parse(self): project = _GetProject(self.project) iap_web_id = self._IapWebId() return self.registry.Parse( None, params={ 'project': project.projectNumber, 'iapWebId': iap_web_id, }, collection=IAP_WEB_COLLECTION) class ForwardingRule(IapIamResource): """IAP IAM forwarding rule resource. """ def __init__(self, release_track, project, region_id, service_id): super(ForwardingRule, self).__init__(release_track, project) self.region_id = region_id self.service_id = service_id def _Name(self): return 'forwarding rule' def _IapWebId(self): if self.region_id: return '%s-%s' % (FORWARDING_RULE, self.region_id) else: return FORWARDING_RULE def _Parse(self): project = _GetProject(self.project) iap_web_id = self._IapWebId() return self.registry.Parse( None, params={ 'project': project.projectNumber, 'iapWebId': iap_web_id, 'serviceId': self.service_id, }, collection=IAP_WEB_SERVICES_COLLECTION) CLOUD_RUN = 'cloud_run' class CloudRuns(IapIamResource): """IAP IAM cloud runs resource. """ def __init__(self, release_track, project, region_id): super(CloudRuns, self).__init__(release_track, project) self.region_id = region_id def _Name(self): return 'cloud runs' def _IapWebId(self): return '%s-%s' % (CLOUD_RUN, self.region_id) def _Parse(self): project = _GetProject(self.project) iap_web_id = self._IapWebId() return self.registry.Parse( None, params={ 'project': project.projectNumber, 'iapWebId': iap_web_id, }, collection=IAP_WEB_COLLECTION) class CloudRun(IapIamResource): """IAP IAM cloud run resource. """ def __init__(self, release_track, project, region_id, service_id): super(CloudRun, self).__init__(release_track, project) self.region_id = region_id self.service_id = service_id def _Name(self): return 'cloud run' def _IapWebId(self): return '%s-%s' % (CLOUD_RUN, self.region_id) def _Parse(self): project = _GetProject(self.project) iap_web_id = self._IapWebId() return self.registry.Parse( None, params={ 'project': project.projectNumber, 'iapWebId': iap_web_id, 'serviceId': self.service_id, }, collection=IAP_WEB_SERVICES_COLLECTION) def _MakeIAPKwargs(is_backend_service, existing_iap_settings, enabled, oauth2_client_id, oauth2_client_secret): """Make IAP kwargs for IAP settings. Args: is_backend_service: boolean, True if we are applying IAP to a backend service. existing_iap_settings: appengine IdentityAwareProxy or compute BackendServiceIAP, IAP settings. enabled: boolean, True if IAP is enabled. oauth2_client_id: OAuth2 client ID to use. oauth2_client_secret: OAuth2 client secret to use. Returns: IAP kwargs for appengine IdentityAwareProxy or compute BackendServiceIAP """ if (is_backend_service and enabled and not (existing_iap_settings and existing_iap_settings.enabled)): log.warning('IAP only protects requests that go through the Cloud Load ' 'Balancer. See the IAP documentation for important security ' 'best practices: https://cloud.google.com/iap/.') kwargs = { 'enabled': enabled, } if oauth2_client_id: kwargs['oauth2ClientId'] = oauth2_client_id if oauth2_client_secret: kwargs['oauth2ClientSecret'] = oauth2_client_secret return kwargs class IapSettingsResource(object): """Class for IAP settings resources.""" def __init__(self, release_track, resource_name): """Constructor for IAP setting resource. Args: release_track: base.ReleaseTrack, release track of command. resource_name: resource name for the iap settings. """ self.release_track = release_track self.resource_name = resource_name self.api_version = _ApiVersion(release_track) self.client = apis.GetClientInstance(IAP_API, self.api_version) self.registry = _GetRegistry(self.api_version) @property def messages(self): return self.client.MESSAGES_MODULE @property def service(self): return getattr(self.client, self.api_version) def _ParseIapSettingsFile(self, iap_settings_file_path, iap_settings_message_type): """Create an iap settings message from a JSON formatted file. Args: iap_settings_file_path: Path to iap_setttings JSON file iap_settings_message_type: iap settings message type to convert JSON to Returns: the iap_settings message filled from JSON file Raises: BadFileException if JSON file is malformed. """ iap_settings_to_parse = yaml.load_path(iap_settings_file_path) if ( 'access_settings' in iap_settings_to_parse and 'oauth_settings' in iap_settings_to_parse['access_settings'] and 'login_hint' in iap_settings_to_parse['access_settings']['oauth_settings'] ): log.warning( 'login_hint setting is not a replacement for access control. Always' ' enforce an appropriate access policy if you want to restrict' ' access to users outside your domain.' ) if ( 'access_settings' in iap_settings_to_parse and 'gcip_settings' in iap_settings_to_parse['access_settings'] ): log.warning( 'Enabling gcip_settings significantly changes the way IAP' ' authenticates users. Identity Platform does not support IAM, so' ' IAP will not enforce any IAM policies for requests to your' ' application.' ) try: iap_settings_message = encoding.PyValueToMessage( iap_settings_message_type, iap_settings_to_parse ) except AttributeError as e: raise calliope_exceptions.BadFileException( 'Iap settings file {0} does not contain properly formatted JSON {1}' .format(iap_settings_file_path, six.text_type(e)) ) return iap_settings_message def GetIapSetting(self): """Get the setting for an IAP resource.""" request = self.messages.IapGetIapSettingsRequest(name=self.resource_name) return self.service.GetIapSettings(request) def SetIapSetting(self, setting_file): """Set the setting for an IAP resource.""" iap_settings = self._ParseIapSettingsFile( setting_file, self.messages.IapSettings ) iap_settings.name = self.resource_name request = self.messages.IapUpdateIapSettingsRequest( iapSettings=iap_settings, name=self.resource_name ) return self.service.UpdateIapSettings(request) class IapTunnelDestGroupResource(IapIamResource): """IAP TCP tunnelDestGroup IAM resource.""" def __init__(self, release_track, project, region='-', group_name=None): super(IapTunnelDestGroupResource, self).__init__(release_track, project) self.region = region self.group_name = group_name def ResourceService(self): return getattr(self.client, 'projects_iap_tunnel_locations_destGroups') def _Name(self): return 'iap_tunneldestgroups' def _Parse(self): if self.group_name is None: return self._ParseWithoutGroupId() return self._ParseWithGroupId() def _ParseWithGroupId(self): project_number = _GetProject(self.project).projectNumber return self.registry.Parse( None, params={ 'projectsId': project_number, 'locationsId': self.region, 'destGroupsId': self.group_name, }, collection=IAP_TCP_DESTGROUP_COLLECTION) def _ParseWithoutGroupId(self): self.project_number = _GetProject(self.project).projectNumber return self.registry.Parse( None, params={ 'projectsId': self.project_number, 'locationsId': self.region, }, collection=IAP_TCP_LOCATIONS_COLLECTION) def _CreateTunnelDestGroupObject(self, cidr_list, fqdn_list): return { 'name': self.group_name, 'cidrs': cidr_list.split(',') if cidr_list else [], 'fqdns': fqdn_list.split(',') if fqdn_list else [], } def Create(self, cidr_list, fqdn_list): """Creates a TunnelDestGroup.""" tunnel_dest_group = self._CreateTunnelDestGroupObject(cidr_list, fqdn_list) request = ( self.messages.IapProjectsIapTunnelLocationsDestGroupsCreateRequest( parent=self._ParseWithoutGroupId().RelativeName(), tunnelDestGroup=tunnel_dest_group, tunnelDestGroupId=self.group_name, ) ) return self.ResourceService().Create(request) def Delete(self): """Deletes the TunnelDestGroup.""" request = ( self.messages.IapProjectsIapTunnelLocationsDestGroupsDeleteRequest( name=self._Parse().RelativeName() ) ) return self.ResourceService().Delete(request) def List(self, page_size=None, limit=None, list_filter=None): """Yields TunnelDestGroups.""" list_req = self.messages.IapProjectsIapTunnelLocationsDestGroupsListRequest( parent=self._ParseWithoutGroupId().RelativeName() ) return list_pager.YieldFromList( self.ResourceService(), list_req, batch_size=page_size, limit=limit, field='tunnelDestGroups', batch_size_attribute='pageSize', ) def Get(self): """Get TunnelDestGroup.""" request = self.messages.IapProjectsIapTunnelLocationsDestGroupsGetRequest( name=self._Parse().RelativeName() ) return self.ResourceService().Get(request) def Update(self, cidr_list, fqdn_list, update_mask): """Update TunnelDestGroup.""" tunnel_dest_group = self._CreateTunnelDestGroupObject(cidr_list, fqdn_list) request = self.messages.IapProjectsIapTunnelLocationsDestGroupsPatchRequest( name=self._Parse().RelativeName(), tunnelDestGroup=tunnel_dest_group, updateMask=update_mask) return self.ResourceService().Patch(request)