🔒️(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.
This commit is contained in:
@@ -21,6 +21,8 @@ and this project adheres to
|
|||||||
- 🐛(back) allow only images to be used with the cors-proxy #781
|
- 🐛(back) allow only images to be used with the cors-proxy #781
|
||||||
- 🐛(backend) stop returning inactive users on the list endpoint #636
|
- 🐛(backend) stop returning inactive users on the list endpoint #636
|
||||||
- 🔒️(backend) require at least 5 characters to search for users #636
|
- 🔒️(backend) require at least 5 characters to search for users #636
|
||||||
|
- 🔒️(back) throttle user list endpoint #636
|
||||||
|
- 🔒️(back) remove pagination and limit to 5 for user list endpoint #636
|
||||||
|
|
||||||
|
|
||||||
## [2.5.0] - 2025-03-18
|
## [2.5.0] - 2025-03-18
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ class UserViewSet(
|
|||||||
permission_classes = [permissions.IsSelf]
|
permission_classes = [permissions.IsSelf]
|
||||||
queryset = models.User.objects.filter(is_active=True)
|
queryset = models.User.objects.filter(is_active=True)
|
||||||
serializer_class = serializers.UserSerializer
|
serializer_class = serializers.UserSerializer
|
||||||
|
pagination_class = None
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""
|
"""
|
||||||
@@ -157,10 +158,10 @@ class UserViewSet(
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
# Exclude all users already in the given document
|
# Exclude all users already in the given document
|
||||||
if document_id := self.request.GET.get("document_id", ""):
|
if document_id := self.request.query_params.get("document_id", ""):
|
||||||
queryset = queryset.exclude(documentaccess__document_id=document_id)
|
queryset = queryset.exclude(documentaccess__document_id=document_id)
|
||||||
|
|
||||||
if not (query := self.request.GET.get("q", "")) or len(query) < 5:
|
if not (query := self.request.query_params.get("q", "")) or len(query) < 5:
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
# For emails, match emails by Levenstein distance to prevent typing errors
|
# For emails, match emails by Levenstein distance to prevent typing errors
|
||||||
@@ -170,7 +171,7 @@ class UserViewSet(
|
|||||||
distance=RawSQL("levenshtein(email::text, %s::text)", (query,))
|
distance=RawSQL("levenshtein(email::text, %s::text)", (query,))
|
||||||
)
|
)
|
||||||
.filter(distance__lte=3)
|
.filter(distance__lte=3)
|
||||||
.order_by("distance", "email")
|
.order_by("distance", "email")[: settings.API_USERS_LIST_LIMIT]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use trigram similarity for non-email-like queries
|
# Use trigram similarity for non-email-like queries
|
||||||
@@ -180,7 +181,7 @@ class UserViewSet(
|
|||||||
queryset.filter(email__trigram_word_similar=query)
|
queryset.filter(email__trigram_word_similar=query)
|
||||||
.annotate(similarity=TrigramSimilarity("email", query))
|
.annotate(similarity=TrigramSimilarity("email", query))
|
||||||
.filter(similarity__gt=0.2)
|
.filter(similarity__gt=0.2)
|
||||||
.order_by("-similarity", "email")
|
.order_by("-similarity", "email")[: settings.API_USERS_LIST_LIMIT]
|
||||||
)
|
)
|
||||||
|
|
||||||
@drf.decorators.action(
|
@drf.decorators.action(
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def test_api_users_list_authenticated():
|
|||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
content = response.json()
|
content = response.json()
|
||||||
assert content["results"] == []
|
assert content == []
|
||||||
|
|
||||||
|
|
||||||
def test_api_users_list_query_email():
|
def test_api_users_list_query_email():
|
||||||
@@ -58,24 +58,54 @@ def test_api_users_list_query_email():
|
|||||||
"/api/v1.0/users/?q=david.bowman@work.com",
|
"/api/v1.0/users/?q=david.bowman@work.com",
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
user_ids = [user["id"] for user in response.json()["results"]]
|
user_ids = [user["id"] for user in response.json()]
|
||||||
assert user_ids == [str(dave.id)]
|
assert user_ids == [str(dave.id)]
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
"/api/v1.0/users/?q=davig.bovman@worm.com",
|
"/api/v1.0/users/?q=davig.bovman@worm.com",
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
user_ids = [user["id"] for user in response.json()["results"]]
|
user_ids = [user["id"] for user in response.json()]
|
||||||
assert user_ids == [str(dave.id)]
|
assert user_ids == [str(dave.id)]
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
"/api/v1.0/users/?q=davig.bovman@worm.cop",
|
"/api/v1.0/users/?q=davig.bovman@worm.cop",
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
user_ids = [user["id"] for user in response.json()["results"]]
|
user_ids = [user["id"] for user in response.json()]
|
||||||
assert user_ids == []
|
assert user_ids == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_users_list_limit(settings):
|
||||||
|
"""
|
||||||
|
Authenticated users should be able to list users and the number of results
|
||||||
|
should be limited to 10.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
# Use a base name with a length equal 5 to test that the limit is applied
|
||||||
|
base_name = "alice"
|
||||||
|
for i in range(15):
|
||||||
|
factories.UserFactory(email=f"{base_name}.{i}@example.com")
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1.0/users/?q=alice",
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 5
|
||||||
|
|
||||||
|
# if the limit is changed, all users should be returned
|
||||||
|
settings.API_USERS_LIST_LIMIT = 100
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1.0/users/?q=alice",
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()) == 15
|
||||||
|
|
||||||
|
|
||||||
def test_api_users_list_query_email_matching():
|
def test_api_users_list_query_email_matching():
|
||||||
"""While filtering by email, results should be filtered and sorted by Levenstein distance."""
|
"""While filtering by email, results should be filtered and sorted by Levenstein distance."""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
@@ -94,13 +124,13 @@ def test_api_users_list_query_email_matching():
|
|||||||
"/api/v1.0/users/?q=alice.johnson@example.gouv.fr",
|
"/api/v1.0/users/?q=alice.johnson@example.gouv.fr",
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
user_ids = [user["id"] for user in response.json()["results"]]
|
user_ids = [user["id"] for user in response.json()]
|
||||||
assert user_ids == [str(user1.id), str(user2.id), str(user3.id), str(user4.id)]
|
assert user_ids == [str(user1.id), str(user2.id), str(user3.id), str(user4.id)]
|
||||||
|
|
||||||
response = client.get("/api/v1.0/users/?q=alicia.johnnson@example.gouv.fr")
|
response = client.get("/api/v1.0/users/?q=alicia.johnnson@example.gouv.fr")
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
user_ids = [user["id"] for user in response.json()["results"]]
|
user_ids = [user["id"] for user in response.json()]
|
||||||
assert user_ids == [str(user4.id), str(user2.id), str(user1.id), str(user5.id)]
|
assert user_ids == [str(user4.id), str(user2.id), str(user1.id), str(user5.id)]
|
||||||
|
|
||||||
|
|
||||||
@@ -126,7 +156,7 @@ def test_api_users_list_query_email_exclude_doc_user():
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
user_ids = [user["id"] for user in response.json()["results"]]
|
user_ids = [user["id"] for user in response.json()]
|
||||||
assert user_ids == [str(nicole_fool.id)]
|
assert user_ids == [str(nicole_fool.id)]
|
||||||
|
|
||||||
|
|
||||||
@@ -143,15 +173,15 @@ def test_api_users_list_query_short_queries():
|
|||||||
|
|
||||||
response = client.get("/api/v1.0/users/?q=jo")
|
response = client.get("/api/v1.0/users/?q=jo")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json()["results"] == []
|
assert response.json() == []
|
||||||
|
|
||||||
response = client.get("/api/v1.0/users/?q=john")
|
response = client.get("/api/v1.0/users/?q=john")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json()["results"] == []
|
assert response.json() == []
|
||||||
|
|
||||||
response = client.get("/api/v1.0/users/?q=john.")
|
response = client.get("/api/v1.0/users/?q=john.")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert len(response.json()["results"]) == 2
|
assert len(response.json()) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_api_users_list_query_inactive():
|
def test_api_users_list_query_inactive():
|
||||||
@@ -166,7 +196,7 @@ def test_api_users_list_query_inactive():
|
|||||||
response = client.get("/api/v1.0/users/?q=john.")
|
response = client.get("/api/v1.0/users/?q=john.")
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
user_ids = [user["id"] for user in response.json()["results"]]
|
user_ids = [user["id"] for user in response.json()]
|
||||||
assert user_ids == [str(lennon.id)]
|
assert user_ids == [str(lennon.id)]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -604,6 +604,12 @@ class Base(Configuration):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
API_USERS_LIST_LIMIT = values.PositiveIntegerValue(
|
||||||
|
default=5,
|
||||||
|
environ_name="API_USERS_LIST_LIMIT",
|
||||||
|
environ_prefix=None,
|
||||||
|
)
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
@property
|
@property
|
||||||
def ENVIRONMENT(self):
|
def ENVIRONMENT(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user