diff --git a/CHANGELOG.md b/CHANGELOG.md index 432303eda..bb65c5338 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to ### Changed - ⚡️(backend) hash application secrets with SHA-256 +- ⚡️(backend) reduce domain queries on the application token endpoint ### Fixed diff --git a/src/backend/core/models.py b/src/backend/core/models.py index 8d4d13a3e..211a248b3 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -903,11 +903,12 @@ class Application(BaseModel): def can_delegate_email(self, email): """Check if this application can delegate the given email.""" - if not self.allowed_domains.exists(): + allowed_domains = {d.domain for d in self.allowed_domains.all()} + if not allowed_domains: return True # No domain restrictions domain = get_domain_from_email(email) - return self.allowed_domains.filter(domain__iexact=domain).exists() + return bool(domain) and domain.lower() in allowed_domains class ApplicationDomain(BaseModel): diff --git a/src/backend/core/tests/test_external_api_token.py b/src/backend/core/tests/test_external_api_token.py index a4e151227..37036f797 100644 --- a/src/backend/core/tests/test_external_api_token.py +++ b/src/backend/core/tests/test_external_api_token.py @@ -87,6 +87,60 @@ def test_api_applications_generate_token_success(settings): } +@pytest.mark.parametrize( + "restricted", [True, False], ids=["restricted", "unrestricted"] +) +def test_api_applications_generate_token_success_query_count( + restricted, django_assert_num_queries +): + """An existing user needs one query each for application, domains, and user.""" + user = UserFactory(email="user@example.com") + plain_secret = "test-secret-123" + application = ApplicationFactory(client_secret=plain_secret) + if restricted: + ApplicationDomainFactory(application=application, domain="example.com") + + client = APIClient() + with django_assert_num_queries(3): + response = client.post( + "/external-api/v1.0/application/token/", + { + "client_id": application.client_id, + "client_secret": plain_secret, + "grant_type": "client_credentials", + "scope": user.email, + }, + format="json", + ) + + assert response.status_code == 200 + assert "access_token" in response.data + + +def test_api_applications_generate_token_invalid_credentials_query_count( + django_assert_num_queries, +): + """An invalid secret must be rejected before querying domains or users.""" + application = ApplicationFactory(client_secret="test-secret-123") + ApplicationDomainFactory(application=application, domain="example.com") + + client = APIClient() + with django_assert_num_queries(1): + response = client.post( + "/external-api/v1.0/application/token/", + { + "client_id": application.client_id, + "client_secret": "wrong-secret", + "grant_type": "client_credentials", + "scope": "user@example.com", + }, + format="json", + ) + + assert response.status_code == 401 + assert "Invalid credentials" in str(response.data) + + def test_api_applications_generate_token_form_urlencoded(settings): """The token endpoint should accept "application/x-www-form-urlencoded" requests, as mandated by RFC 6749 (sections 3.2 and 4.4.2) for OAuth 2.0