acl.py 12.2 KB
Newer Older
1 2 3 4 5 6
# -*- 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
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
7
# Copyright © 2017  Lara Kermarec
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
# 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
Laouen Fernet's avatar
Laouen Fernet committed
37
from django.utils.translation import ugettext as _
38

39 40 41
from re2o.utils import get_group_having_permission


42 43
def acl_error_message(msg, permissions):
    """Create an error message for msg and permissions."""
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
44 45
    if permissions is None:
        return msg
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
46
    groups = ", ".join([g.name for g in get_group_having_permission(*permissions)])
47
    message = msg or _("You don't have the right to edit this option.")
48
    if groups:
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
49 50
        return (
            message
51
            + _("You need to be a member of one of these groups: %s.") % groups
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
52
        )
53
    else:
54
        return message + _("No group has the %s permission(s)!") % " or ".join(
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
55 56 57
            [",".join(permissions[:-1]), permissions[-1]]
            if len(permissions) > 2
            else permissions
58
        )
59

60

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
61
def acl_base_decorator(method_name, *targets, on_instance=True):
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
    """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
77
            `(can, reason, permissions)` with `can` being a boolean stating
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
78
            whether the access is granted, `reason` an arror message to be
79
            displayed if `can` equals `False` (can be `None`) and `permissions`
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
80 81 82
            a list of permissions needed for access (can be `None`). If can is
            True and permission is not `None`, a warning message will be
            displayed.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
        *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
110 111 112 113 114
        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.
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129

    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.
130
    """
131 132

    def group_targets():
133 134 135
        """This generator parses the targets of the decorator, yielding
        2-tuples of (model, [fields]).
        """
136 137
        current_target = None
        current_fields = []
138
        for target in targets:
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
139
            if not isinstance(target, str):
140 141
                if current_target:
                    yield (current_target, current_fields)
142
                current_target = target
143 144
                current_fields = []
            else:
145
                current_fields.append(target)
146 147
        yield (current_target, current_fields)

148
    def decorator(view):
Maël Kervella's avatar
Maël Kervella committed
149 150
        """The decorator to use on a specific view
        """
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
151

152
        def wrapper(request, *args, **kwargs):
153 154 155 156
            """The wrapper used for a specific request"""
            instances = []

            def process_target(target, fields):
157 158 159 160 161
                """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.
                """
162 163 164 165 166
                if on_instance:
                    try:
                        target = target.get_instance(*args, **kwargs)
                        instances.append(target)
                    except target.DoesNotExist:
167
                        yield False, _("Nonexistent entry."), []
168 169 170 171 172
                        return
                if hasattr(target, method_name):
                    can_fct = getattr(target, method_name)
                    yield can_fct(request.user, *args, **kwargs)
                for field in fields:
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
173
                    can_change_fct = getattr(target, "can_change_" + field)
174
                    yield can_change_fct(request.user, *args, **kwargs)
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
175

176
            error_messages = []
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
177
            warning_messages = []
178 179 180
            for target, fields in group_targets():
                for can, msg, permissions in process_target(target, fields):
                    if not can:
181
                        error_messages.append(acl_error_message(msg, permissions))
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
182 183 184 185 186 187
                    elif msg:
                        warning_messages.append(acl_error_message(msg, permissions))

            if warning_messages:
                for msg in warning_messages:
                    messages.warning(request, msg)
188

189 190 191
            if error_messages:
                for msg in error_messages:
                    messages.error(
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
192
                        request,
193
                        msg or _("You don't have the right to access this menu."),
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
194
                    )
195
                if request.user.id is not None:
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
196 197 198
                    return redirect(
                        reverse("users:profil", kwargs={"userid": str(request.user.id)})
                    )
199
                else:
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
200
                    return redirect(reverse("index"))
201
            return view(request, *chain(instances, args), **kwargs)
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
202

203
        return wrapper
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
204

205 206 207
    return decorator


208
def can_create(*models):
209 210 211
    """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.
212
    """
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
213
    return acl_base_decorator("can_create", *models, on_instance=False)
214 215 216


def can_edit(*targets):
217 218 219 220
    """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.
221
    """
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
222
    return acl_base_decorator("can_edit", *targets)
223 224


225
def can_change(*targets):
226
    """Decorator to check if an user can edit a field of a model class.
227 228 229 230
    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.
231
    """
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
232
    return acl_base_decorator("can_change", *targets, on_instance=False)
233 234


235
def can_delete(*targets):
236
    """Decorator to check if an user can delete a model.
237 238 239
    It runs `acl_base_decorator` with the flag `on_instance=True` and the
    method 'can_edit'. See `acl_base_decorator` documentation for further
    details.
240
    """
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
241
    return acl_base_decorator("can_delete", *targets)
242 243 244 245 246


def can_delete_set(model):
    """Decorator which returns a list of detable models by request user.
    If none of them, return an error"""
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
247

248
    def decorator(view):
Maël Kervella's avatar
Maël Kervella committed
249 250
        """The decorator to use on a specific view
        """
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
251

252
        def wrapper(request, *args, **kwargs):
Maël Kervella's avatar
Maël Kervella committed
253 254
            """The wrapper used for a specific request
            """
255 256 257
            all_objects = model.objects.all()
            instances_id = []
            for instance in all_objects:
Gabriel Detraz's avatar
Gabriel Detraz committed
258
                can, _msg, _reason = instance.can_delete(request.user)
259 260 261 262
                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
263
                messages.error(
Laouen Fernet's avatar
Laouen Fernet committed
264 265
                    request, _("You don't have the right to access this menu.")
                )
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
266 267 268
                return redirect(
                    reverse("users:profil", kwargs={"userid": str(request.user.id)})
                )
269
            return view(request, instances, *args, **kwargs)
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
270

271
        return wrapper
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
272

273 274 275
    return decorator


276
def can_view(*targets):
277
    """Decorator to check if an user can view a model.
278 279 280
    It runs `acl_base_decorator` with the flag `on_instance=True` and the
    method 'can_view'. See `acl_base_decorator` documentation for further
    details.
281
    """
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
282
    return acl_base_decorator("can_view", *targets)
283 284


285
def can_view_all(*targets):
286
    """Decorator to check if an user can view a class of model.
287 288 289
    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.
290
    """
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
291
    return acl_base_decorator("can_view_all", *targets, on_instance=False)
292 293


294 295
def can_view_app(*apps_name):
    """Decorator to check if an user can view the applications.
296
    """
297 298
    for app_name in apps_name:
        assert app_name in sys.modules.keys()
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
299
    return acl_base_decorator(
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
300
        "can_view",
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
301 302 303
        *chain(sys.modules[app_name] for app_name in apps_name),
        on_instance=False
    )
304 305 306 307


def can_edit_history(view):
    """Decorator to check if an user can edit history."""
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
308

309
    def wrapper(request, *args, **kwargs):
Maël Kervella's avatar
Maël Kervella committed
310 311
        """The wrapper used for a specific request
        """
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
312
        if request.user.has_perm("admin.change_logentry"):
313
            return view(request, *args, **kwargs)
Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
314 315 316
        messages.error(request, _("You don't have the right to edit the history."))
        return redirect(
            reverse("users:profil", kwargs={"userid": str(request.user.id)})
317
        )
Laouen Fernet's avatar
Laouen Fernet committed
318

Hugo Levy-Falk's avatar
Hugo Levy-Falk committed
319
    return wrapper