diff --git a/api/api_keys/models.py b/api/api_keys/models.py index 1a986e36840e..7fcfaaf589a7 100644 --- a/api/api_keys/models.py +++ b/api/api_keys/models.py @@ -33,6 +33,8 @@ def delete_role_api_keys( # type: ignore[no-untyped-def] self, ): if settings.IS_RBAC_INSTALLED: - from rbac.models import MasterAPIKeyRole # type: ignore[import-not-found] + from rbac.models import ( # type: ignore[import-not-found,unused-ignore] + MasterAPIKeyRole, + ) MasterAPIKeyRole.objects.filter(master_api_key=self.id).delete() diff --git a/api/custom_auth/jwt_cookie/authentication.py b/api/custom_auth/jwt_cookie/authentication.py index a51e8a5f381f..bc83ee5d4517 100644 --- a/api/custom_auth/jwt_cookie/authentication.py +++ b/api/custom_auth/jwt_cookie/authentication.py @@ -1,5 +1,10 @@ from rest_framework.request import Request from rest_framework_simplejwt.authentication import JWTAuthentication +from rest_framework_simplejwt.exceptions import ( + AuthenticationFailed, + InvalidToken, + TokenError, +) from rest_framework_simplejwt.tokens import Token from custom_auth.jwt_cookie.constants import JWT_SLIDING_COOKIE_KEY @@ -12,6 +17,9 @@ def authenticate_header(self, request: Request) -> str: def authenticate(self, request: Request) -> tuple[FFAdminUser, Token] | None: if raw_token := request.COOKIES.get(JWT_SLIDING_COOKIE_KEY): - validated_token = self.get_validated_token(raw_token) # type: ignore[arg-type] - return self.get_user(validated_token), validated_token # type: ignore[return-value] + try: + validated_token = self.get_validated_token(raw_token) # type: ignore[arg-type] + return self.get_user(validated_token), validated_token # type: ignore[return-value] + except (InvalidToken, TokenError, AuthenticationFailed): + return None return None diff --git a/api/organisations/urls.py b/api/organisations/urls.py index 80913d8f3f4e..b04144a751bb 100644 --- a/api/organisations/urls.py +++ b/api/organisations/urls.py @@ -171,7 +171,7 @@ if settings.IS_RBAC_INSTALLED: - from rbac.views import ( # type: ignore[import-not-found] + from rbac.views import ( # type: ignore[import-not-found,unused-ignore] GroupRoleViewSet, MasterAPIKeyRoleViewSet, RoleEnvironmentPermissionsViewSet, diff --git a/api/permissions/rbac_wrapper.py b/api/permissions/rbac_wrapper.py index 2049fdf9f8af..e99526f85d1f 100644 --- a/api/permissions/rbac_wrapper.py +++ b/api/permissions/rbac_wrapper.py @@ -9,10 +9,10 @@ from projects.models import Project if settings.IS_RBAC_INSTALLED: # pragma: no cover - from rbac.permission_service import ( # type: ignore[import-not-found] + from rbac.permission_service import ( # type: ignore[import-not-found,unused-ignore] get_role_permission_filter, ) - from rbac.permissions_calculator import ( # type: ignore[import-not-found] + from rbac.permissions_calculator import ( # type: ignore[import-not-found,unused-ignore] RolePermissionData, get_roles_permission_data_for_environment, get_roles_permission_data_for_organisation, diff --git a/api/pyproject.toml b/api/pyproject.toml index a938204801c7..6876943fc0a2 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -77,6 +77,14 @@ strict = true module = ["admin_sso.models"] ignore_missing_imports = true +[[tool.mypy.overrides]] +module = ["rbac.*"] +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = ["saml.*"] +ignore_missing_imports = true + [tool.django-stubs] django_settings_module = "app.settings.local" diff --git a/api/tests/unit/custom_auth/jwt_cookie/test_unit_jwt_cookie_authentication.py b/api/tests/unit/custom_auth/jwt_cookie/test_unit_jwt_cookie_authentication.py new file mode 100644 index 000000000000..5c3def9b164b --- /dev/null +++ b/api/tests/unit/custom_auth/jwt_cookie/test_unit_jwt_cookie_authentication.py @@ -0,0 +1,78 @@ +from typing import Type + +import pytest +from pytest_mock import MockerFixture +from rest_framework.request import Request +from rest_framework_simplejwt.exceptions import ( + AuthenticationFailed, + InvalidToken, + TokenError, +) +from rest_framework_simplejwt.tokens import Token + +from custom_auth.jwt_cookie.authentication import JWTCookieAuthentication +from custom_auth.jwt_cookie.constants import JWT_SLIDING_COOKIE_KEY +from users.models import FFAdminUser + + +def test_authenticate_without_cookie(mocker: MockerFixture) -> None: + # Given + auth = JWTCookieAuthentication() + request = mocker.MagicMock(spec=Request) + request.COOKIES = {} + + # When + result = auth.authenticate(request) + + # Then + assert result is None + + +def test_authenticate_valid_cookie(mocker: MockerFixture) -> None: + # Given + auth = JWTCookieAuthentication() + request = mocker.MagicMock(spec=Request) + raw_token = "valid_token" + request.COOKIES = {JWT_SLIDING_COOKIE_KEY: raw_token} + + validated_token = mocker.MagicMock(spec=Token) + user = mocker.MagicMock(spec=FFAdminUser) + + # Mock the validation and user retrieval + mock_validate = mocker.patch.object( + auth, "get_validated_token", return_value=validated_token + ) + mock_get_user = mocker.patch.object(auth, "get_user", return_value=user) + + # When + result = auth.authenticate(request) + + # Then + assert result == (user, validated_token) + mock_validate.assert_called_once_with(raw_token) + mock_get_user.assert_called_once_with(validated_token) + + +@pytest.mark.parametrize( + "exception_class", [InvalidToken, TokenError, AuthenticationFailed] +) +def test_authenticate_invalid_cookie( + mocker: MockerFixture, + exception_class: Type[Exception], +) -> None: + # Given + auth = JWTCookieAuthentication() + request = mocker.MagicMock(spec=Request) + raw_token = "invalid_token" + request.COOKIES = {JWT_SLIDING_COOKIE_KEY: raw_token} + + # Test that no further exceptions are raised if the token is invalid in any way + mocker.patch.object( + auth, "get_validated_token", side_effect=exception_class("Error") + ).side_effect = exception_class("Error") + + # When + result = auth.authenticate(request) + + # Then + assert result is None