File: permissions.rst

package info (click to toggle)
debusine 0.14.4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 15,344 kB
  • sloc: python: 198,722; sh: 850; javascript: 335; makefile: 117
file content (157 lines) | stat: -rw-r--r-- 5,982 bytes parent folder | download | duplicates (2)
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.