From 3e903b316cfd95d7c24f5a98b43e15f1301950b7 Mon Sep 17 00:00:00 2001
From: Paul Marillonnet <pmarillonnet@entrouvert.com>
Date: Thu, 30 Aug 2018 14:51:56 +0200
Subject: [PATCH] api: include roles in users API (#25645)

---
 src/authentic2/api_views.py | 122 +++++++++++++++++++++---------------
 tests/test_api.py           |  60 ++++++++++++++++++
 2 files changed, 132 insertions(+), 50 deletions(-)

diff --git a/src/authentic2/api_views.py b/src/authentic2/api_views.py
index 6470cd77..d81846f5 100644
--- a/src/authentic2/api_views.py
+++ b/src/authentic2/api_views.py
@@ -321,6 +321,77 @@ def user(request):
     return request.user.to_json()
 
 
+def attributes_hash(attributes):
+    attributes = sorted(attributes, key=lambda at: at.name)
+    return hash(tuple((at.name, at.required) for at in attributes))
+
+
+class RoleSerializer(serializers.ModelSerializer):
+    ou = serializers.SlugRelatedField(
+        many=False,
+        required=False,
+        default=CreateOnlyDefault(get_default_ou),
+        queryset=get_ou_model().objects.all(),
+        slug_field='slug')
+
+    @property
+    def user(self):
+        return self.context['request'].user
+
+    def __init__(self, instance=None, **kwargs):
+        super(RoleSerializer, self).__init__(instance, **kwargs)
+        if self.instance:
+            self.fields['ou'].read_only = True
+
+    def create(self, validated_data):
+        ou = validated_data.get('ou')
+        # Creating roles also means being allowed to within the OU:
+        if not self.user.has_ou_perm('a2_rbac.add_role', ou):
+            raise PermissionDenied(u'User %s can\'t create role in OU %s' % (self.user, ou))
+        return super(RoleSerializer, self).create(validated_data)
+
+    def update(self, instance, validated_data):
+        # Check role-updating permissions:
+        if not self.user.has_perm('a2_rbac.change_role', obj=instance):
+            raise PermissionDenied(u'User %s can\'t change role %s' % (self.user, instance))
+        super(RoleSerializer, self).update(instance, validated_data)
+        return instance
+
+    def partial_update(self, instance, validated_data):
+        # Check role-updating permissions:
+        if not self.user.has_perm('a2_rbac.change_role', obj=instance):
+            raise PermissionDenied(u'User %s can\'t change role %s' % (self.user, instance))
+        super(RoleSerializer, self).partial_update(instance, validated_data)
+        return instance
+
+    class Meta:
+        model = get_role_model()
+        fields = ('uuid', 'name', 'slug', 'ou',)
+        extra_kwargs = {'uuid': {'read_only': True}}
+
+
+class BaseOrganizationalUnitSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = get_ou_model()
+        fields = '__all__'
+
+
+class OrganizationalUnitConciseSerializer(BaseOrganizationalUnitSerializer):
+    class Meta(BaseOrganizationalUnitSerializer.Meta):
+        fields = ('uuid', 'slug', 'name',)
+
+
+class RoleCustomField(RoleSerializer):
+    ou = OrganizationalUnitConciseSerializer(
+        many=False,
+        required=False,
+        read_only=True)
+
+    class Meta(RoleSerializer.Meta):
+        fields = ('description', 'external_id', 'name', 'ou', 'service', 'slug',
+                'uuid',)
+
+
 class BaseUserSerializer(serializers.ModelSerializer):
     ou = serializers.SlugRelatedField(
         queryset=get_ou_model().objects.all(),
@@ -335,6 +406,7 @@ class BaseUserSerializer(serializers.ModelSerializer):
                                      default=CreateOnlyDefault(utils.generate_password),
                                      required=False)
     force_password_reset = serializers.BooleanField(write_only=True, required=False, default=False)
+    roles = RoleCustomField(many=True, read_only=True)
 
     def __init__(self, *args, **kwargs):
         super(BaseUserSerializer, self).__init__(*args, **kwargs)
@@ -490,50 +562,6 @@ class BaseUserSerializer(serializers.ModelSerializer):
         exclude = ('date_joined', 'user_permissions', 'groups', 'last_login')
 
 
-class RoleSerializer(serializers.ModelSerializer):
-    ou = serializers.SlugRelatedField(
-        many=False,
-        required=False,
-        default=CreateOnlyDefault(get_default_ou),
-        queryset=get_ou_model().objects.all(),
-        slug_field='slug')
-
-    @property
-    def user(self):
-        return self.context['request'].user
-
-    def __init__(self, instance=None, **kwargs):
-        super(RoleSerializer, self).__init__(instance, **kwargs)
-        if self.instance:
-            self.fields['ou'].read_only = True
-
-    def create(self, validated_data):
-        ou = validated_data.get('ou')
-        # Creating roles also means being allowed to within the OU:
-        if not self.user.has_ou_perm('a2_rbac.add_role', ou):
-            raise PermissionDenied(u'User %s can\'t create role in OU %s' % (self.user, ou))
-        return super(RoleSerializer, self).create(validated_data)
-
-    def update(self, instance, validated_data):
-        # Check role-updating permissions:
-        if not self.user.has_perm('a2_rbac.change_role', obj=instance):
-            raise PermissionDenied(u'User %s can\'t change role %s' % (self.user, instance))
-        super(RoleSerializer, self).update(instance, validated_data)
-        return instance
-
-    def partial_update(self, instance, validated_data):
-        # Check role-updating permissions:
-        if not self.user.has_perm('a2_rbac.change_role', obj=instance):
-            raise PermissionDenied(u'User %s can\'t change role %s' % (self.user, instance))
-        super(RoleSerializer, self).partial_update(instance, validated_data)
-        return instance
-
-    class Meta:
-        model = get_role_model()
-        fields = ('uuid', 'name', 'slug', 'ou',)
-        extra_kwargs = {'uuid': {'read_only': True}}
-
-
 class UsersFilter(FilterSet):
     class Meta:
         model = get_user_model()
@@ -732,12 +760,6 @@ class RoleMembershipsAPI(ExceptionHandlerMixin, APIView):
 role_memberships = RoleMembershipsAPI.as_view()
 
 
-class BaseOrganizationalUnitSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = get_ou_model()
-        fields = '__all__'
-
-
 class OrganizationalUnitAPI(ExceptionHandlerMixin, ModelViewSet):
     permission_classes = (DjangoPermission('a2_rbac.search_organizationalunit'),)
     serializer_class = BaseOrganizationalUnitSerializer
diff --git a/tests/test_api.py b/tests/test_api.py
index 7eae768d..9a6ca07f 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1107,3 +1107,63 @@ def test_validate_password_regex(app, settings):
     assert response.json['checks'][3]['result'] is True
     assert response.json['checks'][4]['label'] == 'must contain "ok"'
     assert response.json['checks'][4]['result'] is True
+
+
+def test_roles_in_users_api(app, admin, ou1, ou2):
+
+    User = get_user_model()
+    user1 = User(username='john.doe', email='john.doe@example.com')
+    user1.set_password('password')
+    user1.save()
+    user2 = User(username='bob.smith', email='bob.smith@example.com')
+    user2.set_password('password')
+    user2.save()
+
+    Role = get_role_model()
+    role1 = Role.objects.create(name='Role1')
+    role1.ou = ou1
+    role1.members.add(user1)
+    role1.save()
+    role2 = Role.objects.create(name='Role2')
+    role2.ou = ou2
+    role2.members.add(user1)
+    role2.members.add(user2)
+    role2.save()
+    role3 = Role.objects.create(name='Role3')
+    role3.ou = get_default_ou()
+    role3.members.add(user2)
+    role3.save()
+
+    app.authorization = ('Basic', (admin.username, admin.username))
+    response = app.get(u'/api/users/', status=200)
+    assert len(response.json['results']) == 3
+    for user in response.json['results']:
+        assert user['roles']
+        for role in user['roles']:
+            assert set(['slug', 'name', 'uuid', 'service', 'description',
+                    'external_id', 'ou']) == set(role.keys())
+            role_object = Role.objects.get(uuid=role['uuid'])
+            if getattr(role_object, 'ou', None) is not None:
+                for ou_attr in ('uuid', 'slug', 'name'):
+                    assert getattr(role_object.ou, ou_attr) == role['ou'][ou_attr]
+
+    url = u'/api/users/%s/' % admin.uuid
+    response = app.get(url, status=200)
+    assert len(response.json['roles']) == 1
+    assert response.json['roles'][0]['slug'] == '_a2-manager'
+
+    url = u'/api/users/%s/' % user1.uuid
+    response = app.get(url, status=200)
+    assert len(response.json['roles']) == 2
+    user_roles = ['Role1', 'Role2']
+    for role in response.json['roles']:
+        assert role['name'] in user_roles
+        user_roles.remove(role['name'])
+
+    url = u'/api/users/%s/' % user2.uuid
+    response = app.get(url, status=200)
+    assert len(response.json['roles']) == 2
+    user_roles = ['Role2', 'Role3']
+    for role in response.json['roles']:
+        assert role['name'] in user_roles
+        user_roles.remove(role['name'])
-- 
2.20.1

