Projet

Général

Profil

0001-authenticators-add-manager-role-66984.patch

Valentin Deniaud, 26 octobre 2022 17:12

Télécharger (23 ko)

Voir les différences:

Subject: [PATCH] authenticators: add manager role (#66984)

 src/authentic2/a2_rbac/management.py          |   4 +
 .../apps/authenticators/manager_urls.py       | 137 +++++++-----------
 src/authentic2/apps/authenticators/views.py   |   9 +-
 src/authentic2/manager/urls.py                |   4 +-
 src/authentic2/manager/views.py               |   2 +-
 tests/test_a2_rbac.py                         |  30 ++--
 tests/test_manager.py                         |  29 ++--
 tests/test_manager_authenticators.py          |  27 +++-
 tests/test_role_manager.py                    |   9 +-
 9 files changed, 129 insertions(+), 122 deletions(-)
src/authentic2/a2_rbac/management.py
91 91
        'name': _('Manager of services'),
92 92
        'scoped_name': _('Services - {ou}'),
93 93
    },
94
    ('authenticators', 'baseauthenticator'): {
95
        'name': _('Manager of authenticators'),
96
        'scoped_name': _('Authenticators - {ou}'),
97
    },
94 98
}
95 99

  
96 100

  
src/authentic2/apps/authenticators/manager_urls.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
from django.contrib.auth.decorators import user_passes_test
18
from django.core.exceptions import PermissionDenied
19 17
from django.urls import path
20
from django.utils.functional import lazy
21

  
22
from authentic2.decorators import required
23
from authentic2.utils import misc as utils_misc
24 18

  
25 19
from . import views
26 20

  
27

  
28
def superuser_required(function, login_url):
29
    def check_superuser(user):
30
        if user and user.is_superuser:
31
            return True
32
        if user and not user.is_anonymous:
33
            raise PermissionDenied()
34
        return False
35

  
36
    actual_decorator = user_passes_test(check_superuser, login_url=login_url)
37
    return actual_decorator(function)
38

  
39

  
40
def superuser_login_required(func):
41
    return superuser_required(func, login_url=lazy(utils_misc.get_manager_login_url, str)())
42

  
43

  
44
urlpatterns = required(
45
    superuser_login_required,
46
    [
47
        # Authenticators
48
        path('authenticators/', views.authenticators, name='a2-manager-authenticators'),
49
        path(
50
            'authenticators/add/',
51
            views.add,
52
            name='a2-manager-authenticator-add',
53
        ),
54
        path(
55
            'authenticators/<int:pk>/detail/',
56
            views.detail,
57
            name='a2-manager-authenticator-detail',
58
        ),
59
        path(
60
            'authenticators/<int:pk>/edit/',
61
            views.edit,
62
            name='a2-manager-authenticator-edit',
63
        ),
64
        path(
65
            'authenticators/<int:pk>/delete/',
66
            views.delete,
67
            name='a2-manager-authenticator-delete',
68
        ),
69
        path(
70
            'authenticators/<int:pk>/toggle/',
71
            views.toggle,
72
            name='a2-manager-authenticator-toggle',
73
        ),
74
        path(
75
            'authenticators/<int:pk>/journal/',
76
            views.journal,
77
            name='a2-manager-authenticator-journal',
78
        ),
79
        path('authenticators/<int:pk>/export/', views.export_json, name='a2-manager-authenticator-export'),
80
        path('authenticators/import/', views.import_json, name='a2-manager-authenticator-import'),
81
        path(
82
            'authenticators/order/',
83
            views.order,
84
            name='a2-manager-authenticators-order',
85
        ),
86
        path(
87
            'authenticators/<int:authenticator_pk>/<slug:model_name>/add/',
88
            views.add_related_object,
89
            name='a2-manager-authenticators-add-related-object',
90
        ),
91
        path(
92
            'authenticators/<int:authenticator_pk>/<slug:model_name>/<int:pk>/edit/',
93
            views.edit_related_object,
94
            name='a2-manager-authenticators-edit-related-object',
95
        ),
96
        path(
97
            'authenticators/<int:authenticator_pk>/<slug:model_name>/<int:pk>/delete/',
98
            views.delete_related_object,
99
            name='a2-manager-authenticators-delete-related-object',
100
        ),
101
    ],
102
)
21
urlpatterns = [
22
    path('authenticators/', views.authenticators, name='a2-manager-authenticators'),
23
    path(
24
        'authenticators/add/',
25
        views.add,
26
        name='a2-manager-authenticator-add',
27
    ),
28
    path(
29
        'authenticators/<int:pk>/detail/',
30
        views.detail,
31
        name='a2-manager-authenticator-detail',
32
    ),
33
    path(
34
        'authenticators/<int:pk>/edit/',
35
        views.edit,
36
        name='a2-manager-authenticator-edit',
37
    ),
38
    path(
39
        'authenticators/<int:pk>/delete/',
40
        views.delete,
41
        name='a2-manager-authenticator-delete',
42
    ),
43
    path(
44
        'authenticators/<int:pk>/toggle/',
45
        views.toggle,
46
        name='a2-manager-authenticator-toggle',
47
    ),
48
    path(
49
        'authenticators/<int:pk>/journal/',
50
        views.journal,
51
        name='a2-manager-authenticator-journal',
52
    ),
53
    path('authenticators/<int:pk>/export/', views.export_json, name='a2-manager-authenticator-export'),
54
    path('authenticators/import/', views.import_json, name='a2-manager-authenticator-import'),
55
    path(
56
        'authenticators/order/',
57
        views.order,
58
        name='a2-manager-authenticators-order',
59
    ),
60
    path(
61
        'authenticators/<int:authenticator_pk>/<slug:model_name>/add/',
62
        views.add_related_object,
63
        name='a2-manager-authenticators-add-related-object',
64
    ),
65
    path(
66
        'authenticators/<int:authenticator_pk>/<slug:model_name>/<int:pk>/edit/',
67
        views.edit_related_object,
68
        name='a2-manager-authenticators-edit-related-object',
69
    ),
70
    path(
71
        'authenticators/<int:authenticator_pk>/<slug:model_name>/<int:pk>/delete/',
72
        views.delete_related_object,
73
        name='a2-manager-authenticators-delete-related-object',
74
    ),
75
]
src/authentic2/apps/authenticators/views.py
32 32

  
33 33
from authentic2.apps.journal.views import JournalViewWithContext
34 34
from authentic2.manager.journal_views import BaseJournalView
35
from authentic2.manager.views import MediaMixin, TitleMixin
35
from authentic2.manager.views import MediaMixin, PermissionMixin, TitleMixin
36 36

  
37 37
from . import forms
38 38
from .models import AuthenticatorImportError, BaseAuthenticator
39 39

  
40 40

  
41
class AuthenticatorsMixin(MediaMixin, TitleMixin):
41
class AuthenticatorsMixin(MediaMixin, TitleMixin, PermissionMixin):
42 42
    model = BaseAuthenticator
43
    permissions = ['authenticators.search_baseauthenticator']
43 44

  
44 45
    def get_queryset(self):
45 46
        return self.model.authenticators.all()
......
268 269
order = AuthenticatorsOrderView.as_view()
269 270

  
270 271

  
271
class AuthenticatorRelatedObjectMixin(MediaMixin, TitleMixin):
272
class AuthenticatorRelatedObjectMixin(MediaMixin, TitleMixin, PermissionMixin):
273
    permissions = ['authenticators.search_baseauthenticator']
274

  
272 275
    def dispatch(self, request, *args, **kwargs):
273 276
        self.authenticator = get_object_or_404(
274 277
            BaseAuthenticator.authenticators.all(), pk=kwargs.get('authenticator_pk')
src/authentic2/manager/urls.py
207 207
        path('api-clients/<int:pk>/', apiclient_views.detail, name='a2-manager-api-client-detail'),
208 208
        path('api-clients/<int:pk>/edit/', apiclient_views.edit, name='a2-manager-api-client-edit'),
209 209
        path('api-clients/<int:pk>/delete/', apiclient_views.delete, name='a2-manager-api-client-delete'),
210
    ],
210
    ]
211
    + authenticator_urlpatterns,
211 212
)
212 213

  
213
urlpatterns += authenticator_urlpatterns
214 214
urlpatterns += oidc_manager_urlpatterns
215 215

  
216 216
urlpatterns += [
src/authentic2/manager/views.py
669 669
            'label': _('Authentication frontends'),
670 670
            'slug': 'authn',
671 671
            'href': reverse_lazy('a2-manager-authenticators'),
672
            'permissions': 'superuser',
672
            'permissions': 'authenticators.search_baseauthenticator',
673 673
            'place': 'sidebar',
674 674
        },
675 675
        {
tests/test_a2_rbac.py
30 30

  
31 31

  
32 32
def test_update_rbac(db):
33
    # 3 content types managers and 1 global manager
34
    assert Role.objects.count() == 5
35
    # 3 content type global permissions, 1 role administration permissions (for the main manager
33
    # 5 content types managers and 1 global manager
34
    assert Role.objects.count() == 6
35
    # 4 content type global permissions, 1 role administration permissions (for the main manager
36 36
    # role which is self-administered)
37 37
    # and 1 user view permission (for the role administrator)
38 38
    # and 1 user manage authorizations permission (for the role administrator)
39 39
    # and 1 ou view permission (for the user and role administrators)
40
    assert Permission.objects.count() == 8
40
    assert Permission.objects.count() == 9
41 41

  
42 42

  
43 43
def test_delete_role(db):
......
405 405
    from django.core.management.sql import emit_post_migrate_signal
406 406

  
407 407
    call_command('flush', verbosity=0, interactive=False, database='default', reset_sequences=False)
408
    assert Role.objects.count() == 5
408
    assert Role.objects.count() == 6
409 409
    OU.objects.create(name='OU1', slug='ou1')
410 410
    emit_post_migrate_signal(verbosity=0, interactive=False, db='default', created_models=[])
411
    assert Role.objects.count() == 5 + 4 + 4
411
    assert Role.objects.count() == 6 + 5 + 5
412 412
    settings.A2_RBAC_MANAGED_CONTENT_TYPES = ()
413 413
    call_command('flush', verbosity=0, interactive=False, database='default', reset_sequences=False)
414 414
    assert Role.objects.count() == 0
......
424 424
    user_manager = Role.objects.get(ou__isnull=True, slug='_a2-manager-of-users')
425 425
    role_manager = Role.objects.get(ou__isnull=True, slug='_a2-manager-of-roles')
426 426
    service_manager = Role.objects.get(ou__isnull=True, slug='_a2-manager-of-services')
427
    authenticator_manager = Role.objects.get(ou__isnull=True, slug='_a2-manager-of-authenticators')
427 428
    assert ou_manager in manager.parents()
428 429
    assert user_manager in manager.parents()
429 430
    assert role_manager in manager.parents()
430 431
    assert service_manager in manager.parents()
431
    assert manager.parents(include_self=False).count() == 4
432
    assert Role.objects.count() == 5
432
    assert authenticator_manager in manager.parents()
433
    assert manager.parents(include_self=False).count() == 5
434
    assert Role.objects.count() == 6
433 435
    assert OU.objects.count() == 1
434 436

  
435 437

  
......
439 441
    user_manager = Role.objects.get(ou__isnull=True, slug='_a2-manager-of-users')
440 442
    role_manager = Role.objects.get(ou__isnull=True, slug='_a2-manager-of-roles')
441 443
    service_manager = Role.objects.get(ou__isnull=True, slug='_a2-manager-of-services')
444
    authenticator_manager = Role.objects.get(ou__isnull=True, slug='_a2-manager-of-authenticators')
442 445
    assert ou_manager in manager.parents()
443 446
    assert user_manager in manager.parents()
444 447
    assert role_manager in manager.parents()
445 448
    assert service_manager in manager.parents()
446
    assert manager.parents(include_self=False).count() == 4
449
    assert authenticator_manager in manager.parents()
450
    assert manager.parents(include_self=False).count() == 5
447 451

  
448 452
    for ou in [get_default_ou(), ou1]:
449 453
        manager = Role.objects.get(ou__isnull=True, slug=f'_a2-managers-of-{ou.slug}')
450 454
        user_manager = Role.objects.get(ou=ou, slug=f'_a2-manager-of-users-{ou.slug}')
451 455
        role_manager = Role.objects.get(ou=ou, slug=f'_a2-manager-of-roles-{ou.slug}')
452 456
        service_manager = Role.objects.get(ou=ou, slug=f'_a2-manager-of-services-{ou.slug}')
457
        authenticator_manager = Role.objects.get(ou=ou, slug=f'_a2-manager-of-authenticators-{ou.slug}')
453 458

  
454 459
        assert user_manager in manager.parents()
455 460
        assert role_manager in manager.parents()
456 461
        assert service_manager in manager.parents()
457
        assert manager.parents(include_self=False).count() == 3
462
        assert authenticator_manager in manager.parents()
463
        assert manager.parents(include_self=False).count() == 4
458 464

  
459
    # 5 global roles and 4 ou roles for both ous
460
    assert Role.objects.count() == 5 + 4 + 4
465
    # 6 global roles and 5 ou roles for both ous
466
    assert Role.objects.count() == 6 + 5 + 5
461 467

  
462 468

  
463 469
@pytest.mark.parametrize(
tests/test_manager.py
84 84
    for section in sections:
85 85
        path = reverse('a2-manager-%s' % section)
86 86
        assert response.pyquery.remove_namespaces()('a.button[href=\'%s\']' % path)
87
    # only the journal is visible in the sidebar
88
    assert len(response.pyquery('div.a2-manager-id-tools_content').children()) == 2
87
    # only journal, api clients and authenticators are visible in the sidebar
88
    assert len(response.pyquery('div.a2-manager-id-tools_content').children()) == 3
89 89
    assert response.pyquery('a#journal')
90 90
    assert response.pyquery('a#api-clients')
91
    assert response.pyquery('a#authn')
91 92

  
92 93

  
93 94
def test_manager_create_ou(superuser_or_admin, app):
......
467 468
        form.set('search-internals', True)
468 469
        response = form.submit()
469 470
        q = response.pyquery.remove_namespaces()
470
        assert len(q('table tbody tr')) == 6
471
        assert len(q('table tbody tr')) == 7
471 472
        # admin enroled only in the Manager role, other roles are inherited
472
        assert len(q('table tbody tr td.via')) == 6
473
        assert len(q('table tbody tr td.via')) == 7
473 474
        assert len(q('table tbody tr td.via:empty')) == 2
474 475
        for elt in q('table tbody td.name a'):
475 476
            assert 'Manager' in elt.text or elt.text == 'simple role'
......
491 492
        response.form.set('search-internals', True)
492 493
        response = response.form.submit()
493 494
        q = response.pyquery.remove_namespaces()
494
        assert len(q('table tbody tr')) == 6
495
        assert len(q('table tbody tr')) == 7
495 496
        for elt in q('table tbody td.name a'):
496 497
            assert 'Manager' in elt.text or elt.text == 'simple role'
497 498

  
......
542 543
        form.set('search-internals', True)
543 544
        response = form.submit()
544 545
        q = response.pyquery.remove_namespaces()
545
        assert len(q('table tbody tr')) == 5
546
        assert len(q('table tbody tr')) == 6
546 547
        # admin enroled only in the Manager role, other roles are inherited
547
        assert len(q('table tbody tr td.via')) == 5
548
        assert len(q('table tbody tr td.via')) == 6
548 549
        assert len(q('table tbody tr td.via:empty')) == 1
549 550
        for elt in q('table tbody td.name a'):
550 551
            assert 'Manager' in elt.text
......
554 555
        form.set('search-internals', True)
555 556
        response = form.submit()
556 557
        q = response.pyquery.remove_namespaces()
557
        assert len(q('table tbody tr')) == 7
558
        assert len(q('table tbody tr')) == 8
558 559
        for elt in q('table tbody td.name a'):
559 560
            assert 'Manager' in elt.text
560 561

  
......
586 587
        response.form.set('search-internals', True)
587 588
        response = response.form.submit()
588 589
        q = response.pyquery.remove_namespaces()
589
        assert len(q('table tbody tr')) == 15
590
        assert len(q('table tbody tr')) == 18
590 591
        for elt in q('table tbody td.name a'):
591 592
            assert (
592 593
                'OU1' in elt.text
......
600 601
        response.form.set('search-internals', True)
601 602
        response = response.form.submit()
602 603
        q = response.pyquery.remove_namespaces()
603
        assert len(q('table tbody tr')) == 7
604
        assert len(q('table tbody tr')) == 8
604 605
        for elt in q('table tbody td.name a'):
605 606
            assert 'Manager' in elt.text
606 607

  
......
647 648
        response.form.set('search-internals', True)
648 649
        response = response.form.submit()
649 650
        q = response.pyquery.remove_namespaces()
650
        assert len(q('table tbody tr')) == 4
651
        assert len(q('table tbody tr')) == 5
651 652
        names = {elt.text for elt in q('table tbody td.name a')}
652
        assert names == {'Roles - OU1', 'Users - OU1', 'Services - OU1', 'role_ou1'}
653
        assert names == {'Roles - OU1', 'Users - OU1', 'Services - OU1', 'role_ou1', 'Authenticators - OU1'}
653 654

  
654 655
        # test role listing
655 656
        response = app.get('/manage/roles/')
......
668 669
        response.form.set('search-internals', True)
669 670
        response = response.form.submit()
670 671
        q = response.pyquery.remove_namespaces()
671
        assert len(q('table tbody tr')) == 4
672
        assert len(q('table tbody tr')) == 5
672 673
        names = {elt.text for elt in q('table tbody td.name a')}
673
        assert names == {'Roles - OU1', 'Users - OU1', 'Services - OU1', 'role_ou1'}
674
        assert names == {'Roles - OU1', 'Users - OU1', 'Services - OU1', 'role_ou1', 'Authenticators - OU1'}
674 675

  
675 676
    test_user_listing_ou_admin(admin_ou1)
676 677

  
tests/test_manager_authenticators.py
21 21
from django.utils.html import escape
22 22
from webtest import Upload
23 23

  
24
from authentic2.a2_rbac.models import Role
24 25
from authentic2.a2_rbac.utils import get_default_ou
25 26
from authentic2.apps.authenticators.models import AddRoleAction, BaseAuthenticator, LoginPasswordAuthenticator
26 27
from authentic2.models import Attribute
......
31 32
from .utils import assert_event, login, logout
32 33

  
33 34

  
34
def test_authenticators_authorization(app, admin, superuser):
35
    resp = login(app, admin, path='/manage/')
35
def test_authenticators_authorization(app, simple_user, simple_role, admin, superuser):
36
    simple_user.roles.add(simple_role.get_admin_role())  # grant user access to /manage/
37

  
38
    resp = login(app, simple_user, path='/manage/')
36 39
    assert 'Authenticators' not in resp.text
37 40
    app.get('/manage/authenticators/', status=403)
38 41

  
42
    role = Role.objects.get(name='Manager of authenticators')
43
    simple_user.roles.add(role)
44

  
45
    resp = app.get('/manage/')
46
    resp = resp.click('Authentication frontends')
47
    assert 'Authenticators' in resp.text
48

  
49
    logout(app)
50
    resp = login(app, admin, path='/manage/')
51
    assert 'Authentication frontends' in resp.text
52

  
53
    resp = resp.click('Authentication frontends')
54
    assert 'Authenticators' in resp.text
55

  
39 56
    logout(app)
40 57
    resp = login(app, superuser, path='/manage/')
41 58
    assert 'Authentication frontends' in resp.text
......
44 61
    assert 'Authenticators' in resp.text
45 62

  
46 63

  
47
def test_authenticators_password(app, superuser):
48
    resp = login(app, superuser, path='/manage/authenticators/')
64
def test_authenticators_password(app, superuser_or_admin):
65
    resp = login(app, superuser_or_admin, path='/manage/authenticators/')
49 66
    # Password authenticator already exists
50 67
    assert 'Password' in resp.text
51 68
    authenticator = LoginPasswordAuthenticator.objects.get()
......
85 102
            "Show condition: &#x27;backoffice&#x27; in login_hint or remotre_addr == &#x27;1.2.3.4&#x27;"
86 103
            in resp.text
87 104
        )
88
    assert_event('authenticator.edit', user=superuser, session=app.session)
105
    assert_event('authenticator.edit', user=superuser_or_admin, session=app.session)
89 106

  
90 107
    resp = resp.click('Edit')
91 108
    resp.form['show_condition'] = "remote_addr in dnsbl('ddns.entrouvert.org')"
tests/test_role_manager.py
524 524
    assert select2_json['more'] is True
525 525

  
526 526
    select2_json = request_select2(app, resp, fetch_all=True)
527
    assert len(select2_json['results']) == 17
527
    assert len(select2_json['results']) == 20
528 528
    choices = [x['text'] for x in select2_json['results']]
529 529
    assert choices == [
530
        'Default organizational unit - Authenticators - Default organizational unit',
530 531
        'Default organizational unit - Managers of role "simple role"',
531 532
        'Default organizational unit - Roles - Default organizational unit',
532 533
        'Default organizational unit - Services - Default organizational unit',
533 534
        'Default organizational unit - Users - Default organizational unit',
535
        'OU1 - Authenticators - OU1',
534 536
        'OU1 - role_ou1',
535 537
        'OU1 - Roles - OU1',
536 538
        'OU1 - Services - OU1',
537 539
        'OU1 - Users - OU1',
538 540
        'Manager',
541
        'Manager of authenticators',
539 542
        'Manager of organizational units',
540 543
        'Manager of roles',
541 544
        'Manager of services',
......
558 561
    assert select2_json['more'] is False
559 562

  
560 563
    select2_json = request_select2(app, resp, term='Manager')
561
    assert len(select2_json['results']) == 8
564
    assert len(select2_json['results']) == 9
562 565
    select2_json = request_select2(app, resp, term='Manager of')
563
    assert len(select2_json['results']) == 7
566
    assert len(select2_json['results']) == 8
564 567
    select2_json = request_select2(app, resp, term='Manager of serv')
565 568
    assert len(select2_json['results']) == 1
566 569

  
567
-