From e4d53e67916f295a972160f0c039b85a0ef9f7f2 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 16 Apr 2018 23:57:22 +0200 Subject: [PATCH 01/25] Fix #86 --- machines/templates/machines/aff_machines.html | 1 - users/templates/users/profil.html | 2 +- users/views.py | 2 ++ 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/machines/templates/machines/aff_machines.html b/machines/templates/machines/aff_machines.html index f6da4165..56eec21d 100644 --- a/machines/templates/machines/aff_machines.html +++ b/machines/templates/machines/aff_machines.html @@ -28,7 +28,6 @@ with this program; if not, write to the Free Software Foundation, Inc., {% include "pagination.html" with list=machines_list %} {% endif %} - diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html index 169524f5..abba61a2 100644 --- a/users/templates/users/profil.html +++ b/users/templates/users/profil.html @@ -245,7 +245,7 @@ non adhérent{% endif %} et votre connexion est {% if users.has_access %}

Machines - {{machines_list.count}} + {{nb_machines}}

diff --git a/users/views.py b/users/views.py index a86a6e47..2614cb9c 100644 --- a/users/views.py +++ b/users/views.py @@ -814,6 +814,7 @@ def profil(request, users, **_kwargs): pagination_large_number = GeneralOption.get_cached_value( 'pagination_large_number' ) + nb_machines = machines.count() machines = re2o_paginator(request, machines, pagination_large_number) factures = Facture.objects.filter(user=users) factures = SortTable.sort( @@ -844,6 +845,7 @@ def profil(request, users, **_kwargs): { 'users': users, 'machines_list': machines, + 'nb_machines' : nb_machines, 'facture_list': factures, 'ban_list': bans, 'white_list': whitelists, From d2946a94b584f6b7cf0c3f5c9128ac7e1b5bc810 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Tue, 17 Apr 2018 00:32:37 +0200 Subject: [PATCH 02/25] affiche les superuser --- users/templates/users/aff_listright.html | 44 ++++++++++++++++++++++-- users/views.py | 6 +++- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/users/templates/users/aff_listright.html b/users/templates/users/aff_listright.html index be94b146..7807c1d3 100644 --- a/users/templates/users/aff_listright.html +++ b/users/templates/users/aff_listright.html @@ -33,6 +33,44 @@ with this program; if not, write to the Free Software Foundation, Inc.,
+ {% if superuser_right %} + + + + + + + + + + + + + {% endif %} {% for listright in listright_list %} - + + + {% endif %}
SuperuserTrue + + + Donne tous les droits sur Re2o. + +
+
+
+
+
    + {% for user in superuser_right %} +
  • + {{user}} + + + +
  • + {% endfor %} +
+
+
+
+
@@ -48,9 +86,9 @@ with this program; if not, write to the Free Software Foundation, Inc., - - {{ listright.details }}{{ listright.details }} {% include 'buttons/edit.html' with href='users:edit-listright' id=listright.id %} {% include 'buttons/history.html' with href='users:history' name='listright' id=listright.id %} diff --git a/users/views.py b/users/views.py index 2614cb9c..797b3eb2 100644 --- a/users/views.py +++ b/users/views.py @@ -768,10 +768,14 @@ def index_listright(request): """ Affiche l'ensemble des droits""" listright_list = ListRight.objects.order_by('unix_name')\ .prefetch_related('permissions').prefetch_related('user_set') + superuser_right = User.objects.filter(is_superuser=True) return render( request, 'users/index_listright.html', - {'listright_list': listright_list} + { + 'listright_list': listright_list, + 'superuser_right' : superuser_right, + } ) From 9569e22df9d44239b0eca0a5f5d9247f54d1a879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Mon, 16 Apr 2018 18:07:54 +0000 Subject: [PATCH 03/25] Set language button --- re2o/locale/fr/LC_MESSAGES/django.mo | Bin 3605 -> 3694 bytes re2o/locale/fr/LC_MESSAGES/django.po | 8 +++++ re2o/settings.py | 5 +++ re2o/urls.py | 1 + static/css/base.css | 20 +++++++++++ templates/base.html | 43 ++---------------------- templates/buttons/setlang.html | 48 +++++++++++++++++++++++++++ 7 files changed, 85 insertions(+), 40 deletions(-) create mode 100644 templates/buttons/setlang.html diff --git a/re2o/locale/fr/LC_MESSAGES/django.mo b/re2o/locale/fr/LC_MESSAGES/django.mo index eb533f3e22010443cc48455dd1c5342fd6f88cd5..f5ba827358e0722273624a066f190f9160ccc7aa 100644 GIT binary patch delta 409 zcmXxgy-EW?6o%n1KbypuKbL?OD*<~!v=>rDq68BJErmriyCN%?%EmO_fhJ8#ixjC$ z3NOGeO)4wFJJ7}(@I8w;?94M~n4L2-1MkP{f4S*_h%(KiJ=4}{g#;ZDU=u?;!RdeQ zB&8PlG0s!c2wn24jFiWFT*F5!;1IL;f_$3LQLnElgH0wDSjBJLp-7U|N921s=?ow6 z5I?YuwH2wJmTqx_{Iw`maE^9?Pqf0nXoXdhSgIV>(8VovrCkQL!6{NVKAkOG2LEey znV@ZIVYjs4;&E6Jdqo~24z?3#6=#$)+fM8|rAE}f344JK?k=N?V0NA9WXnwXVUH<4 MdLR4Y(_8WU4-D)n&Hw-a delta 329 zcmXZXy=uZ>6vpuRV(5uBW)x^#B4B_MQg5xThP9k_&2hfZD8TX1q|ouqdl zbhnFF5PAg;{!j40!!L)tdC$qezxKnEmt2d=6Q3+dkwnDm!I1{o#TT4k`(F&`D+%dv zBTdkw@1&(H_R;PMFo&;b_s_6_A6S+`ZFsODKlsdx$dyL;n310F6Nh-jeo|`RN;Uc^ z8yD~$tGGfNw8bXwv4wwV=g-I(v1?qmq|}qRGyc$8&WIloGn68Cgnh(raj}DzUrv4Z P=Co3~@AS}JmhQ(FvLhah diff --git a/re2o/locale/fr/LC_MESSAGES/django.po b/re2o/locale/fr/LC_MESSAGES/django.po index 2cabc6ec..cb06fb57 100644 --- a/re2o/locale/fr/LC_MESSAGES/django.po +++ b/re2o/locale/fr/LC_MESSAGES/django.po @@ -30,6 +30,14 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +#: settings.py:140 +msgid "English" +msgstr "Anglais" + +#: settings.py:141 +msgid "French" +msgstr "Français" + #: templates/re2o/about.html:29 templates/re2o/about.html:35 msgid "About Re2o" msgstr "A propos de Re2o" diff --git a/re2o/settings.py b/re2o/settings.py index bac8982b..52606b6d 100644 --- a/re2o/settings.py +++ b/re2o/settings.py @@ -37,6 +37,7 @@ from __future__ import unicode_literals import os from .settings_local import * +from django.utils.translation import ugettext_lazy as _ # The root directory for the project # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -135,6 +136,10 @@ LOCALE_PATHS = [ # For translations outside of apps os.path.join(BASE_DIR, 'templates', 'locale').replace('\\', '/') ] +LANGUAGES = [ + ('en', _('English')), + ('fr', _('French')) +] # Should use time zone ? USE_TZ = True diff --git a/re2o/urls.py b/re2o/urls.py index b1cccfd9..47172521 100644 --- a/re2o/urls.py +++ b/re2o/urls.py @@ -55,6 +55,7 @@ urlpatterns = [ url(r'^about/$', about_page, name='about'), url('^logout/', auth_views.logout, {'next_page': '/'}), url('^', include('django.contrib.auth.urls')), + url(r'^i18n/', include('django.conf.urls.i18n')), url(r'^admin/', include(admin.site.urls)), url(r'^users/', include('users.urls', namespace='users')), url(r'^search/', include('search.urls', namespace='search')), diff --git a/static/css/base.css b/static/css/base.css index 7338fab6..b6a7ae26 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -35,6 +35,26 @@ footer a { border-radius: 0; } +/* Add right colors for buttons in dropdown in navbar-inverse (else it is light + * gray on white bg and white when hovered */ +.navbar-inverse .dropdown-menu .btn-link { + text-decoration: none; + color: #262626; + padding: 0; +} +.navbar-inverse .dropdown-menu .btn-link:hover { + background-color: #f5f5f5; +} +@media screen and (max-width: 767px) { + .navbar-inverse .dropdown-menu .btn-link { + color: #9d9d9d; + } + .navbar-inverse .dropdown-menu .btn-link:hover { + color: #fff; + background-color: transparent; + } +} + /* Set height of the grid so .sidenav can be 100% (adjust as needed) */ .row.content { height: 100%; diff --git a/templates/base.html b/templates/base.html index 297cac89..f198feec 100644 --- a/templates/base.html +++ b/templates/base.html @@ -118,7 +118,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% acl_end %}
  • - {% trans "About" %} + {% trans "About" %}
  • {% if not request.user.is_authenticated %} {% if var_sa %} @@ -145,48 +145,11 @@ with this program; if not, write to the Free Software Foundation, Inc., - {% endif %} - - - {% comment %} - - - {% endcomment %} diff --git a/templates/buttons/setlang.html b/templates/buttons/setlang.html new file mode 100644 index 00000000..0789685a --- /dev/null +++ b/templates/buttons/setlang.html @@ -0,0 +1,48 @@ +{% comment %} +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 © 2018 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. +{% endcomment %} + +{% load i18n %} + +{% get_current_language as LANGUAGE_CODE %} +{% get_available_languages as LANGUAGES %} +{% get_language_info_list for LANGUAGES as languages %} + + + + + From 80036345638f31d8849bce210236a28e5e34aef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Wed, 18 Apr 2018 20:13:11 +0000 Subject: [PATCH 04/25] Add warning button: translation in dev --- re2o/locale/fr/LC_MESSAGES/django.mo | Bin 3694 -> 3766 bytes re2o/locale/fr/LC_MESSAGES/django.po | 4 ++++ templates/buttons/setlang.html | 2 ++ 3 files changed, 6 insertions(+) diff --git a/re2o/locale/fr/LC_MESSAGES/django.mo b/re2o/locale/fr/LC_MESSAGES/django.mo index f5ba827358e0722273624a066f190f9160ccc7aa..b05137c32af683c0024d684c70fd58fd78896414 100644 GIT binary patch delta 396 zcmXZYu}i~16vy#jw5BRHYEdjD(c&sn#kqr<7C{gZ94r)bL!qWcYfzWY&ecV96^diP z#jS1*{sp4{ft!ny-%EYu^0}9H$=%Dh{~UBb^O;)_GYp?`&seZ?hZ}K$TX>0kIQY-! zN2DF{8orH6Pnai1+5@a0OSOSZDoYh6w(}!g#w)ZRdO-G1 zPitSW;q?NioU{LmmeZI{4xMbZkY9{hv&y> R*-(D%r}eh6D|+i*vsj2Xe8dpPn4If!K^jmG zF)KkkBF}p4s^MYq&ZvJ(qF^~Axx^vCH;fi^9 QX=u&L>B)X|;)VC{4}tL?pa1{> diff --git a/re2o/locale/fr/LC_MESSAGES/django.po b/re2o/locale/fr/LC_MESSAGES/django.po index cb06fb57..1ece2026 100644 --- a/re2o/locale/fr/LC_MESSAGES/django.po +++ b/re2o/locale/fr/LC_MESSAGES/django.po @@ -155,6 +155,10 @@ msgstr "" msgid "Dependencies" msgstr "Dépendances" +#: templates/re2o/buttons/setlang.html:34 +msgid "Translation in development" +msgstr "Traduction en développement" + #: views.py:172 msgid "No Git repository configured" msgstr "Aucun repository git configuré" diff --git a/templates/buttons/setlang.html b/templates/buttons/setlang.html index 0789685a..3d435ef3 100644 --- a/templates/buttons/setlang.html +++ b/templates/buttons/setlang.html @@ -31,6 +31,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,

    {{utilisateur.last}}

    + {% if droit != 'Superuser' %} + {% else %} + + {% endif %} @@ -79,4 +83,4 @@ with this program; if not, write to the Free Software Foundation, Inc., -{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/logs/views.py b/logs/views.py index afb0a118..0acd4bd9 100644 --- a/logs/views.py +++ b/logs/views.py @@ -41,7 +41,7 @@ from django.urls import reverse from django.shortcuts import render, redirect from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.db.models import Count, Max +from django.db.models import Count, Max, F from reversion.models import Revision from reversion.models import Version, ContentType @@ -469,9 +469,14 @@ def stats_droits(request): for droit in ListRight.objects.all().select_related('group_ptr'): stats_list[droit] = droit.user_set.all().annotate( num=Count('revision'), - last=Max('revision__date_created') + last=Max('revision__date_created'), ) + stats_list['Superuser'] = User.objects.filter(is_superuser=True).annotate( + num=Count('revision'), + last=Max('revision__date_created'), + ) + return render( request, 'logs/stats_droits.html', diff --git a/users/forms.py b/users/forms.py index f939c6cd..17170058 100644 --- a/users/forms.py +++ b/users/forms.py @@ -457,7 +457,7 @@ class GroupForm(FormRevMixin, ModelForm): class Meta: model = User - fields = ['groups', 'is_superuser'] + fields = ['is_superuser', 'groups'] def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) From a5013920daa26da6dedb9b0ffb593eef98e255cf Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Thu, 3 May 2018 14:22:52 +0200 Subject: [PATCH 17/25] ACL --- users/forms.py | 5 +++-- users/models.py | 12 ++++++++++++ users/views.py | 4 ++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/users/forms.py b/users/forms.py index 17170058..23617c53 100644 --- a/users/forms.py +++ b/users/forms.py @@ -447,7 +447,7 @@ class StateForm(FormRevMixin, ModelForm): super(StateForm, self).__init__(*args, prefix=prefix, **kwargs) -class GroupForm(FormRevMixin, ModelForm): +class GroupForm(FieldPermissionFormMixin, FormRevMixin, ModelForm): """ Gestion des groupes d'un user""" groups = forms.ModelMultipleChoiceField( Group.objects.all(), @@ -462,7 +462,8 @@ class GroupForm(FormRevMixin, ModelForm): def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(GroupForm, self).__init__(*args, prefix=prefix, **kwargs) - self.fields['is_superuser'].label = "Superuser" + if 'is_superuser' in self.fields: + self.fields['is_superuser'].label = "Superuser" class SchoolForm(FormRevMixin, ModelForm): diff --git a/users/models.py b/users/models.py index f7bfc128..156c26ba 100644 --- a/users/models.py +++ b/users/models.py @@ -812,6 +812,18 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, "Droit requis pour éditer les groupes de l'user" ) + @staticmethod + def can_change_is_superuser(user_request, *_args, **_kwargs): + """ Check if an user can change a is_superuser flag + + :param user_request: The user who request + :returns: a message and a boolean which is True if permission is granted. + """ + return ( + user_request.is_superuser, + "Droit superuser requis pour éditer le flag superuser" + ) + def can_view(self, user_request, *_args, **_kwargs): """Check if an user can view an user object. diff --git a/users/views.py b/users/views.py index 2ed4f3fb..1b65a923 100644 --- a/users/views.py +++ b/users/views.py @@ -246,7 +246,7 @@ def state(request, user, userid): @can_edit(User, 'groups') def groups(request, user, userid): """ View to edit the groups of a user """ - group_form = GroupForm(request.POST or None, instance=user) + group_form = GroupForm(request.POST or None, instance=user, user=request.user) if group_form.is_valid(): if group_form.changed_data: group_form.save() @@ -295,7 +295,7 @@ def del_group(request, user, listrightid, **_kwargs): @login_required -@can_edit(User, 'groups') +@can_edit(User, 'is_superuser') def del_superuser(request, user, **_kwargs): """Remove the superuser right of an user.""" user.is_superuser = False From 8c2b2ca8df1ea1bb5478a9f5d32fd633951f5a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Thu, 3 May 2018 12:57:43 +0000 Subject: [PATCH 18/25] Fix #111 properly --- re2o/utils.py | 24 ++++++++++-------------- templates/pagination.html | 5 +++-- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/re2o/utils.py b/re2o/utils.py index 8a4e4553..75304369 100644 --- a/re2o/utils.py +++ b/re2o/utils.py @@ -340,20 +340,16 @@ def re2o_paginator(request, query_set, pagination_number): :request: :query_set: Query_set to paginate :pagination_number: Number of entries to display""" - if query_set.count() > pagination_number: - paginator = Paginator(query_set, pagination_number) - page = request.GET.get('page') - try: - results = paginator.page(page) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - results = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - results = paginator.page(paginator.num_pages) - else: - #If there is only one page, deliver the page without paginator - return query_set + paginator = Paginator(query_set, pagination_number) + page = request.GET.get('page') + try: + results = paginator.page(page) + except PageNotAnInteger: + # If page is not an integer, deliver first page. + results = paginator.page(1) + except EmptyPage: + # If page is out of range (e.g. 9999), deliver last page of results. + results = paginator.page(paginator.num_pages) return results diff --git a/templates/pagination.html b/templates/pagination.html index a3a51602..d0bb3801 100644 --- a/templates/pagination.html +++ b/templates/pagination.html @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load url_insert_param %} +{% if list.paginator.num_pages > 1 %} - + +{% endif %} From 586321fd8a326f719b09040448af8d0e64e6e1da Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 7 May 2018 17:20:55 +0200 Subject: [PATCH 19/25] =?UTF-8?q?Un=20seul=20d=C3=A9corateur=20pour=20les?= =?UTF-8?q?=20gouverner=20(presque)=20tous.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- re2o/acl.py | 215 ++++++++++++++++++---------------------------------- 1 file changed, 74 insertions(+), 141 deletions(-) diff --git a/re2o/acl.py b/re2o/acl.py index 6ab34350..b8be7a8d 100644 --- a/re2o/acl.py +++ b/re2o/acl.py @@ -28,135 +28,108 @@ Here are defined some decorators that can be used in views to handle ACL. from __future__ import unicode_literals import sys +from itertools import chain +from django.db.models import Model from django.contrib import messages from django.shortcuts import redirect from django.urls import reverse -def can_create(model): +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. + """ + on_instance = kwargs.get('on_instance', True) + + def group_targets(): + current_target = None + current_fields = [] + for t in targets: + if isinstance(t, type) and issubclass(t, Model): + if current_target: + yield (current_target, current_fields) + current_target = t + current_fields = [] + else: + current_fields.append(t) + yield (current_target, current_fields) + + def decorator(view): + """The decorator to use on a specific view + """ + def wrapper(request, *args, **kwargs): + """The wrapper used for a specific request""" + instances = [] + + def process_target(target, fields): + 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) + error_messages = [ + x[1] for x in chain.from_iterable( + process_target(x[0], x[1]) for x in group_targets() + ) if not x[0] + ] + if error_messages: + for msg in error_messages: + 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, *chain(instances, args), **kwargs) + return wrapper + return decorator + + +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. """ - def decorator(view): - """The decorator to use on a specific view - """ - def wrapper(request, *args, **kwargs): - """The wrapper used for a specific request - """ - 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 + return acl_base_decorator('can_create', *models, on_instance=False) -def can_edit(model, *field_list): +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. """ - 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( - 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 + return acl_base_decorator('can_edit', *targets) -def can_change(model, *field_list): +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 """ - def decorator(view): - """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 + return acl_base_decorator('can_change', *targets) -def can_delete(model): +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. """ - 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_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 + return acl_base_decorator('can_delete', *targets) def can_delete_set(model): @@ -187,60 +160,20 @@ def can_delete_set(model): return decorator -def can_view(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. """ - 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_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 + return acl_base_decorator('can_view', *targets) -def can_view_all(model): +def can_view_all(*targets): """Decorator to check if an user can view a class of model. """ - def decorator(view): - """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 + return acl_base_decorator('can_view_all', *targets, on_instance=False) def can_view_app(app_name): From 84a901e3fc82ef4ccaf54491f0b5c50025a75654 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 7 May 2018 18:57:08 +0200 Subject: [PATCH 20/25] =?UTF-8?q?Documentation=20des=20d=C3=A9corateurs=20?= =?UTF-8?q?d'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) From f7d7a41586c068236bad9f6a6bcf4c7fd8dbf367 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 7 May 2018 19:33:06 +0200 Subject: [PATCH 21/25] Autorise plusieurs apps dans can_view_app --- re2o/acl.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/re2o/acl.py b/re2o/acl.py index bce941b5..0c20828f 100644 --- a/re2o/acl.py +++ b/re2o/acl.py @@ -248,10 +248,11 @@ def can_view_all(*targets): return acl_base_decorator('can_view_all', *targets, on_instance=False) -def can_view_app(app_name): - """Decorator to check if an user can view an application. +def can_view_app(*apps_name): + """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): """The decorator to use on a specific view @@ -259,15 +260,16 @@ def can_view_app(app_name): def wrapper(request, *args, **kwargs): """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)} - )) + for app_name in apps_name: + app = sys.modules[app_name] + can, msg = app.can_view(request.user) + if not can: + messages.error(request, msg) + return redirect(reverse( + 'users:profil', + kwargs={'userid': str(request.user.id)} + )) + return view(request, *args, **kwargs) return wrapper return decorator From a61d8d6ebd2dbae8741cdcdbe5eec4a556cf3b0c Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 7 May 2018 19:43:53 +0200 Subject: [PATCH 22/25] =?UTF-8?q?Factorisation=20des=20d=C3=A9corateurs=20?= =?UTF-8?q?ACL=20dans=20les=20vues.=20Fix=20#121?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- logs/views.py | 9 ++------- machines/views.py | 7 +------ preferences/views.py | 12 ++++-------- topologie/views.py | 9 +++------ 4 files changed, 10 insertions(+), 27 deletions(-) diff --git a/logs/views.py b/logs/views.py index 0acd4bd9..1fdda9fb 100644 --- a/logs/views.py +++ b/logs/views.py @@ -195,9 +195,7 @@ def revert_action(request, revision_id): @login_required -@can_view_all(IpList) -@can_view_all(Interface) -@can_view_all(User) +@can_view_all(IpList, Interface, User) def stats_general(request): """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, @@ -313,10 +311,7 @@ def stats_general(request): @login_required -@can_view_app('users') -@can_view_app('cotisations') -@can_view_app('machines') -@can_view_app('topologie') +@can_view_app('users', 'cotisations', 'machines', 'topologie') def stats_models(request): """Statistiques générales, affiche les comptages par models: nombre d'users, d'écoles, de droits, de bannissements, diff --git a/machines/views.py b/machines/views.py index c3033049..75c2f483 100644 --- a/machines/views.py +++ b/machines/views.py @@ -1272,12 +1272,7 @@ def index_nas(request): @login_required -@can_view_all(SOA) -@can_view_all(Mx) -@can_view_all(Ns) -@can_view_all(Txt) -@can_view_all(Srv) -@can_view_all(Extension) +@can_view_all(SOA, Mx, Ns, Txt, Srv, Extension) def index_extension(request): """ View displaying the list of existing extensions, the list of existing SOA records, the list of existing MX records , the list of diff --git a/preferences/views.py b/preferences/views.py index b2a6ba4c..f04a4084 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -58,13 +58,8 @@ from . import forms @login_required -@can_view_all(OptionalUser) -@can_view_all(OptionalMachine) -@can_view_all(OptionalTopologie) -@can_view_all(GeneralOption) -@can_view_all(AssoOption) -@can_view_all(MailMessageOption) -@can_view_all(HomeOption) +@can_view_all(OptionalUser, OptionalMachine, OptionalTopologie, GeneralOption, + AssoOption, MailMessageOption, HomeOption) def display_options(request): """Vue pour affichage des options (en vrac) classé selon les models correspondants dans un tableau""" @@ -149,7 +144,8 @@ def add_service(request): @can_edit(Service) def edit_service(request, service_instance, **_kwargs): """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(): with transaction.atomic(), reversion.create_revision(): service.save() diff --git a/topologie/views.py b/topologie/views.py index 283f4ffe..0c9eb0ab 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -200,9 +200,7 @@ def index_ap(request): @login_required -@can_view_all(Stack) -@can_view_all(Building) -@can_view_all(SwitchBay) +@can_view_all(Stack, Building, SwitchBay) def index_physical_grouping(request): """Affichage de la liste des stacks (affiche l'ensemble des switches)""" stack_list = (Stack.objects @@ -241,8 +239,7 @@ def index_physical_grouping(request): @login_required -@can_view_all(ModelSwitch) -@can_view_all(ConstructorSwitch) +@can_view_all(ModelSwitch, ConstructorSwitch) def index_model_switch(request): """ Affichage de l'ensemble des modèles de switches""" model_switch_list = ModelSwitch.objects.select_related('constructor') @@ -957,7 +954,7 @@ edge[arrowhead=odot,arrowtail=dot]'''] {} -{} +{}
    {} From b1ac9fffbb557fd8109cbcdf0857df3c8efece88 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 7 May 2018 20:03:12 +0200 Subject: [PATCH 23/25] PEP 8 pour preferences/views.py --- preferences/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/preferences/views.py b/preferences/views.py index f04a4084..b8ca39d2 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -145,7 +145,10 @@ def add_service(request): def edit_service(request, service_instance, **_kwargs): """Edition des services affichés sur la page d'accueil""" service = ServiceForm( - request.POST or None, request.FILES or None, instance=service_instance) + request.POST or None, + request.FILES or None, + instance=service_instance + ) if service.is_valid(): with transaction.atomic(), reversion.create_revision(): service.save() From 664fb7ae005a8efc33a58094214932da9ad5053c Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 7 May 2018 20:24:04 +0200 Subject: [PATCH 24/25] Utilisation de inutile. --- re2o/acl.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/re2o/acl.py b/re2o/acl.py index 0c20828f..7c0d5d73 100644 --- a/re2o/acl.py +++ b/re2o/acl.py @@ -36,7 +36,7 @@ from django.shortcuts import redirect from django.urls import reverse -def acl_base_decorator(method_name, *targets, **kwargs): +def acl_base_decorator(method_name, *targets, on_instance=True): """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 @@ -82,12 +82,11 @@ on_instance=False) 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. + 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 @@ -104,7 +103,6 @@ ModelC) ``` 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 From 5c254fa026ec561056b84ce9ea4e7480897b744c Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Mon, 7 May 2018 22:01:32 +0200 Subject: [PATCH 25/25] factorise les can_view_app --- re2o/acl.py | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/re2o/acl.py b/re2o/acl.py index 7c0d5d73..d96a281a 100644 --- a/re2o/acl.py +++ b/re2o/acl.py @@ -111,7 +111,7 @@ ModelC) current_target = None current_fields = [] for target in targets: - if isinstance(target, type) and issubclass(target, Model): + if not isinstance(target, str): if current_target: yield (current_target, current_fields) current_target = target @@ -146,6 +146,7 @@ ModelC) for field in fields: can_change_fct = getattr(target, 'can_change_' + field) yield can_change_fct(request.user, *args, **kwargs) + error_messages = [ x[1] for x in chain.from_iterable( process_target(x[0], x[1]) for x in group_targets() @@ -251,25 +252,11 @@ def can_view_app(*apps_name): """ for app_name in apps_name: assert app_name in sys.modules.keys() - - def decorator(view): - """The decorator to use on a specific view - """ - def wrapper(request, *args, **kwargs): - """The wrapper used for a specific request - """ - for app_name in apps_name: - app = sys.modules[app_name] - can, msg = app.can_view(request.user) - if not can: - messages.error(request, msg) - return redirect(reverse( - 'users:profil', - kwargs={'userid': str(request.user.id)} - )) - return view(request, *args, **kwargs) - return wrapper - return decorator + return acl_base_decorator( + 'can_view', + *chain(sys.modules[app_name] for app_name in apps_name), + on_instance=False + ) def can_edit_history(view):