Projet

Général

Profil

0003-misc-integration-of-journal-in-manager-47155.patch

Benjamin Dauvergne, 14 octobre 2020 13:33

Télécharger (90,2 ko)

Voir les différences:

Subject: [PATCH 3/3] misc: integration of journal in manager (#47155)

 src/authentic2/custom_user/managers.py        |   8 +-
 src/authentic2/manager/forms.py               |   2 +-
 src/authentic2/manager/journal_event_types.py | 503 ++++++++++
 src/authentic2/manager/journal_views.py       |  97 ++
 src/authentic2/manager/role_views.py          |  74 +-
 .../static/authentic2/manager/js/manager.js   |  15 +
 .../templates/authentic2/manager/journal.html |  43 +
 .../authentic2/manager/role_common.html       |   4 +
 .../authentic2/manager/role_journal.html      |  10 +
 .../authentic2/manager/role_members.html      |   7 +-
 .../templates/authentic2/manager/roles.html   |   3 +
 .../authentic2/manager/roles_journal.html     |   9 +
 .../authentic2/manager/user_detail.html       |   1 +
 .../authentic2/manager/user_journal.html      |   7 +
 src/authentic2/manager/urls.py                |  22 +-
 src/authentic2/manager/user_views.py          |  55 +-
 src/authentic2/manager/views.py               |  23 +-
 tests/test_manager_journal.py                 | 892 ++++++++++++++++++
 18 files changed, 1748 insertions(+), 27 deletions(-)
 create mode 100644 src/authentic2/manager/journal_event_types.py
 create mode 100644 src/authentic2/manager/journal_views.py
 create mode 100644 src/authentic2/manager/templates/authentic2/manager/journal.html
 create mode 100644 src/authentic2/manager/templates/authentic2/manager/role_journal.html
 create mode 100644 src/authentic2/manager/templates/authentic2/manager/roles_journal.html
 create mode 100644 src/authentic2/manager/templates/authentic2/manager/user_journal.html
 create mode 100644 tests/test_manager_journal.py
src/authentic2/custom_user/managers.py
78 78
            self = self.distinct()
79 79
        return self
80 80

  
81
    def find_duplicates(self, first_name, last_name, birthdate=None):
81
    def find_duplicates(self, first_name=None, last_name=None, fullname=None, birthdate=None):
82 82
        with connection.cursor() as cursor:
83 83
            cursor.execute(
84 84
                "SET pg_trgm.similarity_threshold = %f" % app_settings.A2_DUPLICATES_THRESHOLD
85 85
            )
86 86

  
87
        name = '%s %s' % (first_name, last_name)
87
        if fullname is not None:
88
            name = fullname
89
        else:
90
            assert first_name is not None and last_name is not None
91
            name = '%s %s' % (first_name, last_name)
88 92
        name = unicodedata.normalize('NFKD', name).encode('ascii', 'ignore').decode('ascii').lower()
89 93

  
90 94
        qs = self.filter(deleted__isnull=True)
src/authentic2/manager/forms.py
1 1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
2
# Copyright (C) 2010-2020 Entr'ouvert
3 3
#
4 4
# This program is free software: you can redistribute it and/or modify it
5 5
# under the terms of the GNU Affero General Public License as published
src/authentic2/manager/journal_event_types.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2020 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.contrib.auth import get_user_model
18
from django.utils.translation import ugettext_lazy as _
19

  
20
from authentic2.journal_event_types import get_attributes_label, EventTypeWithService
21
from authentic2.apps.journal.models import EventTypeDefinition
22
from authentic2.apps.journal.utils import form_to_old_new
23

  
24

  
25
from django_rbac.utils import get_role_model
26

  
27
User = get_user_model()
28
Role = get_role_model()
29

  
30

  
31
class ManagerUserCreation(EventTypeDefinition):
32
    name = 'manager.user.creation'
33
    label = _('user creation')
34

  
35
    @classmethod
36
    def record(cls, user, session, form):
37
        super().record(user=user, session=session, references=[form.instance])
38

  
39
    @classmethod
40
    def get_message(cls, event, context):
41
        (user,) = event.get_typed_references(User)
42
        # user journal page
43
        if context and context == user:
44
            return _('creation by administrator')
45
        elif user:
46
            # manager gloabal journal page
47
            return _('creation of user "%s"') % user.get_full_name()
48
        return super().get_message(event, context)
49

  
50

  
51
class ManagerUserProfileEdit(EventTypeDefinition):
52
    name = 'manager.user.profile.edit'
53
    label = _('user profile edit')
54

  
55
    @classmethod
56
    def record(cls, user, session, form):
57
        super().record(user=user, session=session, references=[form.instance], data=form_to_old_new(form))
58

  
59
    @classmethod
60
    def get_message(cls, event, context):
61
        (user,) = event.get_typed_references(User)
62
        new = event.get_data('new') or {}
63
        edited_attributes = ', '.join(get_attributes_label(new)) or ''
64
        if context and context == user:
65
            return _('edit by administrator (%s)') % edited_attributes
66
        elif user:
67
            user_full_name = user.get_full_name()
68
            return _('edit of user "{0}" ({1})').format(user_full_name, edited_attributes)
69
        return super().get_message(event, context)
70

  
71

  
72
class ManagerUserEmailChangeRequest(EventTypeDefinition):
73
    name = 'manager.user.email.change.request'
74
    label = _('email change request')
75

  
76
    @classmethod
77
    def record(cls, user, session, form):
78
        data = {
79
            'old_email': form.instance.email,
80
            'email': form.cleaned_data.get('new_email'),
81
        }
82
        super().record(user=user, session=session, references=[form.instance], data=data)
83

  
84
    @classmethod
85
    def get_message(cls, event, context):
86
        (user,) = event.get_typed_references(User)
87
        new_email = event.get_data('email')
88
        if context and context == user:
89
            return _('email change for email address "%s" requested by administrator') % new_email
90
        elif user:
91
            user_full_name = user.get_full_name()
92
            return _('email change of user "{0}" for email address "{1}"').format(user_full_name, new_email)
93
        return super().get_message(event, context)
94

  
95

  
96
class ManagerUserPasswordChange(EventTypeDefinition):
97
    name = 'manager.user.password.change'
98
    label = _('user password change')
99

  
100
    @classmethod
101
    def record(cls, user, session, form):
102
        data = {
103
            'generate_password': form.cleaned_data['generate_password'],
104
            'send_mail': form.cleaned_data['send_mail'],
105
        }
106
        super().record(user=user, session=session, references=[form.instance], data=data)
107

  
108
    @classmethod
109
    def get_message(cls, event, context):
110
        (user,) = event.get_typed_references(User)
111
        send_mail = event.get_data('send_mail')
112
        if context and context == user:
113
            if send_mail:
114
                return _('password change by administrator and notification by mail')
115
            else:
116
                return _('password change by administrator')
117
        elif user:
118
            user_full_name = user.get_full_name()
119
            if send_mail:
120
                return _('password change of user "%s" and notification by mail') % user_full_name
121
            else:
122
                return _('password change of user "%s"') % user_full_name
123
        return super().get_message(event, context)
124

  
125

  
126
class ManagerUserPasswordResetRequest(EventTypeDefinition):
127
    name = 'manager.user.password.reset.request'
128
    label = _('user password reset request')
129

  
130
    @classmethod
131
    def record(cls, user, session, target_user):
132
        super().record(
133
            user=user, session=session, references=[target_user], data={'email': target_user.email}
134
        )
135

  
136
    @classmethod
137
    def get_message(cls, event, context):
138
        (user,) = event.get_typed_references(User)
139
        email = event.get_data('email')
140
        if context and context == user:
141
            return _('password reset request by administrator sent to "%s"') % email
142
        elif user:
143
            return _('password reset request of "{0}" sent to "{1}"').format(user.get_full_name(), email)
144
        return super().get_message(event, context)
145

  
146

  
147
class ManagerUserPasswordChangeForce(EventTypeDefinition):
148
    name = 'manager.user.password.change.force'
149
    label = _('mandatory password change at next login set')
150

  
151
    @classmethod
152
    def record(cls, user, session, target_user):
153
        super().record(user=user, session=session, references=[target_user])
154

  
155
    @classmethod
156
    def get_message(cls, event, context):
157
        (user,) = event.get_typed_references(User)
158
        if context and context == user:
159
            return _('mandatory password change at next login set by administrator')
160
        elif user:
161
            return _('mandatory password change at next login set for user "%s"') % user.get_full_name()
162
        return super().get_message(event, context)
163

  
164

  
165
class ManagerUserPasswordChangeUnforce(EventTypeDefinition):
166
    name = 'manager.user.password.change.unforce'
167
    label = _('mandatory password change at next login unset')
168

  
169
    @classmethod
170
    def record(cls, user, session, target_user):
171
        super().record(user=user, session=session, references=[target_user])
172

  
173
    @classmethod
174
    def get_message(cls, event, context):
175
        (user,) = event.get_typed_references(User)
176
        if context and context == user:
177
            return _('mandatory password change at next login unset by administrator')
178
        elif user:
179
            return _('mandatory password change at next login unset for user "%s"') % user.get_full_name()
180
        return super().get_message(event, context)
181

  
182

  
183
class ManagerUserActivation(EventTypeDefinition):
184
    name = 'manager.user.activation'
185
    label = _('user activation')
186

  
187
    @classmethod
188
    def record(cls, user, session, target_user):
189
        super().record(user=user, session=session, references=[target_user])
190

  
191
    @classmethod
192
    def get_message(cls, event, context):
193
        (user,) = event.get_typed_references(User)
194
        if context and context == user:
195
            return _('activation by administrator')
196
        elif user:
197
            return _('activation of user "%s"') % user.get_full_name()
198
        return super().get_message(event, context)
199

  
200

  
201
class ManagerUserDeactivation(EventTypeDefinition):
202
    name = 'manager.user.deactivation'
203
    label = _('user deactivation')
204

  
205
    @classmethod
206
    def record(cls, user, session, target_user):
207
        super().record(user=user, session=session, references=[target_user])
208

  
209
    @classmethod
210
    def get_message(cls, event, context):
211
        (user,) = event.get_typed_references(User)
212
        if context and context == user:
213
            return _('deactivation by administrator')
214
        elif user:
215
            return _('deactivation of user "%s"') % user.get_full_name()
216
        return super().get_message(event, context)
217

  
218

  
219
class ManagerUserDeletion(EventTypeDefinition):
220
    name = 'manager.user.deletion'
221
    label = _('user deletion')
222

  
223
    @classmethod
224
    def record(cls, user, session, target_user):
225
        super().record(user=user, session=session, references=[target_user])
226

  
227
    @classmethod
228
    def get_message(cls, event, context):
229
        (user,) = event.get_typed_references(User)
230
        if context and context == user:
231
            return _('deletion by administrator')
232
        elif user:
233
            return _('deletion of user "%s"') % user.get_full_name()
234
        return super().get_message(event, context)
235

  
236

  
237
class ManagerUserSSOAuthorizationDeletion(EventTypeWithService):
238
    name = 'manager.user.sso.authorization.deletion'
239
    label = _('delete authorization')
240

  
241
    @classmethod
242
    def record(cls, user, session, service, target_user):
243
        super().record(user=user, session=session, service=service, references=[target_user])
244

  
245
    @classmethod
246
    def get_message(cls, event, context):
247
        # first reference is to the service
248
        __, user = event.get_typed_references(None, User)
249
        service_name = cls.get_service_name(event)
250
        if context and context == user:
251
            return _('deletion of authorization of single sign on with "%s" by administrator') % service_name
252
        elif user:
253
            return _('deletion of authorization of single sign on with "%s" of user "%s"') % (
254
                service_name,
255
                user.get_full_name(),
256
            )
257
        return super().get_message(event, context)
258

  
259

  
260
class RoleEventsMixin(EventTypeDefinition):
261
    @classmethod
262
    def record(self, user, session, role, references=None, data=None):
263
        references = references or []
264
        references = [role] + references
265
        data = data or {}
266
        data.update(
267
            {'role_name': str(role), 'role_uuid': role.uuid,}
268
        )
269
        super().record(
270
            user=user, session=session, references=references, data=data,
271
        )
272

  
273

  
274
class ManagerRoleCreation(RoleEventsMixin):
275
    name = 'manager.role.creation'
276
    label = _('role creation')
277

  
278
    @classmethod
279
    def get_message(cls, event, context):
280
        (role,) = event.get_typed_references(Role)
281
        role = role or event.get_data('role_name')
282
        if context != role:
283
            return _('creation of role "%s"') % role
284
        else:
285
            return _('creation')
286

  
287

  
288
class ManagerRoleEdit(RoleEventsMixin):
289
    name = 'manager.role.edit'
290
    label = _('role edit')
291

  
292
    @classmethod
293
    def record(cls, user, session, role, form):
294
        super().record(user=user, session=session, role=role, data=form_to_old_new(form))
295

  
296
    @classmethod
297
    def get_message(cls, event, context):
298
        (role,) = event.get_typed_references(Role)
299
        role = role or event.get_data('role_name')
300
        new = event.get_data('new')
301
        edited_attributes = ', '.join(get_attributes_label(new)) or ''
302
        if context != role:
303
            return _('edit of role "%s" (%s)') % (role, edited_attributes)
304
        else:
305
            return _('edit (%s)') % edited_attributes
306

  
307

  
308
class ManagerRoleDeletion(RoleEventsMixin):
309
    name = 'manager.role.deletion'
310
    label = _('role deletion')
311

  
312
    @classmethod
313
    def get_message(cls, event, context):
314
        (role,) = event.get_typed_references(Role)
315
        role = role or event.get_data('role_name')
316
        if context != role:
317
            return _('deletion of role "%s"') % role
318
        else:
319
            return _('deletion')
320

  
321

  
322
class ManagerRoleMembershipGrant(RoleEventsMixin):
323
    name = 'manager.role.membership.grant'
324
    label = _('role membership grant')
325

  
326
    @classmethod
327
    def record(cls, user, session, role, member):
328
        data = {'member_name': member.get_full_name()}
329
        super().record(user=user, session=session, role=role, references=[member], data=data)
330

  
331
    @classmethod
332
    def get_message(cls, event, context):
333
        role, member = event.get_typed_references(Role, User)
334
        role = role or event.get_data('role_name')
335
        member = member or event.get_data('member_name')
336
        if context == member:
337
            return _('membership grant in role "%s"') % role
338
        elif context == role:
339
            return _('membership grant to user "%s"') % member
340
        else:
341
            return _('membership grant to user "{member}" in role "{role}"').format(member=member, role=role)
342

  
343

  
344
class ManagerRoleMembershipRemoval(RoleEventsMixin):
345
    name = 'manager.role.membership.removal'
346
    label = _('role membership removal')
347

  
348
    @classmethod
349
    def record(cls, user, session, role, member):
350
        data = {'member_name': member.get_full_name()}
351
        super().record(user=user, session=session, role=role, references=[member], data=data)
352

  
353
    @classmethod
354
    def get_message(cls, event, context):
355
        role, member = event.get_typed_references(Role, User)
356
        role = role or event.get_data('role_name')
357
        member = member or event.get_data('member_name')
358
        if context == member:
359
            return _('membership removal from role "%s"') % role
360
        elif context == role:
361
            return _('membership removal of user "%s"') % member
362
        else:
363
            return _('membership removal of user "{member}" from role "{role}"').format(
364
                member=member, role=role
365
            )
366

  
367

  
368
class ManagerRoleInheritanceAddition(RoleEventsMixin):
369
    name = 'manager.role.inheritance.addition'
370
    label = _('role inheritance addition')
371

  
372
    @classmethod
373
    def record(cls, user, session, parent, child):
374
        data = {
375
            'child_name': str(child),
376
            'child_uuid': child.uuid,
377
        }
378
        super().record(user=user, session=session, role=parent, references=[child], data=data)
379

  
380
    @classmethod
381
    def get_message(cls, event, context):
382
        parent, child = event.get_typed_references(Role, Role)
383
        parent = parent or event.get_data('role_name')
384
        child = child or event.get_data('child_name')
385
        if context == child:
386
            return _('inheritance addition from parent role "%s"') % parent
387
        elif context == parent:
388
            return _('inheritance addition to child role "%s"') % child
389
        else:
390
            return _('inheritance addition from parent role "{parent}" to child role "{child}"').format(
391
                parent=parent, child=child
392
            )
393

  
394

  
395
class ManagerRoleInheritanceRemoval(ManagerRoleInheritanceAddition):
396
    name = 'manager.role.inheritance.removal'
397
    label = _('role inheritance removal')
398

  
399
    @classmethod
400
    def get_message(cls, event, context):
401
        parent, child = event.get_typed_references(Role, Role)
402
        parent = parent or event.get_data('role_name')
403
        child = child or event.get_data('child_name')
404
        if context == child:
405
            return _('inheritance removal from parent role "%s"') % parent
406
        elif context == parent:
407
            return _('inheritance removal to child role "%s"') % child
408
        else:
409
            return _('inheritance removal from parent role "{parent}" to child role "{child}"').format(
410
                parent=parent, child=child
411
            )
412

  
413

  
414
class ManagerRoleAdministratorRoleAddition(RoleEventsMixin):
415
    name = 'manager.role.administrator.role.addition'
416
    label = _('role administrator role addition')
417

  
418
    @classmethod
419
    def record(cls, user, session, role, admin_role):
420
        data = {
421
            'admin_role_name': str(admin_role),
422
            'admin_role_uuid': admin_role.uuid,
423
        }
424
        super().record(user=user, session=session, role=role, references=[admin_role], data=data)
425

  
426
    @classmethod
427
    def get_message(cls, event, context):
428
        role, admin_role = event.get_typed_references(Role, Role)
429
        role = role or event.get_data('role_name')
430
        admin_role = admin_role or event.get('admin_role_name')
431
        if context == role:
432
            return _('addition of role "%s" as administrator') % admin_role
433
        elif context == admin_role:
434
            return _('addition as administrator of role "%s"') % role
435
        else:
436
            return _('addition of role "{admin_role}" as administrator of role "{role}"').format(
437
                admin_role=admin_role, role=role
438
            )
439

  
440

  
441
class ManagerRoleAdministratorRoleRemoval(ManagerRoleAdministratorRoleAddition):
442
    name = 'manager.role.administrator.role.removal'
443
    label = _('role administrator role removal')
444

  
445
    @classmethod
446
    def get_message(cls, event, context):
447
        role, admin_role = event.get_typed_references(Role, Role)
448
        role = role or event.get_data('role_name')
449
        admin_role = admin_role or event.get('admin_role_name')
450
        if context == role:
451
            return _('removal of role "%s" as administrator') % admin_role
452
        elif context == admin_role:
453
            return _('removal as administrator of role "%s"') % role
454
        else:
455
            return _('removal of role "{admin_role}" as administrator of role "{role}"').format(
456
                admin_role=admin_role, role=role
457
            )
458

  
459

  
460
class ManagerRoleAdministratorUserAddition(RoleEventsMixin):
461
    name = 'manager.role.administrator.user.addition'
462
    label = _('role administrator user addition')
463

  
464
    @classmethod
465
    def record(cls, user, session, role, admin_user):
466
        data = {
467
            'admin_user_name': admin_user.get_full_name(),
468
            'admin_user_uuid': admin_user.uuid,
469
        }
470
        super().record(user=user, session=session, role=role, references=[admin_user], data=data)
471

  
472
    @classmethod
473
    def get_message(cls, event, context):
474
        role, admin_user = event.get_typed_references(Role, User)
475
        role = role or event.get_data('role_name')
476
        admin_user = admin_user or event.get_data('admin_user_name')
477
        if context == role:
478
            return _('addition of user "%s" as administrator') % admin_user
479
        elif context == admin_user:
480
            return _('addition as administrator of role "%s"') % role
481
        else:
482
            return _('addition of user "{admin_user}" as administrator of role "{role}"').format(
483
                admin_user=admin_user, role=role
484
            )
485

  
486

  
487
class ManagerRoleAdministratorUserRemoval(ManagerRoleAdministratorUserAddition):
488
    name = 'manager.role.administrator.user.removal'
489
    label = _('role administrator user removal')
490

  
491
    @classmethod
492
    def get_message(cls, event, context):
493
        role, admin_user = event.get_typed_references(Role, User)
494
        role = role or event.get_data('role_name')
495
        admin_user = admin_user or event.get_data('admin_user_name')
496
        if context == role:
497
            return _('removal of user "%s" as administrator') % admin_user
498
        elif context == admin_user:
499
            return _('removal as administrator of role "%s"') % role
500
        else:
501
            return _('removal of user "{admin_user}" as administrator of role "{role}"').format(
502
                admin_user=admin_user, role=role
503
            )
src/authentic2/manager/journal_views.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2020 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
import uuid
18

  
19
from django.contrib.auth import get_user_model
20
from django.core.exceptions import PermissionDenied, ValidationError
21
from django.core.validators import EmailValidator
22
from django.db.models import Q
23
from django.utils.translation import ugettext_lazy as _
24

  
25
from authentic2.apps.journal.forms import JournalForm
26
from authentic2.apps.journal.search_engine import JournalSearchEngine
27
from authentic2.apps.journal.views import JournalView
28

  
29
from . import views
30

  
31
User = get_user_model()
32

  
33

  
34
class JournalSearchEngine(JournalSearchEngine):
35
    def search_by_uuid(self, lexem):
36
        # by user uuid
37
        try:
38
            user_uuid = uuid.UUID(lexem)
39
        except ValueError:
40
            yield self.q_false
41
        else:
42
            yield Q(user__uuid=user_uuid.hex)
43
    search_by_uuid.documentation = _(
44
        '''\
45
You can use <tt>uuid:1234</tt> to find all events related \
46
to user whose UUID is <tt>1234</tt>.'''
47
    )
48
    unmatched = None
49

  
50
    def lexem_queries(self, lexem):
51
        queries = list(super().lexem_queries(lexem))
52
        if queries:
53
            yield from queries
54
        elif '@' in lexem:
55
            # fallback for raw email
56
            try:
57
                EmailValidator(lexem)
58
            except ValidationError:
59
                pass
60
            else:
61
                yield from super().lexem_queries('email:' + lexem)
62
                yield from super().lexem_queries('username:' + lexem)
63

  
64
    def unmatched_lexems_query(self, lexems):
65
        fullname = ' '.join(lexem.strip() for lexem in lexems if lexem.strip())
66
        if fullname:
67
            users = User.objects.find_duplicates(fullname=fullname)
68
            return self.query_for_users(users)
69

  
70

  
71
class JournalForm(JournalForm):
72
    search_engine_class = JournalSearchEngine
73

  
74

  
75
class BaseJournalView(views.TitleMixin, views.MediaMixin, JournalView):
76
    template_name = 'authentic2/manager/journal.html'
77
    title = _('Journal')
78
    form_class = JournalForm
79

  
80
    def get_context_data(self, **kwargs):
81
        ctx = super().get_context_data(**kwargs)
82
        date_hierarchy = ctx['date_hierarchy']
83
        if date_hierarchy.title:
84
            ctx['title'] = _('Journal of %s') % date_hierarchy.title
85
        return ctx
86

  
87

  
88
class GlobalJournalView(BaseJournalView):
89
    template_name = 'authentic2/manager/journal.html'
90

  
91
    def dispatch(self, request, *args, **kwargs):
92
        if not request.user.is_superuser:
93
            raise PermissionDenied
94
        return super().dispatch(request, *args, **kwargs)
95

  
96

  
97
journal = GlobalJournalView.as_view()
src/authentic2/manager/role_views.py
17 17
import json
18 18

  
19 19
from django.core.exceptions import PermissionDenied, ValidationError
20
from django.utils.functional import cached_property
20 21
from django.utils.translation import ugettext_lazy as _
21 22
from django.urls import reverse
22 23
from django.views.generic import FormView, TemplateView
......
27 28
from django.db.models.query import Q, Prefetch
28 29
from django.db.models import Count, F
29 30
from django.contrib.auth import get_user_model
31
from django.shortcuts import get_object_or_404
30 32

  
31 33
from django_rbac.utils import get_role_model, get_permission_model, get_ou_model
32 34

  
33 35
from authentic2.forms.profile import modelform_factory
34 36
from authentic2.utils import redirect
35 37
from authentic2 import hooks, data_transfer
38
from authentic2.apps.journal.views import JournalViewWithContext
36 39

  
37 40
from . import tables, views, resources, forms, app_settings
38 41
from .utils import has_show_username
42
from .journal_views import BaseJournalView
43

  
44
OU = get_ou_model()
39 45

  
40 46

  
41 47
class RolesMixin(object):
......
48 54
        Permission = get_permission_model()
49 55
        permission_ct = ContentType.objects.get_for_model(Permission)
50 56
        ct_ct = ContentType.objects.get_for_model(ContentType)
51
        ou_ct = ContentType.objects.get_for_model(get_ou_model())
57
        ou_ct = ContentType.objects.get_for_model(OU)
52 58
        permission_qs = Permission.objects.filter(target_ct_id__in=[ct_ct.id, ou_ct.id]) \
53 59
            .values_list('id', flat=True)
54 60
        # only non role-admin roles, they are accessed through the
......
61 67
        return qs
62 68

  
63 69

  
64
class RolesView(views.HideOUColumnMixin, RolesMixin, views.BaseTableView):
70
class SearchOUMixin:
71
    @cached_property
72
    def ou(self):
73
        try:
74
            ou_id = int(self.request.GET['search-ou'])
75
        except (ValueError, KeyError):
76
            return None
77
        else:
78
            return OU.objects.filter(pk=ou_id).first()
79

  
80
    def get_context_data(self, **kwargs):
81
        return super().get_context_data(ou=self.ou, **kwargs)
82

  
83

  
84
class RolesView(SearchOUMixin, views.HideOUColumnMixin, RolesMixin,
85
                views.BaseTableView):
65 86
    template_name = 'authentic2/manager/roles.html'
66 87
    model = get_role_model()
67 88
    table_class = tables.RoleTable
......
98 119
        response = super(RoleAddView, self).form_valid(form)
99 120
        hooks.call_hooks('event', name='manager-add-role', user=self.request.user,
100 121
                         instance=form.instance, form=form)
122
        self.request.journal.record('manager.role.creation', role=form.instance)
101 123
        return response
102 124

  
103 125

  
......
138 160
        response = super(RoleEditView, self).form_valid(form)
139 161
        hooks.call_hooks('event', name='manager-edit-role', user=self.request.user,
140 162
                         instance=form.instance, form=form)
163
        self.request.journal.record('manager.role.edit', role=form.instance, form=form)
141 164
        return response
142 165

  
143 166
edit = RoleEditView.as_view()
......
179 202
                    self.object.members.add(user)
180 203
                    hooks.call_hooks('event', name='manager-add-role-member',
181 204
                                     user=self.request.user, role=self.object, member=user)
205
                    self.request.journal.record('manager.role.membership.grant', role=self.object, member=user)
182 206
            elif action == 'remove':
183 207
                if not self.object.members.filter(pk=user.pk).exists():
184 208
                    messages.warning(self.request, _('User was not in this role.'))
......
186 210
                    self.object.members.remove(user)
187 211
                    hooks.call_hooks('event', name='manager-remove-role-member',
188 212
                                     user=self.request.user, role=self.object, member=user)
213
                    self.request.journal.record('manager.role.membership.removal', role=self.object, member=user)
189 214
        else:
190 215
            messages.warning(self.request, _('You are not authorized'))
191 216
        return super(RoleMembersView, self).form_valid(form)
......
203 228
                                            self.object.children(include_self=False, annotate=True))
204 229
        ctx['parents'] = views.filter_view(self.request, self.object.parents(
205 230
            include_self=False, annotate=True).order_by(F('ou').asc(nulls_first=True), 'name'))
206
        ctx['has_multiple_ou'] = get_ou_model().objects.count() > 1
231
        ctx['has_multiple_ou'] = OU.objects.count() > 1
207 232
        ctx['admin_roles'] = views.filter_view(self.request,
208 233
                                               self.object.get_admin_role().children(include_self=False,
209 234
                                                                                     annotate=True))
......
324 349
            parent.add_child(role)
325 350
            hooks.call_hooks('event', name='manager-add-child-role', user=self.request.user,
326 351
                             parent=parent, child=role)
352
            self.request.journal.record('manager.role.inheritance.addition', parent=parent, child=role)
327 353
        return super(RoleAddChildView, self).form_valid(form)
328 354

  
329 355
add_child = RoleAddChildView.as_view()
......
349 375
            child.add_parent(role)
350 376
            hooks.call_hooks('event', name='manager-add-child-role', user=self.request.user,
351 377
                             parent=role, child=child)
378
            self.request.journal.record('manager.role.inheritance.addition', parent=role, child=child)
352 379
        return super(RoleAddParentView, self).form_valid(form)
353 380

  
354 381
add_parent = RoleAddParentView.as_view()
......
376 403
        self.object.remove_child(self.child)
377 404
        hooks.call_hooks('event', name='manager-remove-child-role', user=self.request.user,
378 405
                         parent=self.object, child=self.child)
406
        self.request.journal.record('manager.role.inheritance.removal', parent=self.object, child=self.child)
379 407
        return redirect(self.request, self.success_url)
380 408

  
381 409
remove_child = RoleRemoveChildView.as_view()
......
406 434
        self.object.remove_parent(self.parent)
407 435
        hooks.call_hooks('event', name='manager-remove-child-role', user=self.request.user,
408 436
                         parent=self.parent, child=self.object)
437
        self.request.journal.record('manager.role.inheritance.removal', parent=self.parent, child=self.object)
409 438
        return redirect(self.request, self.success_url)
410 439

  
411 440
remove_parent = RoleRemoveParentView.as_view()
......
431 460
            administered_role.get_admin_role().add_child(role)
432 461
            hooks.call_hooks('event', name='manager-add-admin-role', user=self.request.user,
433 462
                             role=administered_role, admin_role=role)
463
            self.request.journal.record('manager.role.administrator.role.addition',
464
                                        role=administered_role, admin_role=role)
434 465
        return super(RoleAddAdminRoleView, self).form_valid(form)
435 466

  
436 467
add_admin_role = RoleAddAdminRoleView.as_view()
......
459 490
        self.object.get_admin_role().remove_child(self.child)
460 491
        hooks.call_hooks('event', name='manager-remove-admin-role',
461 492
                         user=self.request.user, role=self.object, admin_role=self.child)
493
        self.request.journal.record('manager.role.administrator.role.removal',
494
                                    role=self.object, admin_role=self.child)
462 495
        return redirect(self.request, self.success_url)
463 496

  
464 497
remove_admin_role = RoleRemoveAdminRoleView.as_view()
......
484 517
            administered_role.get_admin_role().members.add(user)
485 518
            hooks.call_hooks('event', name='manager-add-admin-role-user', user=self.request.user,
486 519
                             role=administered_role, admin=user)
520
            self.request.journal.record('manager.role.administrator.user.addition',
521
                                        role=administered_role, admin_user=user)
487 522
        return super(RoleAddAdminUserView, self).form_valid(form)
488 523

  
489 524
add_admin_user = RoleAddAdminUserView.as_view()
......
512 547
        self.object.get_admin_role().members.remove(self.user)
513 548
        hooks.call_hooks('event', name='remove-remove-admin-role-user', user=self.request.user,
514 549
                         role=self.object, admin=self.user)
550
        self.request.journal.record('manager.role.administrator.user.removal',
551
                                    role=self.object, admin_user=self.user)
515 552
        return redirect(self.request, self.success_url)
516 553

  
517 554
remove_admin_user = RoleRemoveAdminUserView.as_view()
......
557 594

  
558 595

  
559 596
roles_import = RolesImportView.as_view()
597

  
598

  
599
class RoleJournal(views.MultipleOUMixin, views.PermissionMixin, JournalViewWithContext, BaseJournalView):
600
    template_name = 'authentic2/manager/role_journal.html'
601
    permissions = ['a2_rbac.view_role']
602
    title = _('Journal')
603

  
604
    @cached_property
605
    def context(self):
606
        return get_object_or_404(get_role_model(), pk=self.kwargs['pk'])
607

  
608
    def get_context_data(self, **kwargs):
609
        ctx = super().get_context_data(**kwargs)
610
        ctx['object'] = self.context
611
        return ctx
612

  
613
journal = RoleJournal.as_view()
614

  
615

  
616
class RolesJournal(SearchOUMixin, views.MultipleOUMixin, views.PermissionMixin,
617
                   JournalViewWithContext, BaseJournalView):
618
    template_name = 'authentic2/manager/roles_journal.html'
619
    permissions = ['a2_rbac.view_role']
620
    title = _('Journal')
621

  
622
    @cached_property
623
    def context(self):
624
        return get_role_model()
625

  
626

  
627
roles_journal = RolesJournal.as_view()
src/authentic2/manager/static/authentic2/manager/js/manager.js
219 219
            window.history.replaceState({'form': '#search-form', 'values': $('#search-form').values()}, window.document.title, window.location.href)
220 220
        }
221 221
        $(window.document).trigger('gadjo:content-update');
222
        function FitToContent(id, maxHeight)
223
        {
224
           var text = id && id.style ? id : document.getElementById(id);
225
           if ( !text )
226
              return;
227

  
228
        }
229
        $('textarea.js-autoresize').each(function () {
230
          this.setAttribute('style', 'height:' + (this.scrollHeight) + 'px;overflow-y:hidden;');
231
        })
232
        $(document).on('input', 'textarea.js-autoresize', function () {
233
          this.style.height = 'auto';
234
          this.style.height = (this.scrollHeight) + 'px';
235
          return true;
236
        });
222 237
    });
223 238
})(jQuery, window)
src/authentic2/manager/templates/authentic2/manager/journal.html
1
{% extends "authentic2/manager/base.html" %}
2
{% load i18n gadjo %}
3

  
4
{% block page-title %}{{ block.super }} - {% trans "Journal" %}{% endblock %}
5

  
6
{% block breadcrumb %}
7
  {{ block.super }}
8
  {% block breadcrumb-before-title %}
9
  {% endblock %}
10
  <a href="#">{% trans 'Journal' %}</a>
11
  {% for caption, url in date_hierarchy.back_urls %}
12
      <a href="{{ url }}">{{ caption }}</a>
13
  {% endfor %}
14
{% endblock %}
15

  
16
{% block sidebar %}
17
  <aside id="sidebar">
18
      <h3>{% trans "Search" context "title" %}</h3>
19
      <div class="journal-list--search-form">
20
          <form action="{{ form.url }}">
21
              {{ form|with_template }}
22
              <button>{% trans "Search" %}</button>
23
          </form>
24
      </div>
25
      {% if date_hierarchy.choice_name %}
26
          <h4>{{ date_hierarchy.choice_name }}</h4>
27
          <p>
28
              {% for caption, url in date_hierarchy.choice_urls %}
29
                  <a href="{{ url }}">{{ caption }}</a>
30
              {% endfor %}
31
          </p>
32
      {% endif %}
33
      <div class="documentation">
34
          {% for documentation in form.search_engine_class.documentation %}
35
            <p>{{ documentation|safe }}</p>
36
          {% endfor %}
37
      </div>
38
  </aside>
39
{% endblock %}
40

  
41
{% block main %}
42
  {% include "journal/event_list.html" %}
43
{% endblock %}
src/authentic2/manager/templates/authentic2/manager/role_common.html
6 6
{% block breadcrumb %}
7 7
  {{ block.super }}
8 8
  <a href="{% url 'a2-manager-roles' %}">{% trans 'Roles' %}</a>
9
  {% firstof ou object.ou as current_ou %}
10
  {% if multiple_ou and current_ou %}
11
    <a href="{% url 'a2-manager-roles' %}?search-ou={{ current_ou.id }}">{{ current_ou }}</a>
12
  {% endif %}
9 13
{% endblock %}
src/authentic2/manager/templates/authentic2/manager/role_journal.html
1
{% extends "authentic2/manager/journal.html" %}
2
{% load i18n gadjo %}
3

  
4
{% block breadcrumb-before-title %}
5
  <a href="{% url 'a2-manager-roles' %}">{% trans 'Roles' %}</a>
6
  {% if multiple_ou and object.ou %}
7
    <a href="../?search-ou={{ object.ou.pk }}">{{ object.ou }}</a>
8
  {% endif %}
9
  <a href="../">{{ object }}</a>
10
{% endblock %}
src/authentic2/manager/templates/authentic2/manager/role_members.html
3 3

  
4 4
{% block breadcrumb %}
5 5
  {{ block.super }}
6
  {% if multiple_ou and object.ou %}
7
    <a href="../?search-ou={{ object.ou.pk }}">{{ object.ou }}</a>
8
  {% endif %}
9 6
  <a href="#">{{ object }}</a>
10 7
{% endblock %}
11 8

  
......
19 16
{% block appbar %}
20 17
  {{ block.super }}
21 18
  <span class="actions">
19
    <a class="extra-actions-menu-opener"></a>
22 20
  {% if not object.is_internal and view.can_delete %}
23 21
    <a rel="popup" href="{% url "a2-manager-role-delete" pk=object.pk %}">{% trans "Delete" %}</a>
24 22
  {% else %}
......
36 34
  {% if perms.a2_rbac.admin_permission %}
37 35
    <a href="{% url "a2-manager-role-permissions" pk=object.pk %}">{% trans "Permissions" %}</a>
38 36
  {% endif %}
37
    <ul class="extra-actions-menu">
38
      <li><a href="{% url "a2-manager-role-journal" pk=object.pk %}">{% trans "Journal" %}</a></li>
39
    </ul>
39 40
  </span>
40 41
{% endblock %}
41 42

  
src/authentic2/manager/templates/authentic2/manager/roles.html
3 3

  
4 4
{% block page-title %}{{ block.super }} - {% trans "Roles" %}{% endblock %}
5 5

  
6
{% block page_title %}{% if multiple_ou and ou %}{{ ou }}{% else %}{{ block.super }}{% endif %}{% endblock %}
7

  
6 8
{% block appbar %}
7 9
  {{ block.super }}
8 10
  <span class="actions">
......
13 15
    <a href="#" class="disabled" rel="popup">{% trans "Add role" %}</a>
14 16
  {% endif %}
15 17
  <ul class="extra-actions-menu">
18
      <li><a href="{% url "a2-manager-roles-journal" %}{% if multiple_ou and ou %}?search-ou={{ ou.id }}{% endif %}">{% trans "Journal" %}</a></li>
16 19
    <li><a download href="{% url 'a2-manager-roles-export' format="json" %}?{{ request.GET.urlencode }}">{% trans 'Export' %}</a></li>
17 20
    {% if view.can_add %}
18 21
    <li><a href="{% url 'a2-manager-roles-import' %}?{{ request.GET.urlencode }}" rel="popup">{% trans 'Import' %}</a></li>
src/authentic2/manager/templates/authentic2/manager/roles_journal.html
1
{% extends "authentic2/manager/journal.html" %}
2
{% load i18n gadjo %}
3

  
4
{% block breadcrumb-before-title %}
5
  <a href="{% url 'a2-manager-roles' %}">{% trans 'Roles' %}</a>
6
  {% if multiple_ou and ou %}
7
    <a href="../?search-ou={{ ou.id }}">{{ ou }}</a>
8
  {% endif %}
9
{% endblock %}
src/authentic2/manager/templates/authentic2/manager/user_detail.html
22 22
      {% if view.is_oidc_services %}
23 23
      <li><a href="{% url "a2-manager-user-authorizations" pk=object.pk %}">{% trans "Consents" %}</a></li>
24 24
      {% endif %}
25
      <li><a href="{% url "a2-manager-user-journal" pk=object.pk %}">{% trans "Journal" %}</a></li>
25 26
    </ul>
26 27
  </span>
27 28
{% endblock %}
src/authentic2/manager/templates/authentic2/manager/user_journal.html
1
{% extends "authentic2/manager/journal.html" %}
2
{% load i18n gadjo %}
3

  
4
{% block breadcrumb-before-title %}
5
  <a href="{% url 'a2-manager-users' %}">{% trans 'Users' %}</a>
6
  <a href="{% url 'a2-manager-user-detail' pk=object.pk %}">{{ object.get_full_name }}</a>
7
{% endblock %}
src/authentic2/manager/urls.py
19 19
from django.views.i18n import JavaScriptCatalog
20 20
from django.contrib.auth.decorators import login_required
21 21
from django.utils.functional import lazy
22
from . import views, role_views, ou_views, user_views, service_views
22
from . import views, role_views, ou_views, user_views, service_views, journal_views
23 23
from ..decorators import required
24 24
from authentic2 import utils
25 25

  
......
67 67
            name='a2-manager-user-change-email'),
68 68
        url(r'^users/(?P<pk>\d+)/su/$', user_views.su,
69 69
            name='a2-manager-user-su'),
70
        url(r'^users/(?P<pk>\d+)/authorizations/$',
71
            user_views.user_authorizations,
72
            name='a2-manager-user-authorizations'),
73
        url(r'^users/(?P<pk>\d+)/journal/$',
74
            user_views.user_journal,
75
            name='a2-manager-user-journal'),
70 76
        # by uuid
71 77
        url(r'^users/uuid:(?P<slug>[a-z0-9]+)/$', user_views.user_detail,
72 78
            name='a2-manager-user-by-uuid-detail'),
......
81 87
        url(r'^users/uuid:(?P<slug>[a-z0-9]+)/change-email/$',
82 88
            user_views.user_change_email,
83 89
            name='a2-manager-user-by-uuid-change-email'),
84
        url(r'^users/(?P<pk>\d+)/authorizations/$',
85
            user_views.user_authorizations,
86
            name='a2-manager-user-authorizations'),
90
        url(r'^users/uuid:(?P<slug>[a-z0-9]+)/journal/$',
91
            user_views.user_journal,
92
            name='a2-manager-user-journal'),
87 93

  
88 94
        # Authentic2 roles
89 95
        url(r'^roles/$', role_views.listing,
......
94 100
            name='a2-manager-role-add'),
95 101
        url(r'^roles/export/(?P<format>csv|json)/$',
96 102
            role_views.export, name='a2-manager-roles-export'),
103
        url(r'^roles/journal/$', role_views.roles_journal,
104
            name='a2-manager-roles-journal'),
97 105
        url(r'^roles/(?P<pk>\d+)/$', role_views.members,
98 106
            name='a2-manager-role-members'),
99 107
        url(r'^roles/(?P<pk>\d+)/add-child/$', role_views.add_child,
......
124 132
            name='a2-manager-role-edit'),
125 133
        url(r'^roles/(?P<pk>\d+)/permissions/$', role_views.permissions,
126 134
            name='a2-manager-role-permissions'),
135
        url(r'^roles/(?P<pk>\d+)/journal/$', role_views.journal,
136
            name='a2-manager-role-journal'),
127 137

  
128 138

  
129 139
        # Authentic2 organizational units
......
152 162
        url(r'^services/(?P<service_pk>\d+)/edit/$', service_views.edit,
153 163
            name='a2-manager-service-edit'),
154 164

  
165
        # Journal
166
        url(r'^journal/$', journal_views.journal,
167
            name='a2-manager-journal'),
168

  
155 169
        # backoffice menu as json
156 170
        url(r'^menu.json$', views.menu_json),
157 171

  
src/authentic2/manager/user_views.py
20 20
import operator
21 21

  
22 22
from django.db import models
23
from django.utils.functional import cached_property
23 24
from django.utils.translation import ugettext_lazy as _, pgettext_lazy, ugettext
24 25
from django.utils.html import format_html
25 26
from django.urls import reverse
......
33 34
from django.views.generic.edit import BaseFormView
34 35
from django.views.generic.detail import SingleObjectMixin
35 36
from django.http import Http404, FileResponse, HttpResponseRedirect
37
from django.shortcuts import get_object_or_404
36 38

  
37 39
import tablib
38 40

  
......
41 43
from authentic2.a2_rbac.utils import get_default_ou
42 44
from authentic2 import hooks
43 45
from authentic2_idp_oidc.models import OIDCAuthorization, OIDCClient
44
from django_rbac.utils import get_role_model, get_role_parenting_model, get_ou_model
46
from authentic2.apps.journal.views import JournalViewWithContext
45 47

  
48
from django_rbac.utils import get_role_model, get_role_parenting_model, get_ou_model
46 49

  
47 50
from .views import (BaseTableView, BaseAddView, BaseEditView, ActionMixin,
48 51
                    OtherActionsMixin, Action, ExportMixin, BaseSubTableView,
......
55 58
                    UserEditImportForm, ChooseUserAuthorizationsForm)
56 59
from .resources import UserResource
57 60
from .utils import get_ou_count, has_show_username
61
from .journal_views import BaseJournalView
58 62
from . import app_settings
59 63

  
60 64
User = get_user_model()
......
199 203
        response = super(UserAddView, self).form_valid(form)
200 204
        hooks.call_hooks('event', name='manager-add-user', user=self.request.user,
201 205
                         instance=form.instance, form=form)
206
        self.request.journal.record('manager.user.creation', form=form)
202 207
        return response
203 208

  
204 209
    def get_initial(self, *args, **kwargs):
......
270 275

  
271 276
    def action_force_password_change(self, request, *args, **kwargs):
272 277
        PasswordReset.objects.get_or_create(user=self.object)
278
        request.journal.record('manager.user.password.change.force', target_user=self.object)
273 279

  
274 280
    def action_activate(self, request, *args, **kwargs):
275 281
        self.object.is_active = True
276 282
        self.object.save()
283
        request.journal.record('manager.user.activation', target_user=self.object)
277 284

  
278 285
    def action_deactivate(self, request, *args, **kwargs):
279 286
        if request.user == self.object:
......
282 289
        else:
283 290
            self.object.is_active = False
284 291
            self.object.save()
292
            request.journal.record('manager.user.deactivation', target_user=self.object)
285 293

  
286 294
    def action_password_reset(self, request, *args, **kwargs):
287 295
        user = self.object
......
292 300
            return
293 301
        send_password_reset_mail(user, request=request)
294 302
        messages.info(request, _('A mail was sent to %s') % self.object.email)
303
        request.journal.record('manager.user.password.reset.request', target_user=self.object)
295 304

  
296 305
    def action_delete_password_reset(self, request, *args, **kwargs):
297 306
        PasswordReset.objects.filter(user=self.object).delete()
307
        request.journal.record('manager.user.password.change.unforce', target_user=self.object)
298 308

  
299 309
    def action_su(self, request, *args, **kwargs):
300 310
        return redirect(request, 'auth_logout',
......
414 424
            self.object.email_verified = False
415 425
            self.object.save()
416 426
        response = super(UserEditView, self).form_valid(form)
417
        hooks.call_hooks('event', name='manager-edit-user', user=self.request.user,
418
                         instance=form.instance, form=form)
427
        if form.has_changed():
428
            hooks.call_hooks('event', name='manager-edit-user', user=self.request.user,
429
                             instance=form.instance, form=form)
430
            self.request.journal.record('manager.user.profile.edit', form=form)
419 431
        return response
420 432

  
421 433
user_edit = UserEditView.as_view()
......
502 514
        response = super(UserChangePasswordView, self).form_valid(form)
503 515
        hooks.call_hooks('event', name='manager-change-password', user=self.request.user,
504 516
                         instance=form.instance, form=form)
517
        self.request.journal.record('manager.user.password.change', form=form)
505 518
        return response
506 519

  
507 520

  
......
617 630
                user.roles.add(role)
618 631
                hooks.call_hooks('event', name='manager-add-role-member',
619 632
                                 user=self.request.user, role=role, member=user)
633
                self.request.journal.record('manager.role.membership.grant', member=user, role=role)
620 634
        elif action == 'remove':
621
            user.roles.remove(role)
622
            hooks.call_hooks('event', name='manager-remove-role-member', user=self.request.user,
623
                             role=role, member=user)
635
            if not user.roles.filter(pk=role.pk).exists():
636
                user.roles.remove(role)
637
                hooks.call_hooks('event', name='manager-remove-role-member', user=self.request.user,
638
                                 role=role, member=user)
639
                self.request.journal.record('manager.role.membership.removal', member=user, role=role)
624 640
        return super(UserRolesView, self).form_valid(form)
625 641

  
626 642
    def get_search_form_kwargs(self):
......
658 674
        self.get_object().mark_as_deleted()
659 675
        hooks.call_hooks('event', name='manager-delete-user', user=request.user,
660 676
                         instance=self.object)
677
        request.journal.record('manager.user.deletion', target_user=self.object)
661 678
        return HttpResponseRedirect(self.get_success_url())
662 679

  
663 680

  
......
882 899
        if self.can_manage_authorizations:
883 900
            qs = OIDCAuthorization.objects.filter(user=self.get_object())
884 901
            qs = qs.filter(id=auth_id.pk)
885
            qs.delete()
902
            oidc_authorization = qs.first()
903
            count, cascade = qs.delete()
904
            if count:
905
                self.request.journal.record(
906
                    'manager.user.sso.authorization.deletion',
907
                    service=oidc_authorization.client,
908
                    target_user=self.object)
886 909
        return response
887 910

  
888 911

  
889 912
user_authorizations = UserAuthorizationsView.as_view()
913

  
914

  
915
class UserJournal(PermissionMixin, JournalViewWithContext, BaseJournalView):
916
    template_name = 'authentic2/manager/user_journal.html'
917
    permissions = ['custom_user.view_user']
918
    title = _('Journal')
919

  
920
    @cached_property
921
    def context(self):
922
        return get_object_or_404(User, pk=self.kwargs['pk'])
923

  
924
    def get_context_data(self, **kwargs):
925
        ctx = super().get_context_data(**kwargs)
926
        ctx['object'] = self.context
927
        return ctx
928

  
929

  
930
user_journal = UserJournal.as_view()
src/authentic2/manager/views.py
16 16

  
17 17
import base64
18 18
import json
19
import inspect
19
import itertools
20 20
import pickle
21 21

  
22 22
from django.core import signing
......
597 597
            'permission': 'authentic2.search_service',
598 598
            'slug': 'services',
599 599
        },
600
        {
601
            'class': 'icon-journal',
602
            'href': reverse_lazy('a2-manager-journal'),
603
            'label': _('Journal'),
604
            'order': -1,
605
            'permission': 'superuser',
606
            'slug': 'journal',
607
        },
600 608
    ]
601 609

  
602 610
    def dispatch(self, request, *args, **kwargs):
......
606 614

  
607 615
    def get_homepage_entries(self):
608 616
        entries = []
609
        for entry in self.default_entries:
610
            if 'permission' in entry and not self.request.user.has_perm_any(entry['permission']):
611
                continue
612
            entries.append(entry)
613
        for hook_entries in hooks.call_hooks('manager_homepage_entries', self):
617
        for hook_entries in itertools.chain(
618
                self.default_entries,
619
                hooks.call_hooks('manager_homepage_entries', self)):
614 620
            if not hasattr(hook_entries, 'append'):
615 621
                hook_entries = [hook_entries]
616 622
            for entry in hook_entries:
617
                if 'permission' in entry and not self.request.user.has_perm_any(entry['permission']):
623
                permission = entry.get('permission')
624
                if permission == 'superuser' and not self.request.user.is_superuser:
625
                    continue
626
                elif permission and not self.request.user.has_perm_any(permission):
618 627
                    continue
619 628
                entries.append(entry)
620 629
        # use possible key order to sort
tests/test_manager_journal.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2020 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
import datetime
18
import mock
19

  
20
from authentic2.custom_user.models import User
21
from authentic2.a2_rbac.utils import get_default_ou
22
from authentic2.a2_rbac.models import Role
23
from authentic2.models import Service
24
from authentic2.apps.journal.models import Event, _registry
25
from authentic2.journal import journal
26

  
27
from django.contrib.sessions.models import Session
28
from django.utils.timezone import make_aware
29

  
30
import pytest
31

  
32
from .utils import login, text_content
33

  
34

  
35
def test_journal_authorization(app, db, admin):
36
    response = login(app, admin, path='/manage/')
37
    assert 'Journal' not in response
38
    app.get('/manage/journal/', status=403)
39

  
40

  
41
@pytest.fixture(autouse=True)
42
def events(db, freezer):
43
    session1 = Session(session_key="1234")
44
    session2 = Session(session_key="abcd")
45

  
46
    ou = get_default_ou()
47
    user = User.objects.create(
48
        username="user", email="user@example.com", ou=ou, uuid="1" * 32, first_name='Johnny', last_name='doe'
49
    )
50
    agent = User.objects.create(username="agent", email="agent@example.com", ou=ou, uuid="2" * 32)
51
    role_user = Role.objects.create(name="role1", ou=ou)
52
    role_agent = Role.objects.create(name="role2", ou=ou)
53
    service = Service.objects.create(name="service")
54

  
55
    class EventFactory:
56
        date = make_aware(datetime.datetime(2020, 1, 1))
57

  
58
        def __call__(self, name, **kwargs):
59
            freezer.move_to(self.date)
60
            journal.record(name, **kwargs)
61
            assert Event.objects.latest("timestamp").type.name == name
62
            self.date += datetime.timedelta(hours=1)
63

  
64
    make = EventFactory()
65
    make("user.registration.request", email=user.email)
66
    make(
67
        "user.registration", user=user, session=session1, service=service, how="franceconnect",
68
    )
69
    make("user.logout", user=user, session=session1)
70

  
71
    make("user.login.failure", username="user")
72
    make("user.login.failure", username="agent")
73
    make("user.login", user=user, session=session1, how="password")
74
    make("user.password.change", user=user, session=session1)
75
    edit_profile_form = mock.Mock()
76
    edit_profile_form.initial = {'email': "user@example.com", 'first_name': "John"}
77
    edit_profile_form.changed_data = ["first_name"]
78
    edit_profile_form.cleaned_data = {'first_name': "Jane"}
79
    make("user.profile.edit", user=user, session=session1, form=edit_profile_form)
80
    make("user.service.sso.authorization", user=user, session=session1, service=service)
81
    make("user.service.sso", user=user, session=session1, service=service, how="password")
82
    make("user.service.sso.unauthorization", user=user, session=session1, service=service)
83
    make("user.deletion", user=user, session=session1, service=service)
84

  
85
    make("user.password.reset.request", email="USER@example.com", user=user)
86
    make("user.password.reset.failure", email="USER@example.com")
87
    make("user.password.reset", user=user)
88

  
89
    make("user.login", user=agent, session=session2, how="saml")
90

  
91
    create_form = mock.Mock(spec=["instance"])
92
    create_form.instance = user
93
    make("manager.user.creation", user=agent, session=session2, form=create_form)
94

  
95
    edit_form = mock.Mock(spec=["instance", "initial", "changed_data", "cleaned_data"])
96
    edit_form.instance = user
97
    edit_form.initial = {'email': "user@example.com", 'first_name': "John"}
98
    edit_form.changed_data = ["first_name"]
99
    edit_form.cleaned_data = {'first_name': "Jane"}
100
    make("manager.user.profile.edit", user=agent, session=session2, form=edit_form)
101

  
102
    change_email_form = mock.Mock(spec=["instance", "cleaned_data"])
103
    change_email_form.instance = user
104
    change_email_form.cleaned_data = {'new_email': "jane@example.com"}
105
    make(
106
        "manager.user.email.change.request", user=agent, session=session2, form=change_email_form,
107
    )
108

  
109
    password_change_form = mock.Mock(spec=["instance", "cleaned_data"])
110
    password_change_form.instance = user
111
    password_change_form.cleaned_data = {'generate_password': False, 'send_mail': False}
112
    make(
113
        "manager.user.password.change", user=agent, session=session2, form=password_change_form,
114
    )
115

  
116
    password_change_form.cleaned_data["send_mail"] = True
117
    make(
118
        "manager.user.password.change", user=agent, session=session2, form=password_change_form,
119
    )
120

  
121
    make(
122
        "manager.user.password.reset.request", user=agent, session=session2, target_user=user,
123
    )
124

  
125
    make(
126
        "manager.user.password.change.force", user=agent, session=session2, target_user=user,
127
    )
128
    make(
129
        "manager.user.password.change.unforce", user=agent, session=session2, target_user=user,
130
    )
131

  
132
    make("manager.user.activation", user=agent, session=session2, target_user=user)
133
    make("manager.user.deactivation", user=agent, session=session2, target_user=user)
134
    make("manager.user.deletion", user=agent, session=session2, target_user=user)
135
    make(
136
        "manager.user.sso.authorization.deletion",
137
        user=agent,
138
        session=session2,
139
        service=service,
140
        target_user=user,
141
    )
142

  
143
    make("manager.role.creation", user=agent, session=session2, role=role_user)
144
    role_edit_form = mock.Mock(spec=["instance", "initial", "changed_data", "cleaned_data"])
145
    role_edit_form.instance = role_user
146
    role_edit_form.initial = {'name': role_user.name}
147
    role_edit_form.changed_data = ["name"]
148
    role_edit_form.cleaned_data = {'name': "changed role name"}
149
    make(
150
        "manager.role.edit", user=agent, session=session2, role=role_user, form=role_edit_form,
151
    )
152
    make("manager.role.deletion", user=agent, session=session2, role=role_user)
153
    make(
154
        "manager.role.membership.grant", user=agent, session=session2, role=role_user, member=user,
155
    )
156
    make(
157
        "manager.role.membership.removal", user=agent, session=session2, role=role_user, member=user,
158
    )
159

  
160
    make(
161
        "manager.role.inheritance.addition", user=agent, session=session2, parent=role_agent, child=role_user,
162
    )
163
    make(
164
        "manager.role.inheritance.removal", user=agent, session=session2, parent=role_agent, child=role_user,
165
    )
166

  
167
    make(
168
        "manager.role.administrator.role.addition",
169
        user=agent,
170
        session=session2,
171
        role=role_user,
172
        admin_role=role_agent,
173
    )
174
    make(
175
        "manager.role.administrator.role.removal",
176
        user=agent,
177
        session=session2,
178
        role=role_user,
179
        admin_role=role_agent,
180
    )
181

  
182
    make(
183
        "manager.role.administrator.user.addition",
184
        user=agent,
185
        session=session2,
186
        role=role_user,
187
        admin_user=user,
188
    )
189
    make(
190
        "manager.role.administrator.user.removal",
191
        user=agent,
192
        session=session2,
193
        role=role_user,
194
        admin_user=user,
195
    )
196

  
197
    # verify we created at least one event for each type
198
    assert set(Event.objects.values_list("type__name", flat=True)) == set(_registry)
199

  
200
    return locals()
201

  
202

  
203
def extract_journal(response):
204
    rows = []
205
    seen_event_ids = set()
206
    while True:
207
        for tr in response.pyquery("tr[data-event-type]"):
208
            # page can overlap when they contain less than 20 items (to prevent orphan rows)
209
            event_id = tr.attrib["data-event-id"]
210
            if event_id not in seen_event_ids:
211
                rows.append(response.pyquery(tr))
212
                seen_event_ids.add(event_id)
213
        if "Previous page" not in response:
214
            break
215
        response = response.click("Previous page", index=0)
216

  
217
    rows.reverse()
218
    content = [
219
        {
220
            'timestamp': text_content(row.find(".journal-list--timestamp-column")[0]).strip(),
221
            'type': row[0].attrib["data-event-type"],
222
            'user': text_content(row.find(".journal-list--user-column")[0]).strip(),
223
            'message': text_content(row.find(".journal-list--message-column")[0]),
224
        }
225
        for row in rows
226
    ]
227
    return content
228

  
229

  
230
def test_global_journal(app, superuser, events):
231
    response = login(app, user=superuser, path="/manage/")
232

  
233
    # remove event about admin login
234
    Event.objects.filter(user=superuser).delete()
235

  
236
    response = response.click(href="journal")
237

  
238
    content = extract_journal(response)
239

  
240
    assert content == [
241
        {
242
            'message': 'registration request with email "user@example.com"',
243
            'timestamp': 'Jan. 1, 2020, midnight',
244
            'type': 'user.registration.request',
245
            'user': '-',
246
        },
247
        {
248
            'message': 'registration using franceconnect',
249
            'timestamp': 'Jan. 1, 2020, 1 a.m.',
250
            'type': 'user.registration',
251
            'user': 'Johnny doe',
252
        },
253
        {
254
            'message': 'logout',
255
            'timestamp': 'Jan. 1, 2020, 2 a.m.',
256
            'type': 'user.logout',
257
            'user': 'Johnny doe',
258
        },
259
        {
260
            'message': 'login failure with username "user"',
261
            'timestamp': 'Jan. 1, 2020, 3 a.m.',
262
            'type': 'user.login.failure',
263
            'user': '-',
264
        },
265
        {
266
            'message': 'login failure with username "agent"',
267
            'timestamp': 'Jan. 1, 2020, 4 a.m.',
268
            'type': 'user.login.failure',
269
            'user': '-',
270
        },
271
        {
272
            'message': 'login using password',
273
            'timestamp': 'Jan. 1, 2020, 5 a.m.',
274
            'type': 'user.login',
275
            'user': 'Johnny doe',
276
        },
277
        {
278
            'message': 'password change',
279
            'timestamp': 'Jan. 1, 2020, 6 a.m.',
280
            'type': 'user.password.change',
281
            'user': 'Johnny doe',
282
        },
283
        {
284
            'message': 'profile edit (first name)',
285
            'timestamp': 'Jan. 1, 2020, 7 a.m.',
286
            'type': 'user.profile.edit',
287
            'user': 'Johnny doe',
288
        },
289
        {
290
            'message': 'authorization of single sign on with "service"',
291
            'timestamp': 'Jan. 1, 2020, 8 a.m.',
292
            'type': 'user.service.sso.authorization',
293
            'user': 'Johnny doe',
294
        },
295
        {
296
            'message': 'service single sign on with "service"',
297
            'timestamp': 'Jan. 1, 2020, 9 a.m.',
298
            'type': 'user.service.sso',
299
            'user': 'Johnny doe',
300
        },
301
        {
302
            'message': 'unauthorization of single sign on with "service"',
303
            'timestamp': 'Jan. 1, 2020, 10 a.m.',
304
            'type': 'user.service.sso.unauthorization',
305
            'user': 'Johnny doe',
306
        },
307
        {
308
            'message': 'deletion',
309
            'timestamp': 'Jan. 1, 2020, 11 a.m.',
310
            'type': 'user.deletion',
311
            'user': 'Johnny doe',
312
        },
313
        {
314
            'message': 'password reset request with email "user@example.com"',
315
            'timestamp': 'Jan. 1, 2020, noon',
316
            'type': 'user.password.reset.request',
317
            'user': 'Johnny doe',
318
        },
319
        {
320
            'message': 'password reset failure with email "USER@example.com"',
321
            'timestamp': 'Jan. 1, 2020, 1 p.m.',
322
            'type': 'user.password.reset.failure',
323
            'user': '-',
324
        },
325
        {
326
            'message': 'password reset',
327
            'timestamp': 'Jan. 1, 2020, 2 p.m.',
328
            'type': 'user.password.reset',
329
            'user': 'Johnny doe',
330
        },
331
        {
332
            'message': 'login using SAML',
333
            'timestamp': 'Jan. 1, 2020, 3 p.m.',
334
            'type': 'user.login',
335
            'user': 'agent',
336
        },
337
        {
338
            'message': 'creation of user "Johnny doe"',
339
            'timestamp': 'Jan. 1, 2020, 4 p.m.',
340
            'type': 'manager.user.creation',
341
            'user': 'agent',
342
        },
343
        {
344
            'message': 'edit of user "Johnny doe" (first name)',
345
            'timestamp': 'Jan. 1, 2020, 5 p.m.',
346
            'type': 'manager.user.profile.edit',
347
            'user': 'agent',
348
        },
349
        {
350
            'message': 'email change of user "Johnny doe" for email address "jane@example.com"',
351
            'timestamp': 'Jan. 1, 2020, 6 p.m.',
352
            'type': 'manager.user.email.change.request',
353
            'user': 'agent',
354
        },
355
        {
356
            'message': 'password change of user "Johnny doe"',
357
            'timestamp': 'Jan. 1, 2020, 7 p.m.',
358
            'type': 'manager.user.password.change',
359
            'user': 'agent',
360
        },
361
        {
362
            'message': 'password change of user "Johnny doe" and notification by mail',
363
            'timestamp': 'Jan. 1, 2020, 8 p.m.',
364
            'type': 'manager.user.password.change',
365
            'user': 'agent',
366
        },
367
        {
368
            'message': 'password reset request of "Johnny doe" sent to "user@example.com"',
369
            'timestamp': 'Jan. 1, 2020, 9 p.m.',
370
            'type': 'manager.user.password.reset.request',
371
            'user': 'agent',
372
        },
373
        {
374
            'message': 'mandatory password change at next login set for user "Johnny doe"',
375
            'timestamp': 'Jan. 1, 2020, 10 p.m.',
376
            'type': 'manager.user.password.change.force',
377
            'user': 'agent',
378
        },
379
        {
380
            'message': 'mandatory password change at next login unset for user "Johnny doe"',
381
            'timestamp': 'Jan. 1, 2020, 11 p.m.',
382
            'type': 'manager.user.password.change.unforce',
383
            'user': 'agent',
384
        },
385
        {
386
            'message': 'activation of user "Johnny doe"',
387
            'timestamp': 'Jan. 2, 2020, midnight',
388
            'type': 'manager.user.activation',
389
            'user': 'agent',
390
        },
391
        {
392
            'message': 'deactivation of user "Johnny doe"',
393
            'timestamp': 'Jan. 2, 2020, 1 a.m.',
394
            'type': 'manager.user.deactivation',
395
            'user': 'agent',
396
        },
397
        {
398
            'message': 'deletion of user "Johnny doe"',
399
            'timestamp': 'Jan. 2, 2020, 2 a.m.',
400
            'type': 'manager.user.deletion',
401
            'user': 'agent',
402
        },
403
        {
404
            'message': 'deletion of authorization of single sign on with "service" of ' 'user "Johnny doe"',
405
            'timestamp': 'Jan. 2, 2020, 3 a.m.',
406
            'type': 'manager.user.sso.authorization.deletion',
407
            'user': 'agent',
408
        },
409
        {
410
            'message': 'creation of role "role1"',
411
            'timestamp': 'Jan. 2, 2020, 4 a.m.',
412
            'type': 'manager.role.creation',
413
            'user': 'agent',
414
        },
415
        {
416
            'message': 'edit of role "role1" (name)',
417
            'timestamp': 'Jan. 2, 2020, 5 a.m.',
418
            'type': 'manager.role.edit',
419
            'user': 'agent',
420
        },
421
        {
422
            'message': 'deletion of role "role1"',
423
            'timestamp': 'Jan. 2, 2020, 6 a.m.',
424
            'type': 'manager.role.deletion',
425
            'user': 'agent',
426
        },
427
        {
428
            'message': 'membership grant to user "user (111111)" in role "role1"',
429
            'timestamp': 'Jan. 2, 2020, 7 a.m.',
430
            'type': 'manager.role.membership.grant',
431
            'user': 'agent',
432
        },
433
        {
434
            'message': 'membership removal of user "user (111111)" from role "role1"',
435
            'timestamp': 'Jan. 2, 2020, 8 a.m.',
436
            'type': 'manager.role.membership.removal',
437
            'user': 'agent',
438
        },
439
        {
440
            'message': 'inheritance addition from parent role "role2" to child role ' '"role1"',
441
            'timestamp': 'Jan. 2, 2020, 9 a.m.',
442
            'type': 'manager.role.inheritance.addition',
443
            'user': 'agent',
444
        },
445
        {
446
            'message': 'inheritance removal from parent role "role2" to child role ' '"role1"',
447
            'timestamp': 'Jan. 2, 2020, 10 a.m.',
448
            'type': 'manager.role.inheritance.removal',
449
            'user': 'agent',
450
        },
451
        {
452
            'message': 'addition of role "role2" as administrator of role "role1"',
453
            'timestamp': 'Jan. 2, 2020, 11 a.m.',
454
            'type': 'manager.role.administrator.role.addition',
455
            'user': 'agent',
456
        },
457
        {
458
            'message': 'removal of role "role2" as administrator of role "role1"',
459
            'timestamp': 'Jan. 2, 2020, noon',
460
            'type': 'manager.role.administrator.role.removal',
461
            'user': 'agent',
462
        },
463
        {
464
            'message': 'addition of user "user (111111)" as administrator of role ' '"role1"',
465
            'timestamp': 'Jan. 2, 2020, 1 p.m.',
466
            'type': 'manager.role.administrator.user.addition',
467
            'user': 'agent',
468
        },
469
        {
470
            'message': 'removal of user "user (111111)" as administrator of role "role1"',
471
            'timestamp': 'Jan. 2, 2020, 2 p.m.',
472
            'type': 'manager.role.administrator.user.removal',
473
            'user': 'agent',
474
        },
475
    ]
476

  
477

  
478
def test_user_journal(app, superuser, events):
479
    response = login(app, user=superuser, path="/manage/")
480
    user = User.objects.get(username="user")
481

  
482
    response = app.get("/manage/users/%s/journal/" % user.id)
483
    content = extract_journal(response)
484

  
485
    assert content == [
486
        {
487
            'message': 'registration using franceconnect',
488
            'timestamp': 'Jan. 1, 2020, 1 a.m.',
489
            'type': 'user.registration',
490
            'user': 'Johnny doe',
491
        },
492
        {
493
            'message': 'logout',
494
            'timestamp': 'Jan. 1, 2020, 2 a.m.',
495
            'type': 'user.logout',
496
            'user': 'Johnny doe',
497
        },
498
        {
499
            'message': 'login using password',
500
            'timestamp': 'Jan. 1, 2020, 5 a.m.',
501
            'type': 'user.login',
502
            'user': 'Johnny doe',
503
        },
504
        {
505
            'message': 'password change',
506
            'timestamp': 'Jan. 1, 2020, 6 a.m.',
507
            'type': 'user.password.change',
508
            'user': 'Johnny doe',
509
        },
510
        {
511
            'message': 'profile edit (first name)',
512
            'timestamp': 'Jan. 1, 2020, 7 a.m.',
513
            'type': 'user.profile.edit',
514
            'user': 'Johnny doe',
515
        },
516
        {
517
            'message': 'authorization of single sign on with "service"',
518
            'timestamp': 'Jan. 1, 2020, 8 a.m.',
519
            'type': 'user.service.sso.authorization',
520
            'user': 'Johnny doe',
521
        },
522
        {
523
            'message': 'service single sign on with "service"',
524
            'timestamp': 'Jan. 1, 2020, 9 a.m.',
525
            'type': 'user.service.sso',
526
            'user': 'Johnny doe',
527
        },
528
        {
529
            'message': 'unauthorization of single sign on with "service"',
530
            'timestamp': 'Jan. 1, 2020, 10 a.m.',
531
            'type': 'user.service.sso.unauthorization',
532
            'user': 'Johnny doe',
533
        },
534
        {
535
            'message': 'deletion',
536
            'timestamp': 'Jan. 1, 2020, 11 a.m.',
537
            'type': 'user.deletion',
538
            'user': 'Johnny doe',
539
        },
540
        {
541
            'message': 'password reset request with email "user@example.com"',
542
            'timestamp': 'Jan. 1, 2020, noon',
543
            'type': 'user.password.reset.request',
544
            'user': 'Johnny doe',
545
        },
546
        {
547
            'message': 'password reset',
548
            'timestamp': 'Jan. 1, 2020, 2 p.m.',
549
            'type': 'user.password.reset',
550
            'user': 'Johnny doe',
551
        },
552
        {
553
            'message': 'creation by administrator',
554
            'timestamp': 'Jan. 1, 2020, 4 p.m.',
555
            'type': 'manager.user.creation',
556
            'user': 'agent',
557
        },
558
        {
559
            'message': 'edit by administrator (first name)',
560
            'timestamp': 'Jan. 1, 2020, 5 p.m.',
561
            'type': 'manager.user.profile.edit',
562
            'user': 'agent',
563
        },
564
        {
565
            'message': 'email change for email address "jane@example.com" requested by administrator',
566
            'timestamp': 'Jan. 1, 2020, 6 p.m.',
567
            'type': 'manager.user.email.change.request',
568
            'user': 'agent',
569
        },
570
        {
571
            'message': 'password change by administrator',
572
            'timestamp': 'Jan. 1, 2020, 7 p.m.',
573
            'type': 'manager.user.password.change',
574
            'user': 'agent',
575
        },
576
        {
577
            'message': 'password change by administrator and notification by mail',
578
            'timestamp': 'Jan. 1, 2020, 8 p.m.',
579
            'type': 'manager.user.password.change',
580
            'user': 'agent',
581
        },
582
        {
583
            'message': "password reset request by administrator sent to " '"user@example.com"',
584
            'timestamp': 'Jan. 1, 2020, 9 p.m.',
585
            'type': 'manager.user.password.reset.request',
586
            'user': 'agent',
587
        },
588
        {
589
            'message': 'mandatory password change at next login set by administrator',
590
            'timestamp': 'Jan. 1, 2020, 10 p.m.',
591
            'type': 'manager.user.password.change.force',
592
            'user': 'agent',
593
        },
594
        {
595
            'message': 'mandatory password change at next login unset by administrator',
596
            'timestamp': 'Jan. 1, 2020, 11 p.m.',
597
            'type': 'manager.user.password.change.unforce',
598
            'user': 'agent',
599
        },
600
        {
601
            'message': 'activation by administrator',
602
            'timestamp': 'Jan. 2, 2020, midnight',
603
            'type': 'manager.user.activation',
604
            'user': 'agent',
605
        },
606
        {
607
            'message': 'deactivation by administrator',
608
            'timestamp': 'Jan. 2, 2020, 1 a.m.',
609
            'type': 'manager.user.deactivation',
610
            'user': 'agent',
611
        },
612
        {
613
            'message': 'deletion by administrator',
614
            'timestamp': 'Jan. 2, 2020, 2 a.m.',
615
            'type': 'manager.user.deletion',
616
            'user': 'agent',
617
        },
618
        {
619
            'message': 'deletion of authorization of single sign on with "service" by ' "administrator",
620
            'timestamp': 'Jan. 2, 2020, 3 a.m.',
621
            'type': 'manager.user.sso.authorization.deletion',
622
            'user': 'agent',
623
        },
624
        {
625
            'message': 'membership grant in role "role1"',
626
            'timestamp': 'Jan. 2, 2020, 7 a.m.',
627
            'type': 'manager.role.membership.grant',
628
            'user': 'agent',
629
        },
630
        {
631
            'message': 'membership removal from role "role1"',
632
            'timestamp': 'Jan. 2, 2020, 8 a.m.',
633
            'type': 'manager.role.membership.removal',
634
            'user': 'agent',
635
        },
636
        {
637
            'message': 'addition as administrator of role "role1"',
638
            'timestamp': 'Jan. 2, 2020, 1 p.m.',
639
            'type': 'manager.role.administrator.user.addition',
640
            'user': 'agent',
641
        },
642
        {
643
            'message': 'removal as administrator of role "role1"',
644
            'timestamp': 'Jan. 2, 2020, 2 p.m.',
645
            'type': 'manager.role.administrator.user.removal',
646
            'user': 'agent',
647
        },
648
    ]
649

  
650

  
651
def test_role_journal(app, superuser, events):
652
    response = login(app, user=superuser, path="/manage/")
653
    role1 = Role.objects.get(name="role1")
654
    role2 = Role.objects.get(name="role2")
655

  
656
    response = app.get("/manage/roles/%s/journal/" % role1.id)
657
    content = extract_journal(response)
658

  
659
    assert content == [
660
        {
661
            'message': 'creation',
662
            'timestamp': 'Jan. 2, 2020, 4 a.m.',
663
            'type': 'manager.role.creation',
664
            'user': 'agent',
665
        },
666
        {
667
            'message': 'edit (name)',
668
            'timestamp': 'Jan. 2, 2020, 5 a.m.',
669
            'type': 'manager.role.edit',
670
            'user': 'agent',
671
        },
672
        {
673
            'message': 'deletion',
674
            'timestamp': 'Jan. 2, 2020, 6 a.m.',
675
            'type': 'manager.role.deletion',
676
            'user': 'agent',
677
        },
678
        {
679
            'message': 'membership grant to user "user (111111)"',
680
            'timestamp': 'Jan. 2, 2020, 7 a.m.',
681
            'type': 'manager.role.membership.grant',
682
            'user': 'agent',
683
        },
684
        {
685
            'message': 'membership removal of user "user (111111)"',
686
            'timestamp': 'Jan. 2, 2020, 8 a.m.',
687
            'type': 'manager.role.membership.removal',
688
            'user': 'agent',
689
        },
690
        {
691
            'message': 'inheritance addition from parent role "role2"',
692
            'timestamp': 'Jan. 2, 2020, 9 a.m.',
693
            'type': 'manager.role.inheritance.addition',
694
            'user': 'agent',
695
        },
696
        {
697
            'message': 'inheritance removal from parent role "role2"',
698
            'timestamp': 'Jan. 2, 2020, 10 a.m.',
699
            'type': 'manager.role.inheritance.removal',
700
            'user': 'agent',
701
        },
702
        {
703
            'message': 'addition of role "role2" as administrator',
704
            'timestamp': 'Jan. 2, 2020, 11 a.m.',
705
            'type': 'manager.role.administrator.role.addition',
706
            'user': 'agent',
707
        },
708
        {
709
            'message': 'removal of role "role2" as administrator',
710
            'timestamp': 'Jan. 2, 2020, noon',
711
            'type': 'manager.role.administrator.role.removal',
712
            'user': 'agent',
713
        },
714
        {
715
            'message': 'addition of user "user (111111)" as administrator',
716
            'timestamp': 'Jan. 2, 2020, 1 p.m.',
717
            'type': 'manager.role.administrator.user.addition',
718
            'user': 'agent',
719
        },
720
        {
721
            'message': 'removal of user "user (111111)" as administrator',
722
            'timestamp': 'Jan. 2, 2020, 2 p.m.',
723
            'type': 'manager.role.administrator.user.removal',
724
            'user': 'agent',
725
        },
726
    ]
727

  
728
    response = app.get("/manage/roles/%s/journal/" % role2.id)
729
    content = extract_journal(response)
730

  
731
    assert content == [
732
        {
733
            'message': 'inheritance addition to child role "role1"',
734
            'timestamp': 'Jan. 2, 2020, 9 a.m.',
735
            'type': 'manager.role.inheritance.addition',
736
            'user': 'agent',
737
        },
738
        {
739
            'message': 'inheritance removal to child role "role1"',
740
            'timestamp': 'Jan. 2, 2020, 10 a.m.',
741
            'type': 'manager.role.inheritance.removal',
742
            'user': 'agent',
743
        },
744
        {
745
            'message': 'addition as administrator of role "role1"',
746
            'timestamp': 'Jan. 2, 2020, 11 a.m.',
747
            'type': 'manager.role.administrator.role.addition',
748
            'user': 'agent',
749
        },
750
        {
751
            'message': 'removal as administrator of role "role1"',
752
            'timestamp': 'Jan. 2, 2020, noon',
753
            'type': 'manager.role.administrator.role.removal',
754
            'user': 'agent',
755
        },
756
    ]
757

  
758

  
759
def test_roles_journal(app, superuser, events):
760
    response = login(app, user=superuser, path='/manage/')
761
    response = response.click('Role')
762
    response = response.click('Journal')
763

  
764
    content = extract_journal(response)
765

  
766
    assert content == [
767
        {
768
            'message': 'creation of role "role1"',
769
            'timestamp': 'Jan. 2, 2020, 4 a.m.',
770
            'type': 'manager.role.creation',
771
            'user': 'agent',
772
        },
773
        {
774
            'message': 'edit of role "role1" (name)',
775
            'timestamp': 'Jan. 2, 2020, 5 a.m.',
776
            'type': 'manager.role.edit',
777
            'user': 'agent',
778
        },
779
        {
780
            'message': 'deletion of role "role1"',
781
            'timestamp': 'Jan. 2, 2020, 6 a.m.',
782
            'type': 'manager.role.deletion',
783
            'user': 'agent',
784
        },
785
        {
786
            'message': 'membership grant to user "user (111111)" in role "role1"',
787
            'timestamp': 'Jan. 2, 2020, 7 a.m.',
788
            'type': 'manager.role.membership.grant',
789
            'user': 'agent',
790
        },
791
        {
792
            'message': 'membership removal of user "user (111111)" from role "role1"',
793
            'timestamp': 'Jan. 2, 2020, 8 a.m.',
794
            'type': 'manager.role.membership.removal',
795
            'user': 'agent',
796
        },
797
        {
798
            'message': 'inheritance addition from parent role "role2" to child role ' '"role1"',
799
            'timestamp': 'Jan. 2, 2020, 9 a.m.',
800
            'type': 'manager.role.inheritance.addition',
801
            'user': 'agent',
802
        },
803
        {
804
            'message': 'inheritance removal from parent role "role2" to child role ' '"role1"',
805
            'timestamp': 'Jan. 2, 2020, 10 a.m.',
806
            'type': 'manager.role.inheritance.removal',
807
            'user': 'agent',
808
        },
809
        {
810
            'message': 'addition of role "role2" as administrator of role "role1"',
811
            'timestamp': 'Jan. 2, 2020, 11 a.m.',
812
            'type': 'manager.role.administrator.role.addition',
813
            'user': 'agent',
814
        },
815
        {
816
            'message': 'removal of role "role2" as administrator of role "role1"',
817
            'timestamp': 'Jan. 2, 2020, noon',
818
            'type': 'manager.role.administrator.role.removal',
819
            'user': 'agent',
820
        },
821
        {
822
            'message': 'addition of user "user (111111)" as administrator of role ' '"role1"',
823
            'timestamp': 'Jan. 2, 2020, 1 p.m.',
824
            'type': 'manager.role.administrator.user.addition',
825
            'user': 'agent',
826
        },
827
        {
828
            'message': 'removal of user "user (111111)" as administrator of role "role1"',
829
            'timestamp': 'Jan. 2, 2020, 2 p.m.',
830
            'type': 'manager.role.administrator.user.removal',
831
            'user': 'agent',
832
        },
833
    ]
834

  
835

  
836
def test_date_navigation(app, superuser, events):
837
    response = login(app, user=superuser, path="/manage/journal/")
838
    response = response.click('2020')
839
    response = response.click('January')
840
    response = response.click('1')
841
    response = response.click('January 2020')
842
    response = response.click('2020')
843
    response = response.click('All dates')
844

  
845

  
846
def test_search(app, superuser, events):
847
    response = login(app, user=superuser, path="/manage/journal/")
848
    response.form.set('search', 'event:registration')
849
    response = response.form.submit()
850
    assert len(response.pyquery('tbody tr')) == 1
851

  
852
    response.form.set('search', 'username:agent event:login')
853
    response = response.form.submit()
854
    assert len(response.pyquery('tbody tr')) == 1
855
    assert all(
856
        'agent' == text_content(node) for node in response.pyquery('tbody tr td.journal-list--user-column')
857
    )
858

  
859
    response.form.set('search', 'uuid:%s event:reset' % events['user'].uuid)
860
    response = response.form.submit()
861
    assert len(response.pyquery('tbody tr')) == 1
862

  
863
    response.form.set('search', 'session:1234')
864
    response = response.form.submit()
865
    assert len(response.pyquery('tbody tr')) == 9
866
    assert all(
867
        text_content(node) == 'Johnny doe'
868
        for node in response.pyquery('tbody tr td.journal-list--user-column')
869
    )
870

  
871
    response.form.set('search', 'email:jane@example.com')
872
    response = response.form.submit()
873
    assert (
874
        text_content(response.pyquery('tbody tr td.journal-list--message-column')[0]).strip()
875
        == 'email change of user "Johnny doe" for email address "jane@example.com"'
876
    )
877

  
878
    response.form.set('search', 'jane@example.com')
879
    response = response.form.submit()
880
    assert (
881
        text_content(response.pyquery('tbody tr td.journal-list--message-column')[0]).strip()
882
        == 'email change of user "Johnny doe" for email address "jane@example.com"'
883
    )
884

  
885
    response.form.set('search', 'johny doe event:login')
886
    response = response.form.submit()
887
    pq = response.pyquery
888

  
889
    assert [
890
        list(map(text_content, p))
891
        for p in zip(pq('tbody td.journal-list--user-column'), pq('tbody td.journal-list--message-column'))
892
    ] == [['Johnny doe', 'login using password']]
0
-