diff --git a/onadata/apps/api/tests/viewsets/test_user_profile_viewset.py b/onadata/apps/api/tests/viewsets/test_user_profile_viewset.py index cdcef75cd1..b96b47464c 100644 --- a/onadata/apps/api/tests/viewsets/test_user_profile_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_user_profile_viewset.py @@ -25,6 +25,9 @@ from onadata.apps.api.tests.viewsets.test_abstract_viewset import TestAbstractViewSet from onadata.apps.api.viewsets.connect_viewset import ConnectViewSet from onadata.apps.api.viewsets.user_profile_viewset import UserProfileViewSet +from onadata.apps.messaging.constants import ( + USER, USER_CREATED, USER_UPDATED, USER_PASSWORD_CHANGED +) from onadata.apps.logger.models.instance import Instance from onadata.apps.logger.models.project_invitation import ProjectInvitation from onadata.apps.main.models import UserProfile @@ -252,13 +255,14 @@ def test_profiles_get_org_anon(self): @override_settings(CELERY_TASK_ALWAYS_EAGER=True) @override_settings(ENABLE_EMAIL_VERIFICATION=True) + @patch("onadata.libs.serializers.user_profile_serializer.send_message") @patch( ( "onadata.libs.serializers.user_profile_serializer." "send_verification_email.delay" ) ) - def test_profile_create(self, mock_send_verification_email): + def test_profile_create(self, mock_send_verification_email, mock_send_message): request = self.factory.get("/", **self.extra) response = self.view(request) self.assertEqual(response.status_code, 200) @@ -290,6 +294,16 @@ def test_profile_create(self, mock_send_verification_email): self.assertTrue(user.is_active) self.assertTrue(user.check_password(password), password) + # Save on user creation event + self.assertTrue(mock_send_message.called) + mock_send_message.assert_called_with( + instance_id=user.username, + target_id=user.id, + target_type=USER, + user=user, + message_verb=USER_CREATED + ) + def _create_user_using_profiles_endpoint(self, data): request = self.factory.post( "/api/v1/profiles", @@ -572,7 +586,8 @@ def test_split_long_name_to_first_name_and_last_name(self): self.assertEqual(first_name, "(CPLTGL) Centre Pour la Promot") self.assertEqual(last_name, "ion de la Liberte D'Expression") - def test_partial_updates(self): + @patch("onadata.apps.api.viewsets.user_profile_viewset.send_message") + def test_partial_updates(self, mock_send_message): self.assertEqual(self.user.profile.country, "US") country = "KE" username = "george" @@ -587,7 +602,18 @@ def test_partial_updates(self): self.assertEqual(profile.metadata, metadata) self.assertEqual(profile.user.username, username) - def test_partial_updates_empty_metadata(self): + # Save on user update (Patch) event + self.assertTrue(mock_send_message.called) + mock_send_message.assert_called_with( + instance_id=request.user.username, + target_id=request.user.id, + target_type=USER, + user=request.user, + message_verb=USER_UPDATED + ) + + @patch("onadata.apps.api.viewsets.user_profile_viewset.send_message") + def test_partial_updates_empty_metadata(self, mock_send_message): profile = UserProfile.objects.get(user=self.user) profile.metadata = {} profile.save() @@ -600,6 +626,16 @@ def test_partial_updates_empty_metadata(self): self.assertEqual(response.status_code, 200) self.assertEqual(profile.metadata, metadata) + # Save on user update (Patch) event + self.assertTrue(mock_send_message.called) + mock_send_message.assert_called_with( + instance_id=request.user.username, + target_id=request.user.id, + target_type=USER, + user=request.user, + message_verb=USER_UPDATED + ) + def test_partial_updates_too_long(self): # the max field length for username is 30 in django username = "a" * 31 @@ -678,7 +714,8 @@ def test_partial_update_metadata_field(self): self.assertEqual(response.status_code, 200) self.assertEqual(profile.metadata, {"b": "caah"}) - def test_put_update(self): + @patch("onadata.apps.api.viewsets.user_profile_viewset.send_message") + def test_put_update(self, mock_send_message): data = _profile_data() # create profile request = self.factory.post( @@ -715,6 +752,16 @@ def test_put_update(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.data["city"], data["city"]) + # Save on user update (Put) event + self.assertTrue(mock_send_message.called) + mock_send_message.assert_called_with( + instance_id=request.user.username, + target_id=request.user.id, + target_type=USER, + user=request.user, + message_verb=USER_UPDATED + ) + def test_profile_create_mixed_case(self): request = self.factory.get("/", **self.extra) response = self.view(request) @@ -757,7 +804,8 @@ def test_profile_create_mixed_case(self): {"NAME": "onadata.libs.utils.validators.PreviousPasswordValidator"} ] ) - def test_change_password(self): + @patch("onadata.apps.api.viewsets.user_profile_viewset.send_message") + def test_change_password(self, mock_send_message): view = UserProfileViewSet.as_view({"post": "change_password"}) current_password = "bobbob" new_password = "bobbob1" @@ -822,6 +870,16 @@ def test_change_password(self): self.assertNotEqual(response.data["access_token"], old_token) new_token = response.data["access_token"] + # Save password change event + self.assertTrue(mock_send_message.called) + mock_send_message.assert_called_with( + instance_id=request.user.username, + target_id=request.user.id, + target_type=USER, + user=request.user, + message_verb=USER_PASSWORD_CHANGED + ) + # Assert requests made with the old tokens are rejected post_data = {"current_password": new_password, "new_password": "random"} request = self.factory.post("/", data=post_data, **self.extra) diff --git a/onadata/apps/api/viewsets/user_profile_viewset.py b/onadata/apps/api/viewsets/user_profile_viewset.py index 80aeae83b8..08b7729fb0 100644 --- a/onadata/apps/api/viewsets/user_profile_viewset.py +++ b/onadata/apps/api/viewsets/user_profile_viewset.py @@ -7,6 +7,7 @@ import json from django.conf import settings +from django.contrib.auth import get_user_model from django.contrib.auth.password_validation import validate_password from django.core.cache import cache from django.core.validators import ValidationError @@ -32,6 +33,10 @@ from onadata.apps.api.tools import get_baseviewset_class from onadata.apps.logger.models.instance import Instance from onadata.apps.main.models import UserProfile +from onadata.apps.messaging.constants import ( + USER, USER_UPDATED, USER_PASSWORD_CHANGED +) +from onadata.apps.messaging.serializers import send_message from onadata.libs import filters from onadata.libs.mixins.authenticate_header_mixin import AuthenticateHeaderMixin from onadata.libs.mixins.cache_control_mixin import CacheControlMixin @@ -50,6 +55,9 @@ from onadata.libs.utils.email import get_verification_email_data, get_verification_url from onadata.libs.utils.user_auth import invalidate_and_regen_tokens +# pylint: disable=invalid-name +User = get_user_model() + BaseViewset = get_baseviewset_class() # pylint: disable=invalid-name LOCKOUT_TIME = getattr(settings, "LOCKOUT_TIME", 1800) MAX_CHANGE_PASSWORD_ATTEMPTS = getattr(settings, "MAX_CHANGE_PASSWORD_ATTEMPTS", 10) @@ -211,6 +219,16 @@ def update(self, request, *args, **kwargs): username = kwargs.get("user") response = super().update(request, *args, **kwargs) cache.set(f"{USER_PROFILE_PREFIX}{username}", response.data) + + # send notification on updating user profile + send_message( + instance_id=request.user.username, + target_id=request.user.id, + target_type=USER, + user=request.user, + message_verb=USER_UPDATED, + ) + return response def retrieve(self, request, *args, **kwargs): @@ -283,6 +301,15 @@ def change_password(self, request, *args, **kwargs): # noqa user_profile.save() data.update(invalidate_and_regen_tokens(user=user_profile.user)) + # send notification account password change + send_message( + instance_id=request.user.username, + target_id=request.user.id, + target_type=USER, + user=request.user, + message_verb=USER_PASSWORD_CHANGED, + ) + return Response(status=status.HTTP_200_OK, data=data) def partial_update(self, request, *args, **kwargs): @@ -303,6 +330,16 @@ def partial_update(self, request, *args, **kwargs): profile.metadata = metadata profile.save() + + # send notification on updating user profile + send_message( + instance_id=request.user.username, + target_id=request.user.id, + target_type=USER, + user=request.user, + message_verb=USER_UPDATED, + ) + return Response(data=profile.metadata, status=status.HTTP_200_OK) return super().partial_update(request, *args, **kwargs) diff --git a/onadata/apps/messaging/constants.py b/onadata/apps/messaging/constants.py index dfe7599ddf..7a82873efe 100644 --- a/onadata/apps/messaging/constants.py +++ b/onadata/apps/messaging/constants.py @@ -26,13 +26,20 @@ SUBMISSION_DELETED = "submission_deleted" SUBMISSION_REVIEWED = "submission_reviewed" FORM_UPDATED = "form_updated" +USER_CREATED = "user_created" +USER_UPDATED = "user_updated" +USER_PASSWORD_CHANGED = "password_changed" # nosec # noqa MESSAGE_VERBS = [ MESSAGE, SUBMISSION_REVIEWED, SUBMISSION_CREATED, SUBMISSION_EDITED, - SUBMISSION_DELETED, FORM_UPDATED] + SUBMISSION_DELETED, FORM_UPDATED, USER_CREATED, USER_UPDATED, + USER_PASSWORD_CHANGED] VERB_TOPIC_DICT = { SUBMISSION_CREATED: "submission/created", SUBMISSION_EDITED: "submission/edited", SUBMISSION_DELETED: "submission/deleted", SUBMISSION_REVIEWED: "submission/reviewed", - FORM_UPDATED: "form/updated" + FORM_UPDATED: "form/updated", + USER_CREATED: "user/created", + USER_UPDATED: "user/updated", + USER_PASSWORD_CHANGED: "user/password_changed" } diff --git a/onadata/libs/serializers/user_profile_serializer.py b/onadata/libs/serializers/user_profile_serializer.py index 713071d8ce..aefc33761a 100644 --- a/onadata/libs/serializers/user_profile_serializer.py +++ b/onadata/libs/serializers/user_profile_serializer.py @@ -28,6 +28,8 @@ ) from onadata.apps.main.forms import RegistrationFormUserProfile from onadata.apps.main.models import UserProfile +from onadata.apps.messaging.constants import USER, USER_CREATED +from onadata.apps.messaging.serializers import send_message from onadata.libs.authentication import expired from onadata.libs.permissions import CAN_VIEW_PROFILE, is_organization from onadata.libs.serializers.fields.json_field import JsonField @@ -319,6 +321,15 @@ def create(self, validated_data): ) profile.save() + # send notification on creating new user account + send_message( + instance_id=new_user.username, + target_id=new_user.id, + target_type=USER, + user=new_user, + message_verb=USER_CREATED, + ) + if getattr(settings, "ENABLE_EMAIL_VERIFICATION", False): redirect_url = params.get("redirect_url") _send_verification_email(redirect_url, new_user, request)