Projet

Général

Profil

0002-manager-add-separate-role-inheritance-table-views-53.patch

Valentin Deniaud, 28 juillet 2021 12:29

Télécharger (23,3 ko)

Voir les différences:

Subject: [PATCH 2/2] manager: add separate role inheritance table views
 (#53481)

 src/authentic2/manager/forms.py               |   6 +-
 src/authentic2/manager/role_views.py          | 138 +++++++++++++-----
 src/authentic2/manager/tables.py              |  21 +++
 .../authentic2/manager/role_members.html      |   4 +-
 .../authentic2/manager/roles_inheritance.html |  25 ++++
 .../authentic2/manager/user_ou_roles.html     |   2 +-
 tests/test_manager.py                         | 135 ++++++++---------
 7 files changed, 217 insertions(+), 114 deletions(-)
 create mode 100644 src/authentic2/manager/templates/authentic2/manager/roles_inheritance.html
src/authentic2/manager/forms.py
133 133

  
134 134

  
135 135
class RolesForm(LimitQuerysetFormMixin, CssClass, forms.Form):
136

  
137 136
    roles = fields.ChooseRolesField(label=_('Add some roles'))
138 137

  
139 138

  
140
class RoleParentsForm(LimitQuerysetFormMixin, CssClass, forms.Form):
141
    roles = fields.ChooseManageableMemberRolesField(label=_('Add some roles'))
139
class RoleParentForm(LimitQuerysetFormMixin, CssClass, forms.Form):
140
    role = fields.ChooseManageableMemberRoleField(label=_('Add some roles'))
141
    action = forms.CharField(initial='add', widget=forms.HiddenInput)
142 142

  
143 143

  
144 144
class ChooseUserRoleForm(LimitQuerysetFormMixin, CssClass, forms.Form):
src/authentic2/manager/role_views.py
21 21
from django.contrib.contenttypes.models import ContentType
22 22
from django.core.exceptions import PermissionDenied, ValidationError
23 23
from django.db import transaction
24
from django.db.models import Count, F
24
from django.db.models import BooleanField, Count, ExpressionWrapper, F, Prefetch
25
from django.db.models.functions import Cast
25 26
from django.db.models.query import Prefetch, Q
26 27
from django.shortcuts import get_object_or_404
27 28
from django.urls import reverse
......
34 35
from authentic2.apps.journal.views import JournalViewWithContext
35 36
from authentic2.forms.profile import modelform_factory
36 37
from authentic2.utils.misc import redirect
37
from django_rbac.utils import get_ou_model, get_permission_model, get_role_model
38
from django_rbac.utils import get_ou_model, get_permission_model, get_role_model, get_role_parenting_model
38 39

  
39 40
from . import app_settings, forms, resources, tables, views
40 41
from .journal_views import BaseJournalView
......
365 366
members_export = RoleMembersExportView.as_view()
366 367

  
367 368

  
368
class RoleAddChildView(
369
    views.AjaxFormViewMixin,
370
    views.TitleMixin,
371
    views.PermissionMixin,
372
    views.FormNeedsRequest,
373
    SingleObjectMixin,
374
    FormView,
375
):
369
class RoleAddChildView(RoleViewMixin, views.HideOUColumnMixin, views.BaseSubTableView):
376 370
    title = _('Add child role')
377
    model = get_role_model()
378
    form_class = forms.RolesForm
379
    success_url = '..'
380
    template_name = 'authentic2/manager/form.html'
371
    form_class = forms.ChooseRoleForm
372
    table_class = tables.InheritanceRolesTable
373
    search_form_class = forms.RoleSearchForm
374
    template_name = 'authentic2/manager/roles_inheritance.html'
381 375
    permissions = ['a2_rbac.manage_members_role']
376
    success_url = '.'
377
    slug_field = 'uuid'
382 378

  
383
    def dispatch(self, request, *args, **kwargs):
384
        self.object = self.get_object()
385
        return super().dispatch(request, *args, **kwargs)
379
    def get_table_queryset(self):
380
        qs = super().get_table_queryset()
381
        qs = qs.exclude(pk=self.object.pk)
382
        children = self.object.children(annotate=True, include_self=False)
383
        children = children.annotate(is_direct=Cast('direct', output_field=BooleanField()))
384
        qs = qs.annotate(
385
            checked=ExpressionWrapper(Q(pk__in=children.filter(is_direct=True)), output_field=BooleanField())
386
        )
387
        qs = qs.annotate(
388
            indeterminate=ExpressionWrapper(
389
                Q(pk__in=children.filter(is_direct=False)), output_field=BooleanField()
390
            )
391
        )
392
        RoleParenting = get_role_parenting_model()
393
        rp_qs = RoleParenting.objects.filter(parent__in=children).annotate(name=F('parent__name'))
394
        qs = qs.prefetch_related(Prefetch('parent_relation', queryset=rp_qs, to_attr='via'))
395
        return qs
386 396

  
387 397
    def form_valid(self, form):
388
        parent = self.get_object()
389
        for role in form.cleaned_data['roles']:
390
            parent.add_child(role)
398
        role = form.cleaned_data['role']
399
        action = form.cleaned_data['action']
400
        if action == 'add':
401
            self.object.add_child(role)
391 402
            hooks.call_hooks(
392
                'event', name='manager-add-child-role', user=self.request.user, parent=parent, child=role
403
                'event', name='manager-add-child-role', user=self.request.user, parent=self.object, child=role
393 404
            )
394
            self.request.journal.record('manager.role.inheritance.addition', parent=parent, child=role)
405
            self.request.journal.record('manager.role.inheritance.addition', parent=self.object, child=role)
406
        elif action == 'remove':
407
            self.object.remove_child(role)
408
            hooks.call_hooks(
409
                'event',
410
                name='manager-remove-child-role',
411
                user=self.request.user,
412
                parent=self.object,
413
                child=role,
414
            )
415
            self.request.journal.record('manager.role.inheritance.removal', parent=self.object, child=role)
395 416
        return super().form_valid(form)
396 417

  
418
    def get_search_form_kwargs(self):
419
        kwargs = super().get_search_form_kwargs()
420
        kwargs['show_all_ou'] = app_settings.SHOW_ALL_OU
421
        kwargs['queryset'] = self.request.user.filter_by_perm(
422
            'a2_rbac.view_role', get_role_model().objects.all()
423
        )
424
        return kwargs
425

  
397 426

  
398 427
add_child = RoleAddChildView.as_view()
399 428

  
400 429

  
401
class RoleAddParentView(
402
    views.AjaxFormViewMixin, views.TitleMixin, views.FormNeedsRequest, SingleObjectMixin, FormView
403
):
430
class RoleAddParentView(RoleViewMixin, views.HideOUColumnMixin, views.BaseSubTableView):
404 431
    title = _('Add parent role')
405
    model = get_role_model()
406
    form_class = forms.RoleParentsForm
407
    success_url = '..'
408
    template_name = 'authentic2/manager/form.html'
432
    form_class = forms.RoleParentForm
433
    table_class = tables.InheritanceRolesTable
434
    search_form_class = forms.RoleSearchForm
435
    template_name = 'authentic2/manager/roles_inheritance.html'
436
    success_url = '.'
437
    slug_field = 'uuid'
409 438

  
410 439
    def dispatch(self, request, *args, **kwargs):
411
        self.object = self.get_object()
412
        if self.object.is_internal():
440
        if self.get_object().is_internal():
413 441
            raise PermissionDenied
414 442
        return super().dispatch(request, *args, **kwargs)
415 443

  
444
    def get_table_queryset(self):
445
        qs = super().get_table_queryset()
446
        qs = self.request.user.filter_by_perm('a2_rbac.manage_members_role', qs)
447
        qs = qs.exclude(pk=self.object.pk)
448
        parents = self.object.parents(annotate=True, include_self=False)
449
        parents = parents.annotate(is_direct=Cast('direct', output_field=BooleanField()))
450
        qs = qs.annotate(
451
            checked=ExpressionWrapper(Q(pk__in=parents.filter(is_direct=True)), output_field=BooleanField())
452
        )
453
        qs = qs.annotate(
454
            indeterminate=ExpressionWrapper(
455
                Q(pk__in=parents.filter(is_direct=False)), output_field=BooleanField()
456
            )
457
        )
458
        RoleParenting = get_role_parenting_model()
459
        rp_qs = RoleParenting.objects.filter(child__in=parents).annotate(name=F('child__name'))
460
        qs = qs.prefetch_related(Prefetch('child_relation', queryset=rp_qs, to_attr='via'))
461
        return qs
462

  
416 463
    def form_valid(self, form):
417
        child = self.get_object()
418
        for role in form.cleaned_data['roles']:
419
            child.add_parent(role)
464
        role = form.cleaned_data['role']
465
        action = form.cleaned_data['action']
466
        if action == 'add':
467
            self.object.add_parent(role)
468
            hooks.call_hooks(
469
                'event', name='manager-add-child-role', user=self.request.user, parent=role, child=self.object
470
            )
471
            self.request.journal.record('manager.role.inheritance.addition', parent=role, child=self.object)
472
        elif action == 'remove':
473
            self.object.remove_parent(role)
420 474
            hooks.call_hooks(
421
                'event', name='manager-add-child-role', user=self.request.user, parent=role, child=child
475
                'event',
476
                name='manager-remove-child-role',
477
                user=self.request.user,
478
                parent=role,
479
                child=self.object,
422 480
            )
423
            self.request.journal.record('manager.role.inheritance.addition', parent=role, child=child)
481
            self.request.journal.record('manager.role.inheritance.removal', parent=role, child=self.object)
424 482
        return super().form_valid(form)
425 483

  
484
    def get_search_form_kwargs(self):
485
        kwargs = super().get_search_form_kwargs()
486
        kwargs['show_all_ou'] = app_settings.SHOW_ALL_OU
487
        kwargs['queryset'] = self.request.user.filter_by_perm(
488
            'a2_rbac.manage_members_role', get_role_model().objects.all()
489
        )
490
        return kwargs
491

  
426 492

  
427 493
add_parent = RoleAddParentView.as_view()
428 494

  
src/authentic2/manager/tables.py
240 240
        attrs = {'class': 'main plaintable', 'id': 'user-authorizations-table'}
241 241
        fields = ('client', 'created', 'expired')
242 242
        empty_text = _('This user has not granted profile data access to any service yet.')
243

  
244

  
245
class InheritanceRolesTable(tables.Table):
246
    name = tables.LinkColumn(
247
        viewname='a2-manager-role-members', kwargs={'pk': A('pk')}, accessor='name', verbose_name=_('label')
248
    )
249
    via = tables.TemplateColumn(
250
        '''{% for rel in record.via %}{{ rel.name }} {% if not forloop.last %}, {% endif %}{% endfor %}''',
251
        verbose_name=_('Inherited from'),
252
        orderable=False,
253
    )
254
    member = tables.TemplateColumn(
255
        '<input class="role-member{% if record.indeterminate %} indeterminate{% endif %}" name="role-{{ record.pk }}" type="checkbox" {% if record.checked %}checked{% endif %}/>',
256
        verbose_name='',
257
    )
258

  
259
    class Meta:
260
        model = get_role_model()
261
        attrs = {'class': 'main plaintable', 'id': 'inheritance-role-table'}
262
        fields = ('name', 'ou')
263
        empty_text = _('None')
src/authentic2/manager/templates/authentic2/manager/role_members.html
116 116
     {% endif %}
117 117
   {% endfor %}
118 118
  {% if view.can_manage_members %}
119
    <a rel="popup" href="{% url "a2-manager-role-add-child" pk=object.pk %}" class="role-add icon-add-sign"></a>
119
    <a href="{% url "a2-manager-role-add-child" pk=object.pk %}" class="role-add icon-add-sign"></a>
120 120
  {% else %}
121 121
    <a title="{% trans "Permission denied" %}" class="disabled role-add icon-add-sign"></a>
122 122
  {% endif %}
......
138 138
     {% endif %}
139 139
   {% endfor %}
140 140
   {% if not object.is_internal %}
141
     <a rel="popup" href="{% url "a2-manager-role-add-parent" pk=object.pk %}" class="role-add icon-add-sign"></a>
141
     <a href="{% url "a2-manager-role-add-parent" pk=object.pk %}" class="role-add icon-add-sign"></a>
142 142
   {% else %}
143 143
     <a title="{% trans "This role is technical, you cannot modify its permissions." %}" class="disabled role-add icon-add-sign"></a>
144 144
   {% endif %}
src/authentic2/manager/templates/authentic2/manager/roles_inheritance.html
1
{% extends "authentic2/manager/role_common.html" %}
2
{% load i18n static django_tables2 %}
3

  
4
{% block breadcrumb %}
5
  {{ block.super }}
6
  <a href="..">{{ object }}</a>
7
  <a href="#">{% trans "Role inheritance" %}</a>
8
{% endblock %}
9

  
10
{% block extrascripts %}
11
  {{ block.super }}
12
  <script src="{% static "authentic2/manager/js/roles_ajax_checkbox.js" %}"></script>
13
{% endblock %}
14

  
15
{% block main %}
16
 {% with row_link=0 %}
17
   {% render_table table "authentic2/manager/table.html" %}
18
 {% endwith %}
19
{% endblock %}
20

  
21
{% block sidebar %}
22
  <aside id="sidebar">
23
    {% include "authentic2/manager/search_form.html" %}
24
  </aside>
25
{% endblock %}
src/authentic2/manager/templates/authentic2/manager/user_ou_roles.html
1 1
{% extends "authentic2/manager/user_common_roles.html" %}
2
{% load django_tables2 %}
2
{% load static django_tables2 %}
3 3

  
4 4
{% block extrascripts %}
5 5
  {{ block.super }}
tests/test_manager.py
861 861

  
862 862

  
863 863
def test_roles_for_change_widget(admin, app, db):
864
    from authentic2.manager.forms import RoleParentsForm
864
    from authentic2.manager.forms import RoleParentForm
865 865

  
866 866
    login(app, admin, '/manage/')
867 867
    Role.objects.create(name='admin 1')
868 868
    Role.objects.create(name='user 1')
869 869

  
870
    form = RoleParentsForm(request=None)
870
    form = RoleParentForm(request=None)
871 871
    assert form.as_p()
872
    field_id = form.fields['roles'].widget.build_attrs({})['data-field_id']
872
    field_id = form.fields['role'].widget.build_attrs({})['data-field_id']
873 873
    url = reverse('django_select2-json')
874 874
    response = app.get(url, params={'field_id': field_id, 'term': 'admin'})
875 875
    assert len(response.json['results']) == 1
......
940 940
    simple_role.permissions.add(view_role_perm)
941 941
    simple_user.roles.add(simple_role)
942 942
    admin.roles.add(role)
943

  
943 944
    response = app.get('/manage/roles/%s/add-child/' % simple_role.pk)
944
    form = response.form
945
    form['roles'].force_value(role.pk)
946
    form.submit().follow()
945
    token = str(response.context['csrf_token'])
946
    params = {'action': 'add', 'role': role.pk, 'csrfmiddlewaretoken': token}
947
    response = app.post('/manage/roles/%s/add-child/' % simple_role.pk, params=params)
947 948
    assert role in simple_role.children()
948 949

  
950
    response = app.get('/manage/roles/%s/' % simple_role.pk)
949 951
    url = '/manage/roles/%s/remove-child/%s/' % (simple_role.pk, role.pk)
950 952
    token = str(response.context['csrf_token'])
951 953
    app.post(url, params={'csrfmiddlewaretoken': token})
952 954
    assert not role in simple_role.children()
953 955

  
954 956
    response = app.get('/manage/roles/%s/add-parent/' % role.pk)
955
    form = response.form
956
    form['roles'].force_value(simple_role.pk)
957
    form.submit().follow()
957
    token = str(response.context['csrf_token'])
958
    params = {'action': 'add', 'role': simple_role.pk, 'csrfmiddlewaretoken': token}
959
    response = app.post('/manage/roles/%s/add-parent/' % role.pk, params=params)
958 960
    assert simple_role in role.parents()
959 961

  
962
    # try to add arbitrary role
963
    admin_role = Role.objects.get(slug='_a2-manager')
964
    response = app.get('/manage/roles/%s/add-parent/' % role.pk)
965
    token = str(response.context['csrf_token'])
966
    params = {'action': 'add', 'role': admin_role.pk, 'csrfmiddlewaretoken': token}
967
    response = app.post('/manage/roles/%s/add-parent/' % simple_role.pk, params=params)
968
    assert admin_role not in role.parents()
969

  
970
    response = app.get('/manage/roles/%s/' % simple_role.pk)
960 971
    url = '/manage/roles/%s/remove-parent/%s/' % (role.pk, simple_role.pk)
961 972
    token = str(response.context['csrf_token'])
962 973
    app.post(url, params={'csrfmiddlewaretoken': token})
......
978 989
    app.get('/manage/roles/%s/delete/' % simple_role.pk, status=403)
979 990

  
980 991

  
981
def test_manager_permission_inheritance(app, simple_user, admin, simple_role):
982
    admin_role = Role.objects.get(slug='_a2-manager')
983
    view_role_perm = get_permission_model().objects.create(
984
        operation=get_operation(VIEW_OP),
985
        target_ct=ContentType.objects.get_for_model(Role),
986
        target_id=simple_role.pk,
987
    )
988
    simple_role.permissions.add(view_role_perm)
989
    simple_user.roles.add(simple_role)
990
    login(app, simple_user, '/manage/')
991

  
992
    response = app.get('/manage/roles/%s/add-parent/' % simple_role.pk)
993
    form = response.form
994
    form['roles'].force_value(admin_role.pk)
995
    response = form.submit()
996

  
997
    assert response.status_code == 200
998
    assert not admin_role in simple_role.parents()
999

  
1000

  
1001 992
def test_manager_widget_fields_validation(app, simple_user, simple_role):
1002 993
    '''Verify that fields corresponding to widget implement queryset restrictions.'''
1003 994
    from authentic2.manager.forms import (
1004 995
        ChooseRoleForm,
1005 996
        ChooseUserForm,
1006 997
        ChooseUserRoleForm,
1007
        RoleParentsForm,
998
        RoleParentForm,
1008 999
        RolesForm,
1009 1000
        UsersForm,
1010 1001
    )
......
1056 1047
    assert error_message in form.errors['roles'][0]
1057 1048

  
1058 1049
    # For those we need manage_members permission
1059
    form = RoleParentsForm(request=request, data={'roles': [visible_role.pk]})
1060
    assert error_message in form.errors['roles'][0]
1050
    form = RoleParentForm(request=request, data={'role': visible_role.pk, 'action': 'add'})
1051
    assert error_message in form.errors['role'][0]
1061 1052

  
1062 1053
    form = ChooseUserRoleForm(request=request, data={'role': visible_role.pk, 'action': 'add'})
1063 1054
    assert error_message in form.errors['role'][0]
......
1070 1061
    simple_role.permissions.add(change_role_perm)
1071 1062
    del simple_user._rbac_perms_cache
1072 1063

  
1073
    form = RoleParentsForm(request=request, data={'roles': [visible_role.pk]})
1064
    form = RoleParentForm(request=request, data={'role': visible_role.pk, 'action': 'add'})
1074 1065
    assert form.is_valid()
1075 1066

  
1076 1067
    form = ChooseUserRoleForm(request=request, data={'role': visible_role.pk, 'action': 'add'})
1077 1068
    assert form.is_valid()
1078 1069

  
1079 1070

  
1080
def test_manager_role_widgets_choices(app, simple_user, simple_role):
1081
    def get_choices(response):
1082
        select2_json = request_select2(app, response)
1083
        assert select2_json['more'] is False
1084
        return {result['id'] for result in select2_json['results']}
1085

  
1071
def test_manager_role_inheritance_list(app, simple_user, simple_role, ou1):
1086 1072
    visible_role = Role.objects.create(name='visible_role', ou=simple_user.ou)
1073
    visible_role_2 = Role.objects.create(name='visible_role_2', ou=ou1)
1087 1074
    Role.objects.create(name='invisible_role', ou=simple_user.ou)
1088 1075
    admin_of_simple_role = simple_role.get_admin_role()
1089 1076

  
1090 1077
    admin_of_simple_role.members.add(simple_user)
1091
    view_role_perm = get_permission_model().objects.create(
1092
        operation=get_operation(VIEW_OP),
1093
        target_ct=ContentType.objects.get_for_model(Role),
1094
        target_id=visible_role.pk,
1095
    )
1096
    simple_role.permissions.add(view_role_perm)
1078
    for role in (visible_role, visible_role_2):
1079
        view_role_perm = get_permission_model().objects.create(
1080
            operation=get_operation(VIEW_OP),
1081
            target_ct=ContentType.objects.get_for_model(Role),
1082
            target_id=role.pk,
1083
        )
1084
        simple_role.permissions.add(view_role_perm)
1097 1085
    simple_user.roles.add(simple_role)
1098 1086

  
1099 1087
    response = login(app, simple_user, '/manage/roles/')
1100 1088

  
1101
    # all visible roles are shown
1089
    # all visible roles are shown, except current role
1102 1090
    response = app.get('/manage/roles/%s/add-child/' % simple_role.pk)
1103
    assert {visible_role.pk, simple_role.pk} == get_choices(response)
1104

  
1105
    # all roles with manage_members permissions are shown
1106
    response = app.get('/manage/roles/%s/add-parent/' % simple_role.pk)
1107
    assert {simple_role.pk, admin_of_simple_role.pk} == get_choices(response)
1108

  
1109
    response = app.get('/manage/roles/%s/add-parent/' % visible_role.pk)
1110
    assert {simple_role.pk, admin_of_simple_role.pk} == get_choices(response)
1111

  
1112

  
1113
def test_manager_widgets_field_id_other_user(app, admin, simple_user, simple_role):
1114
    other_role = Role.objects.create(name='visible_role', ou=simple_user.ou)
1115
    simple_role.get_admin_role().members.add(simple_user)
1091
    q = response.pyquery.remove_namespaces()
1092
    assert len(q('table tbody tr')) == 2
1093
    assert {e.text_content() for e in q('table tbody td.name')} == {visible_role.name, visible_role_2.name}
1116 1094

  
1117
    response = login(app, admin, '/manage/roles/%s/add-child/' % simple_role.pk)
1118
    select2_json = request_select2(app, response)
1119
    assert select2_json['more'] is False
1095
    # filter by ou
1096
    response.form['search-ou'] = ou1.pk
1097
    response = response.form.submit()
1098
    q = response.pyquery.remove_namespaces()
1099
    assert len(q('table tbody tr')) == 1
1100
    assert {e.text_content() for e in q('table tbody td.name')} == {visible_role_2.name}
1120 1101

  
1121
    # admin can see every roles
1122
    assert {simple_role.pk, other_role.pk} == {result['id'] for result in select2_json['results']}
1102
    # filter by name
1103
    response.form['search-text'] = '2'
1104
    response.form['search-ou'] = 'all'
1105
    response = response.form.submit()
1106
    q = response.pyquery.remove_namespaces()
1107
    assert len(q('table tbody tr')) == 1
1108
    assert {e.text_content() for e in q('table tbody td.name')} == {visible_role_2.name}
1123 1109

  
1124
    login(app, simple_user)
1125
    # same request from the page served for admin
1126
    select2_json = request_select2(app, response)
1127
    # simple_user doesn't see all roles
1128
    assert simple_role.pk == select2_json['results'][0]['id']
1110
    # all roles with manage_members permissions are shown
1111
    response = app.get('/manage/roles/%s/add-parent/' % visible_role.pk)
1112
    q = response.pyquery.remove_namespaces()
1113
    assert len(q('table tbody tr')) == 1
1114
    assert {e.text_content() for e in q('table tbody td.name')} == {simple_role.name}
1129 1115

  
1130
    # anymous user receive 404
1131
    app.session.flush()
1132
    select2_json = request_select2(app, response, get_kwargs={'status': 404})
1116
    response.form['search-internals'] = True
1117
    response = response.form.submit()
1118
    q = response.pyquery.remove_namespaces()
1119
    assert len(q('table tbody tr')) == 2
1120
    assert {e.text_content() for e in q('table tbody td.name')} == {
1121
        simple_role.name,
1122
        admin_of_simple_role.name,
1123
    }
1133 1124

  
1134 1125

  
1135 1126
def test_display_parent_roles_on_role_page(app, superuser, settings):
1136
-