From ebf48ecefee5a6ba4223f46cdf0e76e3d0136d0b Mon Sep 17 00:00:00 2001
From: Paul Marillonnet <pmarillonnet@entrouvert.com>
Date: Sat, 26 Feb 2022 08:44:04 +0100
Subject: [PATCH 2/2] api: add identifier management to user profile endpoint
 (#58556)

---
 src/authentic2/api_urls.py  |  2 +-
 src/authentic2/api_views.py | 63 +++++++++++++++++++-------
 tests/test_api.py           | 90 ++++++++++++++++++++++++++++---------
 3 files changed, 117 insertions(+), 38 deletions(-)

diff --git a/src/authentic2/api_urls.py b/src/authentic2/api_urls.py
index dc0cf70ba..cbc25950d 100644
--- a/src/authentic2/api_urls.py
+++ b/src/authentic2/api_urls.py
@@ -33,7 +33,7 @@ urlpatterns = [
         name='a2-api-role-member',
     ),
     url(
-        r'^users/(?P<user_uuid>[\w+]*)/profiles/(?P<profile_type_slug>[^/]+)/default/$',
+        r'^users/(?P<user_uuid>[\w+]*)/profiles/(?P<profile_type_slug>[^/]+)/$',
         api_views.user_profiles,
         name='a2-api-user-profiles',
     ),
diff --git a/src/authentic2/api_views.py b/src/authentic2/api_views.py
index 84be8debb..2956fb0f8 100644
--- a/src/authentic2/api_views.py
+++ b/src/authentic2/api_views.py
@@ -965,7 +965,17 @@ class ProfileSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = Profile
-        fields = ('data',)
+        fields = (
+            'identifier',
+            'data',
+        )
+        extra_kwargs = {
+            'identifier': {
+                'required': False,
+                'allow_blank': True,
+                'max_length': 256,
+            }
+        }
 
 
 class UserProfilesAPI(ExceptionHandlerMixin, APIView):
@@ -977,23 +987,39 @@ class UserProfilesAPI(ExceptionHandlerMixin, APIView):
         User = get_user_model()
         self.profile_type = get_object_or_404(ProfileType, slug=kwargs['profile_type_slug'])
         self.user = get_object_or_404(User, uuid=kwargs['user_uuid'])
+        self.identifier = request.GET.get('identifier', '')
 
     def get(self, request, *args, **kwargs):
-        profile = get_object_or_404(Profile, user=self.user, profile_type=self.profile_type)
-        if not request.user.has_perm('custom_user.view_profile', obj=profile):
+        if not request.user.has_perm('custom_user.view_profile'):
             raise PermissionDenied('User not allowed to view user profiles')
-        return Response(self.serializer_class(profile).data)
+        if 'identifier' in request.GET:
+            # request on a single entity
+            profile = get_object_or_404(
+                Profile, user=self.user, profile_type=self.profile_type, identifier=self.identifier
+            )
+            return Response(self.serializer_class(profile).data)
+        else:
+            # user's profiles of a given type
+            profiles = Profile.objects.filter(
+                user=self.user,
+                profile_type=self.profile_type,
+            )
+            return Response([self.serializer_class(profile).data for profile in profiles])
 
     def post(self, request, *args, **kwargs):
         if not request.user.has_perm('custom_user.create_profile'):
             raise PermissionDenied('User not allowed to create user profiles')
-        serializer = self.serializer_class(data=request.data)
-        if not serializer.is_valid():
-            response = {'data': [], 'err': 1, 'err_desc': serializer.errors}
-            return Response(response, status.HTTP_400_BAD_REQUEST)
         try:
-            profile = Profile.objects.get(user=self.user, profile_type=self.profile_type)
+            profile = Profile.objects.get(
+                user=self.user, profile_type=self.profile_type, identifier=self.identifier
+            )
         except Profile.DoesNotExist:
+            data = request.data.copy()
+            data.update({'identifier': self.identifier})
+            serializer = self.serializer_class(data=data)
+            if not serializer.is_valid():
+                response = {'data': [], 'err': 1, 'err_desc': serializer.errors}
+                return Response(response, status.HTTP_400_BAD_REQUEST)
             profile = serializer.save()
             profile.profile_type = self.profile_type
             profile.user = self.user
@@ -1006,13 +1032,18 @@ class UserProfilesAPI(ExceptionHandlerMixin, APIView):
             return Response(
                 {'result': 1, 'detail': _('Profile successfully assigned to user')}, status=status.HTTP_200_OK
             )
+        else:
+            response = {
+                'data': [],
+                'err': 1,
+                'err_desc': 'cannot overwrite already existing profile. use PUT verb instead',
+            }
+            return Response(response, status.HTTP_400_BAD_REQUEST)
 
     def patch(self, request, *args, **kwargs):
-        serializer = self.serializer_class(data=request.data)
-        if not serializer.is_valid():
-            response = {'data': [], 'err': 1, 'err_desc': serializer.errors}
-            return Response(response, status.HTTP_400_BAD_REQUEST)
-        profile = get_object_or_404(Profile, user=self.user, profile_type=self.profile_type)
+        profile = get_object_or_404(
+            Profile, user=self.user, profile_type=self.profile_type, identifier=self.identifier
+        )
         if not request.user.has_perm('custom_user.change_profile', obj=profile):
             raise PermissionDenied('User not allowed to change user profiles')
         profile.data = request.data.get('data', {})
@@ -1028,7 +1059,9 @@ class UserProfilesAPI(ExceptionHandlerMixin, APIView):
         return self.patch(request, *args, **kwargs)
 
     def delete(self, request, *args, **kwargs):
-        profile = get_object_or_404(Profile, user=self.user, profile_type=self.profile_type)
+        profile = get_object_or_404(
+            Profile, user=self.user, profile_type=self.profile_type, identifier=self.identifier
+        )
         if not request.user.has_perm('custom_user.delete_profile', obj=profile):
             raise PermissionDenied('User not allowed to delete user profiles')
         request.journal.record(
diff --git a/tests/test_api.py b/tests/test_api.py
index bb2caeb54..098f34fed 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -2626,16 +2626,34 @@ def test_user_profile_get(app, superuser):
     user = User.objects.create(username='john.doe', email='john.doe@example.com', ou=get_default_ou())
 
     # wrong type
-    resp = app.get('/api/users/%s/profiles/non-existant-slug/default/' % user.uuid, status=404)
+    app.get('/api/users/%s/profiles/non-existant-slug/' % user.uuid, status=404)
 
     profile_type = ProfileType.objects.create(slug='one-type', name='One Type')
 
     # wrong user
-    resp = app.get('/api/users/1234/profiles/one-type/default/', status=404)
+    app.get('/api/users/1234/profiles/one-type/', status=404)
 
-    Profile.objects.create(profile_type=profile_type, user=user, data={'foo': 'bar'})
-    resp = app.get('/api/users/%s/profiles/one-type/default/' % user.uuid)
-    assert resp.json == {'data': {'foo': 'bar'}}
+    profile = Profile.objects.create(profile_type=profile_type, user=user, data={'foo': 'bar'})
+    resp = app.get('/api/users/%s/profiles/one-type/' % user.uuid)
+    assert resp.json == [{'data': {'foo': 'bar'}, 'identifier': ''}]
+
+    resp = app.get('/api/users/%s/profiles/one-type/?identifier=' % user.uuid)
+    assert resp.json == {'data': {'foo': 'bar'}, 'identifier': ''}
+
+    profile.identifier = 'Company ABC'
+    profile.save()
+
+    # specify identifier in qs
+    resp = app.get(
+        '/api/users/%s/profiles/one-type/?identifier=Company ABC' % user.uuid,
+    )
+    assert resp.json == {'data': {'foo': 'bar'}, 'identifier': 'Company ABC'}
+
+    # not found on empty identifier
+    app.get(
+        '/api/users/%s/profiles/one-type/?identifier=' % user.uuid,
+        status=404,
+    )
 
 
 def test_user_profile_put(app, superuser):
@@ -2644,37 +2662,48 @@ def test_user_profile_put(app, superuser):
     user = User.objects.create(username='john.doe', email='john.doe@example.com', ou=get_default_ou())
 
     # wrong type
-    resp = app.put('/api/users/%s/profiles/non-existant-slug/default/' % user.uuid, status=404)
+    resp = app.put('/api/users/%s/profiles/non-existant-slug/' % user.uuid, status=404)
 
     profile_type = ProfileType.objects.create(slug='one-type', name='One Type')
 
     # wrong user
-    resp = app.put('/api/users/1234/profiles/one-type/default/', status=404)
+    resp = app.put('/api/users/1234/profiles/one-type/', status=404)
 
     # missing profile
     resp = app.put_json(
-        '/api/users/%s/profiles/one-type/default/' % user.uuid, params={'data': {'foo': 'bar'}}, status=404
+        '/api/users/%s/profiles/one-type/' % user.uuid, params={'data': {'foo': 'bar'}}, status=404
     )
 
     Profile.objects.create(user=user, profile_type=profile_type, data={})
 
-    resp = app.put_json(
-        '/api/users/%s/profiles/one-type/default/' % user.uuid, params={'data': {'foo': 'bar'}}
-    )
+    resp = app.put_json('/api/users/%s/profiles/one-type/' % user.uuid, params={'data': {'foo': 'bar'}})
     assert resp.json == {'result': 1, 'detail': 'Profile successfully updated'}
     assert len(user.subprofiles.all()) == 1
     assert user.subprofiles.last().data == {'foo': 'bar'}
 
     # attempt at overwriting profile data
-    resp = app.put_json(
-        '/api/users/%s/profiles/one-type/default/' % user.uuid, params={'data': {'baz': 'bob'}}
-    )
+    resp = app.put_json('/api/users/%s/profiles/one-type/' % user.uuid, params={'data': {'baz': 'bob'}})
 
     # profile has been updated, no extra profile has been created
     assert resp.json == {'result': 1, 'detail': 'Profile successfully updated'}
     assert len(user.subprofiles.all()) == 1
     assert user.subprofiles.last().data == {'baz': 'bob'}
 
+    Profile.objects.create(
+        user=user,
+        profile_type=profile_type,
+        identifier='Company DEF',
+        data={},
+    )
+
+    # overwrite profile data while specifying the identifier
+    resp = app.put_json(
+        '/api/users/%s/profiles/one-type/?identifier=Company DEF' % user.uuid,
+        params={'data': {'baz': 'bob'}},
+    )
+    assert resp.json == {'result': 1, 'detail': 'Profile successfully updated'}
+    assert user.subprofiles.get(identifier='Company DEF').data == {'baz': 'bob'}
+
 
 def test_user_profile_post(app, superuser):
     app.authorization = ('Basic', (superuser.username, superuser.username))
@@ -2682,22 +2711,32 @@ def test_user_profile_post(app, superuser):
     user = User.objects.create(username='john.doe', email='john.doe@example.com', ou=get_default_ou())
 
     # wrong type
-    resp = app.get('/api/users/%s/profiles/non-existant-slug/default/' % user.uuid, status=404)
+    resp = app.get('/api/users/%s/profiles/non-existant-slug/' % user.uuid, status=404)
 
     ProfileType.objects.create(slug='one-type', name='One Type')
 
     # wrong user
-    resp = app.get('/api/users/1234/profiles/one-type/default/', status=404)
+    resp = app.get('/api/users/1234/profiles/one-type/', status=404)
 
     assert len(user.subprofiles.all()) == 0
 
-    resp = app.post_json(
-        '/api/users/%s/profiles/one-type/default/' % user.uuid, params={'data': {'baz': 'bob'}}
-    )
+    resp = app.post_json('/api/users/%s/profiles/one-type/' % user.uuid, params={'data': {'baz': 'bob'}})
     assert resp.json == {'result': 1, 'detail': 'Profile successfully assigned to user'}
     assert len(user.subprofiles.all()) == 1
     assert user.subprofiles.last().data == {'baz': 'bob'}
 
+    app.post_json('/api/users/%s/profiles/one-type/' % user.uuid, status=400)
+
+    resp = app.post_json(
+        '/api/users/%s/profiles/one-type/?identifier=FooBar' % user.uuid,
+        params={'data': {'foo': 'bar'}},
+    )
+    assert resp.json == {'result': 1, 'detail': 'Profile successfully assigned to user'}
+    assert len(user.subprofiles.all()) == 2
+    assert user.subprofiles.get(identifier='FooBar').data == {'foo': 'bar'}
+
+    app.post_json('/api/users/%s/profiles/one-type/?identifier=FooBar' % user.uuid, status=400)
+
 
 def test_user_profile_delete(app, superuser):
 
@@ -2707,9 +2746,16 @@ def test_user_profile_delete(app, superuser):
     profile_type = ProfileType.objects.create(slug='one-type', name='One Type')
     Profile.objects.create(user=user, profile_type=profile_type)
 
-    app.delete('/api/users/%s/profiles/non-existant-slug/default/' % user.uuid, status=404)
-    app.delete('/api/users/%s/profiles/one-type/default/' % user.uuid)
+    app.delete('/api/users/%s/profiles/non-existant-slug/' % user.uuid, status=404)
+    app.delete('/api/users/%s/profiles/one-type/' % user.uuid)
 
     assert not Profile.objects.filter(user=user, profile_type=profile_type)
 
-    app.delete('/api/users/%s/profiles/one-type/default/' % user.uuid, status=404)
+    app.delete('/api/users/%s/profiles/one-type/' % user.uuid, status=404)
+
+    Profile.objects.create(user=user, profile_type=profile_type, identifier='FooBar')
+    app.delete('/api/users/%s/profiles/one-type/?identifier=FooBar' % user.uuid)
+    app.delete(
+        '/api/users/%s/profiles/one-type/?identifier=FooBar' % user.uuid,
+        status=404,
+    )
-- 
2.35.1

