diff --git a/docs/docs/guides/errors.md b/docs/docs/guides/errors.md index 6766603f..b7c9c525 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 838a44b4..94c9b845 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 95a13008..abc4c883 100644 --- a/ninja/errors.py +++ b/ninja/errors.py @@ -14,6 +14,7 @@ __all__ = [ "ConfigError", "AuthenticationError", + "AuthorizationError", "ValidationError", "HttpError", "set_default_exc_handlers", @@ -31,6 +32,10 @@ class AuthenticationError(Exception): pass +class AuthorizationError(AuthenticationError): + pass + + class ValidationError(Exception): """ This exception raised when operation params do not validate @@ -80,6 +85,10 @@ def set_default_exc_handlers(api: "NinjaAPI") -> None: AuthenticationError, partial(_default_authentication_error, api=api), ) + api.add_exception_handler( + AuthorizationError, + partial(_default_authorization_error, api=api), + ) def _default_404(request: HttpRequest, exc: Exception, api: "NinjaAPI") -> HttpResponse: @@ -107,6 +116,12 @@ def _default_authentication_error( return api.create_response(request, {"detail": "Unauthorized"}, status=401) +def _default_authorization_error( + request: HttpRequest, exc: AuthorizationError, api: "NinjaAPI" +) -> HttpResponse: + return api.create_response(request, {"detail": "Forbidden"}, status=403) + + def _default_exception( request: HttpRequest, exc: Exception, api: "NinjaAPI" ) -> HttpResponse: diff --git a/tests/test_auth.py b/tests/test_auth.py index 01121bd7..01fb6481 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",