diff --git a/ninja_extra/permissions/base.py b/ninja_extra/permissions/base.py index 71608cab..ad7d7e02 100644 --- a/ninja_extra/permissions/base.py +++ b/ninja_extra/permissions/base.py @@ -48,8 +48,7 @@ def __invert__( # type:ignore[misc] return SingleOperandHolder(NOT, self) -class BasePermissionMetaclass(OperationHolderMixin, ABCMeta): - ... +class BasePermissionMetaclass(OperationHolderMixin, ABCMeta): ... class BasePermission(ABC, metaclass=BasePermissionMetaclass): # pragma: no cover diff --git a/ninja_extra/permissions/common.py b/ninja_extra/permissions/common.py index 5b2a72dc..6e16d525 100644 --- a/ninja_extra/permissions/common.py +++ b/ninja_extra/permissions/common.py @@ -38,6 +38,7 @@ class IsAdminUser(BasePermission): """ Allows access only to admin users. """ + message: str = "You must be an admin user to access this resource." def has_permission( diff --git a/ninja_extra/security/session.py b/ninja_extra/security/session.py index 3349fb03..6d729978 100644 --- a/ninja_extra/security/session.py +++ b/ninja_extra/security/session.py @@ -1,5 +1,6 @@ from django.conf import settings from django.http import HttpRequest +from ninja.signature import is_async from ninja_extra.security.api_key import AsyncAPIKeyCookie @@ -16,6 +17,11 @@ class AsyncSessionAuth(AsyncAPIKeyCookie): async def authenticate( self, request: HttpRequest, key: Optional[str] ) -> Optional[Any]: - if request.user.is_authenticated: - return request.user + if hasattr(request, "auser") and is_async(request.auser): + current_user = await request.auser() + else: + current_user = request.user + + if current_user.is_authenticated: + return current_user return None diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 97f50ce0..21f47b98 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -12,7 +12,6 @@ class TestPermissionsCompositions: - @classmethod def get_real_user_request(cls): _request = Mock() @@ -43,8 +42,8 @@ def test_is_authenticated_and_read_only(self, method, auth, result): request = self.get_real_user_request() request.method = method assert ( - permissions.IsAuthenticatedOrReadOnly().has_permission(request, Mock()) - == result + permissions.IsAuthenticatedOrReadOnly().has_permission(request, Mock()) + == result ) def test_and_false(self): @@ -83,8 +82,8 @@ def test_not_false(self): composed_perm = ~permissions.IsAuthenticated assert composed_perm().has_permission(anonymous_request, None) is True assert ( - composed_perm().has_object_permission(anonymous_request, None, None) - is False + composed_perm().has_object_permission(anonymous_request, None, None) + is False ) # Message assert composed_perm().message == permissions.IsAuthenticated.message @@ -99,10 +98,10 @@ def test_not_true(self): def test_several_levels_without_negation(self): request = self.get_real_user_request() composed_perm = ( - permissions.IsAuthenticated - & permissions.IsAuthenticated - & permissions.IsAuthenticated - & permissions.IsAuthenticated + permissions.IsAuthenticated + & permissions.IsAuthenticated + & permissions.IsAuthenticated + & permissions.IsAuthenticated ) assert composed_perm().has_permission(request, None) is True assert composed_perm().has_object_permission(request, None, None) is True @@ -111,10 +110,10 @@ def test_several_levels_without_negation(self): def test_several_levels_and_precedence_with_negation(self): request = self.get_real_user_request() composed_perm = ( - permissions.IsAuthenticated - & ~permissions.IsAdminUser - & permissions.IsAuthenticated - & ~(permissions.IsAdminUser & permissions.IsAdminUser) + permissions.IsAuthenticated + & ~permissions.IsAdminUser + & permissions.IsAuthenticated + & ~(permissions.IsAdminUser & permissions.IsAdminUser) ) assert composed_perm().has_permission(request, None) is True @@ -122,17 +121,17 @@ def test_several_levels_and_precedence_with_negation(self): def test_several_levels_and_precedence(self): request = self.get_real_user_request() composed_perm = ( - permissions.IsAuthenticated & permissions.IsAuthenticated - | permissions.IsAuthenticated & permissions.IsAuthenticated + permissions.IsAuthenticated & permissions.IsAuthenticated + | permissions.IsAuthenticated & permissions.IsAuthenticated ) assert composed_perm().has_permission(request, None) is True def test_or_lazyness(self): with mock.patch.object( - permissions.AllowAny, "has_permission", return_value=True + permissions.AllowAny, "has_permission", return_value=True ) as mock_allow: with mock.patch.object( - permissions.IsAuthenticated, "has_permission", return_value=False + permissions.IsAuthenticated, "has_permission", return_value=False ) as mock_deny: composed_perm = permissions.AllowAny | permissions.IsAuthenticated hasperm = composed_perm().has_permission(anonymous_request, None) @@ -141,10 +140,10 @@ def test_or_lazyness(self): mock_deny.assert_not_called() with mock.patch.object( - permissions.AllowAny, "has_permission", return_value=True + permissions.AllowAny, "has_permission", return_value=True ) as mock_allow: with mock.patch.object( - permissions.IsAuthenticated, "has_permission", return_value=False + permissions.IsAuthenticated, "has_permission", return_value=False ) as mock_deny: composed_perm = permissions.IsAuthenticated | permissions.AllowAny hasperm = composed_perm().has_permission(anonymous_request, None) @@ -154,10 +153,10 @@ def test_or_lazyness(self): def test_object_or_lazyness(self): with mock.patch.object( - permissions.AllowAny, "has_object_permission", return_value=True + permissions.AllowAny, "has_object_permission", return_value=True ) as mock_allow: with mock.patch.object( - permissions.IsAuthenticated, "has_object_permission", return_value=False + permissions.IsAuthenticated, "has_object_permission", return_value=False ) as mock_deny: composed_perm = permissions.AllowAny | permissions.IsAuthenticated hasperm = composed_perm().has_object_permission( @@ -168,10 +167,10 @@ def test_object_or_lazyness(self): mock_deny.assert_not_called() with mock.patch.object( - permissions.AllowAny, "has_object_permission", return_value=True + permissions.AllowAny, "has_object_permission", return_value=True ) as mock_allow: with mock.patch.object( - permissions.IsAuthenticated, "has_object_permission", return_value=False + permissions.IsAuthenticated, "has_object_permission", return_value=False ) as mock_deny: composed_perm = permissions.IsAuthenticated | permissions.AllowAny hasperm = composed_perm().has_object_permission( @@ -183,10 +182,10 @@ def test_object_or_lazyness(self): def test_and_lazyness(self): with mock.patch.object( - permissions.AllowAny, "has_permission", return_value=True + permissions.AllowAny, "has_permission", return_value=True ) as mock_allow: with mock.patch.object( - permissions.IsAuthenticated, "has_permission", return_value=False + permissions.IsAuthenticated, "has_permission", return_value=False ) as mock_deny: composed_perm = permissions.AllowAny & permissions.IsAuthenticated hasperm = composed_perm().has_permission(anonymous_request, None) @@ -195,10 +194,10 @@ def test_and_lazyness(self): assert mock_deny.call_count == 1 with mock.patch.object( - permissions.AllowAny, "has_permission", return_value=True + permissions.AllowAny, "has_permission", return_value=True ) as mock_allow: with mock.patch.object( - permissions.IsAuthenticated, "has_permission", return_value=False + permissions.IsAuthenticated, "has_permission", return_value=False ) as mock_deny: composed_perm = permissions.IsAuthenticated & permissions.AllowAny hasperm = composed_perm().has_permission(anonymous_request, None) @@ -208,10 +207,10 @@ def test_and_lazyness(self): def test_object_and_lazyness(self): with mock.patch.object( - permissions.AllowAny, "has_object_permission", return_value=True + permissions.AllowAny, "has_object_permission", return_value=True ) as mock_allow: with mock.patch.object( - permissions.IsAuthenticated, "has_object_permission", return_value=False + permissions.IsAuthenticated, "has_object_permission", return_value=False ) as mock_deny: composed_perm = permissions.AllowAny & permissions.IsAuthenticated hasperm = composed_perm().has_object_permission( @@ -222,10 +221,10 @@ def test_object_and_lazyness(self): assert mock_deny.call_count == 1 with mock.patch.object( - permissions.AllowAny, "has_object_permission", return_value=True + permissions.AllowAny, "has_object_permission", return_value=True ) as mock_allow: with mock.patch.object( - permissions.IsAuthenticated, "has_object_permission", return_value=False + permissions.IsAuthenticated, "has_object_permission", return_value=False ) as mock_deny: composed_perm = permissions.IsAuthenticated & permissions.AllowAny hasperm = composed_perm().has_object_permission( diff --git a/tests/test_security/test_session.py b/tests/test_security/test_session.py new file mode 100644 index 00000000..f20e1de6 --- /dev/null +++ b/tests/test_security/test_session.py @@ -0,0 +1,40 @@ +from unittest.mock import AsyncMock, Mock + +import pytest +from django.http import HttpRequest + +from ninja_extra.security.session import AsyncSessionAuth + + +@pytest.mark.asyncio +async def test_async_session_auth(): + auth = AsyncSessionAuth() + request = HttpRequest() + + # Test async authenticated user + async_user = AsyncMock() + async_user.is_authenticated = True + request.auser = AsyncMock(return_value=async_user) + + authenticated_user = await auth.authenticate(request, None) + assert authenticated_user == async_user + request.auser.assert_called_once() + + # Test async non-authenticated user + async_user.is_authenticated = False + authenticated_user = await auth.authenticate(request, None) + assert authenticated_user is None + + # Test non-async authenticated user + delattr(request, "auser") + sync_user = Mock() + sync_user.is_authenticated = True + request.user = sync_user + + authenticated_user = await auth.authenticate(request, None) + assert authenticated_user == sync_user + + # Test non-async non-authenticated user + sync_user.is_authenticated = False + authenticated_user = await auth.authenticate(request, None) + assert authenticated_user is None