Permissions Handling¶
Given the wide scope of Drowsy, particularly with nested resources, the concept of permissions and authorization can feel like a daunting one to dive into. The good news is Drowsy was mindfully built to take permissions into account, and it should be relatively straight forward to properly secure your API.
Because developers like to handle permissions in various ways, Drowsy attempts to be as approach agnostic as possible, while still providing the basic tools and entry points needed to implement permissions successfully.
Reading¶
Fine grained control of what different users can see happens at the Resource
level. Classes that inherit from BaseModelResource
include apply_required_filters()
which can usefully be overriden.
As an example, imagine you have a NotificationResource class, and would
like users to only be able to see their own notifications. To accomplish this,
your notification class might look something like:
class NotificationResource(ModelResource):
class Meta:
schema_cls = NotificationModelSchema
def apply_required_filters(self, query, alias=None):
"""Ensure users can only see their own notifications."""
# Use the provided model alias if applicable.
# Note that this is what helps enforce required filters
# on nested resources.
model = alias or self.model
# How user info is stored in the context dict is up to you.
if self.context.get("user"):
return query.where(model.user_id == user.user_id)
else:
# hacky way to ensure the query always returns nothing
return query.where(model.user_id == -1)
Now when NotificationResource is accessed you can be assured that the above
filters will always be applied. Note that this also applies to nested
resources, so if you were to have a UserResource with a nested
NotificationResource embedded, the above filters are still applied to the
nested collection of notifications.
Access to a particular method can also be denied by overriding
_check_method_allowed() on a Resource.
This method is called at the beginning of each request and has access to any
context the Resource was initialized with (e.g. which user is logged in), and
thus can be used to explicitly deny access to a certain method type (e.g.
denying a user access to GET or DELETE actions).
In such cases there’s no fine grained control involved, the user is denied a particular type of access to all objects in the collection. It’s also important to note that this won’t directly impact nested resources, so denying GET access to a user for a resource won’t block them from viewing that same resource as part of a nested collection.
Create, Update, and Delete¶
Like read permissions, mutation permissions have the option of overriding
_check_method_allowed() to completely
restrict access to an endpoint. Note that this won’t restrict any nested
access via relationships (e.g. you may restrict GET access to the /tracks
endpoint, but the tracks relationship on your /albums endpoint is
still accessible).
More fine grained mutation control is handled at the schema level rather than
at the resource level. This allows validation to occur on an instance by
instance basis as data is being prepared for deserialization. In order to
implement permissions, you can override the
check_permission() method:
class NotificationSchema(ModelResourceSchema):
class Meta:
model = Notification
def check_permission(self, data, instance, action):
"""Test if the proposed action is permissible.
Note that other schema validation will run after this check,
this is simply a high level check.
:param dict data: The data to be loaded into an instance.
:param instance: The existing instance this data is to be
loaded into. ``None`` if creating a new instance.
:param str action: Either ``"create"``, ``"update"``, or
``"delete"``.
:return: None
:raise PermissionDenied: If the action being taken is not
allowed.
"""
# How user info is stored in the context dict is up to you.
user = self.context.get("user")
if action == "delete":
if not user.is_admin:
# Only allow admins to delete a notification.
raise PermissionDenied("Permission denied.")
In the above simple example, only admin users will be allowed to delete a notification.
Relationship Operations¶
On occasion you’ll find that you want to limit how different users can affect
different relationships. As an example, you might want to give a user the
ability to modify some metadata about an album, and some metadata about the
tracks on that album, but not be able to change which tracks belong to it.
In such a case, you’ll need to set a permissions_cls on the relationship
you’re trying to limit.
from drowsy.permissions import DisallowAllOpPermissions
from drowsy.schema import ModelResourceSchema
class AlbumSchema(ModelResourceSchema):
class Meta:
model = Track
include_relationships = True
tracks = Relationship(
"TrackResource",
many=True,
permissions_cls=DisallowAllOpPermissions)
class TrackSchema(ModelResourceSchema):
class Meta:
model = Track
include_relationships = True
album = Relationship(
"AlbumResource",
many=False,
permissions_cls=DisallowAllOpPermissions)
Here we use the provided DisallowAllOpPermissions
class to disallow any attempted changes to the tracks and album
relationships. In most real world use cases, you’ll want to roll your own
implementation of OpPermissionsABC in order to
use the request context (e.g. which user is logged in) to determine what
relationship actions are allowed.
Note that in situations like this where there is a bidirectional relationship, you must define permissions on both sides. This may seem inconvenient, but there are scenarios where you’ll want users to have different permissions depending on which side of the relationship they’re attempting to make changes from. Perhaps you’d want all users who have access to modify albums the ability to add tracks, but not all users who have access to modify tracks the ability to change which album they belong to.
In Drowsy there are multiple different relationship actions that a
OpPermissionsABC implementation can be set to
handle. These options include "add", "remove", "create", and
"replace" for collections, and "set" may be used as an alias for
"add" in single object nested situations. The "add", "remove",
and "set" options should hopefully be self explanatory, while
"create" handles whether newly created instances can be added/set on
a relationship. As an example, if the tracks relationship has "add"
permissions but not "create" permissions, only pre-existing tracks
would be allowed to be added to the relationship. Meanwhile, the "replace"
permission handles whether the user has the ability to replace the entire
contents of a relationship (e.g. clear out all of album.tracks before
making any additions).