From 55032dccd3b836c20348ca31bc986e841aace229 Mon Sep 17 00:00:00 2001 From: dursk Date: Mon, 29 Jul 2019 16:36:58 -0400 Subject: [PATCH] Redo permissions (#13) --- README.md | 15 ++++++++++++--- mbq/api_tools/__version__.py | 2 +- mbq/api_tools/permissions.py | 2 ++ mbq/api_tools/settings.py | 1 - mbq/api_tools/views.py | 23 ++++++++++++++--------- tests/settings.py | 1 - tests/test_views.py | 19 +++++++++++++++++-- 7 files changed, 46 insertions(+), 17 deletions(-) create mode 100644 mbq/api_tools/permissions.py diff --git a/README.md b/README.md index 5978f44..82ba0d5 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,7 @@ Use the `@view` decorator for all function based views. It accepts the following * GET, POST, PATCH, PUT, DELETE * Defaults to GET * `permissions` - * A list of DRF permission classes - * They don't necessarily need to subclass a DRF permission class, they only need to have a `has_permission(request, view)` method. + * A list of DRF permission classes _or_ functions that take in a request object and return `True` if authorized, `False` if unauthorized. * `params` * Schema for validating incoming query parameters. The query parameters will be available at `request.params`. * `payload` @@ -319,6 +318,17 @@ An empty page will be returned if the first page is requested and there is no da If `params` or `payload` is specified in the decorator, then the parsed query parameters will be at `request.params` or `request.payload`. This will be an immutable object. If you would like it in the form of a dictionary, you can do `request.params.as_dict()`. +## Permissions + +Permissions are required. A non-empty list must be passed into the decorator. If you would like to write an unauthorized endpoint, you can do: +```python +from mbq.api_tools import permissions + +@view(permissions=[permissions.NoAuthorization]) +def my_view(request): + pass +```` + ## Settings The default settings are: ```python @@ -326,7 +336,6 @@ API_TOOLS = { "DEFAULT_PAGE_SIZE": 20, "UNKNOWN_PAYLOAD_FIELDS": "raise", "UNKNOWN_PARAM_FIELDS": "raise", - "REQUIRE_PERMISSIONS": True, } ``` Override as you see fit. diff --git a/mbq/api_tools/__version__.py b/mbq/api_tools/__version__.py index 40274bd..b4278b7 100644 --- a/mbq/api_tools/__version__.py +++ b/mbq/api_tools/__version__.py @@ -4,4 +4,4 @@ __license__ = "Apache 2.0" __title__ = "mbq.apitools" __url__ = "https://github.com/managedbyq/mbq.apitools" -__version__ = "1.3.2" +__version__ = "1.4.0" diff --git a/mbq/api_tools/permissions.py b/mbq/api_tools/permissions.py new file mode 100644 index 0000000..2cedec5 --- /dev/null +++ b/mbq/api_tools/permissions.py @@ -0,0 +1,2 @@ +def NoAuthorization(request): + return True diff --git a/mbq/api_tools/settings.py b/mbq/api_tools/settings.py index eb25fd3..8251fcf 100644 --- a/mbq/api_tools/settings.py +++ b/mbq/api_tools/settings.py @@ -2,5 +2,4 @@ "DEFAULT_PAGE_SIZE": 20, "UNKNOWN_PAYLOAD_FIELDS": "raise", "UNKNOWN_PARAM_FIELDS": "raise", - "REQUIRE_PERMISSIONS": True, } diff --git a/mbq/api_tools/views.py b/mbq/api_tools/views.py index 2ac78dd..2870336 100644 --- a/mbq/api_tools/views.py +++ b/mbq/api_tools/views.py @@ -28,7 +28,7 @@ class ViewDecorator: def __init__( self, http_method_name: HttpMethodNames = "GET", - permissions: Optional[PermissionsCollection] = (), + permissions: PermissionsCollection = (), params: Optional[Dict[str, fields.Field]] = None, payload: Optional[Dict[str, fields.Field]] = None, paginated: bool = False, @@ -36,7 +36,7 @@ def __init__( on_unknown_field: Optional[OnUnknownField] = None, _method: Optional[bool] = None, ): - if not permissions and settings.API_TOOLS.get("REQUIRE_PERMISSIONS", True): + if not permissions: raise TypeError("Permissions are required") if http_method_name in {"POST", "PATCH", "PUT"} and payload is None: @@ -210,13 +210,18 @@ def _enrich_request(self): def _perform_authorization(self) -> Optional[str]: """Returns None if authorization succeeds, or an error message if it fails.""" - permissions = [permission() for permission in self.permissions] - - for permission in permissions: - if permission.has_permission(self.request, self) is not True: - # Protecting against mocks with these type checks - message = getattr(permission, "message", None) - return message if isinstance(message, str) else "Authorization Failed" + for permission in self.permissions: + # If it is a class, assume it is a DRF permission class. + if isinstance(permission, type): + if permission().has_permission(self.request, self) is not True: + # Protecting against mocks with these type checks + message = getattr(permission, "message", None) + return ( + message if isinstance(message, str) else "Authorization Failed" + ) + else: + if permission(self.request) is not True: + return "Authorization Failed" return None diff --git a/tests/settings.py b/tests/settings.py index 1746aed..e539000 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -4,5 +4,4 @@ "DEFAULT_PAGE_SIZE": 20, "UNKNOWN_PAYLOAD_FIELDS": "raise", "UNKNOWN_PARAM_FIELDS": "raise", - "REQUIRE_PERMISSIONS": True, } diff --git a/tests/test_views.py b/tests/test_views.py index ef43656..9ea4104 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -4,7 +4,7 @@ from django.http import QueryDict from django.test import SimpleTestCase -from mbq.api_tools import fields +from mbq.api_tools import fields, permissions from mbq.api_tools.responses import ( ClientErrorResponse, DetailResponse, @@ -40,10 +40,16 @@ def setUp(self): super().setUp() self.request = Mock(body="", method="GET", GET=QueryDict()) - def test_perform_authorization(self): + def test_permissions_required(self): with self.assertRaises(TypeError): view(permissions=[])(view_func) + response = view(permissions=[permissions.NoAuthorization])(view_func)( + self.request + ) + self.assertTrue(isinstance(response, DetailResponse)) + + def test_perform_authorization_drf_class(self): self.assertTrue( isinstance( view(permissions=[TruePermissionStub])(view_func)(self.request), @@ -74,6 +80,15 @@ def test_perform_authorization(self): ) ) + def test_perform_authorization_function(self): + wrapped_view = view(permissions=[lambda x: True])(view_func) + response = wrapped_view(self.request) + self.assertTrue(isinstance(response, DetailResponse)) + + wrapped_view = view(permissions=[lambda x: False])(view_func) + response = wrapped_view(self.request) + self.assertTrue(isinstance(response, UnauthorizedResponse)) + def test_method_not_allowed(self): self.assertTrue( isinstance(