diff --git a/api/permissions.py b/api/permissions.py index 1983bdc8..646682c4 100644 --- a/api/permissions.py +++ b/api/permissions.py @@ -239,6 +239,11 @@ class AutodetectACLPermission(permissions.BasePermission): if getattr(view, "_ignore_model_permissions", False): return True + # Bypass permission verifications if it is a functional view + # (permissions are handled by ACL) + if not getattr(view, "queryset", getattr(view, "get_queryset", None)): + return True + if not request.user or not request.user.is_authenticated: return False diff --git a/preferences/views.py b/preferences/views.py index 077fd105..5eab39f4 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -150,7 +150,8 @@ def display_options(request): optionnal_templates_list = [ app.preferences.views.aff_preferences(request) for app in optionnal_apps - if hasattr(app.preferences.views, "aff_preferences") + if hasattr(app, "preferences") + and hasattr(app.preferences.views, "aff_preferences") ] return form( @@ -347,7 +348,10 @@ def add_switchmanagementcred(request): messages.success(request, _("The switch management credentials were added.")) return redirect(reverse("preferences:display-options")) return form( - {"preferenceform": switchmanagementcred, "action_name": _("Add"),}, + { + "preferenceform": switchmanagementcred, + "action_name": _("Add"), + }, "preferences/preferences.html", request, ) @@ -410,7 +414,10 @@ def add_mailcontact(request): messages.success(request, _("The contact email address was created.")) return redirect(reverse("preferences:display-options")) return form( - {"preferenceform": mailcontact, "action_name": _("Add"),}, + { + "preferenceform": mailcontact, + "action_name": _("Add"), + }, "preferences/preferences.html", request, ) diff --git a/re2o/acl.py b/re2o/acl.py index e73ffdfa..f74427ea 100644 --- a/re2o/acl.py +++ b/re2o/acl.py @@ -35,6 +35,7 @@ from django.contrib import messages from django.shortcuts import redirect from django.urls import reverse from django.utils.translation import ugettext as _ +from rest_framework.response import Response from re2o.utils import get_group_having_permission @@ -60,7 +61,7 @@ def acl_error_message(msg, permissions): # This is the function of main interest of this file. Almost all the decorators # use it, and it is a fairly complicated piece of code. Let me guide you through # this ! 🌈😸 -def acl_base_decorator(method_name, *targets, on_instance=True): +def acl_base_decorator(method_name, *targets, on_instance=True, api=False): """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 @@ -121,6 +122,9 @@ on_instance=False) method `get_instance` of the model, with the arguments originally passed to the view. + api: when set to True, errors will no longer trigger redirection and + messages but will send a 403 with errors in JSON + 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 @@ -175,8 +179,7 @@ ModelC) # `wrapper` inside the `decorator` function, you need to read some #  documentation on decorators ! def decorator(view): - """The decorator to use on a specific view - """ + """The decorator to use on a specific view""" def wrapper(request, *args, **kwargs): """The wrapper used for a specific request""" @@ -243,23 +246,36 @@ ModelC) warning_messages.append(acl_error_message(msg, permissions)) # Display the warning messages - if warning_messages: - for msg in warning_messages: - messages.warning(request, msg) + if not api: + if warning_messages: + for msg in warning_messages: + messages.warning(request, msg) # If there is any error message, then the request must be denied. if error_messages: # We display the message - for msg in error_messages: - messages.error( - request, - msg or _("You don't have the right to access this menu."), - ) + if not api: + for msg in error_messages: + messages.error( + request, + msg or _("You don't have the right to access this menu."), + ) # And redirect the user to the right place. if request.user.id is not None: - return redirect( - reverse("users:profil", kwargs={"userid": str(request.user.id)}) - ) + if not api: + return redirect( + reverse( + "users:profil", kwargs={"userid": str(request.user.id)} + ) + ) + else: + return Response( + data={ + "errors": error_messages, + "warning": warning_messages, + }, + status=403, + ) else: return redirect(reverse("index")) return view(request, *chain(instances, args), **kwargs) @@ -310,12 +326,10 @@ def can_delete_set(model): If none of them, return an error""" def decorator(view): - """The decorator to use on a specific view - """ + """The decorator to use on a specific view""" def wrapper(request, *args, **kwargs): - """The wrapper used for a specific request - """ + """The wrapper used for a specific request""" all_objects = model.objects.all() instances_id = [] for instance in all_objects: @@ -356,8 +370,7 @@ def can_view_all(*targets): def can_view_app(*apps_name): - """Decorator to check if an user can view the applications. - """ + """Decorator to check if an user can view the applications.""" for app_name in apps_name: assert app_name in sys.modules.keys() return acl_base_decorator( @@ -371,8 +384,7 @@ def can_edit_history(view): """Decorator to check if an user can edit history.""" def wrapper(request, *args, **kwargs): - """The wrapper used for a specific request - """ + """The wrapper used for a specific request""" if request.user.has_perm("admin.change_logentry"): return view(request, *args, **kwargs) messages.error(request, _("You don't have the right to edit the history.")) @@ -381,3 +393,29 @@ def can_edit_history(view): ) return wrapper + + +def can_view_all_api(*models): + """Decorator to check if an user can see an api page + Only used on functionnal api views (class-based api views ACL are checked + in api/permissions.py) + """ + return acl_base_decorator("can_view_all", *models, on_instance=False, api=True) + + +def can_edit_all_api(*models): + """Decorator to check if an user can edit via the api + We do not always know which instances will be edited, so we may need to know + if the user can edit any instance. + Only used on functionnal api views (class-based api views ACL are checked + in api/permissions.py) + """ + return acl_base_decorator("can_edit_all", *models, on_instance=False, api=True) + + +def can_create_api(*models): + """Decorator to check if an user can create the given models. via the api + Only used on functionnal api views (class-based api views ACL are checked + in api/permissions.py) + """ + return acl_base_decorator("can_create", *models, on_instance=False, api=True) diff --git a/re2o/mixins.py b/re2o/mixins.py index 44cd517e..26034e07 100644 --- a/re2o/mixins.py +++ b/re2o/mixins.py @@ -29,9 +29,9 @@ from django.utils.translation import ugettext as _ class RevMixin(object): - """ A mixin to subclass the save and delete function of a model + """A mixin to subclass the save and delete function of a model to enforce the versioning of the object before those actions - really happen """ + really happen""" def save(self, *args, **kwargs): """ Creates a version of this object and save it to database """ @@ -49,8 +49,8 @@ class RevMixin(object): class FormRevMixin(object): - """ A mixin to subclass the save function of a form - to enforce the versionning of the object before it is really edited """ + """A mixin to subclass the save function of a form + to enforce the versionning of the object before it is really edited""" def save(self, *args, **kwargs): """ Create a version of this object and save it to database """ @@ -129,7 +129,7 @@ class AclMixin(object): Parameters: user_request: User calling for this action - self: Instance to edit + self: Instance to edit Returns: Boolean: True if user_request has the right access to do it, else @@ -150,7 +150,7 @@ class AclMixin(object): Parameters: user_request: User calling for this action - self: Instance to delete + self: Instance to delete Returns: Boolean: True if user_request has the right access to do it, else @@ -187,12 +187,33 @@ class AclMixin(object): (permission,), ) + @classmethod + def can_edit_all(cls, user_request, *_args, **_kwargs): + """Check if a user can edit all instances of an object + + Parameters: + user_request: User calling for this action + + Returns: + Boolean: True if user_request has the right access to do it, else + false with reason for reject authorization + """ + permission = cls.get_modulename() + ".change_" + cls.get_classname() + can = user_request.has_perm(permission) + return ( + can, + _("You don't have the right to edit every %s object.") % cls.get_classname() + if not can + else None, + (permission,), + ) + def can_view(self, user_request, *_args, **_kwargs): """Check if a user can view an instance of an object Parameters: user_request: User calling for this action - self: Instance to view + self: Instance to view Returns: Boolean: True if user_request has the right access to do it, else