diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f810de6455bb5bcae59fa33aa2b39a71d44fb078..37ce94292fc02e9530fb1ea996d246a415cd6eee 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -26,6 +26,8 @@ Fixed * Due to a merge error, the once removed account menu in the sidenav appeared again. * Scheduled notifications were shown on dashboard before time. * Remove broken notifications menu item in favor of item next to account menu. +* [OAuth2] Resources which are protected with client credentials + allowed access if no scopes were allowed. * The site logo could overlap with the menu for logos with an unexpected aspect ratio. Changed diff --git a/aleksis/core/tests/regression/test_regression.py b/aleksis/core/tests/regression/test_regression.py index b9d33e6981312817324b92d4fe67176b96763cda..c2417a56ebd02c015e84f18401d39cfc582b4252 100644 --- a/aleksis/core/tests/regression/test_regression.py +++ b/aleksis/core/tests/regression/test_regression.py @@ -1,10 +1,16 @@ +import base64 + from django.contrib.auth import get_user_model import pytest -from aleksis.core.models import Group, Person +from aleksis.core.models import Group, OAuthApplication, Person pytestmark = pytest.mark.django_db +from django.http import HttpResponse +from django.test import override_settings +from django.urls import path, reverse +from django.views.generic import View def test_all_settigns_registered(): @@ -82,3 +88,61 @@ def test_reassign_user_to_person(): assert user2.groups.count() == 1 assert user1.groups.first().name == "Group 2" assert user2.groups.first().name == "Group 1" + + +@override_settings(ROOT_URLCONF="aleksis.core.tests.regression.view_oauth") +def test_no_access_oauth2_client_credentials_without_allowed_scopes(client): + """Tests that ClientProtectedResourceMixin doesn't allow access if no allowed scopes are set. + + https://edugit.org/AlekSIS/official/AlekSIS-Core/-/issues/688 + """ + + wrong_application = OAuthApplication.objects.create( + name="Test Application", + allowed_scopes=[], + authorization_grant_type=OAuthApplication.GRANT_CLIENT_CREDENTIALS, + client_type=OAuthApplication.CLIENT_CONFIDENTIAL, + redirect_uris=["http://localhost:8000/"], + ) + wrong_application_2 = OAuthApplication.objects.create( + name="Test Application", + allowed_scopes=["read"], + authorization_grant_type=OAuthApplication.GRANT_CLIENT_CREDENTIALS, + client_type=OAuthApplication.CLIENT_CONFIDENTIAL, + redirect_uris=["http://localhost:8000/"], + ) + correct_application = OAuthApplication.objects.create( + name="Test Application", + allowed_scopes=["write"], + authorization_grant_type=OAuthApplication.GRANT_CLIENT_CREDENTIALS, + client_type=OAuthApplication.CLIENT_CONFIDENTIAL, + redirect_uris=["http://localhost:8000/"], + ) + + url = reverse("client_protected_resource_mixin_test") + auth_header = ( + "Basic " + + base64.b64encode( + f"{wrong_application.client_id}:{wrong_application.client_secret}".encode() + ).decode() + ) + r = client.get(url, HTTP_AUTHORIZATION=auth_header) + assert r.status_code == 403 + + auth_header = ( + "Basic " + + base64.b64encode( + f"{wrong_application_2.client_id}:{wrong_application_2.client_secret}".encode() + ).decode() + ) + r = client.get(url, HTTP_AUTHORIZATION=auth_header) + assert r.status_code == 403 + + auth_header = ( + "Basic " + + base64.b64encode( + f"{correct_application.client_id}:{correct_application.client_secret}".encode() + ).decode() + ) + r = client.get(url, HTTP_AUTHORIZATION=auth_header) + assert r.status_code == 200 diff --git a/aleksis/core/tests/regression/view_oauth.py b/aleksis/core/tests/regression/view_oauth.py new file mode 100644 index 0000000000000000000000000000000000000000..d5671208e127e79c30d7bb179bbf2e82a35616fb --- /dev/null +++ b/aleksis/core/tests/regression/view_oauth.py @@ -0,0 +1,24 @@ +from django.http import HttpResponse +from django.test import override_settings +from django.urls import path, reverse +from django.views.generic import View + +from oauth2_provider.views.mixins import ScopedResourceMixin + +from aleksis.core.util.auth_helpers import ClientProtectedResourceMixin + + +class TestViewClientProtectedResourceMixin(ScopedResourceMixin, ClientProtectedResourceMixin, View): + required_scopes = ["write"] + + def get(self, request): + return HttpResponse("OK") + + +urlpatterns = [ + path( + "client_protected_resource_mixin_test/", + TestViewClientProtectedResourceMixin.as_view(), + name="client_protected_resource_mixin_test", + ), +] diff --git a/aleksis/core/util/auth_helpers.py b/aleksis/core/util/auth_helpers.py index 6edfac83373882d077d0a588d822fd3a0d0cc9b4..ca80aeae4a59ac069023465559d599f929bab6d8 100644 --- a/aleksis/core/util/auth_helpers.py +++ b/aleksis/core/util/auth_helpers.py @@ -134,6 +134,10 @@ class ClientProtectedResourceMixin(_ClientProtectedResourceMixin): # Verify scopes of configured application # The OAuth request was enriched with a reference to the Application when using the # validator above. + if not oauth_request.client.allowed_scopes: + # If there are no allowed scopes, the client is not allowed to access this resource + return False + required_scopes = set(self.get_scopes() or []) allowed_scopes = set(AppScopes().get_available_scopes(oauth_request.client) or []) return required_scopes.issubset(allowed_scopes)