# -*- 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."""


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)
