Projet

Général

Profil

0001-authenticators-log-modifications-to-journal-65358.patch

Valentin Deniaud, 08 juin 2022 10:06

Télécharger (18,3 ko)

Voir les différences:

Subject: [PATCH] authenticators: log modifications to journal (#65358)

 .../authenticators/journal_event_types.py     | 108 ++++++++++++++++++
 .../apps/authenticators/manager_urls.py       |   5 +
 .../authenticators/authenticator_detail.html  |   3 +-
 .../authenticators/authenticator_journal.html |   7 ++
 src/authentic2/apps/authenticators/views.py   |  37 ++++++
 tests/test_manager_authenticators.py          |  20 +++-
 tests/test_manager_journal.py                 |  42 +++++++
 7 files changed, 218 insertions(+), 4 deletions(-)
 create mode 100644 src/authentic2/apps/authenticators/journal_event_types.py
 create mode 100644 src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_journal.html
src/authentic2/apps/authenticators/journal_event_types.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2022 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.utils.translation import gettext_lazy as _
18

  
19
from authentic2.apps.journal.models import EventTypeDefinition
20
from authentic2.apps.journal.utils import form_to_old_new
21

  
22
from .models import BaseAuthenticator
23

  
24

  
25
class AuthenticatorCreation(EventTypeDefinition):
26
    name = 'authenticator.creation'
27
    label = _('authenticator creation')
28

  
29
    @classmethod
30
    def record(cls, *, user, session, authenticator):
31
        super().record(user=user, session=session, references=[authenticator])
32

  
33
    @classmethod
34
    def get_message(cls, event, context):
35
        (authenticator,) = event.get_typed_references(BaseAuthenticator)
36
        if context != authenticator:
37
            return _('creation of authenticator "%s"') % authenticator
38
        else:
39
            return _('creation')
40

  
41

  
42
class AuthenticatorEdit(EventTypeDefinition):
43
    name = 'authenticator.edit'
44
    label = _('authenticator edit')
45

  
46
    @classmethod
47
    def record(cls, *, user, session, form):
48
        super().record(user=user, session=session, references=[form.instance], data=form_to_old_new(form))
49

  
50
    @classmethod
51
    def get_message(cls, event, context):
52
        (authenticator,) = event.get_typed_references(BaseAuthenticator)
53
        new = event.get_data('new') or {}
54
        edited_attributes = ', '.join(new) or ''
55
        if context != authenticator:
56
            return _('edit of authenticator "{authenticator}" ({change})').format(
57
                authenticator=authenticator, change=edited_attributes
58
            )
59
        else:
60
            return _('edit ({change})').format(change=edited_attributes)
61

  
62

  
63
class AuthenticatorEnable(EventTypeDefinition):
64
    name = 'authenticator.enable'
65
    label = _('authenticator enable')
66

  
67
    @classmethod
68
    def record(cls, *, user, session, authenticator):
69
        super().record(user=user, session=session, references=[authenticator])
70

  
71
    @classmethod
72
    def get_message(cls, event, context):
73
        (authenticator,) = event.get_typed_references(BaseAuthenticator)
74
        if context != authenticator:
75
            return _('enable of authenticator "%s"') % authenticator
76
        else:
77
            return _('enable')
78

  
79

  
80
class AuthenticatorDisable(EventTypeDefinition):
81
    name = 'authenticator.disable'
82
    label = _('authenticator disable')
83

  
84
    @classmethod
85
    def record(cls, *, user, session, authenticator):
86
        super().record(user=user, session=session, references=[authenticator])
87

  
88
    @classmethod
89
    def get_message(cls, event, context):
90
        (authenticator,) = event.get_typed_references(BaseAuthenticator)
91
        if context != authenticator:
92
            return _('disable of authenticator "%s"') % authenticator
93
        else:
94
            return _('disable')
95

  
96

  
97
class AuthenticatorDeletion(EventTypeDefinition):
98
    name = 'authenticator.deletion'
99
    label = _('authenticator deletion')
100

  
101
    @classmethod
102
    def record(cls, *, user, session, authenticator):
103
        super().record(user=user, session=session, references=[authenticator])
104

  
105
    @classmethod
106
    def get_message(cls, event, context):
107
        (authenticator,) = event.get_typed_references(BaseAuthenticator)
108
        return _('deletion of authenticator "%s"') % authenticator
src/authentic2/apps/authenticators/manager_urls.py
71 71
            views.toggle,
72 72
            name='a2-manager-authenticator-toggle',
73 73
        ),
74
        path(
75
            'authenticators/<int:pk>/journal/',
76
            views.journal,
77
            name='a2-manager-authenticator-journal',
78
        ),
74 79
    ],
75 80
)
src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_detail.html
4 4
{% block appbar %}
5 5
  {{ block.super }}
6 6
  <span class="actions">
7
    {% if not object.internal %}
8 7
    <a class="extra-actions-menu-opener"></a>
9
    {% endif %}
10 8

  
11 9
    {% if object.has_valid_configuration and not object.internal %}
12 10
    <a href="{% url 'a2-manager-authenticator-toggle' pk=object.pk %}">{{ object.enabled|yesno:_("Disable,Enable") }}</a>
13 11
    {% endif %}
14 12
    <a href="{% url 'a2-manager-authenticator-edit' pk=object.pk %}">{% trans "Edit" %}</a>
15 13
    <ul class="extra-actions-menu">
14
      <li><a href="{% url 'a2-manager-authenticator-journal' pk=object.pk %}">{% trans "Journal" %}</a></li>
16 15
      {% if not object.internal %}
17 16
        <li><a rel="popup" href="{% url 'a2-manager-authenticator-delete' pk=object.pk %}">{% trans "Delete" %}</a></li>
18 17
      {% endif %}
src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_journal.html
1
{% extends "authentic2/manager/journal.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb-before-title %}
5
  <a href="{% url 'a2-manager-authenticators' %}">{% trans "Authenticators" %}</a>
6
  <a href="{% url 'a2-manager-authenticator-detail' pk=object.pk %}">{{ object }}</a>
7
{% endblock %}
src/authentic2/apps/authenticators/views.py
17 17
from django.contrib import messages
18 18
from django.core.exceptions import PermissionDenied
19 19
from django.http import HttpResponseRedirect
20
from django.shortcuts import get_object_or_404
20 21
from django.urls import reverse, reverse_lazy
22
from django.utils.functional import cached_property
21 23
from django.utils.translation import ugettext as _
22 24
from django.views.generic import CreateView, DeleteView, DetailView, UpdateView
23 25
from django.views.generic.list import ListView
24 26

  
25 27
from authentic2.apps.authenticators import forms
26 28
from authentic2.apps.authenticators.models import BaseAuthenticator
29
from authentic2.apps.journal.views import JournalViewWithContext
30
from authentic2.manager.journal_views import BaseJournalView
27 31
from authentic2.manager.views import MediaMixin, TitleMixin
28 32

  
29 33

  
......
49 53
    def get_success_url(self):
50 54
        return reverse('a2-manager-authenticator-edit', kwargs={'pk': self.object.pk})
51 55

  
56
    def form_valid(self, form):
57
        resp = super().form_valid(form)
58
        self.request.journal.record('authenticator.creation', authenticator=form.instance)
59
        return resp
60

  
52 61

  
53 62
add = AuthenticatorAddView.as_view()
54 63

  
......
73 82
    def get_form_class(self):
74 83
        return self.object.manager_form_class
75 84

  
85
    def form_valid(self, form):
86
        self.request.journal.record('authenticator.edit', form=form)
87
        return super().form_valid(form)
88

  
76 89

  
77 90
edit = AuthenticatorEditView.as_view()
78 91

  
......
88 101
            raise PermissionDenied
89 102
        return super().dispatch(*args, **kwargs)
90 103

  
104
    def delete(self, *args, **kwargs):
105
        self.request.journal.record('authenticator.deletion', authenticator=self.get_object())
106
        return super().delete(*args, **kwargs)
107

  
91 108

  
92 109
delete = AuthenticatorDeleteView.as_view()
93 110

  
......
105 122
        if self.authenticator.enabled:
106 123
            self.authenticator.enabled = False
107 124
            self.authenticator.save()
125
            self.request.journal.record('authenticator.disable', authenticator=self.authenticator)
108 126
            message = _('Authenticator has been disabled.')
109 127
        else:
110 128
            self.authenticator.enabled = True
111 129
            self.authenticator.save()
130
            self.request.journal.record('authenticator.enable', authenticator=self.authenticator)
112 131
            message = _('Authenticator has been enabled.')
113 132

  
114 133
        messages.info(self.request, message)
......
116 135

  
117 136

  
118 137
toggle = AuthenticatorToggleView.as_view()
138

  
139

  
140
class AuthenticatorJournal(JournalViewWithContext, BaseJournalView):
141
    template_name = 'authentic2/authenticators/authenticator_journal.html'
142
    title = _('Journal')
143

  
144
    @cached_property
145
    def context(self):
146
        return get_object_or_404(BaseAuthenticator.authenticators.all(), pk=self.kwargs['pk'])
147

  
148
    def get_context_data(self, **kwargs):
149
        ctx = super().get_context_data(**kwargs)
150
        ctx['object'] = self.context
151
        ctx['object_name'] = str(self.context)
152
        return ctx
153

  
154

  
155
journal = AuthenticatorJournal.as_view()
tests/test_manager_authenticators.py
20 20
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
21 21
from authentic2_auth_oidc.models import OIDCProvider
22 22

  
23
from .utils import login, logout
23
from .utils import assert_event, login, logout
24 24

  
25 25

  
26 26
def test_authenticators_authorization(app, simple_user, superuser):
......
45 45
    assert 'Click "Edit" to change configuration.' in resp.text
46 46
    # cannot delete password authenticator
47 47
    assert 'Delete' not in resp.text
48
    assert 'extra-actions-menu-opener' not in resp.text
49 48
    assert 'configuration is not complete' not in resp.text
50 49
    app.get('/manage/authenticators/%s/delete/' % authenticator.pk, status=403)
51 50

  
......
77 76
            "Show condition: &#x27;backoffice&#x27; in login_hint or remotre_addr == &#x27;1.2.3.4&#x27;"
78 77
            in resp.text
79 78
        )
79
    assert_event('authenticator.edit', user=superuser, session=app.session)
80 80

  
81 81
    resp = resp.click('Edit')
82 82
    resp.form['show_condition'] = "remote_addr in dnsbl('ddns.entrouvert.org')"
......
87 87
    assert 'Disable' not in resp.text
88 88
    app.get('/manage/authenticators/%s/toggle/' % authenticator.pk, status=403)
89 89

  
90
    resp = resp.click('Journal')
91
    assert resp.text.count('edit (show_condition)') == 2
92

  
90 93
    # cannot add another password authenticator
91 94
    resp = app.get('/manage/authenticators/add/')
92 95
    assert 'Password' not in resp.text
......
102 105
    resp.form['ou'] = ou1.pk
103 106
    resp = resp.form.submit()
104 107
    assert '/edit/' in resp.location
108
    assert_event('authenticator.creation', user=superuser, session=app.session)
105 109

  
106 110
    provider = OIDCProvider.objects.filter(slug='test').get()
107 111
    resp = app.get(provider.get_absolute_url())
......
124 128
    resp.form['userinfo_endpoint'] = 'https://oidc.example.com/user_info'
125 129
    resp.form['idtoken_algo'] = 2
126 130
    resp = resp.form.submit().follow()
131
    assert_event('authenticator.edit', user=superuser, session=app.session)
127 132

  
128 133
    assert 'Issuer: https://oidc.example.com' in resp.text
129 134
    assert 'Scopes: profile email' in resp.text
......
136 141
    resp = resp.click('Configure', index=1)
137 142
    resp = resp.click('Enable').follow()
138 143
    assert 'Authenticator has been enabled.' in resp.text
144
    assert_event('authenticator.enable', user=superuser, session=app.session)
145

  
146
    resp = resp.click('Journal')
147
    assert 'enable' in resp.text
148
    assert (
149
        'edit (issuer, scopes, strategy, idtoken_algo, token_endpoint, userinfo_endpoint, authorization_endpoint)'
150
        in resp.text
151
    )
152
    assert 'creation' in resp.text
139 153

  
140 154
    resp = app.get('/manage/authenticators/')
141 155
    assert 'class="section disabled"' not in resp.text
......
162 176
    resp = resp.click('Configure', index=1)
163 177
    resp = resp.click('Disable').follow()
164 178
    assert 'Authenticator has been disabled.' in resp.text
179
    assert_event('authenticator.disable', user=superuser, session=app.session)
165 180

  
166 181
    resp = app.get('/manage/authenticators/')
167 182
    assert 'class="section disabled"' in resp.text
......
170 185
    resp = resp.click('Delete')
171 186
    resp = resp.form.submit().follow()
172 187
    assert not OIDCProvider.objects.filter(slug='test').exists()
188
    assert_event('authenticator.deletion', user=superuser, session=app.session)
tests/test_manager_journal.py
23 23

  
24 24
from authentic2.a2_rbac.models import Role
25 25
from authentic2.a2_rbac.utils import get_default_ou
26
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
26 27
from authentic2.apps.journal.models import Event, _registry
27 28
from authentic2.custom_user.models import Profile, ProfileType, User
28 29
from authentic2.journal import journal
......
56 57
    role_user = Role.objects.create(name="role1", ou=ou)
57 58
    role_agent = Role.objects.create(name="role2", ou=ou)
58 59
    service = Service.objects.create(name="service")
60
    authenticator = LoginPasswordAuthenticator.objects.create(slug='test')
59 61

  
60 62
    class EventFactory:
61 63
        date = make_aware(datetime.datetime(2020, 1, 1))
......
289 291
    )
290 292
    make('user.notification.inactivity', user=user, days_of_inactivity=120, days_to_deletion=20)
291 293
    make('user.deletion.inactivity', user=user, days_of_inactivity=140)
294
    make('authenticator.creation', user=agent, session=session2, authenticator=authenticator)
295
    authenticator_edit_form = mock.Mock(spec=['instance', 'initial', 'changed_data', 'cleaned_data'])
296
    authenticator_edit_form.instance = authenticator
297
    authenticator_edit_form.initial = {'name': 'old'}
298
    authenticator_edit_form.changed_data = ['name']
299
    authenticator_edit_form.cleaned_data = {'name': 'new'}
300
    make('authenticator.edit', user=agent, session=session2, form=authenticator_edit_form)
301
    make('authenticator.enable', user=agent, session=session2, authenticator=authenticator)
302
    make('authenticator.disable', user=agent, session=session2, authenticator=authenticator)
303
    make('authenticator.deletion', user=agent, session=session2, authenticator=authenticator)
292 304

  
293 305
    # verify we created at least one event for each type
294 306
    assert set(Event.objects.values_list("type__name", flat=True)) == set(_registry)
......
650 662
            'type': 'user.deletion.inactivity',
651 663
            'user': 'Johnny doe',
652 664
        },
665
        {
666
            'message': 'creation of authenticator "Password"',
667
            'timestamp': 'Jan. 3, 2020, 3 a.m.',
668
            'type': 'authenticator.creation',
669
            'user': 'agent',
670
        },
671
        {
672
            'message': 'edit of authenticator "Password" (name)',
673
            'timestamp': 'Jan. 3, 2020, 4 a.m.',
674
            'type': 'authenticator.edit',
675
            'user': 'agent',
676
        },
677
        {
678
            'message': 'enable of authenticator "Password"',
679
            'timestamp': 'Jan. 3, 2020, 5 a.m.',
680
            'type': 'authenticator.enable',
681
            'user': 'agent',
682
        },
683
        {
684
            'message': 'disable of authenticator "Password"',
685
            'timestamp': 'Jan. 3, 2020, 6 a.m.',
686
            'type': 'authenticator.disable',
687
            'user': 'agent',
688
        },
689
        {
690
            'message': 'deletion of authenticator "Password"',
691
            'timestamp': 'Jan. 3, 2020, 7 a.m.',
692
            'type': 'authenticator.deletion',
693
            'user': 'agent',
694
        },
653 695
    ]
654 696

  
655 697
    agent_page = response.click('agent', index=1)
656
-