Skip to content

Commit 34dfb3f

Browse files
committed
🔒️(back) remove pagination and limit to 5 for user list endpoint
The user list endpoint does not use anymore a pagination, the results is directly return in a list and the max results returned is limited to 5. In order to modify this limit the settings API_USERS_LIST_LIMIT is used.
1 parent f9a91ed commit 34dfb3f

File tree

4 files changed

+54
-15
lines changed

4 files changed

+54
-15
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ and this project adheres to
2121
- 🐛(back) allow only images to be used with the cors-proxy #781
2222
- 🐛(backend) stop returning inactive users on the list endpoint #636
2323
- 🔒️(backend) require at least 5 characters to search for users #636
24+
- 🔒️(back) throttle user list endpoint #636
25+
- 🔒️(back) remove pagination and limit to 5 for user list endpoint #636
2426

2527

2628
## [2.5.0] - 2025-03-18

src/backend/core/api/viewsets.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ class UserViewSet(
143143
permission_classes = [permissions.IsSelf]
144144
queryset = models.User.objects.filter(is_active=True)
145145
serializer_class = serializers.UserSerializer
146+
pagination_class = None
146147

147148
def get_queryset(self):
148149
"""
@@ -157,10 +158,10 @@ def get_queryset(self):
157158
return queryset
158159

159160
# Exclude all users already in the given document
160-
if document_id := self.request.GET.get("document_id", ""):
161+
if document_id := self.request.query_params.get("document_id", ""):
161162
queryset = queryset.exclude(documentaccess__document_id=document_id)
162163

163-
if not (query := self.request.GET.get("q", "")) or len(query) < 5:
164+
if not (query := self.request.query_params.get("q", "")) or len(query) < 5:
164165
return queryset.none()
165166

166167
# For emails, match emails by Levenstein distance to prevent typing errors
@@ -170,7 +171,7 @@ def get_queryset(self):
170171
distance=RawSQL("levenshtein(email::text, %s::text)", (query,))
171172
)
172173
.filter(distance__lte=3)
173-
.order_by("distance", "email")
174+
.order_by("distance", "email")[: settings.API_USERS_LIST_LIMIT]
174175
)
175176

176177
# Use trigram similarity for non-email-like queries
@@ -180,7 +181,7 @@ def get_queryset(self):
180181
queryset.filter(email__trigram_word_similar=query)
181182
.annotate(similarity=TrigramSimilarity("email", query))
182183
.filter(similarity__gt=0.2)
183-
.order_by("-similarity", "email")
184+
.order_by("-similarity", "email")[: settings.API_USERS_LIST_LIMIT]
184185
)
185186

186187
@drf.decorators.action(

src/backend/core/tests/test_api_users.py

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def test_api_users_list_authenticated():
3737
)
3838
assert response.status_code == 200
3939
content = response.json()
40-
assert content["results"] == []
40+
assert content == []
4141

4242

4343
def test_api_users_list_query_email():
@@ -58,24 +58,54 @@ def test_api_users_list_query_email():
5858
"/api/v1.0/users/[email protected]",
5959
)
6060
assert response.status_code == 200
61-
user_ids = [user["id"] for user in response.json()["results"]]
61+
user_ids = [user["id"] for user in response.json()]
6262
assert user_ids == [str(dave.id)]
6363

6464
response = client.get(
6565
"/api/v1.0/users/[email protected]",
6666
)
6767
assert response.status_code == 200
68-
user_ids = [user["id"] for user in response.json()["results"]]
68+
user_ids = [user["id"] for user in response.json()]
6969
assert user_ids == [str(dave.id)]
7070

7171
response = client.get(
7272
"/api/v1.0/users/[email protected]",
7373
)
7474
assert response.status_code == 200
75-
user_ids = [user["id"] for user in response.json()["results"]]
75+
user_ids = [user["id"] for user in response.json()]
7676
assert user_ids == []
7777

7878

79+
def test_api_users_list_limit(settings):
80+
"""
81+
Authenticated users should be able to list users and the number of results
82+
should be limited to 10.
83+
"""
84+
user = factories.UserFactory()
85+
86+
client = APIClient()
87+
client.force_login(user)
88+
89+
# Use a base name with a length equal 5 to test that the limit is applied
90+
base_name = "alice"
91+
for i in range(15):
92+
factories.UserFactory(email=f"{base_name}.{i}@example.com")
93+
94+
response = client.get(
95+
"/api/v1.0/users/?q=alice",
96+
)
97+
assert response.status_code == 200
98+
assert len(response.json()) == 5
99+
100+
# if the limit is changed, all users should be returned
101+
settings.API_USERS_LIST_LIMIT = 100
102+
response = client.get(
103+
"/api/v1.0/users/?q=alice",
104+
)
105+
assert response.status_code == 200
106+
assert len(response.json()) == 15
107+
108+
79109
def test_api_users_list_query_email_matching():
80110
"""While filtering by email, results should be filtered and sorted by Levenstein distance."""
81111
user = factories.UserFactory()
@@ -94,13 +124,13 @@ def test_api_users_list_query_email_matching():
94124
"/api/v1.0/users/[email protected]",
95125
)
96126
assert response.status_code == 200
97-
user_ids = [user["id"] for user in response.json()["results"]]
127+
user_ids = [user["id"] for user in response.json()]
98128
assert user_ids == [str(user1.id), str(user2.id), str(user3.id), str(user4.id)]
99129

100130
response = client.get("/api/v1.0/users/[email protected]")
101131

102132
assert response.status_code == 200
103-
user_ids = [user["id"] for user in response.json()["results"]]
133+
user_ids = [user["id"] for user in response.json()]
104134
assert user_ids == [str(user4.id), str(user2.id), str(user1.id), str(user5.id)]
105135

106136

@@ -126,7 +156,7 @@ def test_api_users_list_query_email_exclude_doc_user():
126156
)
127157

128158
assert response.status_code == 200
129-
user_ids = [user["id"] for user in response.json()["results"]]
159+
user_ids = [user["id"] for user in response.json()]
130160
assert user_ids == [str(nicole_fool.id)]
131161

132162

@@ -143,15 +173,15 @@ def test_api_users_list_query_short_queries():
143173

144174
response = client.get("/api/v1.0/users/?q=jo")
145175
assert response.status_code == 200
146-
assert response.json()["results"] == []
176+
assert response.json() == []
147177

148178
response = client.get("/api/v1.0/users/?q=john")
149179
assert response.status_code == 200
150-
assert response.json()["results"] == []
180+
assert response.json() == []
151181

152182
response = client.get("/api/v1.0/users/?q=john.")
153183
assert response.status_code == 200
154-
assert len(response.json()["results"]) == 2
184+
assert len(response.json()) == 2
155185

156186

157187
def test_api_users_list_query_inactive():
@@ -166,7 +196,7 @@ def test_api_users_list_query_inactive():
166196
response = client.get("/api/v1.0/users/?q=john.")
167197

168198
assert response.status_code == 200
169-
user_ids = [user["id"] for user in response.json()["results"]]
199+
user_ids = [user["id"] for user in response.json()]
170200
assert user_ids == [str(lennon.id)]
171201

172202

src/backend/impress/settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,12 @@ class Base(Configuration):
604604
},
605605
}
606606

607+
API_USERS_LIST_LIMIT = values.PositiveIntegerValue(
608+
default=5,
609+
environ_name="API_USERS_LIST_LIMIT",
610+
environ_prefix=None,
611+
)
612+
607613
# pylint: disable=invalid-name
608614
@property
609615
def ENVIRONMENT(self):

0 commit comments

Comments
 (0)