# Copyright 2014 The LUCI Authors. All rights reserved.
# Use of this source code is governed by the Apache v2.0 license that can be
# found in the LICENSE file.

"""Support for authentication of individual hosts.

General idea:
  1. Each host (machine in GCE, for example) knows its "host token" (and
     doesn't know and has no way to generate other host tokens). In GCE case
     a host token may be stored in the instance metadata. GCE guarantees that
     a machine can read only its own metadata.
  2. Host tokens can be generated by members of 'host-token-creators' group via
     POST to /auth/api/v1/host_token. For primary->replicas setup it is
     available only on Primary (but replicas still can verify it).
  3. A host token is simply a hostname and a timestamp tagged with HMAC.
  4. A host token is put into 'X-Host-Token-V1' HTTP request header.
  5. Any service linked to the auth_service can verify host token HMAC and
     extract hostname from the token.
  6. Verified host name is available via auth.get_peer_host() call.
  7. Missing or invalid host token doesn't abort the request. The host will be
     unknown in this case (auth.get_peer_host() returns None) and auth API users
     that care about host authenticity can reject the request themselves.

Host token is an additional authentication mechanism. OAuth token is still the
primary method of authentication. Host tokens are useful for distinguishing
requests coming from large set of machines reusing same service account. It
might be difficult to setup and manage hundreds and hundreds of separate service
accounts for each individual bots.

Hostnames are case insensitive and will be converted to lowercase.
"""

import logging
import re

from . import api
from . import tokens


# Part of public API of 'auth' component, exposed by this module.
__all__ = [
  'can_create_host_token',
  'create_host_token',
  'HTTP_HEADER',
  'is_valid_host',
  'validate_host_token',
]


# Format of a string that can be embedded into the host token. Not very strict,
# just to ensure no fishy values are encoded into the token.
HOST_RE = re.compile(r'^([a-z0-9\-]{1,40}\.?){1,10}$')

# HTTP request header with host token.
HTTP_HEADER = 'X-Host-Token-V1'

# Name of a group whose members can create host tokens via REST API.
HOST_TOKEN_CREATORS = 'host-token-creators'


class HostToken(tokens.TokenKind):
  """Host token generation parameters."""
  # Non-expirable by default, can be overridden in 'create_host_token'. REST API
  # always requires expiration_sec to be passed explicitly.
  expiration_sec = 25 * 365 * 24 * 3600
  secret_key = api.SecretKey('host_token', scope='global')
  version = 1


def is_valid_host(host):
  """True if a string looks like a hostname and can be used for host token."""
  if not isinstance(host, basestring):
    return False
  return bool(HOST_RE.match(host.strip().lower()))


def can_create_host_token():
  """True if current caller can create host tokens, used in @require checks."""
  return api.is_admin() or api.is_group_member(HOST_TOKEN_CREATORS)


def create_host_token(host, expiration_sec=None):
  """Generates a new host token string for given host."""
  host = host.strip().lower()
  if not is_valid_host(host):
    raise ValueError('Invalid host: %s' % host)
  return HostToken.generate(embedded={'h': host}, expiration_sec=expiration_sec)


def validate_host_token(token):
  """Given host token returns host it was created for or None if invalid."""
  try:
    token_data = HostToken.validate(token)
    host = token_data.get('h')
    if not isinstance(host, basestring):
      logging.error('Unexpected token data:\n%s', token_data)
      return None
    if not is_valid_host(host):
      logging.error('Invalid host:\n%s', host)
      return None
    return str(host)
  except tokens.InvalidTokenError as e:
    logging.error('Invalid or expired host token: %s', e)
    return None
