""" 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
