acl.py 13 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
# -*- 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  Maël Kervella
#
# 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.

"""
23
Set of templatetags for using acl in templates:
24 25 26 27 28 29 30
    - can_create (model)
    - cannot_create (model)
    - can_edit (instance)
    - cannot_edit (instance)

Some templatetags require a model to calculate the acl while others are need
an instance of a model (either Model.can_xxx or instance.can_xxx)
31 32

**Parameters**:
33 34 35
    model_name or instance - Either the model_name (if templatetag is based on
        model) or an instantiated object (if templatetag is base on instance)
        that needs to be checked for the current user
36 37
    args - Any other argument that is interpreted as a python object and passed
        to the acl function (can_xxx)
38 39

**Usage**:
40
    {% <acl_name> <obj> [arg1 [arg2 [...]]]%}
41
    <template stuff>
42
    [{% acl_else %}
43
    <template stuff>]
44
    {% acl_end %}
45 46 47 48 49

    where <acl_name> is one of the templatetag names available
    (can_xxx or cannot_xxx)

**Example**:
50
    {% can_create Machine targeted_user %}
51
    <p>I'm authorized to create new machines.models.for this guy \\o/</p>
52
    {% acl_else %}
53
    <p>Why can't I create a little machine for this guy ? :(</p>
54 55 56 57 58 59 60
    {% acl_end %}

    {% can_edit user %}
    <p>Oh I can edit myself oO</p>
    {% acl_else %}
    <p>Sniff can't edit my own infos ...</p>
    {% acl_end %}
61

62
**How to modify**:
63 64 65 66 67 68 69 70
    To add a new acl function (can_xxx or cannot_xxx),
    - if it's based on a model (like can_create), add an entry in
        'get_callback' and register your tag with the other ones juste before
        'acl_model_generic' definition
    - if it's bases on an instance (like can_edit), just register yout tag with
        the other ones juste before 'acl_instance_generic' definition
    To add support for a new model, add an entry in 'get_model' and be sure
    the acl function exists in the model definition
71

72
"""
73
import sys
74 75 76

from django import template
from django.template.base import Node, NodeList
77
from django.contrib.contenttypes.models import ContentType
78 79 80 81


register = template.Library()

82

83 84
def get_model(model_name):
    """Retrieve the model object from its name"""
85 86 87 88 89 90 91 92 93 94
    splitted = model_name.split('.')
    if len(splitted) > 1:
        try:
            app_label, name = splitted
        except ValueError:
            raise template.TemplateSyntaxError(
                "%r is an inconsistent model name" % model_name
            )
    else:
        app_label, name = None, splitted[0]
95
    try:
96 97 98 99 100 101 102 103
        if app_label is not None:
            content_type = ContentType.objects.get(
                model=name,
                app_label=app_label
            )
        else:
            content_type = ContentType.objects.get(model=name)
    except ContentType.DoesNotExist:
104 105 106
        raise template.TemplateSyntaxError(
            "%r is not a valid model for an acl tag" % model_name
        )
107 108 109 110 111 112
    except ContentType.MultipleObjectsReturned:
        raise template.TemplateSyntaxError(
            "More than one model found for %r. Try with `app.model`."
            % model_name
        )
    return content_type.model_class()
113 114


115
def get_callback(tag_name, obj=None):
116 117
    """Return the right function to call back to check for acl"""

118
    if tag_name == 'can_create':
119
        return acl_fct(obj.can_create, False)
120
    if tag_name == 'cannot_create':
121 122 123 124 125
        return acl_fct(obj.can_create, True)
    if tag_name == 'can_edit':
        return acl_fct(obj.can_edit, False)
    if tag_name == 'cannot_edit':
        return acl_fct(obj.can_edit, True)
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
    if tag_name == 'can_edit_all':
        return acl_fct(obj.can_edit_all, False)
    if tag_name == 'cannot_edit_all':
        return acl_fct(obj.can_edit_all, True)
    if tag_name == 'can_delete':
        return acl_fct(obj.can_delete, False)
    if tag_name == 'cannot_delete':
        return acl_fct(obj.can_delete, True)
    if tag_name == 'can_delete_all':
        return acl_fct(obj.can_delete_all, False)
    if tag_name == 'cannot_delete_all':
        return acl_fct(obj.can_delete_all, True)
    if tag_name == 'can_view':
        return acl_fct(obj.can_view, False)
    if tag_name == 'cannot_view':
        return acl_fct(obj.can_view, True)
    if tag_name == 'can_view_all':
        return acl_fct(obj.can_view_all, False)
    if tag_name == 'cannot_view_all':
        return acl_fct(obj.can_view_all, True)
146
    if tag_name == 'can_view_app':
Maël Kervella's avatar
Maël Kervella committed
147 148 149 150 151 152 153
        return acl_fct(
            lambda x: (
                not any(not sys.modules[o].can_view(x) for o in obj),
                None
            ),
            False
        )
154
    if tag_name == 'cannot_view_app':
Maël Kervella's avatar
Maël Kervella committed
155 156 157 158 159 160 161
        return acl_fct(
            lambda x: (
                not any(not sys.modules[o].can_view(x) for o in obj),
                None
            ),
            True
        )
162
    if tag_name == 'can_edit_history':
Maël Kervella's avatar
Maël Kervella committed
163 164 165 166
        return acl_fct(
            lambda user: (user.has_perm('admin.change_logentry'), None),
            False
        )
167
    if tag_name == 'cannot_edit_history':
Maël Kervella's avatar
Maël Kervella committed
168 169 170 171
        return acl_fct(
            lambda user: (user.has_perm('admin.change_logentry'), None),
            True
        )
172
    if tag_name == 'can_view_any_app':
Maël Kervella's avatar
Maël Kervella committed
173 174 175 176
        return acl_fct(
            lambda x: (any(sys.modules[o].can_view(x) for o in obj), None),
            False
        )
177
    if tag_name == 'cannot_view_any_app':
Maël Kervella's avatar
Maël Kervella committed
178 179 180 181
        return acl_fct(
            lambda x: (any(sys.modules[o].can_view(x) for o in obj), None),
            True
        )
182

183 184 185
    raise template.TemplateSyntaxError(
        "%r tag is not a valid can_xxx tag" % tag_name
    )
186 187


188
def acl_fct(callback, reverse):
189 190
    """Build a function to use as an acl checker"""

191
    def acl_fct_normal(user, *args, **kwargs):
192
        """The can_xxx checker callback"""
193
        return callback(user, *args, **kwargs)
194

195
    def acl_fct_reverse(user, *args, **kwargs):
196
        """The cannot_xxx checker callback"""
197
        can, msg = callback(user, *args, **kwargs)
198
        return not can, msg
199

200 201
    return acl_fct_reverse if reverse else acl_fct_normal

202 203 204 205 206

@register.tag('can_edit_history')
@register.tag('cannot_edit_history')
def acl_history_filter(parser, token):
    """Templatetag for acl checking on history."""
207
    tag_name, = token.split_contents()
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222

    callback = get_callback(tag_name)
    oknodes = parser.parse(('acl_else', 'acl_end'))
    token = parser.next_token()
    if token.contents == 'acl_else':
        konodes = parser.parse(('acl_end'))
        token = parser.next_token()
    else:
        konodes = NodeList()

    assert token.contents == 'acl_end'

    return AclNode(callback, oknodes, konodes)


223 224
@register.tag('can_view_any_app')
@register.tag('cannot_view_any_app')
225 226 227 228 229
@register.tag('can_view_app')
@register.tag('cannot_view_app')
def acl_app_filter(parser, token):
    """Templatetag for acl checking on applications."""
    try:
230
        tag_name, *app_name = token.split_contents()
231 232
    except ValueError:
        raise template.TemplateSyntaxError(
Maël Kervella's avatar
Maël Kervella committed
233
            "%r tag require 1 argument: an application"
234 235
            % token.contents.split()[0]
        )
236
    for name in app_name:
Maël Kervella's avatar
Maël Kervella committed
237
        if name not in sys.modules.keys():
238 239 240 241
            raise template.TemplateSyntaxError(
                "%r is not a registered application for acl."
                % name
            )
242 243 244 245 246 247 248 249 250 251 252 253 254

    callback = get_callback(tag_name, app_name)

    oknodes = parser.parse(('acl_else', 'acl_end'))
    token = parser.next_token()
    if token.contents == 'acl_else':
        konodes = parser.parse(('acl_end'))
        token = parser.next_token()
    else:
        konodes = NodeList()

    assert token.contents == 'acl_end'

255
    return AclNode(callback, oknodes, konodes)
256

Maël Kervella's avatar
Maël Kervella committed
257

258 259 260 261 262 263 264
@register.tag('can_change')
@register.tag('cannot_change')
def acl_change_filter(parser, token):
    """Templatetag for acl checking a can_change_xxx function"""

    try:
        tag_content = token.split_contents()
265
        # tag_name = tag_content[0]
266 267 268 269 270
        model_name = tag_content[1]
        field_name = tag_content[2]
        args = tag_content[3:]
    except ValueError:
        raise template.TemplateSyntaxError(
Maël Kervella's avatar
Maël Kervella committed
271
            "%r tag require at least 2 argument: the model and the field"
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
            % token.contents.split()[0]
        )

    model = get_model(model_name)
    callback = getattr(model, 'can_change_'+field_name)

    # {% can_create %}
    oknodes = parser.parse(('acl_else', 'acl_end'))
    token = parser.next_token()

    # {% can_create_else %}
    if token.contents == 'acl_else':
        konodes = parser.parse(('acl_end'))
        token = parser.next_token()
    else:
        konodes = NodeList()

    # {% can_create_end %}
    assert token.contents == 'acl_end'

    return AclNode(callback, oknodes, konodes, *args)
293

Maël Kervella's avatar
Maël Kervella committed
294

295 296
@register.tag('can_create')
@register.tag('cannot_create')
297 298 299 300 301 302
@register.tag('can_edit_all')
@register.tag('cannot_edit_all')
@register.tag('can_delete_all')
@register.tag('cannot_delete_all')
@register.tag('can_view_all')
@register.tag('cannot_view_all')
303 304
def acl_model_filter(parser, token):
    """Generic definition of an acl templatetag for acl based on model"""
305 306

    try:
307 308 309 310
        tag_content = token.split_contents()
        tag_name = tag_content[0]
        model_name = tag_content[1]
        args = tag_content[2:]
311 312
    except ValueError:
        raise template.TemplateSyntaxError(
Maël Kervella's avatar
Maël Kervella committed
313
            "%r tag require at least 1 argument: the model"
314
            % token.contents.split()[0]
315 316
        )

317 318
    model = get_model(model_name)
    callback = get_callback(tag_name, model)
319 320

    # {% can_create %}
321
    oknodes = parser.parse(('acl_else', 'acl_end'))
322 323 324
    token = parser.next_token()

    # {% can_create_else %}
325 326
    if token.contents == 'acl_else':
        konodes = parser.parse(('acl_end'))
327 328 329 330 331
        token = parser.next_token()
    else:
        konodes = NodeList()

    # {% can_create_end %}
332
    assert token.contents == 'acl_end'
333

334
    return AclNode(callback, oknodes, konodes, *args)
335 336


337 338
@register.tag('can_edit')
@register.tag('cannot_edit')
339 340 341 342
@register.tag('can_delete')
@register.tag('cannot_delete')
@register.tag('can_view')
@register.tag('cannot_view')
343 344 345 346 347 348 349 350 351 352
def acl_instance_filter(parser, token):
    """Generic definition of an acl templatetag for acl based on instance"""

    try:
        tag_content = token.split_contents()
        tag_name = tag_content[0]
        instance_name = tag_content[1]
        args = tag_content[2:]
    except ValueError:
        raise template.TemplateSyntaxError(
Maël Kervella's avatar
Maël Kervella committed
353
            "%r tag require at least 1 argument: the instance"
354 355 356 357
            % token.contents.split()[0]
        )

    # {% can_create %}
358
    oknodes = parser.parse(('acl_else', 'acl_end'))
359 360 361
    token = parser.next_token()

    # {% can_create_else %}
362 363
    if token.contents == 'acl_else':
        konodes = parser.parse(('acl_end'))
364 365 366 367 368
        token = parser.next_token()
    else:
        konodes = NodeList()

    # {% can_create_end %}
369
    assert token.contents == 'acl_end'
370 371 372 373

    return AclInstanceNode(tag_name, instance_name, oknodes, konodes, *args)


374 375 376
class AclNode(Node):
    """A node for the compiled ACL block when acl callback doesn't require
    context."""
377

378
    def __init__(self, callback, oknodes, konodes, *args):
379 380 381
        self.callback = callback
        self.oknodes = oknodes
        self.konodes = konodes
382
        self.args = [template.Variable(arg) for arg in args]
383 384

    def render(self, context):
385
        resolved_args = [arg.resolve(context) for arg in self.args]
Gabriel Detraz's avatar
Gabriel Detraz committed
386 387 388 389
        if context['user'].is_anonymous():
            can = False
        else:
            can, _ = self.callback(context['user'], *(resolved_args))
390 391 392
        if can:
            return self.oknodes.render(context)
        return self.konodes.render(context)
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407


class AclInstanceNode(Node):
    """A node for the compiled ACL block when acl is based on instance"""

    def __init__(self, tag_name, instance_name, oknodes, konodes, *args):
        self.tag_name = tag_name
        self.instance = template.Variable(instance_name)
        self.oknodes = oknodes
        self.konodes = konodes
        self.args = [template.Variable(arg) for arg in args]

    def render(self, context):
        callback = get_callback(self.tag_name, self.instance.resolve(context))
        resolved_args = [arg.resolve(context) for arg in self.args]
Gabriel Detraz's avatar
Gabriel Detraz committed
408 409 410 411
        if context['user'].is_anonymous():
            can = False
        else:
            can, _ = callback(context['user'], *(resolved_args))
412 413 414
        if can:
            return self.oknodes.render(context)
        return self.konodes.render(context)