""" Module that holds DISET Authorization class for services
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import six
from DIRAC.ConfigurationSystem.Client.Config import gConfig
from DIRAC.ConfigurationSystem.Client.Helpers import Registry
from DIRAC.FrameworkSystem.Client.Logger import gLogger
from DIRAC.Core.Security import Properties
from DIRAC.Core.Utilities import List

__RCSID__ = "$Id$"


class AuthManager(object):
  """ Handle Service Authorization
  """

  __authLogger = gLogger.getSubLogger("Authorization")
  KW_HOSTS_GROUP = 'hosts'
  KW_DN = 'DN'
  KW_GROUP = 'group'
  KW_EXTRA_CREDENTIALS = 'extraCredentials'
  KW_PROPERTIES = 'properties'
  KW_USERNAME = 'username'

  def __init__(self, authSection):
    """
    Constructor

    :type authSection: string
    :param authSection: Section containing the authorization rules
    """
    self.authSection = authSection

  def authQuery(self, methodQuery, credDict, defaultProperties=False):
    """
    Check if the query is authorized for a credentials dictionary

    :type  methodQuery: string
    :param methodQuery: Method to test
    :type  credDict: dictionary
    :param credDict: dictionary containing credentials for test. The dictionary can contain the DN
                        and selected group.
    :return: Boolean result of test
    """
    userString = ""
    if self.KW_DN in credDict:
      userString += "DN=%s" % credDict[self.KW_DN]
    if self.KW_GROUP in credDict:
      userString += " group=%s" % credDict[self.KW_GROUP]
    if self.KW_EXTRA_CREDENTIALS in credDict:
      userString += " extraCredentials=%s" % str(credDict[self.KW_EXTRA_CREDENTIALS])
    self.__authLogger.debug("Trying to authenticate %s" % userString)
    # Get properties
    requiredProperties = self.getValidPropertiesForMethod(methodQuery, defaultProperties)
    # Extract valid groups
    validGroups = self.getValidGroups(requiredProperties)
    lowerCaseProperties = [prop.lower() for prop in requiredProperties]
    if not lowerCaseProperties:
      lowerCaseProperties = ['any']

    allowAll = "any" in lowerCaseProperties or "all" in lowerCaseProperties
    # Set no properties by default
    credDict[self.KW_PROPERTIES] = []
    # Check non secure backends
    if self.KW_DN not in credDict or not credDict[self.KW_DN]:
      if allowAll and not validGroups:
        self.__authLogger.debug("Accepted request from unsecure transport")
        return True
      else:
        self.__authLogger.debug(
            "Explicit property required and query seems to be coming through an unsecure transport")
        return False
    # Check if query comes though a gateway/web server
    if self.forwardedCredentials(credDict):
      self.__authLogger.debug("Query comes from a gateway")
      self.unpackForwardedCredentials(credDict)
      return self.authQuery(methodQuery, credDict, requiredProperties)
    # Get the properties
    # Check for invalid forwarding
    if self.KW_EXTRA_CREDENTIALS in credDict:
      # Invalid forwarding?
      if not isinstance(credDict[self.KW_EXTRA_CREDENTIALS], six.string_types):
        self.__authLogger.debug("The credentials seem to be forwarded by a host, but it is not a trusted one")
        return False
    # Is it a host?
    if self.KW_EXTRA_CREDENTIALS in credDict and credDict[self.KW_EXTRA_CREDENTIALS] == self.KW_HOSTS_GROUP:
      # Get the nickname of the host
      credDict[self.KW_GROUP] = credDict[self.KW_EXTRA_CREDENTIALS]
    # HACK TO MAINTAIN COMPATIBILITY
    else:
      if self.KW_EXTRA_CREDENTIALS in credDict and self.KW_GROUP not in credDict:
        credDict[self.KW_GROUP] = credDict[self.KW_EXTRA_CREDENTIALS]
    # END OF HACK
    # Get the username
    if self.KW_DN in credDict and credDict[self.KW_DN]:
      if self.KW_GROUP not in credDict:
        result = Registry.findDefaultGroupForDN(credDict[self.KW_DN])
        if not result['OK']:
          credDict[self.KW_USERNAME] = "anonymous"
          credDict[self.KW_GROUP] = "visitor"
        else:
          credDict[self.KW_GROUP] = result['Value']
      if credDict[self.KW_GROUP] == self.KW_HOSTS_GROUP:
        # For host
        if not self.getHostNickName(credDict):
          self.__authLogger.warn("Host is invalid")
          if not allowAll:
            return False
          # If all, then set anon credentials
          credDict[self.KW_USERNAME] = "anonymous"
          credDict[self.KW_GROUP] = "visitor"
      else:
        # For users
        username = self.getUsername(credDict)
        suspended = self.isUserSuspended(credDict)
        if not username:
          self.__authLogger.warn("User is invalid or does not belong to the group it's saying")
        if suspended:
          self.__authLogger.warn("User is Suspended")

        if not username or suspended:
          if not allowAll:
            return False
          # If all, then set anon credentials
          credDict[self.KW_USERNAME] = "anonymous"
          credDict[self.KW_GROUP] = "visitor"
    else:
      if not allowAll:
        return False
      credDict[self.KW_USERNAME] = "anonymous"
      credDict[self.KW_GROUP] = "visitor"

    # If any or all in the props, allow
    allowGroup = not validGroups or credDict[self.KW_GROUP] in validGroups
    if allowAll and allowGroup:
      return True
    # Check authorized groups
    if "authenticated" in lowerCaseProperties and allowGroup:
      return True
    if not self.matchProperties(credDict, requiredProperties):
      self.__authLogger.warn("Client is not authorized\nValid properties: %s\nClient: %s" %
                             (requiredProperties, credDict))
      return False
    elif not allowGroup:
      self.__authLogger.warn("Client is not authorized\nValid groups: %s\nClient: %s" %
                             (validGroups, credDict))
      return False
    return True

  def getHostNickName(self, credDict):
    """
    Discover the host nickname associated to the DN.
    The nickname will be included in the credentials dictionary.

    :type  credDict: dictionary
    :param credDict: Credentials to ckeck
    :return: Boolean specifying whether the nickname was found
    """
    if self.KW_DN not in credDict:
      return True
    if self.KW_GROUP not in credDict:
      return False
    retVal = Registry.getHostnameForDN(credDict[self.KW_DN])
    if not retVal['OK']:
      gLogger.warn("Cannot find hostname for DN %s: %s" % (credDict[self.KW_DN], retVal['Message']))
      return False
    credDict[self.KW_USERNAME] = retVal['Value']
    credDict[self.KW_PROPERTIES] = Registry.getPropertiesForHost(credDict[self.KW_USERNAME], [])
    return True

  def getValidPropertiesForMethod(self, method, defaultProperties=False):
    """
    Get all authorized groups for calling a method

    :type  method: string
    :param method: Method to test
    :return: List containing the allowed groups
    """
    authProps = gConfig.getValue("%s/%s" % (self.authSection, method), [])
    if authProps:
      return authProps
    if defaultProperties:
      self.__authLogger.debug("Using hardcoded properties for method %s : %s" % (method, defaultProperties))
      if not isinstance(defaultProperties, (list, tuple)):
        return List.fromChar(defaultProperties)
      return defaultProperties
    defaultPath = "%s/Default" % "/".join(method.split("/")[:-1])
    authProps = gConfig.getValue("%s/%s" % (self.authSection, defaultPath), [])
    if authProps:
      self.__authLogger.debug("Method %s has no properties defined using %s" % (method, defaultPath))
      return authProps
    self.__authLogger.debug("Method %s has no authorization rules defined. Allowing no properties" % method)
    return []

  def getValidGroups(self, rawProperties):
    """  Get valid groups as specified in the method authorization rules

    :param rawProperties: all method properties
    :type rawProperties: python:list
    :return: list of allowed groups or []
    """
    validGroups = []
    for prop in list(rawProperties):
      if prop.startswith('group:'):
        rawProperties.remove(prop)
        prop = prop.replace('group:', '')
        validGroups.append(prop)
      elif prop.startswith('vo:'):
        rawProperties.remove(prop)
        vo = prop.replace('vo:', '')
        result = Registry.getGroupsForVO(vo)
        if result['OK']:
          validGroups.extend(result['Value'])

    validGroups = list(set(validGroups))
    return validGroups

  def forwardedCredentials(self, credDict):
    """
    Check whether the credentials are being forwarded by a valid source

    :type  credDict: dictionary
    :param credDict: Credentials to ckeck
    :return: Boolean with the result
    """
    if self.KW_EXTRA_CREDENTIALS in credDict and isinstance(credDict[self.KW_EXTRA_CREDENTIALS], (tuple, list)):
      if self.KW_DN in credDict:
        retVal = Registry.getHostnameForDN(credDict[self.KW_DN])
        if retVal['OK']:
          hostname = retVal['Value']
          if Properties.TRUSTED_HOST in Registry.getPropertiesForHost(hostname, []):
            return True
    return False

  def unpackForwardedCredentials(self, credDict):
    """
    Extract the forwarded credentials

    :type  credDict: dictionary
    :param credDict: Credentials to unpack
    """
    credDict[self.KW_DN] = credDict[self.KW_EXTRA_CREDENTIALS][0]
    credDict[self.KW_GROUP] = credDict[self.KW_EXTRA_CREDENTIALS][1]
    del(credDict[self.KW_EXTRA_CREDENTIALS])

  def getUsername(self, credDict):
    """
    Discover the username associated to the DN. It will check if the selected group is valid.
    The username will be included in the credentials dictionary.

    :type  credDict: dictionary
    :param credDict: Credentials to check
    :return: Boolean specifying whether the username was found
    """
    if self.KW_DN not in credDict:
      return True
    if self.KW_GROUP not in credDict:
      result = Registry.findDefaultGroupForDN(credDict[self.KW_DN])
      if not result['OK']:
        return False
      credDict[self.KW_GROUP] = result['Value']
    credDict[self.KW_PROPERTIES] = Registry.getPropertiesForGroup(credDict[self.KW_GROUP], [])
    usersInGroup = Registry.getUsersInGroup(credDict[self.KW_GROUP], [])
    if not usersInGroup:
      return False
    retVal = Registry.getUsernameForDN(credDict[self.KW_DN], usersInGroup)
    if retVal['OK']:
      credDict[self.KW_USERNAME] = retVal['Value']
      return True
    return False

  def isUserSuspended(self, credDict):
    """ Discover if the user is in Suspended status

    :param dict credDict: Credentials to check
    :return: Boolean True if user is Suspended
    """
    # Update credDict if the username is not there
    if self.KW_USERNAME not in credDict:
      self.getUsername(credDict)
    # If username or group is not known we can not judge if the user is suspended
    # These cases are treated elsewhere anyway
    if self.KW_USERNAME not in credDict or self.KW_GROUP not in credDict:
      return False
    suspendedVOList = Registry.getUserOption(credDict[self.KW_USERNAME], 'Suspended', [])
    if not suspendedVOList:
      return False
    vo = Registry.getVOForGroup(credDict[self.KW_GROUP])
    if vo in suspendedVOList:
      return True
    return False

  def matchProperties(self, credDict, validProps, caseSensitive=False):
    """
    Return True if one or more properties are in the valid list of properties

    :type  props: list
    :param props: List of properties to match
    :type  validProps: list
    :param validProps: List of valid properties
    :return: Boolean specifying whether any property has matched the valid ones
    """

    # HACK: Map lower case properties to properties to make the check in lowercase but return the proper case
    if not caseSensitive:
      validProps = dict((prop.lower(), prop) for prop in validProps)
    else:
      validProps = dict((prop, prop) for prop in validProps)
    groupProperties = credDict[self.KW_PROPERTIES]
    foundProps = []
    for prop in groupProperties:
      if not caseSensitive:
        prop = prop.lower()
      if prop in validProps:
        foundProps.append(validProps[prop])
    credDict[self.KW_PROPERTIES] = foundProps
    return foundProps
