From 84a901e3fc82ef4ccaf54491f0b5c50025a75654 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 7 May 2018 18:57:08 +0200 Subject: [PATCH] =?UTF-8?q?Documentation=20des=20d=C3=A9corateurs=20d'ACL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- re2o/acl.py | 124 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 98 insertions(+), 26 deletions(-) diff --git a/re2o/acl.py b/re2o/acl.py index b8be7a8d..bce941b5 100644 --- a/re2o/acl.py +++ b/re2o/acl.py @@ -37,24 +37,89 @@ from django.urls import reverse def acl_base_decorator(method_name, *targets, **kwargs): - """Base decorator for acl. It checks if the 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. + """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. + **kwargs: There is only one keyword argument, `on_instance`, which + default value is `True`. 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. """ on_instance = kwargs.get('on_instance', True) def group_targets(): + """This generator parses the targets of the decorator, yielding + 2-tuples of (model, [fields]). + """ current_target = None current_fields = [] - for t in targets: - if isinstance(t, type) and issubclass(t, Model): + for target in targets: + if isinstance(target, type) and issubclass(target, Model): if current_target: yield (current_target, current_fields) - current_target = t + current_target = target current_fields = [] else: - current_fields.append(t) + current_fields.append(target) yield (current_target, current_fields) def decorator(view): @@ -65,6 +130,11 @@ def acl_base_decorator(method_name, *targets, **kwargs): instances = [] 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) @@ -97,37 +167,37 @@ def acl_base_decorator(method_name, *targets, **kwargs): def can_create(*models): - """Decorator to check if an user can create a model. - It assumes that a valid user exists in the request and that the model has a - method can_create(user) which returns true if the user can create this kind - of 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 a model. - It tries to get an instance of the model, using - `model.get_instance(*args, **kwargs)` and assumes that the model has a - method `can_edit(user)` which returns `true` if the user can edit this - kind of models. + """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. - 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. """ return acl_base_decorator('can_change', *targets) def can_delete(*targets): """Decorator to check if an user can delete a model. - It tries to get an instance of the model, using - `model.get_instance(*args, **kwargs)` and assumes that the model has a - method `can_delete(user)` which returns `true` if the user can delete this - kind of 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_delete', *targets) @@ -162,16 +232,18 @@ def can_delete_set(model): def can_view(*targets): """Decorator to check if an user can view a model. - It tries to get an instance of the model, using - `model.get_instance(*args, **kwargs)` and assumes that the model has a - method `can_view(user)` which returns `true` if the user can view this - kind of models. + It runs `acl_base_decorator` with the flag `on_instance=True` and the + method 'can_view'. See `acl_base_decorator` documentation for further + details. """ return acl_base_decorator('can_view', *targets) def can_view_all(*targets): """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. """ return acl_base_decorator('can_view_all', *targets, on_instance=False)