Skip to content
This repository has been archived by the owner on Oct 3, 2019. It is now read-only.

Commit

Permalink
Redo permissions (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
dursk committed Jul 29, 2019
1 parent ac13914 commit 55032dc
Show file tree
Hide file tree
Showing 7 changed files with 46 additions and 17 deletions.
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -319,14 +318,24 @@ 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
API_TOOLS = {
"DEFAULT_PAGE_SIZE": 20,
"UNKNOWN_PAYLOAD_FIELDS": "raise",
"UNKNOWN_PARAM_FIELDS": "raise",
"REQUIRE_PERMISSIONS": True,
}
```
Override as you see fit.
2 changes: 1 addition & 1 deletion mbq/api_tools/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 2 additions & 0 deletions mbq/api_tools/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def NoAuthorization(request):
return True
1 change: 0 additions & 1 deletion mbq/api_tools/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@
"DEFAULT_PAGE_SIZE": 20,
"UNKNOWN_PAYLOAD_FIELDS": "raise",
"UNKNOWN_PARAM_FIELDS": "raise",
"REQUIRE_PERMISSIONS": True,
}
23 changes: 14 additions & 9 deletions mbq/api_tools/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ 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,
page_size: Optional[int] = None,
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:
Expand Down Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@
"DEFAULT_PAGE_SIZE": 20,
"UNKNOWN_PAYLOAD_FIELDS": "raise",
"UNKNOWN_PARAM_FIELDS": "raise",
"REQUIRE_PERMISSIONS": True,
}
19 changes: 17 additions & 2 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 55032dc

Please sign in to comment.