From 0478ad6d8a9b167017fbfebb263090ee3e8dfb5d Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Mon, 17 May 2021 14:50:42 +0200 Subject: [PATCH] wip --- src/authentic2/api_views.py | 31 +++ src/authentic2/apps/journal/models.py | 3 + src/authentic2/journal_event_types.py | 219 ++++++++++++++++++ src/authentic2/manager/journal_event_types.py | 17 +- src/authentic2/manager/journal_views.py | 8 + tests/test_manager_journal.py | 133 +++++++++++ 6 files changed, 395 insertions(+), 16 deletions(-) diff --git a/src/authentic2/api_views.py b/src/authentic2/api_views.py index 3728bf12..fbc76963 100644 --- a/src/authentic2/api_views.py +++ b/src/authentic2/api_views.py @@ -343,6 +343,7 @@ class PasswordChange(BaseRpcView): def rpc(self, request, serializer): serializer.user.set_password(serializer.validated_data['new_password']) serializer.user.save() + request.journal.record('api.user.password.change', serializer=serializer) return {'result': 1}, status.HTTP_200_OK @@ -778,9 +779,18 @@ class UsersAPI(api_mixins.GetOrCreateMixinView, HookMixin, ExceptionHandlerMixin if not self.request.user.has_perm(perm): raise PermissionDenied(u'You do not have permission %s' % perm) + def perform_create(self, serializer): + super().perform_create(serializer) + self.request.journal.record('api.user.creation', target_user=serializer.instance) + + def perform_update(self, serializer): + super().perform_update(serializer) + self.request.journal.record('api.user.profile.edit', serializer=serializer) + def perform_destroy(self, instance): self.check_perm('custom_user.delete_user', instance.ou) super(UsersAPI, self).perform_destroy(instance) + self.request.journal.record('api.user.deletion', target_user=instance) class SynchronizationSerializer(serializers.Serializer): known_uuids = serializers.ListField(child=serializers.CharField()) @@ -820,6 +830,7 @@ class UsersAPI(api_mixins.GetOrCreateMixinView, HookMixin, ExceptionHandlerMixin ) utils.send_password_reset_mail(user, request=request) + request.journal.record('api.user.password.reset.request', target_user=user) return Response(status=status.HTTP_204_NO_CONTENT) @action(detail=True, methods=['post'], permission_classes=(DjangoPermission('custom_user.change_user'),)) @@ -876,6 +887,15 @@ class RolesAPI(api_mixins.GetOrCreateMixinView, ExceptionHandlerMixin, ModelView if not self.request.user.has_perm(perm='a2_rbac.delete_role', obj=instance): raise PermissionDenied(u'User %s can\'t create role %s' % (self.request.user, instance)) super(RolesAPI, self).perform_destroy(instance) + self.request.journal.record('api.role.deletion', role=instance) + + def perform_create(self, serializer): + super().perform_create(serializer) + self.request.journal.record('api.role.creation', role=serializer.instance) + + def perform_update(self, serializer): + super().perform_update(serializer) + self.request.journal.record('api.role.edit', role=serializer.instance, form=serializer) class RolesMembersAPI(UsersAPI): @@ -919,6 +939,7 @@ class RoleMembershipAPI(ExceptionHandlerMixin, APIView): if not request.user.has_perm('a2_rbac.manage_members_role', obj=self.role): raise PermissionDenied(u'User not allowed to manage role members') self.role.members.add(self.member) + request.journal.record('api.role.membership.grant', role=self.role, member=self.member) return Response( {'result': 1, 'detail': _('User successfully added to role')}, status=status.HTTP_201_CREATED ) @@ -927,6 +948,7 @@ class RoleMembershipAPI(ExceptionHandlerMixin, APIView): if not request.user.has_perm('a2_rbac.manage_members_role', obj=self.role): raise PermissionDenied(u'User not allowed to manage role members') self.role.members.remove(self.member) + request.journal.record('api.role.membership.removal', role=self.role, member=self.member) return Response( {'result': 1, 'detail': _('User successfully removed from role')}, status=status.HTTP_200_OK ) @@ -976,18 +998,27 @@ class RoleMembershipsAPI(ExceptionHandlerMixin, APIView): def post(self, request, *args, **kwargs): self.role.members.add(*self.members) + for member in self.members: + request.journal.record('api.role.membership.grant', role=self.role, member=member) return Response( {'result': 1, 'detail': _('Users successfully added to role')}, status=status.HTTP_201_CREATED ) def delete(self, request, *args, **kwargs): self.role.members.remove(*self.members) + for member in self.members: + request.journal.record('api.role.membership.removal', role=self.role, member=member) return Response( {'result': 1, 'detail': _('Users successfully removed from role')}, status=status.HTTP_200_OK ) def patch(self, request, *args, **kwargs): + old_members = set(self.role.members.all()) self.role.members.set(self.members) + for member in self.members: + request.journal.record('api.role.membership.grant', role=self.role, member=member) + for member in old_members.difference(self.members): + request.journal.record('api.role.membership.removal', role=self.role, member=member) return Response( {'result': 1, 'detail': _('Users successfully assigned to role')}, status=status.HTTP_200_OK ) diff --git a/src/authentic2/apps/journal/models.py b/src/authentic2/apps/journal/models.py index bb6d71b1..644bab13 100644 --- a/src/authentic2/apps/journal/models.py +++ b/src/authentic2/apps/journal/models.py @@ -88,6 +88,9 @@ class EventTypeDefinition(metaclass=EventTypeDefinitionMeta): def record(cls, user=None, session=None, references=None, data=None): event_type = EventType.objects.get_for_name(cls.name) + if user and user.is_anonymous: + user = None + Event.objects.create( type=event_type, user=user, diff --git a/src/authentic2/journal_event_types.py b/src/authentic2/journal_event_types.py index 8ab9a22b..bef6bd8b 100644 --- a/src/authentic2/journal_event_types.py +++ b/src/authentic2/journal_event_types.py @@ -14,15 +14,20 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext_lazy as _ from authentic2.apps.journal.models import EventTypeDefinition, n_2_pairing_rev from authentic2.apps.journal.utils import Statistics, form_to_old_new from authentic2.custom_user.models import User, get_attributes_map +from django_rbac.utils import get_role_model from .models import Service +Role = get_role_model() +User = get_user_model() + class EventTypeWithService(EventTypeDefinition): @classmethod @@ -46,6 +51,21 @@ class EventTypeWithService(EventTypeDefinition): return '' +class RoleEventsMixin(EventTypeDefinition): + @classmethod + def record(self, user, role, session=None, references=None, data=None): + references = references or [] + references = [role] + references + data = data or {} + data.update({'role_name': str(role), 'role_uuid': role.uuid}) + super().record( + user=user, + session=session, + references=references, + data=data, + ) + + class EventTypeWithHow(EventTypeWithService): @classmethod def record(cls, user, session, service, how): @@ -364,3 +384,202 @@ class LdapUserDeactivation(EventTypeDefinition): elif reason == 'old-source': return _('automatic deactivation because user was from an old LDAP source') return super().get_message(event, context) + + +class ApiUserCreation(EventTypeDefinition): + name = 'api.user.creation' + label = _('user creation') + + @classmethod + def record(cls, user, target_user): + super().record(user=user, references=[target_user]) + + @classmethod + def get_message(cls, event, context): + (user,) = event.get_typed_references(User) + if context and context == user: + return _('creation from API') + elif user: + return _('creation of user "%s" from API') % user.get_full_name() + return super().get_message(event, context) + + +class ApiUserProfileEdit(EventTypeDefinition): + name = 'api.user.profile.edit' + label = _('user profile edit') + + @classmethod + def record(cls, user, serializer): + changed_data = serializer.validated_data.copy() + attributes = changed_data.pop('attributes', {}) + changed_data.update(attributes) + super().record(user=user, references=[serializer.instance], data={'new': changed_data}) + + @classmethod + def get_message(cls, event, context): + (user,) = event.get_typed_references(User) + new = event.get_data('new') or {} + edited_attributes = ', '.join(get_attributes_label(new)) or '' + if context and context == user: + return _('edit from API (%s)') % edited_attributes + elif user: + user_full_name = user.get_full_name() + return _('edit of user "{0}" ({1}) from API').format(user_full_name, edited_attributes) + return super().get_message(event, context) + + +class ApiUserPasswordChange(EventTypeDefinition): + name = 'api.user.password.change' + label = _('user password change') + + @classmethod + def record(cls, user, serializer): + super().record( + user=user, references=[serializer.instance], data={'email': serializer.validated_data['email']} + ) + + @classmethod + def get_message(cls, event, context): + (user,) = event.get_typed_references(User) + email = event.get_data('email') + if context and context == user: + return _('password change from API and notification by mail') + elif user: + user_full_name = user.get_full_name() + return _('password change from API of user "%s" and notification by mail') % user_full_name + return super().get_message(event, context) + + +class ApiUserPasswordResetRequest(EventTypeDefinition): + name = 'api.user.password.reset.request' + label = _('user password reset request') + + @classmethod + def record(cls, user, target_user): + super().record(user=user, references=[target_user], data={'email': target_user.email}) + + @classmethod + def get_message(cls, event, context): + (user,) = event.get_typed_references(User) + email = event.get_data('email') + if context and context == user: + return _('password reset request from API sent to "%s"') % email + elif user: + return _('password reset request from API of "{0}" sent to "{1}"').format( + user.get_full_name(), email + ) + return super().get_message(event, context) + + +class ApiUserDeletion(EventTypeDefinition): + name = 'api.user.deletion' + label = _('user deletion') + + @classmethod + def record(cls, user, target_user): + super().record(user=user, references=[target_user]) + + @classmethod + def get_message(cls, event, context): + (user,) = event.get_typed_references(User) + if context and context == user: + return _('deletion from API') + elif user: + return _('deletion of user "%s" from API') % user.get_full_name() + return super().get_message(event, context) + + +class ApiRoleCreation(RoleEventsMixin): + name = 'api.role.creation' + label = _('role creation') + + @classmethod + def get_message(cls, event, context): + (role,) = event.get_typed_references(Role) + role = role or event.get_data('role_name') + if context != role: + return _('creation of role "%s" from API') % role + else: + return _('creation from API') + + +class ApiRoleEdit(RoleEventsMixin): + name = 'api.role.edit' + label = _('role edit') + + @classmethod + def record(cls, user, role, serializer): + super().record(user=user, role=role, data={'new': serializer.validated_data}) + + @classmethod + def get_message(cls, event, context): + (role,) = event.get_typed_references(Role) + role = role or event.get_data('role_name') + new = event.get_data('new') + edited_attributes = ', '.join(get_attributes_label(new)) or '' + if context != role: + return _('edit of role "{role}" from API ({change})').format(role=role, change=edited_attributes) + else: + return _('edit from API ({change})').format(change=edited_attributes) + + +class ApiRoleDeletion(RoleEventsMixin): + name = 'api.role.deletion' + label = _('role deletion') + + @classmethod + def get_message(cls, event, context): + (role,) = event.get_typed_references(Role) + role = role or event.get_data('role_name') + if context != role: + return _('deletion of role "%s" from API') % role + else: + return _('deletion from API') + + +class ApiRoleMembershipGrant(RoleEventsMixin): + name = 'api.role.membership.grant' + label = _('role membership grant') + + @classmethod + def record(cls, user, role, member): + data = {'member_name': member.get_full_name()} + super().record(user=user, role=role, references=[member], data=data) + + @classmethod + def get_message(cls, event, context): + role, member = event.get_typed_references(Role, User) + member = member or event.get_data('member_name') + role = role or event.get_data('role_name') + if context == member: + return _('membership grant in role "%s" from API') % role + elif context == role: + return _('membership grant to user "%s" from API') % member + else: + return _('membership grant to user "{member}" in role "{role}" from API').format( + member=member, role=role + ) + + +class ApiRoleMembershipRemoval(RoleEventsMixin): + name = 'api.role.membership.removal' + label = _('role membership removal') + + @classmethod + def record(cls, user, role, member): + data = {'member_name': member.get_full_name()} + super().record(user=user, role=role, references=[member], data=data) + + @classmethod + def get_message(cls, event, context): + role, member = event.get_typed_references(Role, User) + member = member or event.get_data('member_name') + role = role or event.get_data('role_name') + if context == member: + return _('membership removal from role "%s" from API') % role + elif context == role: + return _('membership removal of user "%s" from API') % member + else: + return _('membership removal of user "{member}" from role "{role}" from API').format( + member=member, role=role + ) diff --git a/src/authentic2/manager/journal_event_types.py b/src/authentic2/manager/journal_event_types.py index 5fc5c9d6..4ef50f4e 100644 --- a/src/authentic2/manager/journal_event_types.py +++ b/src/authentic2/manager/journal_event_types.py @@ -19,7 +19,7 @@ from django.utils.translation import ugettext_lazy as _ from authentic2.apps.journal.models import EventTypeDefinition from authentic2.apps.journal.utils import form_to_old_new -from authentic2.journal_event_types import EventTypeWithService, get_attributes_label +from authentic2.journal_event_types import EventTypeWithService, RoleEventsMixin, get_attributes_label from django_rbac.utils import get_role_model User = get_user_model() @@ -257,21 +257,6 @@ class ManagerUserSSOAuthorizationDeletion(EventTypeWithService): return super().get_message(event, context) -class RoleEventsMixin(EventTypeDefinition): - @classmethod - def record(self, user, session, role, references=None, data=None): - references = references or [] - references = [role] + references - data = data or {} - data.update({'role_name': str(role), 'role_uuid': role.uuid}) - super().record( - user=user, - session=session, - references=references, - data=data, - ) - - class ManagerRoleCreation(RoleEventsMixin): name = 'manager.role.creation' label = _('role creation') diff --git a/src/authentic2/manager/journal_views.py b/src/authentic2/manager/journal_views.py index 70e8fce2..e4989d3a 100644 --- a/src/authentic2/manager/journal_views.py +++ b/src/authentic2/manager/journal_views.py @@ -93,6 +93,14 @@ EVENT_TYPE_CHOICES = ( ('manager.role', _('Role management')), ), ), + ( + _('API'), + ( + ('api', _('All')), + ('api.user', _('User management')), + ('api.role', _('Role management')), + ), + ), ) diff --git a/tests/test_manager_journal.py b/tests/test_manager_journal.py index 63cf0a38..828104ab 100644 --- a/tests/test_manager_journal.py +++ b/tests/test_manager_journal.py @@ -256,6 +256,25 @@ def events(db, freezer): user=user, reason='not-present', ) + make('api.user.creation', user=agent, target_user=user) + user_serializer = mock.Mock(spec=['instance', 'validated_data']) + user_serializer.instance = user + user_serializer.validated_data = {'email': 'jane@example.com', 'attributes': {'city': 'paris'}} + make('api.user.profile.edit', user=agent, serializer=user_serializer) + make('api.user.password.change', user=agent, serializer=user_serializer) + user_serializer = mock.Mock(spec=['instance', 'validated_data']) + user_serializer.instance = user + user_serializer.validated_data = {'email': 'jane@example.com'} + make('api.user.profile.edit', user=agent, serializer=user_serializer) + make('api.user.password.reset.request', user=agent, target_user=user) + make('api.user.deletion', user=agent, target_user=user) + make('api.role.creation', user=agent, role=role_user) + make('api.role.deletion', user=agent, role=role_user) + role_serializer = mock.Mock(spec=['validated_data']) + role_serializer.validated_data = {'name': 'test'} + make('api.role.edit', user=agent, role=role_user, serializer=role_serializer) + make("api.role.membership.grant", user=agent, role=role_user, member=user) + make("api.role.membership.removal", user=agent, role=role_user, member=user) # verify we created at least one event for each type assert set(Event.objects.values_list("type__name", flat=True)) == set(_registry) @@ -553,6 +572,72 @@ def test_global_journal(app, superuser, events): 'type': 'ldap.user.deactivation', 'user': 'Johnny doe', }, + { + 'timestamp': 'Jan. 2, 2020, 6 p.m.', + 'type': 'api.user.creation', + 'user': 'agent', + 'message': 'creation of user "Johnny doe" from API', + }, + { + 'timestamp': 'Jan. 2, 2020, 7 p.m.', + 'type': 'api.user.profile.edit', + 'user': 'agent', + 'message': 'edit of user "Johnny doe" (city, email address) from API', + }, + { + 'timestamp': 'Jan. 2, 2020, 8 p.m.', + 'type': 'api.user.password.change', + 'user': 'agent', + 'message': 'password change from API of user "Johnny doe" and notification by mail', + }, + { + 'timestamp': 'Jan. 2, 2020, 9 p.m.', + 'type': 'api.user.profile.edit', + 'user': 'agent', + 'message': 'edit of user "Johnny doe" (email address) from API', + }, + { + 'timestamp': 'Jan. 2, 2020, 10 p.m.', + 'type': 'api.user.password.reset.request', + 'user': 'agent', + 'message': 'password reset request from API of "Johnny doe" sent to "user@example.com"', + }, + { + 'timestamp': 'Jan. 2, 2020, 11 p.m.', + 'type': 'api.user.deletion', + 'user': 'agent', + 'message': 'deletion of user "Johnny doe" from API', + }, + { + 'timestamp': 'Jan. 3, 2020, midnight', + 'type': 'api.role.creation', + 'user': 'agent', + 'message': 'creation of role "role1" from API', + }, + { + 'timestamp': 'Jan. 3, 2020, 1 a.m.', + 'type': 'api.role.deletion', + 'user': 'agent', + 'message': 'deletion of role "role1" from API', + }, + { + 'timestamp': 'Jan. 3, 2020, 2 a.m.', + 'type': 'api.role.edit', + 'user': 'agent', + 'message': 'edit of role "role1" from API (name)', + }, + { + 'timestamp': 'Jan. 3, 2020, 3 a.m.', + 'type': 'api.role.membership.grant', + 'user': 'agent', + 'message': 'membership grant to user "user (111111)" in role "role1" from API', + }, + { + 'timestamp': 'Jan. 3, 2020, 4 a.m.', + 'type': 'api.role.membership.removal', + 'user': 'agent', + 'message': 'membership removal of user "user (111111)" from role "role1" from API', + }, ] @@ -744,6 +829,54 @@ def test_user_journal(app, superuser, events): 'type': 'ldap.user.deactivation', 'user': 'Johnny doe', }, + { + 'timestamp': 'Jan. 2, 2020, 6 p.m.', + 'type': 'api.user.creation', + 'user': 'agent', + 'message': 'creation from API', + }, + { + 'timestamp': 'Jan. 2, 2020, 7 p.m.', + 'type': 'api.user.profile.edit', + 'user': 'agent', + 'message': 'edit from API (city, email address)', + }, + { + 'timestamp': 'Jan. 2, 2020, 8 p.m.', + 'type': 'api.user.password.change', + 'user': 'agent', + 'message': 'password change from API and notification by mail', + }, + { + 'timestamp': 'Jan. 2, 2020, 9 p.m.', + 'type': 'api.user.profile.edit', + 'user': 'agent', + 'message': 'edit from API (email address)', + }, + { + 'timestamp': 'Jan. 2, 2020, 10 p.m.', + 'type': 'api.user.password.reset.request', + 'user': 'agent', + 'message': 'password reset request from API sent to "user@example.com"', + }, + { + 'timestamp': 'Jan. 2, 2020, 11 p.m.', + 'type': 'api.user.deletion', + 'user': 'agent', + 'message': 'deletion from API', + }, + { + 'timestamp': 'Jan. 3, 2020, 3 a.m.', + 'type': 'api.role.membership.grant', + 'user': 'agent', + 'message': 'membership grant in role "role1" from API', + }, + { + 'timestamp': 'Jan. 3, 2020, 4 a.m.', + 'type': 'api.role.membership.removal', + 'user': 'agent', + 'message': 'membership removal from role "role1" from API', + }, ] -- 2.20.1