"""Paytor rights: implements authorization management via roles (RBAC).

The Python library simple-rbac is currently used to manage roles and access to
resources. The Authorization wrapper class is provided to make it transparent.

The Authentication class is used to manage users and groups, and to authenticate
users (currently only by pwebd).

Samuele Catusian <samuele@valis-e.com> Fri Jan 31 10:48:25 CET 2014
"""
# vim:ft=python

import logging
log = logging.getLogger('paytor.rights')
from paytor.log import printStack

class ConfigError(Exception):
    """Configuration file error."""

class AvoidCheckError(Exception):
    """ Avoid check method in Authorization class."""

class InvalidResource(Exception):
    """Invalid resource specified."""

class InvalidAction(Exception):
    """Invalid action specified."""

class InvalidGroup(Exception):
    """Invalid group specified."""

class Authorization(object):
    """Authorization class for Paytor.

    Used to make the inner RBAC implementation transparent to the programmer.

    Needed arguments are the conf files to read user rights from.
    Currently DVARs rights are placed togheter with the DVARs definition itself
    in pcored.conf, while alarms and trends rights are in pwebd.conf.

    Standard roles are defined as the following groups:
    admin - most powerful, can do everything.
    staff - less powerful than admin, usually intended for OEM.
    operator - defines rights needed by client's everyday use.
    guest - no write access at all.

    They are directly usable in the conf. A new role can be easily added
    to the tree to better tailor a specific rights need, but this require
    coding.
    Default roles tree is actually much like an ordered list:
    member--admin-staff--operator--guest
    but new roles can be added everywhere, as a child of every existing
    leave. The root is "member", which is not directly used but it could
    be useful to add a role without inheriting from admin (thus to
    completely newly define it).

    Roles are defined as *groups*. Users have to be declared in one or
    more groups. However, when assigning a default role it's not
    necessary to use more groups: the role tree is traversed from the
    assigned role to the root, thus the entire sub-tree between the root
    and the specified role have the same rights as the specified role
    itself. For example, if you declare a right for the "operator" group,
    'staff' and 'admin' groups will have that right too. Instead, 'guest'
    will not.
    """
    def __init__(self, dvarsconf, webconf):
        self.dvarsconf = dvarsconf
        self.webconf = webconf

        # Filled later with values from pcore.conf.
        self.roles = {}
        # Action and resource lists.
        # Filled later and used to check for valid values.
        self.actions = []
        self.resources = []
        # Read rights and roles from conf files.
        self.rights, self.roles = self._readConf(dvarsconf, webconf)

        # Needed simple-rbac package.
        import rbac.acl
        self.acl = rbac.acl.Registry()
        self._setRoles(self.acl)
        self._setRules(self.acl)
        # contiene le funzioni di conversione tra il protocollo web ed i
        # permessi di rights in caso di modifiche al protocollo bisogna
        # aggiornare anche la mappatura
        # This contains the functions used to convert web protocol calls
        # to the rights definitions. If the procotol gets modified, then the
        # mapping here has to be updated too.
        self._wpmap = self._createWebProtocolMap()

    def _createWebProtocolMap(self):
        """Dict(action -> com) and return fun that return tuple
        (action, resource) compatible with check method.
        """
        return {
            'pcore': {
                'getDevicesGroup': lambda k: ('', ''),
                'getDevicesData': lambda k: ('', ''),
                'sendData': lambda k: ('write', "%s.dvar.%s" % (k['device'],
                                                                k['dvar'])) \
                if 'dvar' in k.keys() else ('write', "%s.dvar.%s" % (k['device'],
                                                                     k['pvar']))
            },
            'ptrend': {
                'getEntries': lambda k: ('draw', "trend.%s" %k['name']) \
                if 'name' in k.keys() else ('draw', 'trend.default'), 'getCsv': \
                lambda k: ('csv', "trend.%s" %k['name']) \
                if 'name' in k.keys() else ('csv', 'trend.default')
            },
            'palarm': {
                'getAlarmConf': lambda k: ('', ''),
                'getEntries': lambda k: ('read', 'alarm'),
                'getLastEntries': lambda k: ('read', 'alarm'),
                'ackActiveAlarm': lambda k: ('ack', 'alarm'),
            },
            'pweb': {
                'logout': lambda k: ('logout', 'web'),
                'changepwd': lambda k: ('changepwd', 'web'),
            }
        }

    def _setRules(self, acl):
        """Rules definition. Here we bind action, roles and resources together.

        Maps the desired configuration (as stated in the conf) to the
        underlying RBAC library model.
        """
        def allow(role, action, resource):
            """Just a useful function to avoid repetitions."""
            if not resource in self.resources:
                self.resources.append(resource)
            if not action in self.actions:
                self.actions.append(action)
            acl.allow(role, action, resource)
            log.debug("Authorization: '%s' can '%s' on '%s'." % (role,
                                                                 action,
                                                                 resource))

        for resource in self.rights:
            acl.add_resource(resource)
            for action in self.rights[resource]:
                role = self.rights[resource][action]
                if isinstance(role, dict):
                    # This means the resource has multiple role/action
                    # definitions.
                    for role in self.rights[resource][action]:
                        allow(self.rights[resource][action][role],
                              action,
                              resource)
                else:
                    allow(role, action, resource)


    def _setRoles(self, acl):
        """Roles definition.
        """
        # Useful to keep track of already declared fathers.
        addedFathers = []
        def addFather(father):
            if not father in addedFathers and not father in self.roles.keys():
                acl.add_role(father)
                log.debug("Authorization: added father role '%s'." % father)
                addedFathers.append(father)

        for role in self.roles:
            # self.roles is a dict whose keys are the children, and values are
            # the father or a list of fathers.
            if not self.roles[role]:
                # No father declared, then we have a root.
                # Can't use addFather() because of the 'if' clause is too
                # restrictive for this case..
                acl.add_role(role)
                log.debug("Authorization: added root role '%s'." % role)
                addedFathers.append(role)
            else:
                # We have father(s).
                fathers = self.roles[role]
                if isinstance(fathers, list):
                    # We have multiple fathers.
                    for father in fathers:
                        addFather(father)
                else:
                    # We only have one father.
                    addFather(fathers)
                    # But we need a list anyway.
                    fathers = [self.roles[role]]
                # Finally we can add the child.
                acl.add_role(role, fathers)
                log.debug("Authorization: added role '%s' with father(s) %s." %
                          (role, fathers))

    def _readConf(self, cfg_pcore, cfg_pweb):
        """Read rights definition from the conf files.
        """
        # A couple helper functions are useful to add the groupid/realm prefix
        # to all the resource names.
        def hasDict(d):
            """Return True if the given dict contains a nested dict."""
            for v in d.values():
                if isinstance(v, dict):
                    return True
            return False

        def addPrefix(prefix, node):
            """Add a prefix to all the keys of all dicts in node."""
            new = {}
            for k, v in node.iteritems():
                if isinstance(v, dict) and hasDict(v):
                    v = addPrefix(prefix, v)
                new["%s.%s" % (prefix, k)] = v
            return new

        # We'll return a dict formed as the following example.
        # 'demo' is the groupid/realm name.
        #
        #{'demo.1:1': {'write': 'operator'},
        #'demo.1:1.dvar.dbvar1': {'write': 'staff'},
        #'demo.1:1.dvar.dbvar2': {'write': 'operator'},
        #'demo.1:1.dvar.dbvar3': {'write': 'operator'},
        #'demo.1:1.dvar.dbvar4': {'write': 'operator'},
        #'demo.1:1.dvar.var1': {'write': 'child1'},
        #'demo.1:1.dvar.var2': {'write': 'operator'},
        #'demo.1:1.dvar.varb0': {'write': 'operator'},
        #'demo.1:1.dvar.varb1': {'write': 'staff'},
        #'demo.1:1.dvar.varb2': {'write': 'operator'},
        #'demo.1:1.dvar.varb3': {'write': 'operator'},
        #'demo.1:1.dvar.varb4': {'write': 'operator'},
        #'demo.alarm': {'ack': 'operator', 'clear': 'staff', 'read': 'guest'},
        #'demo.trend.default': {'csv': 'admin', 'draw': 'guest'},
        #'demo.trend.emission': {'csv': 'admin', 'draw': 'guest'},
        #'demo.trend.level': {'csv': 'operator', 'draw': 'operator'},
        #'demo.trend.temperature': {'csv': 'staff', 'draw': 'guest'},
        #'demo.trend.vacuum': {'csv': 'admin', 'draw': 'operator'},
        #'demo.web': {'changepw': 'operator', 'logout': 'operator'},

        try:
            ret = dict()
            # Let's start reading DVARs rights from pcored.conf.
            for i in cfg_pcore['devices']:
                # Let's first read basic data we need to extract.
                devkey = i['devid']
                dev = i
                devid = int(devkey)
                busid = dev['busid']
                uuid = "%s:%s" %(devid, busid)
                groupid = dev['groupid']

                # If specified, read the default UUID rights, which will be
                # inherited by all the related DVARs where not differently
                # declared.
                if 'rights' in dev:
                    composedName = '%s.%s' % (groupid, uuid)
                    ret[composedName] = dev['rights']

                # Every resource name is prepended with its realm and resource
                # type (dvar, alarm, trend, web, etc..). This is needed to
                # avoid logged-in web users to forge POST requests so to write
                # on resources of other realms (=groupids) even if they are
                # only allowed to access the same resource of their _current_
                # realm!
                for dvar in dev['dvar']:
                    name = "%s.%s.dvar.%s" % (groupid, uuid, dvar['name'])
                    if 'rights' in dvar:
                        ret[name] = dvar['rights']
                    else:
                        # If dvar has no rights key, check if a rights policy
                        # has been defined for the entire device. If yes,
                        # defaults to it.
                        if 'rights' in dev:
                            ret[name] = dev['rights']
                            # No else case needed:
                            # no UUID default rights, neither DVAR declared
                            # them. Defaults to no action allowed.

                # And now we read alarms and trends rights from pwebd.conf.
                # First of all, let's check for group/realm consistency.
                if groupid not in cfg_pweb['webrights']:
                    raise ConfigError("Authorization: groupid in pcore and " \
                                      "pweb conf must be the same." \
                                      "Found '%s'." % groupid)

                cfg = cfg_pweb['webrights'][groupid]
                # If 'copy-from' is present, copy from the specified groupid.
                # And, as done above, we have to add the groupid/realm prefix
                # to all the resource names.
                if ('copy-from' in cfg) and \
                    (cfg['copy-from'] in cfg_pweb['webrights']):
                    ret.update(addPrefix(groupid, cfg_pweb['webrights'][cfg['copy-from']]))
                else:
                    ret.update(addPrefix(groupid, cfg))
                # Now let's clear things up a bit.
                # At this point, trends have their sub-dict, which we don't
                # like. And the name is 'groupid.trendname', which we want to
                # become 'groupid.trend.trendname'.
                composedName = groupid + ".trend"
                for curTrendName in ret[composedName].keys():
                    _, trendName = curTrendName.split('.')
                    newTrendName = "%s.trend.%s" % (groupid, trendName)
                    ret[newTrendName] = ret[composedName].pop(curTrendName)
                del ret[composedName]

            # Then we search for the roles definition in pcore.conf.
            roles = None
            try:
                roles = cfg_pcore['roles']
            except KeyError, e:
                if e.message != 'roles':
                    raise ConfigError("Authorization: can't find roles " \
                                      "definition in pcore.conf.")

        except Exception, e:
            log.info("Cannot read from configuration file: %s, %s" %
                     (printStack(), e))
            raise ConfigError

        return (ret, roles)

    def _mapWebToRights(self, args):
        """ Return tuple (action, resource) in self.check format

        If Return None, self.check return True
        """
        action = ''
        resource = ''

        try:
            action, resource = self._wpmap[args['action']][args['com']](args)
        except KeyError, e:
            log.info("Check map failed: %s(%s)" % (type(e).__name__, e))
            raise

        return (action, resource)

    def check(self, groupid, user, args):
        """Check for permission on specified resource.
        """
        action, resource = self._mapWebToRights(args)

        if action == '':
            return True

        if action not in self.actions:
            raise InvalidAction(
                "Authorization: invalid action specified: %s" % action)

        resource = "%s.%s" %(groupid, resource)

        if resource not in self.resources:
            raise InvalidResource(
                "Authorization: invalid resource specified: %s" % resource)

        if isinstance(user, list):
            for u in user:
                if u not in self.roles:
                    raise InvalidGroup(
                        "Authorization: invalid group specified: %s" % u)
                try:
                    if self.acl.is_allowed(u, action, resource):
                        return True
                except Exception, e:
                    log.info("Check failed: %s, %s" % (printStack(), e))
            return False
        else:
            return self.acl.is_allowed(user, action, resource)


class Authentication(object):
    """Authentication class for Paytor.

    TODO

    """
    def __init__(self, pwfile):
        self.pwfile = pwfile

    def login(self, user):
        """Authenticates the specified user against Paytor users.
        """
        pass

    def passwd(self, user, password=None):
        """Changes the password for the specified Paytor user.
        """
        pass

    def adduser(self, user, password=None, group=None):
        """Adds the specified user to the Paytor system, or adds an existing
        user to the specified group.
        """
        pass

    def deluser(self, user, group=None):
        """Deletes the specified user from the Paytor system, or deletes it from
        the specified group.
        """
        pass


if __name__ == "__main__":
    from paytor.log import startLogging
    log = logging.getLogger('paytor')
    startLogging(logLevel='DEBUG', enfile=False, enstream=True)

    from paytor.config import ConfigJson
    pcoreconf = '/home/samael/valis/paytor/paytor3/pcored/etc/pcored/pcored.conf'
    pwebconf = '/home/samael/valis/paytor/paytor3/pwebd/etc/pwebd/pwebd.conf'

    cfg_pcore = ConfigJson(pcoreconf).load()
    cfg_pweb = ConfigJson(pwebconf).load()

    def authorization_test():
        authorizathor = Authorization(cfg_pcore, cfg_pweb)
        from pprint import pprint
        pprint(authorizathor.roles)
        pprint(authorizathor.rights)

        arglist = [
            {'action': 'pcore', 'com': 'sendData', 'device':'1:1', 'dvar':'var1'},
            {'action': 'pcore', 'com': 'sendData', 'device':'1:1', 'dvar':'varb2'},
            {'action': 'pcore', 'com': 'sendData', 'device':'1:1', 'dvar':'cippa'},
            {'action': 'pcore', 'com': 'sendData', 'device':'3:4', 'dvar':'var1'},
            {'action': 'pcore', 'com': 'lippa', 'device':'1:1', 'dvar':'var1'},
            {'action': 'puppa', 'com': 'sendData', 'device':'1:1', 'dvar':'var1'},
            # draw
            {"action":"ptrend","com":"getEntries","start":"2014-03-26T09:49:00",
             "end":"2014-03-26T10:49:00","group":"demo","varlist":
             ["1:1.iuapressvacuum","1:1.ibzslinleta","1:1.ibzshinleta"],"nstepmax":400},
            # csv
            {"action":"ptrend","com":"getCsv","start":"2014-03-26T09:49:00",
             "end":"2014-03-26T10:49:00","group":"demo","varlist":
             ["1:1.iuapressvacuum","1:1.ibzslinleta","1:1.ibzshinleta"]}
        ]
        for args in arglist:
            try:
                if authorizathor.check('demo', 'operator', args):
                    print "ok"
                else:
                    print "no"
            except Exception, e:
                print "Exception! %s" % e

    authorization_test()
