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).