diff --git a/apps/permission/backends.py b/apps/permission/backends.py index 37134713..ec136e9e 100644 --- a/apps/permission/backends.py +++ b/apps/permission/backends.py @@ -26,7 +26,7 @@ class PermissionBackend(ModelBackend): @staticmethod @memoize - def get_raw_permissions(request, t): + def get_raw_permissions(request, t): # noqa: C901 """ Query permissions of a certain type for a user, then memoize it. :param request: The current request @@ -42,7 +42,7 @@ class PermissionBackend(ModelBackend): if 'mask' in request.GET: try: rank = int(request.GET['mask']) - except: + except ValueError: rank = 42 query &= Q(mask__rank__lte=rank) for scope in request.auth.scope.split(' '): diff --git a/apps/permission/scopes.py b/apps/permission/scopes.py index d05bf297..93b7a88b 100644 --- a/apps/permission/scopes.py +++ b/apps/permission/scopes.py @@ -12,6 +12,7 @@ from .models import Permission from django.utils.translation import gettext_lazy as _ + class PermissionScopes(BaseScopes): """ An OAuth2 scope is defined by a permission object and a club. @@ -83,8 +84,12 @@ class PermissionOAuth2Validator(OAuth2Validator): valid_scopes = set() + # simple patch for have functionnal ROPB flow + # TODO rewrite + r = get_current_request() + r.user = request.user for t in Permission.PERMISSION_TYPES: - for p in PermissionBackend.get_raw_permissions(get_current_request(), t[0]): + for p in PermissionBackend.get_raw_permissions(r, t[0]): scope = f"{p.id}_{p.membership.club.id}" if scope in scopes: valid_scopes.add(scope) diff --git a/apps/permission/tests/test_oauth2.py b/apps/permission/tests/test_oauth2_access.py similarity index 72% rename from apps/permission/tests/test_oauth2.py rename to apps/permission/tests/test_oauth2_access.py index 579dc5c8..ebbbc7d7 100644 --- a/apps/permission/tests/test_oauth2.py +++ b/apps/permission/tests/test_oauth2_access.py @@ -21,6 +21,7 @@ class OAuth2TestCase(TestCase): def setUp(self): self.user = User.objects.create( username="toto", + password="toto1234", ) self.application = Application.objects.create( name="Test", @@ -92,3 +93,39 @@ class OAuth2TestCase(TestCase): self.assertEqual(resp.status_code, 200) self.assertIn(self.application, resp.context['scopes']) self.assertIn('1_1', resp.context['scopes'][self.application]) # Now the user has this permission + + def test_oidc(self): + """ + Ensure OIDC work + """ + # Create access token that has access to our own user detail + token = AccessToken.objects.create( + user=self.user, + application=self.application, + scope="openid", + token=get_random_string(64), + expires=timezone.now() + timedelta(days=365), + ) + + # No access without token + resp = self.client.get('/o/userinfo/') # userinfo endpoint + self.assertEqual(resp.status_code, 401) + + # Valid token + resp = self.client.get('/o/userinfo/', **{'Authorization': f'Bearer {token.token}'}) + self.assertEqual(resp.status_code, 200) + + # Create membership to test api + NoteUser.objects.create(user=self.user) + membership = Membership.objects.create(user=self.user, club_id=1) + membership.roles.add(Role.objects.get(name="Adhérent⋅e BDE")) + membership.save() + + # Token can always be use to see yourself + resp = self.client.get('/api/me/', + **{'Authorization': f'Bearer {token.token}'}) + + # Token is not granted to see other api + resp = self.client.get(f'/api/user/{self.user.pk}/', + **{'Authorization': f'Bearer {token.token}'}) + self.assertEqual(resp.status_code, 404) diff --git a/apps/permission/tests/test_oauth2_flow.py b/apps/permission/tests/test_oauth2_flow.py new file mode 100644 index 00000000..685da780 --- /dev/null +++ b/apps/permission/tests/test_oauth2_flow.py @@ -0,0 +1,115 @@ +# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +import base64 + +from django.contrib.auth.models import User +from django.test import TestCase +from member.models import Membership, Club +from note.models import NoteUser +from oauth2_provider.models import Application + +from ..models import Role, Permission + + +class OAuth2TestCase(TestCase): + fixtures = ('initial', ) + + def setUp(self): + self.user = User.objects.create( + username="toto", + password="toto1234", + ) + + NoteUser.objects.create(user=self.user) + membership = Membership.objects.create(user=self.user, club_id=1) + membership.roles.add(Role.objects.get(name="Adhérent⋅e BDE")) + membership.save() + + bde = Club.objects.get(name="BDE") + view_user_perm = Permission.objects.get(pk=1) # View own user detail + + self.base_scope = f'{view_user_perm.pk}_{bde.pk}' + + def test_oauth2_authorization_code_flow(self): + """ + Ensure OAuth2 Authorization Code Flow work + """ + pass + + def test_oauth2_implicit_flow(self): + """ + Ensure OAuth2 Implicit Flow work + """ + pass + + def test_oauth2_resource_owner_password_credentials_flow(self): + """ + Ensure OAuth2 Resource Owner Password Credentials Flow work + """ + pass + + def test_oauth2_client_credentials(self): + """ + Ensure OAuth2 Client Credentials work + """ + app = Application.objects.create( + name="Test credentials", + client_type=Application.CLIENT_CONFIDENTIAL, + authorization_grant_type=Application.GRANT_CLIENT_CREDENTIALS, + user=self.user, + hash_client_secret=False, + algorithm=Application.NO_ALGORITHM, + ) + + # No token without credential + resp = self.client.post('/o/token/', + data={"grant_type": "client_credentials"}, + **{"Content-Type": 'application/x-www-form-urlencoded'} + ) + + self.assertEqual(resp.status_code, 401) + + # Access with credential + credential = base64.b64encode(f'{app.client_id}:{app.client_secret}'.encode('utf-8')).decode() + + resp = self.client.post('/o/token/', + data={"grant_type": "client_credentials"}, + **{'HTTP_Authorization': f'Basic {credential}', + "Content-Type": 'application/x-www-form-urlencoded'} + ) + + self.assertEqual(resp.status_code, 200) + + token = resp.json()['access_token'] + + # Token is valid but has no right + resp = self.client.get('/api/user/{self.user.pk}', + **{'Authorization': f'Bearer {token}'} + ) + + self.assertEqual(resp.status_code, 403) + + # RFC6749 4.4.2 allows use of scope in client credential flow + resp = self.client.post('/o/token/', + data={"grant_type": "client_credentials", + "scope": self.base_scope}, + **{'http_Authorization': f'Basic {credential}', + "Content-Type": 'application/x-www-form-urlencoded'} + ) + + self.assertEqual(resp.status_code, 200) + + token = resp.json()['access_token'] + + # Now app can see his creator + resp = self.client.post(f'/api/user/{self.user.pk}/', + **{'Authorization': f'Bearer {token}'}) + + self.assertEqual(resp.status_code, 200) + + def test_oidc_flow(self): + """ + Ensure OIDC Flow work + """ + pass