text: # Copyright 2016 Google Inc. 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.
  """General IAM utilities used by the Cloud SDK."""
  import httplib
  import json
  
  from apitools.base.protorpclite import messages as apitools_messages
  from apitools.base.py import encoding
  
  from googlecloudsdk.calliope import arg_parsers
  from googlecloudsdk.calliope import exceptions as gcloud_exceptions
  from googlecloudsdk.core import apis as core_apis
  from googlecloudsdk.core import exceptions as core_exceptions
  from googlecloudsdk.core import resources
  from googlecloudsdk.core.console import console_io
  
  import yaml
  
  msgs = core_apis.GetMessagesModule('iam', 'v1')
  MANAGED_BY = (msgs.IamProjectsServiceAccountsKeysListRequest
                .KeyTypesValueValuesEnum)
  CREATE_KEY_TYPES = (msgs.CreateServiceAccountKeyRequest
                      .PrivateKeyTypeValueValuesEnum)
  KEY_TYPES = (msgs.ServiceAccountKey.PrivateKeyTypeValueValuesEnum)
  PUBLIC_KEY_TYPES = (
      msgs.IamProjectsServiceAccountsKeysGetRequest.PublicKeyTypeValueValuesEnum)
  
  
  class IamEtagReadError(core_exceptions.Error):
    """IamEtagReadError is raised when etag is badly formatted."""
  
  
  def _AddRoleArgument(
      parser, help_text, completion_resource_arg, completion_resource_collection):
    """Helper function to add the --role flag with remote completion."""
  
    def CompletionCallback(parsed_args):
      resource_ref = resources.REGISTRY.Parse(
          getattr(parsed_args, completion_resource_arg),
          collection=completion_resource_collection)
      resource_uri = resource_ref.SelfLink()
      return ['beta', 'iam', 'list-grantable-roles', '--format=value(name)',
              resource_uri]
    have_completion = (completion_resource_arg and completion_resource_collection)
  
    parser.add_argument(
        '--role', required=True,
        completion_resource='iam.roles' if have_completion else None,
        list_command_callback_fn=CompletionCallback if have_completion else None,
        help=help_text)
  
  
  def AddArgsForAddIamPolicyBinding(
      parser, completion_resource_arg=None, completion_resource_collection=None):
    """Adds the IAM policy binding arguments for role and members.
  
    Args:
      parser: An argparse.ArgumentParser-like object to which we add the argss.
      completion_resource_arg: str, Name of the argument that holds the resource
        upon which the policy is applied to.
      completion_resource_collection: str, Collection of the resource.
        completion_resource_arg and completion_resource_collection are optional,
        but role tab completion is not possible without specifying them.
  
    Raises:
      ArgumentError if one of the arguments is already defined in the parser.
    """
  
    _AddRoleArgument(parser, 'Define the role of the member.',
                     completion_resource_arg, completion_resource_collection)
    parser.add_argument(
        '--member', required=True,
        help='The member to add to the binding.')
  
  
  def AddArgsForRemoveIamPolicyBinding(
      parser, completion_resource_arg=None, completion_resource_collection=None):
    """Adds the IAM policy binding arguments for role and members.
  
    Args:
      parser: An argparse.ArgumentParser-like object to which we add the argss.
      completion_resource_arg: str, Name of the argument that hold the resource
        upon which the policy is applied to.
      completion_resource_collection: str, Collection of the resource.
        completion_resource_arg and completion_resource_collection are optional,
        but role tab completion is not possible without specifying them.
  
    Raises:
      ArgumentError if one of the arguments is already defined in the parser.
    """
  
    _AddRoleArgument(parser, 'The role to remove the member from.',
                     completion_resource_arg, completion_resource_collection)
    parser.add_argument(
        '--member', required=True,
        help='The member to remove from the binding.')
  
  
  def AddBindingToIamPolicy(messages, policy, member, role):
    """Given an IAM policy, add new bindings as specified by args.
  
    An IAM binding is a pair of role and member. Check if the arguments passed
    define both the role and member attribute, create a binding out of their
    values, and append it to the policy.
  
    Args:
      messages: ToolResults API message classes generated by apitools.
          Required to create new bindings of the proper type.
      policy: IAM policy to which we want to add the bindings.
      member: The member to add to IAM policy.
      role: The role the member should have.
    """
  
    # First check all bindings to see if the member is already in a binding with
    # the same role.
    # A policy can have multiple bindings with the same role. This is why we need
    # to explicitly do this as a separate, first, step and check all bindings.
    for binding in policy.bindings:
      if binding.role == role:
        if member in binding.members:
          return  # Nothing to do. Member already has the role.
  
    # Second step: check to see if a binding already exists with the same role and
    # add the member to this binding. This is to not create new bindings with
    # the same role.
    for binding in policy.bindings:
      if binding.role == role:
        binding.members.append(member)
        return
  
    # Third step: no binding was found that has the same role. Create a new one.
    policy.bindings.append(messages.Binding(
        members=[member], role='{0}'.format(role)))
  
  
  def RemoveBindingFromIamPolicy(policy, member, role):
    """Given an IAM policy, add remove bindings as specified by the args.
  
    An IAM binding is a pair of role and member. Check if the arguments passed
    define both the role and member attribute, search the policy for a binding
    that contains this role and member, and remove it from the policy.
  
    Args:
      policy: IAM policy from which we want to remove bindings.
      member: The member to remove from the IAM policy.
      role: The role the member should be removed from.
    """
  
    # First, remove the member from any binding that has the given role.
    # A server policy can have duplicates.
    for binding in policy.bindings:
      if binding.role == role and member in binding.members:
        binding.members.remove(member)
  
    # Second, remove any empty bindings.
    policy.bindings[:] = [b for b in policy.bindings if b.members]
  
  
  def ParsePolicyFile(policy_file_path, policy_message_type):
    """Construct an IAM Policy protorpc.Message from a JSON or YAML formated file.
  
    Args:
      policy_file_path: Path to the JSON or YAML IAM policy file.
      policy_message_type: Policy message type to convert JSON or YAML to.
    Returns:
      a protorpc.Message of type policy_message_type filled in from the JSON or
      YAML policy file.
    Raises:
      BadFileException if the JSON or YAML file is malformed.
    """
    try:
      policy = ParseJsonPolicyFile(policy_file_path, policy_message_type)
    except gcloud_exceptions.BadFileException:
      try:
        policy = ParseYamlPolicyFile(policy_file_path, policy_message_type)
      except gcloud_exceptions.BadFileException:
        raise gcloud_exceptions.BadFileException(
            'Policy file {0} is not a properly formatted JSON or YAML policy file'
            '.'.format(policy_file_path))
  
    if not policy.etag:
      msg = ('The specified policy does not contain an "etag" field '
             'identifying a specific version to replace. Changing a '
             'policy without an "etag" can overwrite concurrent policy '
             'changes.')
      console_io.PromptContinue(
          message=msg, prompt_string='Replace existing policy', cancel_on_no=True)
    return policy
  
  
  def ParseJsonPolicyFile(policy_file_path, policy_message_type):
    """Construct an IAM Policy protorpc.Message from a JSON formated file.
  
    Args:
      policy_file_path: Path to the JSON IAM policy file.
      policy_message_type: Policy message type to convert JSON to.
    Returns:
      a protorpc.Message of type policy_message_type filled in from the JSON
      policy file.
    Raises:
      BadFileException if the JSON file is malformed.
      IamEtagReadError if the etag is badly formatted.
    """
    try:
      with open(policy_file_path) as policy_file:
        policy_json = policy_file.read()
    except EnvironmentError:
      # EnvironmnetError is parent of IOError, OSError and WindowsError.
      # Raised when file does not exist or can't be opened/read.
      raise core_exceptions.Error(
          'Unable to read policy file {0}'.format(policy_file_path))
  
    try:
      policy = encoding.JsonToMessage(policy_message_type, policy_json)
    except (ValueError) as e:
      # ValueError is raised when JSON is badly formatted
      raise gcloud_exceptions.BadFileException(
          'Policy file {0} is not a properly formatted JSON policy file. {1}'
          .format(policy_file_path, str(e)))
    except (apitools_messages.DecodeError) as e:
      # DecodeError is raised when etag is badly formatted (not proper Base64)
      raise IamEtagReadError(
          'The etag of policy file {0} is not properly formatted. {1}'
          .format(policy_file_path, str(e)))
    return policy
  
  
  def ParseYamlPolicyFile(policy_file_path, policy_message_type):
    """Construct an IAM Policy protorpc.Message from a YAML formatted file.
  
    Args:
      policy_file_path: Path to the YAML IAM policy file.
      policy_message_type: Policy message type to convert YAML to.
    Returns:
      a protorpc.Message of type policy_message_type filled in from the YAML
      policy file.
    Raises:
      BadFileException if the YAML file is malformed.
      IamEtagReadError if the etag is badly formatted.
    """
    try:
      with open(policy_file_path) as policy_file:
        policy_to_parse = yaml.safe_load(policy_file)
    except EnvironmentError:
      # EnvironmnetError is parent of IOError, OSError and WindowsError.
      # Raised when file does not exist or can't be opened/read.
      raise core_exceptions.Error('Unable to read policy file {0}'.format(
          policy_file_path))
    except (yaml.scanner.ScannerError, yaml.parser.ParserError) as e:
      # Raised when the YAML file is not properly formatted.
      raise gcloud_exceptions.BadFileException(
          'Policy file {0} is not a properly formatted YAML policy file. {1}'
          .format(policy_file_path, str(e)))
    try:
      policy = encoding.PyValueToMessage(policy_message_type, policy_to_parse)
    except (AttributeError) as e:
      # Raised when the YAML file is not properly formatted YAML policy file.
      raise gcloud_exceptions.BadFileException(
          'Policy file {0} is not a properly formatted YAML policy file. {1}'
          .format(policy_file_path, str(e)))
    except (apitools_messages.DecodeError) as e:
      # DecodeError is raised when etag is badly formatted (not proper Base64)
      raise IamEtagReadError(
          'The etag of policy file {0} is not properly formatted. {1}'
          .format(policy_file_path, str(e)))
    return policy
  
  
  def GetDetailedHelpForSetIamPolicy(collection, example_id, example_see_more=''):
    """Returns a detailed_help for a set-iam-policy command.
  
    Args:
      collection: Name of the command collection (ex: "project", "dataset")
      example_id: Collection identifier to display in a sample command
          (ex: "my-project", '1234')
      example_see_more: Optional "See ... for details" message. If not specified,
          includes a default reference to IAM managing-policies documentation
    Returns:
      a dict with boilerplate help text for the set-iam-policy command
    """
    if not example_see_more:
      example_see_more = """
            See https://cloud.google.com/iam/docs/managing-policies for details
            of the policy file format and contents."""
  
    return {
        'brief': 'Set IAM policy for a {0}.'.format(collection),
        'DESCRIPTION': '{description}',
        'EXAMPLES': """\
            The following command will read an IAM policy defined in a JSON file
            'policy.json' and set it for a {0} with identifier '{1}'
  
              $ {{command}} {1} policy.json
  
            {2}""".format(collection, example_id, example_see_more)
    }
  
  
  def GetDetailedHelpForAddIamPolicyBinding(collection, example_id):
    """Returns a detailed_help for an add-iam-policy-binding command.
  
    Args:
      collection: Name of the command collection (ex: "project", "dataset")
      example_id: Collection identifier to display in a sample command
          (ex: "my-project", '1234')
    Returns:
      a dict with boilerplate help text for the add-iam-policy-binding command
    """
    return {
        'brief': 'Add IAM policy binding for a {0}.'.format(collection),
        'DESCRIPTION': '{description}',
        'EXAMPLES': """\
            The following command will add an IAM policy binding for the role
            of 'roles/editor' for the user 'test-user@gmail.com' on a {0} with
            identifier '{1}'
  
              $ {{command}} {1} --member='user:test-user@gmail.com' --role='roles/editor'
  
            See https://cloud.google.com/iam/docs/managing-policies for details
            of policy role and member types.
            """.format(collection, example_id)
    }
  
  
  def GetDetailedHelpForRemoveIamPolicyBinding(collection, example_id):
    """Returns a detailed_help for a remove-iam-policy-binding command.
  
    Args:
      collection: Name of the command collection (ex: "project", "dataset")
      example_id: Collection identifier to display in a sample command
          (ex: "my-project", '1234')
    Returns:
      a dict with boilerplate help text for the remove-iam-policy-binding command
    """
    return {
        'brief': 'Remove IAM policy binding for a {0}.'.format(collection),
        'DESCRIPTION': '{description}',
        'EXAMPLES': """\
            The following command will remove a IAM policy binding for the role
            of 'roles/editor' for the user 'test-user@gmail.com' on {0} with
            identifier '{1}'
  
              $ {{command}} {1} --member='user:test-user@gmail.com' --role='roles/editor'
  
            See https://cloud.google.com/iam/docs/managing-policies for details
            of policy role and member types.
            """.format(collection, example_id)
    }
  
  
  def ManagedByFromString(managed_by):
    """Parses a string into a MANAGED_BY enum.
  
    MANAGED_BY is an enum of who manages a service account key resource. IAM
    will rotate any SYSTEM_MANAGED keys by default.
  
    Args:
      managed_by: A string representation of a MANAGED_BY. Can be one of *user*,
      *system* or *any*.
  
    Returns:
      A KeyTypeValueValuesEnum (MANAGED_BY) value.
    """
    if managed_by == 'user':
      return [MANAGED_BY.USER_MANAGED]
    elif managed_by == 'system':
      return [MANAGED_BY.SYSTEM_MANAGED]
    elif managed_by == 'any':
      return []
    else:
      return [MANAGED_BY.KEY_TYPE_UNSPECIFIED]
  
  
  def KeyTypeFromString(key_str):
    """Parses a string into a KeyType enum.
  
    Args:
      key_str: A string representation of a KeyType. Can be either *p12* or
      *json*.
  
    Returns:
      A PrivateKeyTypeValueValuesEnum value.
    """
    if key_str == 'p12':
      return KEY_TYPES.TYPE_PKCS12_FILE
    elif key_str == 'json':
      return KEY_TYPES.TYPE_GOOGLE_CREDENTIALS_FILE
    else:
      return KEY_TYPES.TYPE_UNSPECIFIED
  
  
  def KeyTypeToString(key_type):
    """Get a string version of a KeyType enum.
  
    Args:
      key_type: An enum of either KEY_TYPES or CREATE_KEY_TYPES.
  
    Returns:
      The string representation of the key_type, such that
      parseKeyType(keyTypeToString(x)) is a no-op.
    """
    if (key_type == KEY_TYPES.TYPE_PKCS12_FILE or
        key_type == CREATE_KEY_TYPES.TYPE_PKCS12_FILE):
      return 'p12'
    elif (key_type == KEY_TYPES.TYPE_GOOGLE_CREDENTIALS_FILE or
          key_type == CREATE_KEY_TYPES.TYPE_GOOGLE_CREDENTIALS_FILE):
      return 'json'
    else:
      return 'unspecified'
  
  
  def KeyTypeToCreateKeyType(key_type):
    """Transforms between instances of KeyType enums.
  
    Transforms KeyTypes into CreateKeyTypes.
  
    Args:
      key_type: A ServiceAccountKey.PrivateKeyTypeValueValuesEnum value.
  
    Returns:
      A IamProjectsServiceAccountKeysCreateRequest.PrivateKeyTypeValueValuesEnum
      value.
    """
    # For some stupid reason, HTTP requests generates different enum types for
    # each instance of an enum in the proto buffer. What's worse is that they're
    # not equal to one another.
    if key_type == KEY_TYPES.TYPE_PKCS12_FILE:
      return CREATE_KEY_TYPES.TYPE_PKCS12_FILE
    elif key_type == KEY_TYPES.TYPE_GOOGLE_CREDENTIALS_FILE:
      return CREATE_KEY_TYPES.TYPE_GOOGLE_CREDENTIALS_FILE
    else:
      return CREATE_KEY_TYPES.TYPE_UNSPECIFIED
  
  
  def KeyTypeFromCreateKeyType(key_type):
    """The inverse of *toCreateKeyType*."""
    if key_type == CREATE_KEY_TYPES.TYPE_PKCS12_FILE:
      return KEY_TYPES.TYPE_PKCS12_FILE
    elif key_type == CREATE_KEY_TYPES.TYPE_GOOGLE_CREDENTIALS_FILE:
      return KEY_TYPES.TYPE_GOOGLE_CREDENTIALS_FILE
    else:
      return KEY_TYPES.TYPE_UNSPECIFIED
  
  
  class IAMServiceAccountException(core_exceptions.Error):
    """An exception for IAM service account related errors."""
  
    def __init__(self, status_msg, address, key_id=None):
      error_msg = status_msg
      if key_id:
        error_msg = '{0}: key [{1}] for service account [{2}]'.format(
            error_msg, key_id, address)
      elif address:
        error_msg = '{0}: service account [{1}]'.format(error_msg, address)
      super(IAMServiceAccountException, self).__init__(error_msg)
  
  
  def ConvertToServiceAccountException(http_error, address, key_id=None):
    """Convert HTTP error to IAM specific exception, based on the status code."""
    error_msg = None
    if http_error.status_code == httplib.NOT_FOUND:
      error_msg = 'Not found'
    elif http_error.status_code == httplib.FORBIDDEN:
      error_msg = 'Permission denied'
    elif http_error.status_code == httplib.BAD_REQUEST:
      # If the request is not valid then simply report the error message.
      # In this case, the service account email (the 'address' variable)
      # should not be included in the error message.
      content = json.loads(http_error.content)
      # The variable 'error' is the content of the error message, as can be
      # seen by using the --log-http flag, in JSON format.
      error = content.get('error', {})
      error_msg = error.get('message', 0)
      address = None
    elif http_error.status_code == httplib.CONFLICT:
      return http_error  # Let it retry.
  
    if error_msg:
      return IAMServiceAccountException(error_msg, address, key_id)
    # TODO(user): Add a test for this exception type.
    return gcloud_exceptions.ToolException.FromCurrent()
  
  
  def AccountNameValidator():
    # https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/create
    return arg_parsers.RegexpValidator(
        r'[a-z][a-z0-9\-]{4,28}[a-z0-9]',
        'Service account name must be between 6 and 30 characters (inclusive), '
        'must begin with a lowercase letter, and consist of alphanumeric '
        'characters that can be separated by hyphens.')
  
  
  def ProjectToProjectResourceName(project):
    """Turns a project id into a project resource name."""
    return 'projects/{0}'.format(project)
  
  
  def EmailToAccountResourceName(email):
    """Turns an email into a service account resource name."""
    return 'projects/-/serviceAccounts/{0}'.format(email)
  
  
  def EmailAndKeyToResourceName(email, key):
    """Turns an email and key id into a key resource name."""
    return 'projects/-/serviceAccounts/{0}/keys/{1}'.format(email, key)
  
  
  def GetKeyIdFromResourceName(name):
    """Gets the key id from a resource name. No validation is done."""
    return name.split('/')[5]
  
  
  def PublicKeyTypeFromString(key_str):
    """Parses a string into a PublicKeyType enum.
  
    Args:
      key_str: A string representation of a PublicKeyType. Can be either *pem* or
      *raw*.
  
    Returns:
      A PublicKeyTypeValueValuesEnum value.
    """
    if key_str == 'pem':
      return PUBLIC_KEY_TYPES.TYPE_X509_PEM_FILE
    return PUBLIC_KEY_TYPES.TYPE_RAW_PUBLIC_KEY
  