Permissions reference

This is a detailed reference to Debusine permission system. For a high-level explanation, see 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 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 debusine.db.models.scopes.ScopeRole and 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 debusine.db.models.permissions module defines the debusine.db.models.permissions.permission_filter() and debusine.db.models.permissions.permission_check() decorators that help implementing permission predicates.

The one parameter for a permission predicate is of type debusine.db.models.permissions.PermissionUser, which can be None (no user has been set), AnonymousUser (user has not logged in) or a 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 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 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 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.