# -*- coding: utf-8 -*- #
# Copyright 2016 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.

"""A command that prints an access token for Application Default Credentials.
"""

from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals

from google.auth import credentials
from google.auth import exceptions as google_auth_exceptions
from google.auth import impersonated_credentials
from google.oauth2 import credentials as google_auth_creds
from googlecloudsdk.api_lib.auth import util as auth_util
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions as c_exc
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core import requests
from googlecloudsdk.core.credentials import creds as c_creds
from googlecloudsdk.core.credentials import exceptions as creds_exceptions
from googlecloudsdk.core.credentials import google_auth_credentials as c_google_auth
from googlecloudsdk.core.credentials import store as c_store
import six

_DEFAULT_TOKEN_LIFETIME_SECS = 3600  # 1 hour in seconds


@base.UniverseCompatible
class PrintAccessToken(base.Command):
  r"""Print an access token for your current Application Default Credentials.

  {command} generates and prints an access token for the current
  Application Default Credential (ADC). The
  [ADC](https://google.aip.dev/auth/4110) can be specified either by using
  `gcloud auth application-default login`,
  `gcloud auth login --cred-file=/path/to/cred/file --update-adc`, or by
  setting the `GOOGLE_APPLICATION_CREDENTIALS` environment variable.

  The access token generated by {command} is useful for manually testing
  APIs via curl or similar tools.

  In order to print details of the access token, such as the associated account
  and the token's expiration time in seconds, run:

    $ curl -H "Content-Type: application/x-www-form-urlencoded" \
    -d "access_token=$(gcloud auth application-default print-access-token)" \
    https://www.googleapis.com/oauth2/v1/tokeninfo

  Note that token itself may not be enough to access some services.
  If you use the token with curl or similar tools, you may see
  permission errors similar to "Your application has authenticated using end
  user credentials from the Google Cloud SDK or Google Cloud Shell".
  If it happens, you may need to provide a quota project in the
  "X-Goog-User-Project" header. For example,

    $ curl -H "X-Goog-User-Project: your-project" \
        -H \
        "Authorization: Bearer $(gcloud auth application-default \
     print-access-token)" foo.googleapis.com

  The identity that granted the token must have the serviceusage.services.use
  permission on the provided project. See
  https://cloud.google.com/apis/docs/system-parameters for more
  information.
  """

  @staticmethod
  def Args(parser):
    parser.add_argument(
        '--lifetime',
        type=arg_parsers.Duration(upper_bound='43200s'),
        help=(
            'Access token lifetime. The default access token lifetime is 3600'
            ' seconds, but you can use this flag to reduce the lifetime or'
            ' extend it up to 43200 seconds (12 hours). The org policy'
            ' constraint'
            ' `constraints/iam.allowServiceAccountCredentialLifetimeExtension`'
            ' must be set if you want to extend the lifetime beyond 3600'
            ' seconds. Note that this flag is for service account impersonation'
            ' only, so it only works when either'
            ' `--impersonate-service-account` flag or'
            ' `auth/impersonate_service_account` property is set.'
        ),
    )
    parser.add_argument(
        '--scopes',
        type=arg_parsers.ArgList(min_length=1),
        metavar='SCOPE',
        help='The scopes to authorize for. This flag is supported for user '
        'accounts and service accounts only. '
        'The list of possible scopes can be found at: '
        '[](https://developers.google.com/identity/protocols/googlescopes).\n\n'
        'For end-user accounts, the provided '
        'scopes must be from [{0}], or the scopes previously specified through '
        '`gcloud auth application-default login --scopes`.'.format(', '.join(
            map('`{}`'.format, auth_util.DEFAULT_SCOPES))))
    parser.display_info.AddFormat('value(token)')

  def Run(self, args):
    """Run the helper command."""
    # Create the ADC cred.
    try:
      creds, _ = c_creds.GetGoogleAuthDefault().default(
          scopes=args.scopes or [auth_util.CLOUD_PLATFORM_SCOPE])
    except google_auth_exceptions.DefaultCredentialsError as e:
      log.debug(e, exc_info=True)
      raise c_exc.ToolException(six.text_type(e))

    # Check if impersonation is used. There are two scenarios:
    # (1) the ADC file is not an impersonated credential json file, and either
    # --impersonate-service-account flag or auth/impersonate_service_account
    # property is set. In this case, we will create a new impersonated cred,
    # using the ADC cred as the source cred. The impersonated service account
    # value will be parsed and used as target principal and delegates.
    # (2) the ADC file is impersonated cred json file, then the ADC cred
    # created above is an impersonated cred. If users provided impersonated
    # service account via flag or property, we also need to parse the value to
    # target principal and delegates, and override them in the cred.
    is_adc_cred_impersonated = c_creds.IsImpersonatedAccountCredentials(creds)
    impersonation_service_accounts = (
        properties.VALUES.auth.impersonate_service_account.Get()
    )
    is_impersonation_used = is_adc_cred_impersonated or (
        impersonation_service_accounts is not None
    )
    if args.lifetime and not is_impersonation_used:
      raise c_exc.InvalidArgumentException(
          '--lifetime',
          (
              'Lifetime flag is for service account impersonation only. It must'
              ' be used together with either --impersonate-service-account flag'
              ' or auth/impersonate_service_account property, or the'
              ' application default credential json file must have'
              ' `impersonated_service_account` type.'
          ),
      )
    if impersonation_service_accounts:
      (target_principal, delegates) = c_store.ParseImpersonationAccounts(
          impersonation_service_accounts
      )

    # Add scopes to the ADC cred. We don't do so for impersonated cred since
    # for scenario (1): the ADC cred is the source cred, the scopes should be
    # added to the impersonated cred not the source cred
    # for scenario (2): the ADC cred is the impersonated cred, we already added
    # the scope during the cred creation
    if args.scopes and not is_impersonation_used:
      cred_type = c_creds.CredentialTypeGoogleAuth.FromCredentials(creds)
      if cred_type not in [
          c_creds.CredentialTypeGoogleAuth.USER_ACCOUNT,
          c_creds.CredentialTypeGoogleAuth.SERVICE_ACCOUNT
      ]:
        # TODO(b/223649175): Add support for other credential types(e.g GCE).
        log.warning(
            '`--scopes` flag may not work as expected and will be ignored '
            'for account type {}.'.format(cred_type.key)
        )
      scopes = args.scopes + [auth_util.OPENID, auth_util.USER_EMAIL_SCOPE]

      # non user account credential types
      # pylint:disable=protected-access
      if isinstance(creds, credentials.Scoped):
        creds = creds.with_scopes(scopes)
      else:
        creds._scopes = scopes

    # Make adjustments to the ADC cred.
    # - if it's user credentials, convert it so that it can handle reauth
    # during refresh.
    # - if it's impersonated cred, override the target principal and delegates
    # if impersonated service account flag/property is provided, and set
    # lifetime if lifetime flag is provided.
    if isinstance(creds, google_auth_creds.Credentials):
      creds = c_google_auth.Credentials.FromGoogleAuthUserCredentials(
          creds)
    if is_adc_cred_impersonated:
      if impersonation_service_accounts:
        creds._target_principal = target_principal  # pylint: disable=protected-access
        creds._delegates = delegates  # pylint: disable=protected-access
      if args.lifetime:
        creds._lifetime = args.lifetime  # pylint: disable=protected-access

    # The token URI needs to be overridden in case
    # context aware access is enabled.
    # pylint: disable=protected-access
    creds._token_uri = auth_util.GetTokenUri()
    # pylint: enable=protected-access

    # Refresh the ADC cred.
    req = requests.GoogleAuthRequest()
    try:
      with c_store.HandleGoogleAuthCredentialsRefreshError(for_adc=True):
        creds.refresh(req)
    except creds_exceptions.TokenRefreshError as e:
      if args.scopes:
        raise c_exc.InvalidArgumentException(
            '--scopes',
            'Invalid scopes value. Please make sure the scopes are from [{0}], '
            'or the scopes previously specified through '
            '`gcloud auth application-default login --scopes`.'
            .format(', '.join(map('`{}`'.format, auth_util.DEFAULT_SCOPES))))
      else:
        raise e

    # If impersonation is not needed, or the ADC cred is already an impersonated
    # cred, since we already did refresh above, we can return the cred.
    # Otherwise, we need to create an impersonated cred using the ADC cred as
    # the source cred and the provided target principal, delegates and lifetime.
    # We then do a refresh and return the cred.
    if not is_impersonation_used or is_adc_cred_impersonated:
      return creds
    impersonated_creds = impersonated_credentials.Credentials(
        source_credentials=creds,
        target_principal=target_principal,
        delegates=delegates,
        target_scopes=args.scopes or [auth_util.CLOUD_PLATFORM_SCOPE],
        lifetime=args.lifetime or _DEFAULT_TOKEN_LIFETIME_SECS,
    )
    impersonated_creds.refresh(req)
    return impersonated_creds
