# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Utilities for Binary Authorization commands."""


import json

from containerregistry.client import docker_name
from googlecloudsdk.core.exceptions import Error
import six
from six.moves import urllib

_BINAUTHZ_ATTESTATION_ANNOTATION_KEY = (
    'binaryauthorization.googleapis.com/attestations'
)


class BadImageUrlError(Error):
  """Raised when a container image URL cannot be parsed successfully."""


def ReplaceImageUrlScheme(image_url, scheme):
  """Returns the passed `image_url` with the scheme replaced.

  Args:
    image_url: The URL to replace (or strip) the scheme from. (string)
    scheme: The scheme of the returned URL.  If this is an empty string or
      `None`, the scheme is stripped and the leading `//` of the resulting URL
      will be stripped off.
  Raises:
    BadImageUrlError: `image_url` isn't valid.
  """
  scheme = scheme or ''
  parsed_url = urllib.parse.urlparse(image_url)

  # If the URL has a scheme but not a netloc, then it must have looked like
  # 'scheme:///foo/bar', which is invalid for the purpose of attestation.
  if parsed_url.scheme and not parsed_url.netloc:
    raise BadImageUrlError(
        "Image URL '{image_url}' is invalid because it does not have a host "
        'component.'.format(image_url=image_url))

  # If there is neither a scheme nor a netloc, this means that an unqualified
  # URL was passed, like 'gcr.io/foo/bar'.  In this case we canonicalize the URL
  # by prefixing '//', which will cause urlparse to correctly pick up the
  # netloc.
  if not parsed_url.netloc:
    parsed_url = urllib.parse.urlparse('//{}'.format(image_url))

  # Finally, we replace the scheme and generate the URL.  If we were stripping
  # the scheme, the result will be prefixed with '//', which we strip off.  If
  # the scheme is non-empty, the lstrip is a no-op.
  return parsed_url._replace(scheme=scheme).geturl().lstrip('/')


def MakeSignaturePayloadDict(container_image_url):
  """Creates a dict representing a JSON signature object to sign.

  Args:
    container_image_url: See `containerregistry.client.docker_name.Digest` for
      artifact URL validation and parsing details.  `container_image_url` must
      be a fully qualified image URL with a valid sha256 digest.

  Returns:
    Dictionary of nested dictionaries and strings, suitable for passing to
    `json.dumps` or similar.
  """
  url = ReplaceImageUrlScheme(image_url=container_image_url, scheme='')
  try:
    repo_digest = docker_name.Digest(url)
  except docker_name.BadNameException as e:
    raise BadImageUrlError(e)
  return {
      'critical': {
          'identity': {
              'docker-reference': six.text_type(repo_digest.as_repository()),
          },
          'image': {
              'docker-manifest-digest': repo_digest.digest,
          },
          'type': 'Google cloud binauthz container signature',
      },
  }


def MakeSignaturePayload(container_image_url):
  """Creates a JSON bytestring representing a signature object to sign.

  Args:
    container_image_url: See `containerregistry.client.docker_name.Digest` for
      artifact URL validation and parsing details.  `container_image_url` must
      be a fully qualified image URL with a valid sha256 digest.

  Returns:
    A bytestring representing a JSON-encoded structure of nested dictionaries
    and strings.
  """
  payload_dict = MakeSignaturePayloadDict(container_image_url)
  # `separators` is specified as a workaround to the native `json` module's
  # https://bugs.python.org/issue16333 which results in inconsistent
  # serialization in older versions of Python.
  payload = json.dumps(
      payload_dict,
      ensure_ascii=True,
      indent=2,
      separators=(',', ': '),
      sort_keys=True,
  )
  # NOTE: A newline is appended for backwards compatibility with the previous
  # payload serialization which relied on gcloud's default JSON serialization.
  return '{}\n'.format(payload).encode('utf-8')


def RemoveArtifactUrlScheme(artifact_url):
  """Ensures the given URL has no scheme (e.g.

  replaces "https://gcr.io/foo/bar" with "gcr.io/foo/bar" and leaves
  "gcr.io/foo/bar" unchanged).

  Args:
    artifact_url: A URL string.
  Returns:
    The URL with the scheme removed.
  """
  url_without_scheme = ReplaceImageUrlScheme(artifact_url, scheme='')
  try:
    # The validation logic in `docker_name` silently produces incorrect results
    # if the passed URL has a scheme.
    docker_name.Digest(url_without_scheme)
  except docker_name.BadNameException as e:
    raise BadImageUrlError(e)
  return url_without_scheme


def GetImageDigest(artifact_url):
  """Returns the digest of an image given its url.

  Args:
    artifact_url: An image url. e.g. "https://gcr.io/foo/bar@sha256:123"

  Returns:
    The image digest. e.g. "sha256:123"
  """
  url_without_scheme = ReplaceImageUrlScheme(artifact_url, scheme='')

  try:
    # The validation logic in `docker_name` silently produces incorrect results
    # if the passed URL has a scheme.
    digest = docker_name.Digest(url_without_scheme)
  except docker_name.BadNameException as e:
    raise BadImageUrlError(e)
  return digest.digest


def PaeEncode(dsse_type, body):
  """Pae encode input using the specified dsse type.

  Args:
    dsse_type: DSSE envelope type.
    body: payload string.

  Returns:
    Pae-encoded payload byte string.
  """
  dsse_type_bytes = dsse_type.encode('utf-8')
  body_bytes = body.encode('utf-8')
  return b' '.join([
      b'DSSEv1',
      b'%d' % len(dsse_type_bytes),
      dsse_type_bytes,
      b'%d' % len(body_bytes),
      body_bytes,
  ])


def GeneratePodSpecFromImages(images):
  """Creates a minimal PodSpec from a list of images.

  Args:
    images: list of images being evaluated.

  Returns:
    PodSpec object in JSON form.
  """
  spec = {
      'apiVersion': 'v1',
      'kind': 'Pod',
      'metadata': {
          'name': '',
      },
      'spec': {
          'containers': [{'image': image} for image in images],
      },
  }

  return spec


def AddInlineAttestationsToPodSpec(pod_spec, attestations):
  """Inlines attestations into a Kubernetes PodSpec.

  Args:
    pod_spec: The PodSpec provided by the user.
    attestations: List of attestations returned by the policy evaluator in comma
      separated DSSE form.

  Returns:
    Modified PodSpec with attestations inlined.
  """
  annotations = pod_spec['metadata'].get('annotations', {})
  existing_attestations = annotations.get(
      _BINAUTHZ_ATTESTATION_ANNOTATION_KEY, None
  )
  if existing_attestations:
    annotations[_BINAUTHZ_ATTESTATION_ANNOTATION_KEY] = ','.join(
        [existing_attestations] + attestations
    )
  else:
    annotations[_BINAUTHZ_ATTESTATION_ANNOTATION_KEY] = ','.join(attestations)
  pod_spec['metadata']['annotations'] = annotations
  return pod_spec


def AddInlineAttestationsToResource(resource, attestations):
  """Inlines attestations into a Kubernetes resource.

  Args:
    resource: The Kubernetes resource provided by the user.
    attestations: List of attestations returned by the policy evaluator in comma
      separated DSSE form.

  Returns:
    Modified Kubernetes resource with attestations inlined.
  """
  if resource['kind'] != 'Pod':
    resource['spec']['template'] = AddInlineAttestationsToPodSpec(
        resource['spec']['template'], attestations
    )
    return resource
  return AddInlineAttestationsToPodSpec(resource, attestations)
