From c4aa496be58102178997b0c7cc8602dc763dca0f Mon Sep 17 00:00:00 2001 From: c4ffein Date: Fri, 22 Nov 2024 14:14:31 +0100 Subject: [PATCH] Distinguish between AuthenticationError and AuthorizationError (#1257) * 1218 Authorization vs Authentication * AuthenticationError AuthorizationError inherited from HttpError --------- Co-authored-by: Vitaliy Kucheryaviy --- docs/docs/guides/errors.md | 4 ++++ docs/docs/guides/response/index.md | 1 + ninja/errors.py | 25 +++++++++++-------------- tests/test_auth.py | 17 ++++++++++++++++- 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/docs/docs/guides/errors.md b/docs/docs/guides/errors.md index de3b6ea0d..7d7c61785 100644 --- a/docs/docs/guides/errors.md +++ b/docs/docs/guides/errors.md @@ -59,6 +59,10 @@ By default, **Django Ninja** initialized the following exception handlers: Raised when authentication data is not valid +#### `ninja.errors.AuthorizationError` + +Raised when authentication data is valid, but doesn't allow you to access the resource + #### `ninja.errors.ValidationError` Raised when request data does not validate diff --git a/docs/docs/guides/response/index.md b/docs/docs/guides/response/index.md index 838a44b46..94c9b845d 100644 --- a/docs/docs/guides/response/index.md +++ b/docs/docs/guides/response/index.md @@ -262,6 +262,7 @@ In case of authentication, for example, you can return: - **200** successful -> token - **401** -> Unauthorized - **402** -> Payment required +- **403** -> Forbidden - etc.. In fact, the [OpenAPI specification](https://swagger.io/docs/specification/describing-responses/) allows you to pass multiple response schemas. diff --git a/ninja/errors.py b/ninja/errors.py index 95a130085..f18651946 100644 --- a/ninja/errors.py +++ b/ninja/errors.py @@ -14,6 +14,7 @@ __all__ = [ "ConfigError", "AuthenticationError", + "AuthorizationError", "ValidationError", "HttpError", "set_default_exc_handlers", @@ -27,10 +28,6 @@ class ConfigError(Exception): pass -class AuthenticationError(Exception): - pass - - class ValidationError(Exception): """ This exception raised when operation params do not validate @@ -53,6 +50,16 @@ def __str__(self) -> str: return self.message +class AuthenticationError(HttpError): + def __init__(self, status_code: int = 401, message: str = "Unauthorized") -> None: + super().__init__(status_code=status_code, message=message) + + +class AuthorizationError(HttpError): + def __init__(self, status_code: int = 403, message: str = "Forbidden") -> None: + super().__init__(status_code=status_code, message=message) + + class Throttled(HttpError): def __init__(self, wait: Optional[int]) -> None: self.wait = wait @@ -76,10 +83,6 @@ def set_default_exc_handlers(api: "NinjaAPI") -> None: ValidationError, partial(_default_validation_error, api=api), ) - api.add_exception_handler( - AuthenticationError, - partial(_default_authentication_error, api=api), - ) def _default_404(request: HttpRequest, exc: Exception, api: "NinjaAPI") -> HttpResponse: @@ -101,12 +104,6 @@ def _default_validation_error( return api.create_response(request, {"detail": exc.errors}, status=422) -def _default_authentication_error( - request: HttpRequest, exc: AuthenticationError, api: "NinjaAPI" -) -> HttpResponse: - return api.create_response(request, {"detail": "Unauthorized"}, status=401) - - def _default_exception( request: HttpRequest, exc: Exception, api: "NinjaAPI" ) -> HttpResponse: diff --git a/tests/test_auth.py b/tests/test_auth.py index 01121bd78..01fb64814 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -3,7 +3,7 @@ import pytest from ninja import NinjaAPI -from ninja.errors import ConfigError +from ninja.errors import AuthorizationError, ConfigError from ninja.security import ( APIKeyCookie, APIKeyHeader, @@ -60,6 +60,8 @@ class BearerAuth(HttpBearer): def authenticate(self, request, token): if token == "bearertoken": return token + if token == "nottherightone": + raise AuthorizationError def demo_operation(request): @@ -102,6 +104,7 @@ class MockSuperUser(str): BODY_UNAUTHORIZED_DEFAULT = dict(detail="Unauthorized") +BODY_FORBIDDEN_DEFAULT = dict(detail="Forbidden") @pytest.mark.parametrize( @@ -178,6 +181,18 @@ class MockSuperUser(str): 401, BODY_UNAUTHORIZED_DEFAULT, ), + ( + "/bearer", + dict(headers={"Authorization": "Bearer nonexistingtoken"}), + 401, + BODY_UNAUTHORIZED_DEFAULT, + ), + ( + "/bearer", + dict(headers={"Authorization": "Bearer nottherightone"}), + 403, + BODY_FORBIDDEN_DEFAULT, + ), ("/customexception", {}, 401, dict(custom=True)), ( "/customexception",