8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-23 20:03:11 +00:00

Merge branch 'fix_121_prettier_ACL_decorators' into 'master'

Fix #121 prettier acl decorators

Closes #121

See merge request federez/re2o!155
This commit is contained in:
moamoak 2018-05-07 22:50:23 +02:00
commit 05bc645a65
5 changed files with 178 additions and 200 deletions

View file

@ -195,9 +195,7 @@ def revert_action(request, revision_id):
@login_required @login_required
@can_view_all(IpList) @can_view_all(IpList, Interface, User)
@can_view_all(Interface)
@can_view_all(User)
def stats_general(request): def stats_general(request):
"""Statistiques générales affinées sur les ip, activées, utilisées par """Statistiques générales affinées sur les ip, activées, utilisées par
range, et les statistiques générales sur les users : users actifs, range, et les statistiques générales sur les users : users actifs,
@ -313,10 +311,7 @@ def stats_general(request):
@login_required @login_required
@can_view_app('users') @can_view_app('users', 'cotisations', 'machines', 'topologie')
@can_view_app('cotisations')
@can_view_app('machines')
@can_view_app('topologie')
def stats_models(request): def stats_models(request):
"""Statistiques générales, affiche les comptages par models: """Statistiques générales, affiche les comptages par models:
nombre d'users, d'écoles, de droits, de bannissements, nombre d'users, d'écoles, de droits, de bannissements,

View file

@ -1272,12 +1272,7 @@ def index_nas(request):
@login_required @login_required
@can_view_all(SOA) @can_view_all(SOA, Mx, Ns, Txt, Srv, Extension)
@can_view_all(Mx)
@can_view_all(Ns)
@can_view_all(Txt)
@can_view_all(Srv)
@can_view_all(Extension)
def index_extension(request): def index_extension(request):
""" View displaying the list of existing extensions, the list of """ View displaying the list of existing extensions, the list of
existing SOA records, the list of existing MX records , the list of existing SOA records, the list of existing MX records , the list of

View file

@ -58,13 +58,8 @@ from . import forms
@login_required @login_required
@can_view_all(OptionalUser) @can_view_all(OptionalUser, OptionalMachine, OptionalTopologie, GeneralOption,
@can_view_all(OptionalMachine) AssoOption, MailMessageOption, HomeOption)
@can_view_all(OptionalTopologie)
@can_view_all(GeneralOption)
@can_view_all(AssoOption)
@can_view_all(MailMessageOption)
@can_view_all(HomeOption)
def display_options(request): def display_options(request):
"""Vue pour affichage des options (en vrac) classé selon les models """Vue pour affichage des options (en vrac) classé selon les models
correspondants dans un tableau""" correspondants dans un tableau"""
@ -149,7 +144,11 @@ def add_service(request):
@can_edit(Service) @can_edit(Service)
def edit_service(request, service_instance, **_kwargs): def edit_service(request, service_instance, **_kwargs):
"""Edition des services affichés sur la page d'accueil""" """Edition des services affichés sur la page d'accueil"""
service = ServiceForm(request.POST or None, request.FILES or None,instance=service_instance) service = ServiceForm(
request.POST or None,
request.FILES or None,
instance=service_instance
)
if service.is_valid(): if service.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
service.save() service.save()

View file

@ -28,135 +28,177 @@ Here are defined some decorators that can be used in views to handle ACL.
from __future__ import unicode_literals from __future__ import unicode_literals
import sys import sys
from itertools import chain
from django.db.models import Model
from django.contrib import messages from django.contrib import messages
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse from django.urls import reverse
def can_create(model): def acl_base_decorator(method_name, *targets, on_instance=True):
"""Decorator to check if an user can create a model. """Base decorator for acl. It checks if the `request.user` has the
It assumes that a valid user exists in the request and that the model has a permission by calling model.method_name. If the flag on_instance is True,
method can_create(user) which returns true if the user can create this kind tries to get an instance of the model by calling
of models. `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.
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.
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.
""" """
def group_targets():
"""This generator parses the targets of the decorator, yielding
2-tuples of (model, [fields]).
"""
current_target = None
current_fields = []
for target in targets:
if not isinstance(target, str):
if current_target:
yield (current_target, current_fields)
current_target = target
current_fields = []
else:
current_fields.append(target)
yield (current_target, current_fields)
def decorator(view): def decorator(view):
"""The decorator to use on a specific view """The decorator to use on a specific view
""" """
def wrapper(request, *args, **kwargs): def wrapper(request, *args, **kwargs):
"""The wrapper used for a specific request """The wrapper used for a specific request"""
""" instances = []
can, msg = model.can_create(request.user, *args, **kwargs)
if not can:
messages.error(
request, msg or "Vous ne pouvez pas accéder à ce menu")
return redirect(reverse('index'))
return view(request, *args, **kwargs)
return wrapper
return decorator
def process_target(target, fields):
"""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.
"""
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)
def can_edit(model, *field_list): error_messages = [
"""Decorator to check if an user can edit a model. x[1] for x in chain.from_iterable(
It tries to get an instance of the model, using process_target(x[0], x[1]) for x in group_targets()
`model.get_instance(*args, **kwargs)` and assumes that the model has a ) if not x[0]
method `can_edit(user)` which returns `true` if the user can edit this ]
kind of models. if error_messages:
""" for msg in error_messages:
def decorator(view):
"""The decorator to use on a specific view
"""
def wrapper(request, *args, **kwargs):
"""The wrapper used for a specific request
"""
try:
instance = model.get_instance(*args, **kwargs)
except model.DoesNotExist:
messages.error(request, u"Entrée inexistante")
return redirect(reverse(
'users:profil',
kwargs={'userid': str(request.user.id)}
))
can, msg = instance.can_edit(request.user)
if not can:
messages.error(
request, msg or "Vous ne pouvez pas accéder à ce menu")
return redirect(reverse(
'users:profil',
kwargs={'userid': str(request.user.id)}
))
for field in field_list:
can_change_fct = getattr(instance, 'can_change_' + field)
can, msg = can_change_fct(request.user, *args, **kwargs)
if not can:
messages.error( messages.error(
request, msg or "Vous ne pouvez pas accéder à ce menu") request, msg or "Vous ne pouvez pas accéder à ce menu")
return redirect(reverse( return redirect(reverse(
'users:profil', 'users:profil',
kwargs={'userid': str(request.user.id)} kwargs={'userid': str(request.user.id)}
)) ))
return view(request, instance, *args, **kwargs) return view(request, *chain(instances, args), **kwargs)
return wrapper return wrapper
return decorator return decorator
def can_change(model, *field_list): def can_create(*models):
"""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.
"""
return acl_base_decorator('can_create', *models, on_instance=False)
def can_edit(*targets):
"""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.
"""
return acl_base_decorator('can_edit', *targets)
def can_change(*targets):
"""Decorator to check if an user can edit a field of a model class. """Decorator to check if an user can edit a field of a model class.
Difference with can_edit : take a class and not an instance 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.
""" """
def decorator(view): return acl_base_decorator('can_change', *targets)
"""The decorator to use on a specific view
"""
def wrapper(request, *args, **kwargs):
"""The wrapper used for a specific request
"""
for field in field_list:
can_change_fct = getattr(model, 'can_change_' + field)
can, msg = can_change_fct(request.user, *args, **kwargs)
if not can:
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, *args, **kwargs)
return wrapper
return decorator
def can_delete(model): def can_delete(*targets):
"""Decorator to check if an user can delete a model. """Decorator to check if an user can delete a model.
It tries to get an instance of the model, using It runs `acl_base_decorator` with the flag `on_instance=True` and the
`model.get_instance(*args, **kwargs)` and assumes that the model has a method 'can_edit'. See `acl_base_decorator` documentation for further
method `can_delete(user)` which returns `true` if the user can delete this details.
kind of models.
""" """
def decorator(view): return acl_base_decorator('can_delete', *targets)
"""The decorator to use on a specific view
"""
def wrapper(request, *args, **kwargs):
"""The wrapper used for a specific request
"""
try:
instance = model.get_instance(*args, **kwargs)
except model.DoesNotExist:
messages.error(request, u"Entrée inexistante")
return redirect(reverse(
'users:profil',
kwargs={'userid': str(request.user.id)}
))
can, msg = instance.can_delete(request.user)
if not can:
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, instance, *args, **kwargs)
return wrapper
return decorator
def can_delete_set(model): def can_delete_set(model):
@ -187,84 +229,34 @@ def can_delete_set(model):
return decorator return decorator
def can_view(model): def can_view(*targets):
"""Decorator to check if an user can view a model. """Decorator to check if an user can view a model.
It tries to get an instance of the model, using It runs `acl_base_decorator` with the flag `on_instance=True` and the
`model.get_instance(*args, **kwargs)` and assumes that the model has a method 'can_view'. See `acl_base_decorator` documentation for further
method `can_view(user)` which returns `true` if the user can view this details.
kind of models.
""" """
def decorator(view): return acl_base_decorator('can_view', *targets)
"""The decorator to use on a specific view
"""
def wrapper(request, *args, **kwargs):
"""The wrapper used for a specific request
"""
try:
instance = model.get_instance(*args, **kwargs)
except model.DoesNotExist:
messages.error(request, u"Entrée inexistante")
return redirect(reverse(
'users:profil',
kwargs={'userid': str(request.user.id)}
))
can, msg = instance.can_view(request.user)
if not can:
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, instance, *args, **kwargs)
return wrapper
return decorator
def can_view_all(model): def can_view_all(*targets):
"""Decorator to check if an user can view a class of model. """Decorator to check if an user can view a class of model.
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.
""" """
def decorator(view): return acl_base_decorator('can_view_all', *targets, on_instance=False)
"""The decorator to use on a specific view
"""
def wrapper(request, *args, **kwargs):
"""The wrapper used for a specific request
"""
can, msg = model.can_view_all(request.user)
if not can:
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, *args, **kwargs)
return wrapper
return decorator
def can_view_app(app_name): def can_view_app(*apps_name):
"""Decorator to check if an user can view an application. """Decorator to check if an user can view the applications.
""" """
assert app_name in sys.modules.keys() for app_name in apps_name:
assert app_name in sys.modules.keys()
def decorator(view): return acl_base_decorator(
"""The decorator to use on a specific view 'can_view',
""" *chain(sys.modules[app_name] for app_name in apps_name),
def wrapper(request, *args, **kwargs): on_instance=False
"""The wrapper used for a specific request )
"""
app = sys.modules[app_name]
can, msg = app.can_view(request.user)
if can:
return view(request, *args, **kwargs)
messages.error(request, msg)
return redirect(reverse(
'users:profil',
kwargs={'userid': str(request.user.id)}
))
return wrapper
return decorator
def can_edit_history(view): def can_edit_history(view):

View file

@ -200,9 +200,7 @@ def index_ap(request):
@login_required @login_required
@can_view_all(Stack) @can_view_all(Stack, Building, SwitchBay)
@can_view_all(Building)
@can_view_all(SwitchBay)
def index_physical_grouping(request): def index_physical_grouping(request):
"""Affichage de la liste des stacks (affiche l'ensemble des switches)""" """Affichage de la liste des stacks (affiche l'ensemble des switches)"""
stack_list = (Stack.objects stack_list = (Stack.objects
@ -241,8 +239,7 @@ def index_physical_grouping(request):
@login_required @login_required
@can_view_all(ModelSwitch) @can_view_all(ModelSwitch, ConstructorSwitch)
@can_view_all(ConstructorSwitch)
def index_model_switch(request): def index_model_switch(request):
""" Affichage de l'ensemble des modèles de switches""" """ Affichage de l'ensemble des modèles de switches"""
model_switch_list = ModelSwitch.objects.select_related('constructor') model_switch_list = ModelSwitch.objects.select_related('constructor')