acl.py 10.9 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
from django.contrib import messages
from django.shortcuts import redirect
from django.urls import reverse
37
from django.utils.translation import ugettext as _
38 39


Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
40
def acl_base_decorator(method_name, *targets, on_instance=True):
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 85
    """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
86 87 88 89 90
        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.
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105

    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.
106
    """
107 108

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

124
    def decorator(view):
125 126
        """The decorator to use on a specific view
        """
127
        def wrapper(request, *args, **kwargs):
128 129 130 131
            """The wrapper used for a specific request"""
            instances = []

            def process_target(target, fields):
132 133 134 135 136
                """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.
                """
137 138 139 140 141
                if on_instance:
                    try:
                        target = target.get_instance(*args, **kwargs)
                        instances.append(target)
                    except target.DoesNotExist:
142
                        yield False, _("Nonexistent entry.")
143 144 145 146 147 148 149
                        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)
150

151 152 153 154 155 156 157 158
            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(
159 160
                        request, msg or _("You don't have the right to access"
                                          " this menu."))
161 162 163 164 165
                return redirect(reverse(
                    'users:profil',
                    kwargs={'userid': str(request.user.id)}
                ))
            return view(request, *chain(instances, args), **kwargs)
166 167 168 169
        return wrapper
    return decorator


170
def can_create(*models):
171 172 173
    """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.
174 175 176 177 178
    """
    return acl_base_decorator('can_create', *models, on_instance=False)


def can_edit(*targets):
179 180 181 182
    """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.
183
    """
184
    return acl_base_decorator('can_edit', *targets)
185 186


187
def can_change(*targets):
188
    """Decorator to check if an user can edit a field of a model class.
189 190 191 192
    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.
193
    """
194
    return acl_base_decorator('can_change', *targets, on_instance=False)
195 196


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


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):
210 211
        """The decorator to use on a specific view
        """
212
        def wrapper(request, *args, **kwargs):
213 214
            """The wrapper used for a specific request
            """
215 216 217
            all_objects = model.objects.all()
            instances_id = []
            for instance in all_objects:
218
                can, _msg = instance.can_delete(request.user)
219 220 221 222
                if can:
                    instances_id.append(instance.id)
            instances = model.objects.filter(id__in=instances_id)
            if not instances:
223
                messages.error(
224 225
                    request, _("You don't have the right to access this menu.")
                )
226 227 228 229
                return redirect(reverse(
                    'users:profil',
                    kwargs={'userid': str(request.user.id)}
                ))
230 231 232 233 234
            return view(request, instances, *args, **kwargs)
        return wrapper
    return decorator


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


244
def can_view_all(*targets):
245
    """Decorator to check if an user can view a class of model.
246 247 248
    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.
249
    """
250
    return acl_base_decorator('can_view_all', *targets, on_instance=False)
251 252


253 254
def can_view_app(*apps_name):
    """Decorator to check if an user can view the applications.
255
    """
256 257
    for app_name in apps_name:
        assert app_name in sys.modules.keys()
258 259 260 261 262
    return acl_base_decorator(
        'can_view',
        *chain(sys.modules[app_name] for app_name in apps_name),
        on_instance=False
    )
263 264 265 266 267


def can_edit_history(view):
    """Decorator to check if an user can edit history."""
    def wrapper(request, *args, **kwargs):
268 269
        """The wrapper used for a specific request
        """
270
        if request.user.has_perm('admin.change_logentry'):
271 272
            return view(request, *args, **kwargs)
        messages.error(
Yoann Pietri's avatar
Yoann Pietri committed
273
            request,
274
            _("You don't have the right to edit the history.")
275
        )
276 277 278 279
        return redirect(reverse(
            'users:profil',
            kwargs={'userid': str(request.user.id)}
        ))
280
    return wrapper
281