acl.py 10.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017  Gabriel Détraz
# Copyright © 2017  Goulven Kermarec
# Copyright © 2017  Augustin Lemesle
#
# This program 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.
#
# This program 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 this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

"""Handles ACL for re2o.

Here are defined some decorators that can be used in views to handle ACL.
"""
from __future__ import unicode_literals

import sys
31
from itertools import chain
32

33
from django.db.models import Model
34 35 36 37 38
from django.contrib import messages
from django.shortcuts import redirect
from django.urls import reverse


Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
39
def acl_base_decorator(method_name, *targets, on_instance=True):
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
    """Base decorator for acl. It checks if the `request.user` has the
    permission by calling model.method_name. If the flag on_instance is True,
    tries to get an instance of the model by calling
    `model.get_instance(*args, **kwargs)` and runs `instance.mehod_name`
    rather than model.method_name.

    It is not intended to be used as is. It is a base for others ACL
    decorators.

    Args:
        method_name: The name of the method which is to to be used for ACL.
            (ex: 'can_edit') WARNING: if no method called 'method_name' exists,
            then no error will be triggered, the decorator will act as if
            permission was granted. This is to allow you to run ACL tests on
            fields only. If the method exists, it has to return a 2-tuple
            `(can, reason)` with `can` being a boolean stating whether the
            access is granted and `reason` a message to be displayed if `can`
            equals `False` (can be `None`)
        *targets: The targets. Targets are specified like a sequence of models
            and fields names. As an example
            ```
                acl_base_decorator('can_edit', ModelA, 'field1', 'field2', \
ModelB, ModelC, 'field3', on_instance=False)
            ```
            will make the following calls (where `user` is the current user,
            `*args` and `**kwargs` are the arguments initially passed to the
            view):
                - `ModelA.can_edit(user, *args, **kwargs)`
                - `ModelA.can_change_field1(user, *args, **kwargs)`
                - `ModelA.can_change_field2(user, *args, **kwargs)`
                - `ModelB.can_edit(user, *args, **kwargs)`
                - `ModelC.can_edit(user, *args, **kwargs)`
                - `ModelC.can_change_field3(user, *args, **kwargs)`

            Note that
            ```
                acl_base_decorator('can_edit', 'field1', ModelA, 'field2', \
on_instance=False)
            ```
            would have the same effect that
            ```
                acl_base_decorator('can_edit', ModelA, 'field1', 'field2', \
on_instance=False)
            ```
            But don't do that, it's silly.
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
85 86 87 88 89
        on_instance: When `on_instance` equals `False`, the decorator runs the
            ACL method on the model class rather than on an instance. If an
            instance need to fetched, it is done calling the assumed existing
            method `get_instance` of the model, with the arguments originally
            passed to the view.
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104

    Returns:
        The user is either redirected to their own page with an explanation
        message if at least one access is not granted, or to the view. In order
        to avoid duplicate DB calls, when the `on_instance` flag equals `True`,
        the instances are passed to the view. Example, with this decorator:
        ```
            acl_base_decorator('can_edit', ModelA, 'field1', 'field2', ModelB,\
ModelC)
        ```
        The view will be called like this:
        ```
            view(request, instance_of_A, instance_of_b, *args, **kwargs)
        ```
        where `*args` and `**kwargs` are the original view arguments.
105
    """
106 107

    def group_targets():
108 109 110
        """This generator parses the targets of the decorator, yielding
        2-tuples of (model, [fields]).
        """
111 112
        current_target = None
        current_fields = []
113
        for target in targets:
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
114
            if not isinstance(target, str):
115 116
                if current_target:
                    yield (current_target, current_fields)
117
                current_target = target
118 119
                current_fields = []
            else:
120
                current_fields.append(target)
121 122
        yield (current_target, current_fields)

123
    def decorator(view):
Maël Kervella's avatar
Maël Kervella committed
124 125
        """The decorator to use on a specific view
        """
126
        def wrapper(request, *args, **kwargs):
127 128 129 130
            """The wrapper used for a specific request"""
            instances = []

            def process_target(target, fields):
131 132 133 134 135
                """This function calls the methods on the target and checks for
                the can_change_`field` method with the given fields. It also
                stores the instances of models in order to avoid duplicate DB
                calls for the view.
                """
136 137 138 139 140 141 142 143 144 145 146 147 148
                if on_instance:
                    try:
                        target = target.get_instance(*args, **kwargs)
                        instances.append(target)
                    except target.DoesNotExist:
                        yield False, u"Entrée inexistante"
                        return
                if hasattr(target, method_name):
                    can_fct = getattr(target, method_name)
                    yield can_fct(request.user, *args, **kwargs)
                for field in fields:
                    can_change_fct = getattr(target, 'can_change_' + field)
                    yield can_change_fct(request.user, *args, **kwargs)
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
149

150 151 152 153 154 155 156 157 158 159 160 161 162 163
            error_messages = [
                x[1] for x in chain.from_iterable(
                    process_target(x[0], x[1]) for x in group_targets()
                ) if not x[0]
            ]
            if error_messages:
                for msg in error_messages:
                    messages.error(
                        request, msg or "Vous ne pouvez pas accéder à ce menu")
                return redirect(reverse(
                    'users:profil',
                    kwargs={'userid': str(request.user.id)}
                ))
            return view(request, *chain(instances, args), **kwargs)
164 165 166 167
        return wrapper
    return decorator


168
def can_create(*models):
169 170 171
    """Decorator to check if an user can create the given models. It runs
    `acl_base_decorator` with the flag `on_instance=False` and the method
    'can_create'. See `acl_base_decorator` documentation for further details.
172 173 174 175 176
    """
    return acl_base_decorator('can_create', *models, on_instance=False)


def can_edit(*targets):
177 178 179 180
    """Decorator to check if an user can edit the models.
    It runs `acl_base_decorator` with the flag `on_instance=True` and the
    method 'can_edit'. See `acl_base_decorator` documentation for further
    details.
181
    """
182
    return acl_base_decorator('can_edit', *targets)
183 184


185
def can_change(*targets):
186
    """Decorator to check if an user can edit a field of a model class.
187 188 189 190
    Difference with can_edit : takes a class and not an instance
    It runs `acl_base_decorator` with the flag `on_instance=False` and the
    method 'can_change'. See `acl_base_decorator` documentation for further
    details.
191
    """
192
    return acl_base_decorator('can_change', *targets, on_instance=False)
193 194


195
def can_delete(*targets):
196
    """Decorator to check if an user can delete a model.
197 198 199
    It runs `acl_base_decorator` with the flag `on_instance=True` and the
    method 'can_edit'. See `acl_base_decorator` documentation for further
    details.
200
    """
201
    return acl_base_decorator('can_delete', *targets)
202 203 204 205 206 207


def can_delete_set(model):
    """Decorator which returns a list of detable models by request user.
    If none of them, return an error"""
    def decorator(view):
Maël Kervella's avatar
Maël Kervella committed
208 209
        """The decorator to use on a specific view
        """
210
        def wrapper(request, *args, **kwargs):
Maël Kervella's avatar
Maël Kervella committed
211 212
            """The wrapper used for a specific request
            """
213 214 215
            all_objects = model.objects.all()
            instances_id = []
            for instance in all_objects:
Maël Kervella's avatar
Maël Kervella committed
216
                can, _msg = instance.can_delete(request.user)
217 218 219 220
                if can:
                    instances_id.append(instance.id)
            instances = model.objects.filter(id__in=instances_id)
            if not instances:
Maël Kervella's avatar
Maël Kervella committed
221
                messages.error(
Maël Kervella's avatar
Maël Kervella committed
222
                    request, "Vous ne pouvez pas accéder à ce menu")
Maël Kervella's avatar
Maël Kervella committed
223 224 225 226
                return redirect(reverse(
                    'users:profil',
                    kwargs={'userid': str(request.user.id)}
                ))
227 228 229 230 231
            return view(request, instances, *args, **kwargs)
        return wrapper
    return decorator


232
def can_view(*targets):
233
    """Decorator to check if an user can view a model.
234 235 236
    It runs `acl_base_decorator` with the flag `on_instance=True` and the
    method 'can_view'. See `acl_base_decorator` documentation for further
    details.
237
    """
238
    return acl_base_decorator('can_view', *targets)
239 240


241
def can_view_all(*targets):
242
    """Decorator to check if an user can view a class of model.
243 244 245
    It runs `acl_base_decorator` with the flag `on_instance=False` and the
    method 'can_view_all'. See `acl_base_decorator` documentation for further
    details.
246
    """
247
    return acl_base_decorator('can_view_all', *targets, on_instance=False)
248 249


250 251
def can_view_app(*apps_name):
    """Decorator to check if an user can view the applications.
252
    """
253 254
    for app_name in apps_name:
        assert app_name in sys.modules.keys()
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
255 256 257 258 259
    return acl_base_decorator(
        'can_view',
        *chain(sys.modules[app_name] for app_name in apps_name),
        on_instance=False
    )
260 261 262 263 264


def can_edit_history(view):
    """Decorator to check if an user can edit history."""
    def wrapper(request, *args, **kwargs):
Maël Kervella's avatar
Maël Kervella committed
265 266
        """The wrapper used for a specific request
        """
267
        if request.user.has_perm('admin.change_logentry'):
268 269
            return view(request, *args, **kwargs)
        messages.error(
Yoann Pietri's avatar
Yoann Pietri committed
270 271
            request,
            "Vous ne pouvez pas éditer l'historique."
272
        )
Maël Kervella's avatar
Maël Kervella committed
273 274 275 276
        return redirect(reverse(
            'users:profil',
            kwargs={'userid': str(request.user.id)}
        ))
277
    return wrapper