From 720b4c429c6690b58bffa49d390282a9fc4e5e37 Mon Sep 17 00:00:00 2001
From: Paul Marillonnet <pmarillonnet@entrouvert.com>
Date: Thu, 2 Jun 2022 17:28:32 +0200
Subject: [PATCH] idp_oidc: restrict client's returned qs to authorized users
 (#65942)

---
 src/authentic2_idp_oidc/apps.py | 22 ++++++++++++++-----
 tests/idp_oidc/test_api.py      | 38 ++++++++++++++++++++++++++++++++-
 2 files changed, 54 insertions(+), 6 deletions(-)

diff --git a/src/authentic2_idp_oidc/apps.py b/src/authentic2_idp_oidc/apps.py
index b261353fc..97529146a 100644
--- a/src/authentic2_idp_oidc/apps.py
+++ b/src/authentic2_idp_oidc/apps.py
@@ -179,14 +179,26 @@ class AppConfig(django.apps.AppConfig):
 
     def a2_hook_api_modify_queryset(self, view, qs):
         from django.contrib.auth import get_user_model
+        from django.contrib.contenttypes.models import ContentType
+        from django.db.models import Q
+        from django.utils.timezone import now
+
+        from .models import OIDCAuthorization
 
         client = self.get_oidc_client(view)
+        User = get_user_model()
+
+        # reduce qs to users having previously granted an authorization to the client
+        if view.request.method == 'GET' and issubclass(qs.model, User):
+            authorized_users = OIDCAuthorization.objects.filter(
+                Q(expired__isnull=True) | Q(expired__gt=now()),
+                client_id=client.id,
+                client_ct=ContentType.objects.get_for_model(client),
+            ).values_list('user__id', flat=True)
+            qs = qs.filter(id__in=authorized_users)
+
         # fast path
-        if (
-            not issubclass(qs.model, get_user_model())
-            or client is None
-            or not client.authorized_roles.exists()
-        ):
+        if not issubclass(qs.model, User) or client is None or not client.authorized_roles.exists():
             return qs
 
         qs = qs.filter(roles__in=client.authorized_roles.children())
diff --git a/tests/idp_oidc/test_api.py b/tests/idp_oidc/test_api.py
index 8d984bbff..5a946b165 100644
--- a/tests/idp_oidc/test_api.py
+++ b/tests/idp_oidc/test_api.py
@@ -15,11 +15,13 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import random
+from datetime import timedelta
 
+from django.contrib.contenttypes.models import ContentType
 from django.utils.timezone import now
 
 from authentic2.custom_user.models import User
-from authentic2_idp_oidc.models import OIDCClaim, OIDCClient
+from authentic2_idp_oidc.models import OIDCAuthorization, OIDCClaim, OIDCClient
 from authentic2_idp_oidc.utils import make_sub
 
 
@@ -72,6 +74,14 @@ def test_api_users_list_claim_resolution(app, oidc_client):
     )
 
     users = [User.objects.create(username=f'user-{i}', last_name=f'Name-{i}') for i in range(10)]
+    expired = now() + timedelta(hours=1)
+    for user in users:
+        OIDCAuthorization.objects.create(
+            client_id=oidc_client.id,
+            client_ct=ContentType.objects.get_for_model(OIDCClient),
+            user=user,
+            expired=expired,
+        )
     pre_modification = now().strftime('%Y-%m-%dT%H:%M:%S')
     for count, user in enumerate(users):
         user.first_name = f'User {count}'
@@ -103,3 +113,29 @@ def test_api_users_list_claim_resolution(app, oidc_client):
         assert user_dict['email']
         assert user_dict['email'].startswith(user_dict['last_name'])
         assert user_dict['email'].endswith('@templated.nowhere.null')
+
+
+def test_api_users_list_queryset_reduction(app, oidc_client):
+    oidc_client.has_api_access = True
+    oidc_client.identifier_policy = OIDCClient.POLICY_PAIRWISE_REVERSIBLE
+    oidc_client.save()
+
+    pre_modification = now().strftime('%Y-%m-%dT%H:%M:%S')
+
+    users = [User.objects.create(username=f'user-{i}', last_name=f'Name-{i}') for i in range(10)]
+    expired = now() + timedelta(hours=1)
+    for user in random.sample(users, k=5):
+        OIDCAuthorization.objects.create(
+            client_id=oidc_client.id,
+            client_ct=ContentType.objects.get_for_model(OIDCClient),
+            user=user,
+            expired=expired,
+        )
+
+    app.authorization = ('Basic', (oidc_client.client_id, oidc_client.client_secret))
+    response = app.get(
+        f'/api/users/?modified__gt={pre_modification}&claim_resolution',
+        status=200,
+    )
+
+    assert len(response.json['results']) == 5
-- 
2.36.1

