0001-wip.patch
src/authentic2/api_views.py | ||
---|---|---|
343 | 343 |
def rpc(self, request, serializer): |
344 | 344 |
serializer.user.set_password(serializer.validated_data['new_password']) |
345 | 345 |
serializer.user.save() |
346 |
request.journal.record('api.user.password.change', serializer=serializer) |
|
346 | 347 |
return {'result': 1}, status.HTTP_200_OK |
347 | 348 | |
348 | 349 | |
... | ... | |
778 | 779 |
if not self.request.user.has_perm(perm): |
779 | 780 |
raise PermissionDenied(u'You do not have permission %s' % perm) |
780 | 781 | |
782 |
def perform_create(self, serializer): |
|
783 |
super().perform_create(serializer) |
|
784 |
self.request.journal.record('api.user.creation', target_user=serializer.instance) |
|
785 | ||
786 |
def perform_update(self, serializer): |
|
787 |
super().perform_update(serializer) |
|
788 |
self.request.journal.record('api.user.profile.edit', serializer=serializer) |
|
789 | ||
781 | 790 |
def perform_destroy(self, instance): |
782 | 791 |
self.check_perm('custom_user.delete_user', instance.ou) |
783 | 792 |
super(UsersAPI, self).perform_destroy(instance) |
793 |
self.request.journal.record('api.user.deletion', target_user=instance) |
|
784 | 794 | |
785 | 795 |
class SynchronizationSerializer(serializers.Serializer): |
786 | 796 |
known_uuids = serializers.ListField(child=serializers.CharField()) |
... | ... | |
820 | 830 |
) |
821 | 831 | |
822 | 832 |
utils.send_password_reset_mail(user, request=request) |
833 |
request.journal.record('api.user.password.reset.request', target_user=user) |
|
823 | 834 |
return Response(status=status.HTTP_204_NO_CONTENT) |
824 | 835 | |
825 | 836 |
@action(detail=True, methods=['post'], permission_classes=(DjangoPermission('custom_user.change_user'),)) |
... | ... | |
876 | 887 |
if not self.request.user.has_perm(perm='a2_rbac.delete_role', obj=instance): |
877 | 888 |
raise PermissionDenied(u'User %s can\'t create role %s' % (self.request.user, instance)) |
878 | 889 |
super(RolesAPI, self).perform_destroy(instance) |
890 |
self.request.journal.record('api.role.deletion', role=instance) |
|
891 | ||
892 |
def perform_create(self, serializer): |
|
893 |
super().perform_create(serializer) |
|
894 |
self.request.journal.record('api.role.creation', role=serializer.instance) |
|
895 | ||
896 |
def perform_update(self, serializer): |
|
897 |
super().perform_update(serializer) |
|
898 |
self.request.journal.record('api.role.edit', role=serializer.instance, form=serializer) |
|
879 | 899 | |
880 | 900 | |
881 | 901 |
class RolesMembersAPI(UsersAPI): |
... | ... | |
919 | 939 |
if not request.user.has_perm('a2_rbac.manage_members_role', obj=self.role): |
920 | 940 |
raise PermissionDenied(u'User not allowed to manage role members') |
921 | 941 |
self.role.members.add(self.member) |
942 |
request.journal.record('api.role.membership.grant', role=self.role, member=self.member) |
|
922 | 943 |
return Response( |
923 | 944 |
{'result': 1, 'detail': _('User successfully added to role')}, status=status.HTTP_201_CREATED |
924 | 945 |
) |
... | ... | |
927 | 948 |
if not request.user.has_perm('a2_rbac.manage_members_role', obj=self.role): |
928 | 949 |
raise PermissionDenied(u'User not allowed to manage role members') |
929 | 950 |
self.role.members.remove(self.member) |
951 |
request.journal.record('api.role.membership.removal', role=self.role, member=self.member) |
|
930 | 952 |
return Response( |
931 | 953 |
{'result': 1, 'detail': _('User successfully removed from role')}, status=status.HTTP_200_OK |
932 | 954 |
) |
... | ... | |
976 | 998 | |
977 | 999 |
def post(self, request, *args, **kwargs): |
978 | 1000 |
self.role.members.add(*self.members) |
1001 |
for member in self.members: |
|
1002 |
request.journal.record('api.role.membership.grant', role=self.role, member=member) |
|
979 | 1003 |
return Response( |
980 | 1004 |
{'result': 1, 'detail': _('Users successfully added to role')}, status=status.HTTP_201_CREATED |
981 | 1005 |
) |
982 | 1006 | |
983 | 1007 |
def delete(self, request, *args, **kwargs): |
984 | 1008 |
self.role.members.remove(*self.members) |
1009 |
for member in self.members: |
|
1010 |
request.journal.record('api.role.membership.removal', role=self.role, member=member) |
|
985 | 1011 |
return Response( |
986 | 1012 |
{'result': 1, 'detail': _('Users successfully removed from role')}, status=status.HTTP_200_OK |
987 | 1013 |
) |
988 | 1014 | |
989 | 1015 |
def patch(self, request, *args, **kwargs): |
1016 |
old_members = set(self.role.members.all()) |
|
990 | 1017 |
self.role.members.set(self.members) |
1018 |
for member in self.members: |
|
1019 |
request.journal.record('api.role.membership.grant', role=self.role, member=member) |
|
1020 |
for member in old_members.difference(self.members): |
|
1021 |
request.journal.record('api.role.membership.removal', role=self.role, member=member) |
|
991 | 1022 |
return Response( |
992 | 1023 |
{'result': 1, 'detail': _('Users successfully assigned to role')}, status=status.HTTP_200_OK |
993 | 1024 |
) |
src/authentic2/apps/journal/models.py | ||
---|---|---|
88 | 88 |
def record(cls, user=None, session=None, references=None, data=None): |
89 | 89 |
event_type = EventType.objects.get_for_name(cls.name) |
90 | 90 | |
91 |
if user and user.is_anonymous: |
|
92 |
user = None |
|
93 | ||
91 | 94 |
Event.objects.create( |
92 | 95 |
type=event_type, |
93 | 96 |
user=user, |
src/authentic2/journal_event_types.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
from django.contrib.auth import get_user_model |
|
17 | 18 |
from django.contrib.contenttypes.models import ContentType |
18 | 19 |
from django.utils.translation import ugettext_lazy as _ |
19 | 20 | |
20 | 21 |
from authentic2.apps.journal.models import EventTypeDefinition, n_2_pairing_rev |
21 | 22 |
from authentic2.apps.journal.utils import Statistics, form_to_old_new |
22 | 23 |
from authentic2.custom_user.models import User, get_attributes_map |
24 |
from django_rbac.utils import get_role_model |
|
23 | 25 | |
24 | 26 |
from .models import Service |
25 | 27 | |
28 |
Role = get_role_model() |
|
29 |
User = get_user_model() |
|
30 | ||
26 | 31 | |
27 | 32 |
class EventTypeWithService(EventTypeDefinition): |
28 | 33 |
@classmethod |
... | ... | |
46 | 51 |
return '' |
47 | 52 | |
48 | 53 | |
54 |
class RoleEventsMixin(EventTypeDefinition): |
|
55 |
@classmethod |
|
56 |
def record(self, user, role, session=None, references=None, data=None): |
|
57 |
references = references or [] |
|
58 |
references = [role] + references |
|
59 |
data = data or {} |
|
60 |
data.update({'role_name': str(role), 'role_uuid': role.uuid}) |
|
61 |
super().record( |
|
62 |
user=user, |
|
63 |
session=session, |
|
64 |
references=references, |
|
65 |
data=data, |
|
66 |
) |
|
67 | ||
68 | ||
49 | 69 |
class EventTypeWithHow(EventTypeWithService): |
50 | 70 |
@classmethod |
51 | 71 |
def record(cls, user, session, service, how): |
... | ... | |
364 | 384 |
elif reason == 'old-source': |
365 | 385 |
return _('automatic deactivation because user was from an old LDAP source') |
366 | 386 |
return super().get_message(event, context) |
387 | ||
388 | ||
389 |
class ApiUserCreation(EventTypeDefinition): |
|
390 |
name = 'api.user.creation' |
|
391 |
label = _('user creation') |
|
392 | ||
393 |
@classmethod |
|
394 |
def record(cls, user, target_user): |
|
395 |
super().record(user=user, references=[target_user]) |
|
396 | ||
397 |
@classmethod |
|
398 |
def get_message(cls, event, context): |
|
399 |
(user,) = event.get_typed_references(User) |
|
400 |
if context and context == user: |
|
401 |
return _('creation from API') |
|
402 |
elif user: |
|
403 |
return _('creation of user "%s" from API') % user.get_full_name() |
|
404 |
return super().get_message(event, context) |
|
405 | ||
406 | ||
407 |
class ApiUserProfileEdit(EventTypeDefinition): |
|
408 |
name = 'api.user.profile.edit' |
|
409 |
label = _('user profile edit') |
|
410 | ||
411 |
@classmethod |
|
412 |
def record(cls, user, serializer): |
|
413 |
changed_data = serializer.validated_data.copy() |
|
414 |
attributes = changed_data.pop('attributes', {}) |
|
415 |
changed_data.update(attributes) |
|
416 |
super().record(user=user, references=[serializer.instance], data={'new': changed_data}) |
|
417 | ||
418 |
@classmethod |
|
419 |
def get_message(cls, event, context): |
|
420 |
(user,) = event.get_typed_references(User) |
|
421 |
new = event.get_data('new') or {} |
|
422 |
edited_attributes = ', '.join(get_attributes_label(new)) or '' |
|
423 |
if context and context == user: |
|
424 |
return _('edit from API (%s)') % edited_attributes |
|
425 |
elif user: |
|
426 |
user_full_name = user.get_full_name() |
|
427 |
return _('edit of user "{0}" ({1}) from API').format(user_full_name, edited_attributes) |
|
428 |
return super().get_message(event, context) |
|
429 | ||
430 | ||
431 |
class ApiUserPasswordChange(EventTypeDefinition): |
|
432 |
name = 'api.user.password.change' |
|
433 |
label = _('user password change') |
|
434 | ||
435 |
@classmethod |
|
436 |
def record(cls, user, serializer): |
|
437 |
super().record( |
|
438 |
user=user, references=[serializer.instance], data={'email': serializer.validated_data['email']} |
|
439 |
) |
|
440 | ||
441 |
@classmethod |
|
442 |
def get_message(cls, event, context): |
|
443 |
(user,) = event.get_typed_references(User) |
|
444 |
email = event.get_data('email') |
|
445 |
if context and context == user: |
|
446 |
return _('password change from API and notification by mail') |
|
447 |
elif user: |
|
448 |
user_full_name = user.get_full_name() |
|
449 |
return _('password change from API of user "%s" and notification by mail') % user_full_name |
|
450 |
return super().get_message(event, context) |
|
451 | ||
452 | ||
453 |
class ApiUserPasswordResetRequest(EventTypeDefinition): |
|
454 |
name = 'api.user.password.reset.request' |
|
455 |
label = _('user password reset request') |
|
456 | ||
457 |
@classmethod |
|
458 |
def record(cls, user, target_user): |
|
459 |
super().record(user=user, references=[target_user], data={'email': target_user.email}) |
|
460 | ||
461 |
@classmethod |
|
462 |
def get_message(cls, event, context): |
|
463 |
(user,) = event.get_typed_references(User) |
|
464 |
email = event.get_data('email') |
|
465 |
if context and context == user: |
|
466 |
return _('password reset request from API sent to "%s"') % email |
|
467 |
elif user: |
|
468 |
return _('password reset request from API of "{0}" sent to "{1}"').format( |
|
469 |
user.get_full_name(), email |
|
470 |
) |
|
471 |
return super().get_message(event, context) |
|
472 | ||
473 | ||
474 |
class ApiUserDeletion(EventTypeDefinition): |
|
475 |
name = 'api.user.deletion' |
|
476 |
label = _('user deletion') |
|
477 | ||
478 |
@classmethod |
|
479 |
def record(cls, user, target_user): |
|
480 |
super().record(user=user, references=[target_user]) |
|
481 | ||
482 |
@classmethod |
|
483 |
def get_message(cls, event, context): |
|
484 |
(user,) = event.get_typed_references(User) |
|
485 |
if context and context == user: |
|
486 |
return _('deletion from API') |
|
487 |
elif user: |
|
488 |
return _('deletion of user "%s" from API') % user.get_full_name() |
|
489 |
return super().get_message(event, context) |
|
490 | ||
491 | ||
492 |
class ApiRoleCreation(RoleEventsMixin): |
|
493 |
name = 'api.role.creation' |
|
494 |
label = _('role creation') |
|
495 | ||
496 |
@classmethod |
|
497 |
def get_message(cls, event, context): |
|
498 |
(role,) = event.get_typed_references(Role) |
|
499 |
role = role or event.get_data('role_name') |
|
500 |
if context != role: |
|
501 |
return _('creation of role "%s" from API') % role |
|
502 |
else: |
|
503 |
return _('creation from API') |
|
504 | ||
505 | ||
506 |
class ApiRoleEdit(RoleEventsMixin): |
|
507 |
name = 'api.role.edit' |
|
508 |
label = _('role edit') |
|
509 | ||
510 |
@classmethod |
|
511 |
def record(cls, user, role, serializer): |
|
512 |
super().record(user=user, role=role, data={'new': serializer.validated_data}) |
|
513 | ||
514 |
@classmethod |
|
515 |
def get_message(cls, event, context): |
|
516 |
(role,) = event.get_typed_references(Role) |
|
517 |
role = role or event.get_data('role_name') |
|
518 |
new = event.get_data('new') |
|
519 |
edited_attributes = ', '.join(get_attributes_label(new)) or '' |
|
520 |
if context != role: |
|
521 |
return _('edit of role "{role}" from API ({change})').format(role=role, change=edited_attributes) |
|
522 |
else: |
|
523 |
return _('edit from API ({change})').format(change=edited_attributes) |
|
524 | ||
525 | ||
526 |
class ApiRoleDeletion(RoleEventsMixin): |
|
527 |
name = 'api.role.deletion' |
|
528 |
label = _('role deletion') |
|
529 | ||
530 |
@classmethod |
|
531 |
def get_message(cls, event, context): |
|
532 |
(role,) = event.get_typed_references(Role) |
|
533 |
role = role or event.get_data('role_name') |
|
534 |
if context != role: |
|
535 |
return _('deletion of role "%s" from API') % role |
|
536 |
else: |
|
537 |
return _('deletion from API') |
|
538 | ||
539 | ||
540 |
class ApiRoleMembershipGrant(RoleEventsMixin): |
|
541 |
name = 'api.role.membership.grant' |
|
542 |
label = _('role membership grant') |
|
543 | ||
544 |
@classmethod |
|
545 |
def record(cls, user, role, member): |
|
546 |
data = {'member_name': member.get_full_name()} |
|
547 |
super().record(user=user, role=role, references=[member], data=data) |
|
548 | ||
549 |
@classmethod |
|
550 |
def get_message(cls, event, context): |
|
551 |
role, member = event.get_typed_references(Role, User) |
|
552 |
member = member or event.get_data('member_name') |
|
553 |
role = role or event.get_data('role_name') |
|
554 |
if context == member: |
|
555 |
return _('membership grant in role "%s" from API') % role |
|
556 |
elif context == role: |
|
557 |
return _('membership grant to user "%s" from API') % member |
|
558 |
else: |
|
559 |
return _('membership grant to user "{member}" in role "{role}" from API').format( |
|
560 |
member=member, role=role |
|
561 |
) |
|
562 | ||
563 | ||
564 |
class ApiRoleMembershipRemoval(RoleEventsMixin): |
|
565 |
name = 'api.role.membership.removal' |
|
566 |
label = _('role membership removal') |
|
567 | ||
568 |
@classmethod |
|
569 |
def record(cls, user, role, member): |
|
570 |
data = {'member_name': member.get_full_name()} |
|
571 |
super().record(user=user, role=role, references=[member], data=data) |
|
572 | ||
573 |
@classmethod |
|
574 |
def get_message(cls, event, context): |
|
575 |
role, member = event.get_typed_references(Role, User) |
|
576 |
member = member or event.get_data('member_name') |
|
577 |
role = role or event.get_data('role_name') |
|
578 |
if context == member: |
|
579 |
return _('membership removal from role "%s" from API') % role |
|
580 |
elif context == role: |
|
581 |
return _('membership removal of user "%s" from API') % member |
|
582 |
else: |
|
583 |
return _('membership removal of user "{member}" from role "{role}" from API').format( |
|
584 |
member=member, role=role |
|
585 |
) |
src/authentic2/manager/journal_event_types.py | ||
---|---|---|
19 | 19 | |
20 | 20 |
from authentic2.apps.journal.models import EventTypeDefinition |
21 | 21 |
from authentic2.apps.journal.utils import form_to_old_new |
22 |
from authentic2.journal_event_types import EventTypeWithService, get_attributes_label |
|
22 |
from authentic2.journal_event_types import EventTypeWithService, RoleEventsMixin, get_attributes_label
|
|
23 | 23 |
from django_rbac.utils import get_role_model |
24 | 24 | |
25 | 25 |
User = get_user_model() |
... | ... | |
257 | 257 |
return super().get_message(event, context) |
258 | 258 | |
259 | 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({'role_name': str(role), 'role_uuid': role.uuid}) |
|
267 |
super().record( |
|
268 |
user=user, |
|
269 |
session=session, |
|
270 |
references=references, |
|
271 |
data=data, |
|
272 |
) |
|
273 | ||
274 | ||
275 | 260 |
class ManagerRoleCreation(RoleEventsMixin): |
276 | 261 |
name = 'manager.role.creation' |
277 | 262 |
label = _('role creation') |
src/authentic2/manager/journal_views.py | ||
---|---|---|
93 | 93 |
('manager.role', _('Role management')), |
94 | 94 |
), |
95 | 95 |
), |
96 |
( |
|
97 |
_('API'), |
|
98 |
( |
|
99 |
('api', _('All')), |
|
100 |
('api.user', _('User management')), |
|
101 |
('api.role', _('Role management')), |
|
102 |
), |
|
103 |
), |
|
96 | 104 |
) |
97 | 105 | |
98 | 106 |
tests/test_manager_journal.py | ||
---|---|---|
256 | 256 |
user=user, |
257 | 257 |
reason='not-present', |
258 | 258 |
) |
259 |
make('api.user.creation', user=agent, target_user=user) |
|
260 |
user_serializer = mock.Mock(spec=['instance', 'validated_data']) |
|
261 |
user_serializer.instance = user |
|
262 |
user_serializer.validated_data = {'email': 'jane@example.com', 'attributes': {'city': 'paris'}} |
|
263 |
make('api.user.profile.edit', user=agent, serializer=user_serializer) |
|
264 |
make('api.user.password.change', user=agent, serializer=user_serializer) |
|
265 |
user_serializer = mock.Mock(spec=['instance', 'validated_data']) |
|
266 |
user_serializer.instance = user |
|
267 |
user_serializer.validated_data = {'email': 'jane@example.com'} |
|
268 |
make('api.user.profile.edit', user=agent, serializer=user_serializer) |
|
269 |
make('api.user.password.reset.request', user=agent, target_user=user) |
|
270 |
make('api.user.deletion', user=agent, target_user=user) |
|
271 |
make('api.role.creation', user=agent, role=role_user) |
|
272 |
make('api.role.deletion', user=agent, role=role_user) |
|
273 |
role_serializer = mock.Mock(spec=['validated_data']) |
|
274 |
role_serializer.validated_data = {'name': 'test'} |
|
275 |
make('api.role.edit', user=agent, role=role_user, serializer=role_serializer) |
|
276 |
make("api.role.membership.grant", user=agent, role=role_user, member=user) |
|
277 |
make("api.role.membership.removal", user=agent, role=role_user, member=user) |
|
259 | 278 | |
260 | 279 |
# verify we created at least one event for each type |
261 | 280 |
assert set(Event.objects.values_list("type__name", flat=True)) == set(_registry) |
... | ... | |
553 | 572 |
'type': 'ldap.user.deactivation', |
554 | 573 |
'user': 'Johnny doe', |
555 | 574 |
}, |
575 |
{ |
|
576 |
'timestamp': 'Jan. 2, 2020, 6 p.m.', |
|
577 |
'type': 'api.user.creation', |
|
578 |
'user': 'agent', |
|
579 |
'message': 'creation of user "Johnny doe" from API', |
|
580 |
}, |
|
581 |
{ |
|
582 |
'timestamp': 'Jan. 2, 2020, 7 p.m.', |
|
583 |
'type': 'api.user.profile.edit', |
|
584 |
'user': 'agent', |
|
585 |
'message': 'edit of user "Johnny doe" (city, email address) from API', |
|
586 |
}, |
|
587 |
{ |
|
588 |
'timestamp': 'Jan. 2, 2020, 8 p.m.', |
|
589 |
'type': 'api.user.password.change', |
|
590 |
'user': 'agent', |
|
591 |
'message': 'password change from API of user "Johnny doe" and notification by mail', |
|
592 |
}, |
|
593 |
{ |
|
594 |
'timestamp': 'Jan. 2, 2020, 9 p.m.', |
|
595 |
'type': 'api.user.profile.edit', |
|
596 |
'user': 'agent', |
|
597 |
'message': 'edit of user "Johnny doe" (email address) from API', |
|
598 |
}, |
|
599 |
{ |
|
600 |
'timestamp': 'Jan. 2, 2020, 10 p.m.', |
|
601 |
'type': 'api.user.password.reset.request', |
|
602 |
'user': 'agent', |
|
603 |
'message': 'password reset request from API of "Johnny doe" sent to "user@example.com"', |
|
604 |
}, |
|
605 |
{ |
|
606 |
'timestamp': 'Jan. 2, 2020, 11 p.m.', |
|
607 |
'type': 'api.user.deletion', |
|
608 |
'user': 'agent', |
|
609 |
'message': 'deletion of user "Johnny doe" from API', |
|
610 |
}, |
|
611 |
{ |
|
612 |
'timestamp': 'Jan. 3, 2020, midnight', |
|
613 |
'type': 'api.role.creation', |
|
614 |
'user': 'agent', |
|
615 |
'message': 'creation of role "role1" from API', |
|
616 |
}, |
|
617 |
{ |
|
618 |
'timestamp': 'Jan. 3, 2020, 1 a.m.', |
|
619 |
'type': 'api.role.deletion', |
|
620 |
'user': 'agent', |
|
621 |
'message': 'deletion of role "role1" from API', |
|
622 |
}, |
|
623 |
{ |
|
624 |
'timestamp': 'Jan. 3, 2020, 2 a.m.', |
|
625 |
'type': 'api.role.edit', |
|
626 |
'user': 'agent', |
|
627 |
'message': 'edit of role "role1" from API (name)', |
|
628 |
}, |
|
629 |
{ |
|
630 |
'timestamp': 'Jan. 3, 2020, 3 a.m.', |
|
631 |
'type': 'api.role.membership.grant', |
|
632 |
'user': 'agent', |
|
633 |
'message': 'membership grant to user "user (111111)" in role "role1" from API', |
|
634 |
}, |
|
635 |
{ |
|
636 |
'timestamp': 'Jan. 3, 2020, 4 a.m.', |
|
637 |
'type': 'api.role.membership.removal', |
|
638 |
'user': 'agent', |
|
639 |
'message': 'membership removal of user "user (111111)" from role "role1" from API', |
|
640 |
}, |
|
556 | 641 |
] |
557 | 642 | |
558 | 643 | |
... | ... | |
744 | 829 |
'type': 'ldap.user.deactivation', |
745 | 830 |
'user': 'Johnny doe', |
746 | 831 |
}, |
832 |
{ |
|
833 |
'timestamp': 'Jan. 2, 2020, 6 p.m.', |
|
834 |
'type': 'api.user.creation', |
|
835 |
'user': 'agent', |
|
836 |
'message': 'creation from API', |
|
837 |
}, |
|
838 |
{ |
|
839 |
'timestamp': 'Jan. 2, 2020, 7 p.m.', |
|
840 |
'type': 'api.user.profile.edit', |
|
841 |
'user': 'agent', |
|
842 |
'message': 'edit from API (city, email address)', |
|
843 |
}, |
|
844 |
{ |
|
845 |
'timestamp': 'Jan. 2, 2020, 8 p.m.', |
|
846 |
'type': 'api.user.password.change', |
|
847 |
'user': 'agent', |
|
848 |
'message': 'password change from API and notification by mail', |
|
849 |
}, |
|
850 |
{ |
|
851 |
'timestamp': 'Jan. 2, 2020, 9 p.m.', |
|
852 |
'type': 'api.user.profile.edit', |
|
853 |
'user': 'agent', |
|
854 |
'message': 'edit from API (email address)', |
|
855 |
}, |
|
856 |
{ |
|
857 |
'timestamp': 'Jan. 2, 2020, 10 p.m.', |
|
858 |
'type': 'api.user.password.reset.request', |
|
859 |
'user': 'agent', |
|
860 |
'message': 'password reset request from API sent to "user@example.com"', |
|
861 |
}, |
|
862 |
{ |
|
863 |
'timestamp': 'Jan. 2, 2020, 11 p.m.', |
|
864 |
'type': 'api.user.deletion', |
|
865 |
'user': 'agent', |
|
866 |
'message': 'deletion from API', |
|
867 |
}, |
|
868 |
{ |
|
869 |
'timestamp': 'Jan. 3, 2020, 3 a.m.', |
|
870 |
'type': 'api.role.membership.grant', |
|
871 |
'user': 'agent', |
|
872 |
'message': 'membership grant in role "role1" from API', |
|
873 |
}, |
|
874 |
{ |
|
875 |
'timestamp': 'Jan. 3, 2020, 4 a.m.', |
|
876 |
'type': 'api.role.membership.removal', |
|
877 |
'user': 'agent', |
|
878 |
'message': 'membership removal from role "role1" from API', |
|
879 |
}, |
|
747 | 880 |
] |
748 | 881 | |
749 | 882 | |
750 |
- |