Source code for invenio_access.permissions

# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2015, 2016 CERN.
#
# Invenio is free software; you can redistribute it
# and/or modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# Invenio is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Invenio; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
# MA 02111-1307, USA.
#
# In applying this license, CERN does not
# waive the privileges and immunities granted to it by virtue of its status
# as an Intergovernmental Organization or submit itself to any jurisdiction.

"""Invenio module for advanced permission control.

Dynamic permission control is a core feature of *Invenio-Access*
package.  It allows administrator to fine-tune access to various
actions with parameters to a specific user or role.
"""

from collections import namedtuple
from functools import partial
from itertools import chain

from flask_principal import ActionNeed, Permission

from .models import ActionRoles, ActionUsers
from .proxies import current_access

_Need = namedtuple('Need', ['method', 'value', 'argument'])

ParameterizedActionNeed = partial(_Need, 'action')

ParameterizedActionNeed.__doc__ = \
    """A need with the method preset to `"action"` with parameter.

    If it is called with `argument=None` then this need is equivalent
    to ``ActionNeed``.
    """

superuser_access = ActionNeed('superuser-access')
"""SuperUser access allows access to everything on Invenio.

DynamicPermissions handles allowing access to SuperUser.
"""


class _P(namedtuple('Permission', ['needs', 'excludes'])):
    """Helper for simple permission updates."""

    def update(self, permission):
        """In-place update of permissions."""
        self.needs.update(permission.needs)
        self.excludes.update(permission.excludes)


class DynamicPermission(Permission):
    """Utility class providing lazy loading of permissions.

    .. warning::

        If ``ActionNeed`` or ``ParameterizedActionNeed`` is not allowed or
        restricted to any user or role then it is **ALLOWED** to anybody.
        This is a major diference to standard ``Permission`` class.

    """

    def __init__(self, *needs):
        r"""Default constructor.

        :param \*needs: A list of need instances.
        """
        self._permissions = None
        self.explicit_needs = set(needs)
        self.explicit_needs.add(superuser_access)

    def _load_permissions(self):
        """Load permissions associated to actions."""
        result = _P(needs=set(), excludes=set())

        for explicit_need in self.explicit_needs:
            if explicit_need.method == 'action':
                action = current_access.get_action_cache(
                    explicit_need.value
                )
                if action is None:
                    action = _P(needs=set(), excludes=set())

                    actionsusers = ActionUsers.query_by_action(
                        explicit_need
                    ).all()

                    actionsroles = ActionRoles.query_by_action(
                        explicit_need
                    ).join(
                        ActionRoles.role
                    ).all()

                    for db_action in chain(actionsusers, actionsroles):
                        if db_action.exclude:
                            action.excludes.add(db_action.need)
                        else:
                            action.needs.add(db_action.need)

                    current_access.set_action_cache(
                        explicit_need.value, action
                    )
                # in-place update of results
                result.update(action)
            else:
                result.needs.add(explicit_need)
        self._permissions = result

    @property
    def needs(self):
        """Return allowed permissions from database.

        If there is no role or user assigned to an action, everyone is allowed
        to perform that action.

        :returns: A list of need instances.
        """
        if not self._permissions:
            self._load_permissions()
        return self._permissions.needs

    @property
    def excludes(self):
        """Return denied permissions from database.

        If there is no role or user assigned to an action, everyone is allowed
        to perform that action.

        :returns: A list of denied permissions.
        """
        if not self._permissions:
            self._load_permissions()  # pragma: no cover
        return self._permissions.excludes