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:
as a “permission filter” in the resource
QuerySet, to filter elements for which the predicate is trueas 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.