Protecting ModelsΒΆ
By default, models are not protected and all their objects are accessible (i.e. visible).
Protected models control access to their objects. A protected model checks access for each object to be included in the response. If access is not granted, the object is either silently excluded from the response or an appropriate exception is raised, depending on the context.
A model can be protected by setting the access
attribute of the model to an SQL function that
accepts two arguments: the id
of the resource object and the current (logged-in) user id, and returns a boolean.
Here is an example SQL function to protect our article
resource model:
CREATE FUNCTION check_article_access(
p_article_id integer,
p_user_id integer) RETURNS boolean
LANGUAGE plpgsql AS
$$
BEGIN
-- always return true for superusers
PERFORM * FROM users WHERE id = p_user_id AND is_superuser;
IF FOUND THEN
RETURN TRUE;
END IF;
-- return true if the user is the author of the article
PERFORM *
FROM articles
WHERE id = p_article_id
AND author_id = p_user_id;
IF found THEN
RETURN TRUE;
END IF;
-- check if the user has read access
PERFORM *
FROM article_read_access
WHERE article_id = p_article_id
AND user_id = p_user_id;
RETURN found;
END;
$$;
In addition, the user
attribute of the model must evaluate to an object representing the
user in whose behalf the request is made, i.e. the current (logged-in) user:
class User:
def __init__(user_id, ...):
self.id = user_id
...
In a WSGI application, the variable holding this object must be thread-safe.
For this purpose you may want to use LocalProxy
from the werkzeug
library:
from werkzeug.local import LocalProxy
from quart import g
current_user = LocalProxy(lambda: g.get('user'))
Note
The authentication layer should be responsible for ensuring the value of this variable is set correctly (this is outside the scope of this article).
As an example, to protect the article
resource model, we redefine it as follows:
import sqlalchemy as sa
from auth import current_user
class ArticleModel(Model):
from_ = articles_t
fields = ('title', 'body', 'created_on', ...)
access = sa.func.check_article_access
user = current_user
If the current_user variable evaluates to None
, access is not granted for all objects of this type, otherwise
access is granted if the supplied function returns TRUE
.
When fetching a single object, or a related object in a to-one relationship a Forbidden
exception is raised if no user is logged in or the current user does not have access to the object in question:
>>> await ArticleModel().get_object({}, 1)
Traceback (most recent call last):
...
jsonapi.exc.Forbidden: [ArticleModel] access denied for: article(1)
>>> await ArticleModel().get_related({}, 1, 'author')
Traceback (most recent call last):
...
jsonapi.exc.Forbidden: [ArticleModel] access denied for: article(1)
When fetching a collection or related objects in a to-many relationship, objects to which access is not granted are silently excluded from the response.