"""
<Program Name>
  roledb.py

<Author>
  Vladimir Diaz <vladimir.v.diaz@gmail.com>

<Started>
  March 21, 2012.  Based on a previous version of this module by Geremy Condra.
  
<Copyright>
  See LICENSE for licensing information.

<Purpose>
  Represent a collection of roles and their organization.  The caller may create
  a collection of roles from those found in the 'root.json' metadata file by
  calling 'create_roledb_from_rootmeta()', or individually by adding roles with
  'add_role()'.  There are many supplemental functions included here that yield
  useful information about the roles contained in the database, such as
  extracting all the parent rolenames for a specified rolename, deleting all the
  delegated roles, retrieving role paths, etc.  The Update Framework process
  maintains a single roledb.

  The role database is a dictionary conformant to 'tuf.formats.ROLEDICT_SCHEMA'
  and has the form:
  
  {'rolename': {'keyids': ['34345df32093bd12...'],
                'threshold': 1
                'signatures': ['abcd3452...'],
                'paths': ['path/to/role.json'],
                'path_hash_prefixes': ['ab34df13'],
                'delegations': {'keys': {}, 'roles': {}}}
  
  The 'name', 'paths', 'path_hash_prefixes', and 'delegations' dict keys are
  optional.
"""

# Help with Python 3 compatibility, where the print statement is a function, an
# implicit relative import is invalid, and the '/' operator performs true
# division.  Example:  print 'hello world' raises a 'SyntaxError' exception.
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals

import logging
import copy

import tuf
import tuf.formats
import tuf.log
import six

# See 'tuf.log' to learn how logging is handled in TUF.
logger = logging.getLogger('tuf.roledb')

# The role database.
_roledb_dict = {}


def create_roledb_from_root_metadata(root_metadata):
  """
  <Purpose>
    Create a role database containing all of the unique roles found in
    'root_metadata'.

  <Arguments>
    root_metadata:
      A dictionary conformant to 'tuf.formats.ROOT_SCHEMA'.  The roles found
      in the 'roles' field of 'root_metadata' is needed by this function.  

  <Exceptions>
    tuf.FormatError, if 'root_metadata' does not have the correct object format.

    tuf.Error, if one of the roles found in 'root_metadata' contains an invalid
    delegation (i.e., a nonexistent parent role).

  <Side Effects>
    Calls add_role().
    
    The old role database is replaced.

  <Returns>
    None.
  """

  # Does 'root_metadata' have the correct object format?
  # This check will ensure 'root_metadata' has the appropriate number of objects 
  # and object types, and that all dict keys are properly named.
  # Raises tuf.FormatError.
  tuf.formats.ROOT_SCHEMA.check_match(root_metadata)

  # Clear the role database.
  _roledb_dict.clear()

  # Do not modify the contents of the 'root_metadata' argument.
  root_metadata = copy.deepcopy(root_metadata)
  
  # Iterate through the roles found in 'root_metadata'
  # and add them to '_roledb_dict'.  Duplicates are avoided.
  for rolename, roleinfo in six.iteritems(root_metadata['roles']):
    if rolename == 'root':
      roleinfo['version'] = root_metadata['version']
      roleinfo['expires'] = root_metadata['expires']
    
    roleinfo['signatures'] = []
    roleinfo['signing_keyids'] = []
    roleinfo['compressions'] = ['']
    roleinfo['partial_loaded'] = False
    if rolename.startswith('targets'):
      roleinfo['paths'] = {}
      roleinfo['delegations'] = {'keys': {}, 'roles': []}
    
    try:
      add_role(rolename, roleinfo)
    # tuf.Error raised if the parent role of 'rolename' does not exist.  
    except tuf.Error as e:
      logger.error(e)
      raise





def add_role(rolename, roleinfo, require_parent=True):
  """
  <Purpose>
    Add to the role database the 'roleinfo' associated with 'rolename'.

  <Arguments>
    rolename:
      An object representing the role's name, conformant to 'ROLENAME_SCHEMA'
      (e.g., 'root', 'snapshot', 'timestamp').

    roleinfo:
      An object representing the role associated with 'rolename', conformant to
      ROLEDB_SCHEMA.  'roleinfo' has the form: 
      {'keyids': ['34345df32093bd12...'],
       'threshold': 1,
       'signatures': ['ab23dfc32']
       'paths': ['path/to/target1', 'path/to/target2', ...],
       'path_hash_prefixes': ['a324fcd...', ...],
       'delegations': {'keys': }

      The 'paths', 'path_hash_prefixes', and 'delegations' dict keys are
      optional.
      
      The 'target' role has an additional 'paths' key.  Its value is a list of
      strings representing the path of the target file(s).

    require_parent:
      A boolean indicating whether to check for a delegating role.  add_role()
      will raise an exception if this parent role does not exist.

  <Exceptions>
    tuf.FormatError, if 'rolename' or 'roleinfo' does not have the correct
    object format.

    tuf.RoleAlreadyExistsError, if 'rolename' has already been added.

    tuf.InvalidNameError, if 'rolename' is improperly formatted.

  <Side Effects>
    The role database is modified.

  <Returns>
    None.
  """

  # Does 'rolename' have the correct object format?
  # This check will ensure 'rolename' has the appropriate number of objects 
  # and object types, and that all dict keys are properly named.
  tuf.formats.ROLENAME_SCHEMA.check_match(rolename)

  # Does 'roleinfo' have the correct object format?
  tuf.formats.ROLEDB_SCHEMA.check_match(roleinfo)

  # Does 'require_parent' have the correct format?
  tuf.formats.BOOLEAN_SCHEMA.check_match(require_parent)

  # Raises tuf.InvalidNameError.
  _validate_rolename(rolename)

  if rolename in _roledb_dict:
    raise tuf.RoleAlreadyExistsError('Role already exists: '+rolename)

  # Make sure that the delegating role exists. This should be just a
  # sanity check and not a security measure.
  if require_parent and '/' in rolename:
    # Get parent role.  'a/b/c/d' --> 'a/b/c'. 
    parent_role = '/'.join(rolename.split('/')[:-1])

    if parent_role not in _roledb_dict:
      raise tuf.Error('Parent role does not exist: '+parent_role)

  _roledb_dict[rolename] = copy.deepcopy(roleinfo)





def update_roleinfo(rolename, roleinfo):
  """
  <Purpose>

  <Arguments>
    rolename:
      An object representing the role's name, conformant to 'ROLENAME_SCHEMA'
      (e.g., 'root', 'snapshot', 'timestamp').

    roleinfo:
      An object representing the role associated with 'rolename', conformant to
      ROLEDB_SCHEMA.  'roleinfo' has the form: 
      {'name': 'role_name',
       'keyids': ['34345df32093bd12...'],
       'threshold': 1,
       'paths': ['path/to/target1', 'path/to/target2', ...],
       'path_hash_prefixes': ['a324fcd...', ...]}

      The 'name', 'paths', and 'path_hash_prefixes' dict keys are optional.

      The 'target' role has an additional 'paths' key.  Its value is a list of
      strings representing the path of the target file(s).

  <Exceptions>
    tuf.FormatError, if 'rolename' or 'roleinfo' does not have the correct
    object format.

    tuf.UnknownRoleError, if 'rolename' cannot be found in the role database.
    
    tuf.InvalidNameError, if 'rolename' is improperly formatted.

  <Side Effects>
    The role database is modified.

  <Returns>
    None.
  """

  # Does 'rolename' have the correct object format?
  # This check will ensure 'rolename' has the appropriate number of objects 
  # and object types, and that all dict keys are properly named.
  tuf.formats.ROLENAME_SCHEMA.check_match(rolename)

  # Does 'roleinfo' have the correct object format?
  tuf.formats.ROLEDB_SCHEMA.check_match(roleinfo)

  # Raises tuf.InvalidNameError.
  _validate_rolename(rolename)

  if rolename not in _roledb_dict:
    raise tuf.UnknownRoleError('Role does not exist: '+rolename)

  _roledb_dict[rolename] = copy.deepcopy(roleinfo)

  




def get_parent_rolename(rolename):
  """
  <Purpose>
    Return the name of the parent role for 'rolename'.
    Given the rolename 'a/b/c/d', return 'a/b/c'.
    Given 'a', return ''.

  <Arguments>
    rolename:
      An object representing the role's name, conformant to 'ROLENAME_SCHEMA'
      (e.g., 'root', 'snapshot', 'timestamp').

  <Exceptions>
    tuf.FormatError, if 'rolename' does not have the correct object format.

    tuf.UnknownRoleError, if 'rolename' cannot be found in the role database.

    tuf.InvalidNameError, if 'rolename' is incorrectly formatted.

  <Side Effects>
    None.

  <Returns>
    A string representing the name of the parent role.
  """

  # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError.
  _check_rolename(rolename)

  parts = rolename.split('/')
  parent_rolename = '/'.join(parts[:-1])

  return parent_rolename





def get_all_parent_roles(rolename):
  """
  <Purpose>
    Return a list of roles that are parents of 'rolename'.
    Given the rolename 'a/b/c/d', return the list:
    ['a', 'a/b', 'a/b/c'].

    Given 'a', return ['a'].
  
  <Arguments>
    rolename:
      An object representing the role's name, conformant to 'ROLENAME_SCHEMA'
      (e.g., 'root', 'snapshot', 'timestamp').

  <Exceptions>
    tuf.FormatError, if 'rolename' does not have the correct object format. 

    tuf.UnknownRoleError, if 'rolename' cannot be found in the role database.

    tuf.InvalidNameError, if 'rolename' is improperly formatted.

  <Side Effects>
    None.

  <Returns>
    A list containing all the parent roles.
  """
    
  # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError.
  _check_rolename(rolename)

  # List of parent roles returned.
  parent_roles = []

  parts = rolename.split('/')

  # Append the first role to the list.
  parent_roles.append(parts[0])

  # The 'roles_added' string contains the roles already added.  If 'a' and 'a/b'
  # have been added to 'parent_roles', 'roles_added' would contain 'a/b'
  roles_added = parts[0]

  # Add each subsequent role to the previous string (with a '/' separator).
  # This only goes to -1 because we only want to return the parents (so we
  # ignore the last element).
  for next_role in parts[1:-1]:
    parent_roles.append(roles_added+'/'+next_role)
    roles_added = roles_added+'/'+next_role

  return parent_roles





def role_exists(rolename):
  """
  <Purpose>
    Verify whether 'rolename' is stored in the role database.

  <Arguments>
    rolename:
      An object representing the role's name, conformant to 'ROLENAME_SCHEMA'
      (e.g., 'root', 'snapshot', 'timestamp').

  <Exceptions>
    tuf.FormatError, if 'rolename' does not have the correct object format.

    tuf.InvalidNameError, if 'rolename' is incorrectly formatted.

  <Side Effects>
    None.

  <Returns>
    Boolean.  True if 'rolename' is found in the role database, False otherwise.
  """

  # Raise tuf.FormatError, tuf.InvalidNameError.
  try: 
    _check_rolename(rolename)
  except (tuf.FormatError, tuf.InvalidNameError):
    raise
  except tuf.UnknownRoleError:
    return False
  
  return True





def remove_role(rolename):
  """
  <Purpose>
    Remove 'rolename', including its delegations.

  <Arguments>
    rolename:
      An object representing the role's name, conformant to 'ROLENAME_SCHEMA'
      (e.g., 'root', 'snapshot', 'timestamp').

  <Exceptions>
    tuf.FormatError, if 'rolename' does not have the correct object format.

    tuf.UnknownRoleError, if 'rolename' cannot be found in the role database.

    tuf.InvalidNameError, if 'rolename' is incorrectly formatted.

  <Side Effects>
    A role, or roles, may be removed from the role database.

  <Returns>
    None.
  """
 
  # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError.
  _check_rolename(rolename)
  
  remove_delegated_roles(rolename)
 
  # remove_delegated_roles() should have left 'rolename' in the database, and
  # 'rolename' was verified to exist by _check_rolename().
  # Remove 'rolename'.
  del _roledb_dict[rolename]





def remove_delegated_roles(rolename):
  """
  <Purpose>
    Remove a role's delegations (leaving the rest of the role alone).
    All levels of delegation are removed, not just the directly delegated roles.
    If 'rolename' is 'a/b/c' and the role database contains
    ['a/b/c/d/e', 'a/b/c/d', 'a/b/c', 'a/b', 'a'], return
    ['a/b/c', 'a/b', 'a'].

  <Arguments>
    rolename:
      An object representing the role's name, conformant to 'ROLENAME_SCHEMA'
      (e.g., 'root', 'snapshot', 'timestamp').

  <Exceptions>
    tuf.FormatError, if 'rolename' does not have the correct object format. 
   
    tuf.UnknownRoleError, if 'rolename' cannot be found in the role database.

    tuf.InvalidNameError, if 'rolename' is incorrectly formatted.

  <Side Effects>
    Role(s) from the role database may be deleted.

  <Returns>
    None.
  """
  
  # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError.
  _check_rolename(rolename)

  # Ensure that we only care about delegated roles!
  rolename_with_slash = rolename + '/'
  for name in get_rolenames():
    if name.startswith(rolename_with_slash):
      del _roledb_dict[name]





def get_rolenames():
  """
  <Purpose>
    Return a list of the rolenames found in the role database.

  <Arguments>
    None.

  <Exceptions>
    None.

  <Side Effects>
    None.
  
  <Returns>
    A list of rolenames.
  """
  
  return list(_roledb_dict.keys())





def get_roleinfo(rolename):
  """
  <Purpose>
    Return the roleinfo of 'rolename'.

    {'keyids': ['34345df32093bd12...'],
     'threshold': 1,
     'signatures': ['ab453bdf...', ...],
     'paths': ['path/to/target1', 'path/to/target2', ...],
     'path_hash_prefixes': ['a324fcd...', ...],
     'delegations': {'keys': {}, 'roles': []}}

    The 'signatures', 'paths', 'path_hash_prefixes', and 'delegations' dict keys
    are optional.

  <Arguments>
    rolename:
      An object representing the role's name, conformant to 'ROLENAME_SCHEMA'
      (e.g., 'root', 'snapshot', 'timestamp').

  <Exceptions>
    tuf.FormatError, if 'rolename' is improperly formatted.
    
    tuf.UnknownRoleError, if 'rolename' does not exist.

  <Side Effects>
    None.
  
  <Returns>
    The roleinfo of 'rolename'.
  """
  
  # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError.
  _check_rolename(rolename)
  
  return copy.deepcopy(_roledb_dict[rolename])





def get_role_keyids(rolename):
  """
  <Purpose>
    Return a list of the keyids associated with 'rolename'.
    Keyids are used as identifiers for keys (e.g., rsa key).
    A list of keyids are associated with each rolename.
    Signing a metadata file, such as 'root.json' (Root role),
    involves signing or verifying the file with a list of
    keys identified by keyid.

  <Arguments>
    rolename:
      An object representing the role's name, conformant to 'ROLENAME_SCHEMA'
      (e.g., 'root', 'snapshot', 'timestamp').

  <Exceptions>
    tuf.FormatError, if 'rolename' does not have the correct object format. 

    tuf.UnknownRoleError, if 'rolename' cannot be found in the role database.

    tuf.InvalidNameError, if 'rolename' is incorrectly formatted.

  <Side Effects>
    None.

  <Returns>
    A list of keyids.
  """
  
  # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError.
  _check_rolename(rolename)

  roleinfo = _roledb_dict[rolename]
  
  return roleinfo['keyids']





def get_role_threshold(rolename):
  """
  <Purpose>
    Return the threshold value of the role associated with 'rolename'.

  <Arguments>
    rolename:
      An object representing the role's name, conformant to 'ROLENAME_SCHEMA'
      (e.g., 'root', 'snapshot', 'timestamp').

  <Exceptions>
    tuf.FormatError, if 'rolename' does not have the correct object format. 

    tuf.UnknownRoleError, if 'rolename' cannot be found in in the role database.

    tuf.InvalidNameError, if 'rolename' is incorrectly formatted.

  <Side Effects>
    None.

  <Returns>
    A threshold integer value.
  """

  # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError.
  _check_rolename(rolename)

  roleinfo = _roledb_dict[rolename]
  
  return roleinfo['threshold']





def get_role_paths(rolename):
  """
  <Purpose>
    Return the paths of the role associated with 'rolename'.

  <Arguments>
    rolename:
      An object representing the role's name, conformant to 'ROLENAME_SCHEMA'
      (e.g., 'root', 'snapshot', 'timestamp').

  <Exceptions>
    tuf.FormatError, if 'rolename' does not have the correct object format.

    tuf.UnknownRoleError, if 'rolename' cannot be found in the role database.

    tuf.InvalidNameError, if 'rolename' is incorrectly formatted.

  <Side Effects>
    None.

  <Returns>
    A list of paths. 
  """

  # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError.
  _check_rolename(rolename)

  roleinfo = _roledb_dict[rolename]
  
  # Paths won't exist for non-target roles.
  try:
    return roleinfo['paths']
  except KeyError:
    return dict()





def get_delegated_rolenames(rolename):
  """
  <Purpose>
    Return the delegations of a role.  If 'rolename' is 'a/b/c'
    and the role database contains ['a/b/c/d', 'a/b/c/d/e', 'a/b/c'], 
    return ['a/b/c/d', 'a/b/c/d/e']

  <Arguments>
    rolename:
      An object representing the role's name, conformant to 'ROLENAME_SCHEMA'
      (e.g., 'root', 'snapshot', 'timestamp').

  <Exceptions>
    tuf.FormatError, if 'rolename' does not have the correct object format.

    tuf.UnknownRoleError, if 'rolename' cannot be found in the role database.

    tuf.InvalidNameError, if 'rolename' is incorrectly formatted.

  <Side Effects>
    None.

  <Returns>
    A list of rolenames. Note that the rolenames are *NOT* sorted by order of
    delegation.
  """

  # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError.
  _check_rolename(rolename)

  # The list of delegated roles to be returned. 
  delegated_roles = []

  # Ensure that we only care about delegated roles!
  rolename_with_slash = rolename + '/'
  for name in get_rolenames():
    if name.startswith(rolename_with_slash):
      delegated_roles.append(name)
  
  return delegated_roles





def clear_roledb():
  """
  <Purpose>
    Reset the roledb database.

  <Arguments>
    None.

  <Exceptions>
    None.

  <Side Effects>
    None.

  <Returns>
    None.
  """

  _roledb_dict.clear()





def _check_rolename(rolename):
  """
  Raise tuf.FormatError if 'rolename' does not match
  'tuf.formats.ROLENAME_SCHEMA', tuf.UnknownRoleError if 'rolename' is not
  found in the role database, or tuf.InvalidNameError if 'rolename' is
  not formatted correctly.
  """
  
  # Does 'rolename' have the correct object format?
  # This check will ensure 'rolename' has the appropriate number of objects 
  # and object types, and that all dict keys are properly named.
  tuf.formats.ROLENAME_SCHEMA.check_match(rolename)

  # Raises tuf.InvalidNameError.
  _validate_rolename(rolename)
  
  if rolename not in _roledb_dict:
    raise tuf.UnknownRoleError('Role name does not exist: '+rolename)





def _validate_rolename(rolename):
  """
  Raise tuf.InvalidNameError if 'rolename' is not formatted correctly.
  It is assumed 'rolename' has been checked against 'ROLENAME_SCHEMA'
  prior to calling this function.
  """

  if rolename == '':
    raise tuf.InvalidNameError('Rolename must not be an empty string')

  if rolename != rolename.strip():
    raise tuf.InvalidNameError(
             'Invalid rolename. Cannot start or end with whitespace: '+rolename)

  if rolename.startswith('/') or rolename.endswith('/'):
    raise tuf.InvalidNameError(
             'Invalid rolename. Cannot start or end with "/": '+rolename)
