.. _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 `_. 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_(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_(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.