1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
|
.. _permissions-reference:
=====================
Permissions reference
=====================
This is a detailed reference to Debusine permission system. For a high-level
explanation, see :ref:`explanation-permissions`.
Note that, with the exception of groups themselves, Debusine assigns roles to
*groups*, not *users*. Any permission check relies on testing if any of the
user's groups has the required role on a resource.
Resources
=========
In the context of the permission system, resources are Django Model instances
which have permission predicates.
Resources can optionally define a set of resource-specific roles, as a
``Roles`` member class which is a subclass of
:py:class:`debusine.db.models.permissions.Roles`.
Resources can be set up so that roles can explicitly be assigned to them using
a database table. See the :py:class:`debusine.db.models.scopes.ScopeRole` and
:py:class:`debusine.db.models.scopes.WorkspaceRole` models as examples.
If a resource supports direct role assignment, its Manager class has a
``get_roles_model`` method that returns the Model subclass that implements the
role assignment.
Roles inference
===============
Roles for a resource are like string enums which can also define inferences on
other roles. Inference between roles in a resource must satisfy the
requirements for a `partially ordered set
<https://en.wikipedia.org/wiki/Partially_ordered_set>`_.
Each defined role needs to have a method that returns a Django ``Q`` object
that can be used to select the resource instances for which a user has the
role.
Permission predicates
=====================
Each permission predicate on a resource is defined twice:
1. as a "permission filter" in the resource ``QuerySet``, to filter elements for which the predicate is true
2. as a "permission check" in the resource ``Model``, to test the predicate on a single resource instance.
The :py:mod:`debusine.db.models.permissions` module defines the
:py:func:`debusine.db.models.permissions.permission_filter` and
:py:func:`debusine.db.models.permissions.permission_check` decorators that
help implementing permission predicates.
The one parameter for a permission predicate is of type
:py:type:`debusine.db.models.permissions.PermissionUser`, which can be ``None`` (no
user has been set), ``AnonymousUser`` (user has not logged in) or a
:py:class:`debusine.db.models.auth.User` instance.
The ``permission_check`` and ``permission_filter`` decorators take optional
``workers`` and ``anonymous`` parameters that can be used to define default
behaviour for anonymous users and workers.
Caching and invalidation
------------------------
To avoid the hard problem of cache invalidation, any role change only takes
effect on the next request, to allow caching permission information during a
request.
Permission checks
-----------------
Permission checks are model methods that check the predicate on a model
instance. They have the form::
instance.can_<predicate>(self, user: PermissionUser) -> bool
For example: ``can_display``,
``can_create_workspace``, ``can_add_artifact``.
Predicates are normally checked on ``context.user`` to test permissions of the
current user, although one can pass any user as needed, for example to check if
the user that started a work request can access a resource.
Since permission checks are used to gate access to resources, the
``permission_check`` decorator also takes a message template that can be used
to construct error messages for when the check fails.
Permission checks can use shortcuts to avoid hitting the database (for example,
checking ``self.public`` for a ``Workspace``), but if all shortcuts fail, a
permission check can fall back to the permission filter implementation by way of
a query like this::
if ThisModel.objects.can_display(user).filter(pk=self.pk).exists():
..
Permission filters
------------------
Permission filters are ``QuerySet`` methods that choose instances for which the
predicate is true. They have the form::
Model.objects.can_<predicate>(user: User | AnonymousUser) -> QuerySet[Model]
They manipulate the queryset, and so can be further refined with other
``QuerySet`` methods.
Permission filters, by default, filter resource instances regardless of the
current context, and return all accessible resources in any scope and
workspace. Depending on use cases, one may or may not need to restrict results
to the current scope, workspaces and so on.
Resource querysets can therefore have ``in_current_*()`` methods, like
``in_current_scope()`` and ``in_current_workspace()``, that filter results
using different aspects of the current application context.
Enforcing permission predicates
===============================
For web UI views, permission predicates can be enforced with
:py:meth:`debusine.web.views.base.BaseUIView.enforce` method, which takes as its
only argument the permission check function, applies it to the current user and
does the appropriate thing if the permission check fails.
For web API views, the same can be done using the
:py:meth:`debusine.server.views.base.BaseAPIView.enforce` method.
Roles of users in groups
========================
Groups of users need to have permission predicates that check if a user is
allowed to add and remove users to the group. Those checks cannot rely on group
roles, to avoid having to define a group of admins for each group, and an admin
admin group for each admin group, and so on.
Group roles are therefore assigned directly to users, via the
:py:class:`debusine.db.models.auth.GroupMembership`` model.
Provisions for testing
======================
The application context supports a ``disable_permission_checks`` attribute that
is honored by the ``@permission_check`` and ``@permission_filter`` decorators,
to disable all permission checks.
The test suite infrastructure also provides an ``@override_permissions``
decorator, similar to Django's ``@override_settings``, to mock permission
checks.
|