.. _explanation-permissions:

===========
Permissions
===========

Concepts
========

Debusine uses a permission system inspired from `Role-Based Access Control
<https://en.wikipedia.org/wiki/Role-based_access_control>`_, where
*permissions* are assigned on *resources* based on the *roles* that a user has
on them.

Resources
---------

*Resources* are concrete objects in Debusine's data model, like scopes,
workspaces, collections, artifacts, work requests, and so on.

Roles
-----

*Roles* define the access level of a *group* of users on a resource. They are
often written in all-uppercase, like ``OWNER``, ``ADMIN``, ``CONTRIBUTOR``,
``VIEWER``. Note that roles are assigned to groups, not users, as Debusine is
built on the idea that all individual work could become teamwork with minimal
effort.

Groups
------

Groups of users are the main actor in the permission system.

A group is uniquely identified by a scope, a name, and optionally a workspace:
for example, there can be only one "Admin" group in a scope that is not
assigned to a workspace, although each workspace can have its own "Admin"
group.

When a user is added to a group they are also assigned a role on the group,
which can currently be ``MEMBER`` or ``ADMIN``, to explicitly allow some users
to add and remove members from the group.

Roles can be inferred
~~~~~~~~~~~~~~~~~~~~~

Checking for permissions on a resource does not only depend on the roles the
user has on that resource, because roles can be inferred from other roles:

* Having certain roles on a **scope** gives some permissions over its
  workspaces, collections, artifacts, workflows, and so on, without needing
  direct roles over them.
* Having roles on a **workspace** gives permissions over collections,
  artifacts, workflows, and so on, without needing direct roles over them.

In other words, permission checks on a resource may take into consideration
whether the user has broader roles or roles on a containing resource. For
example, any workspace ``CONTRIBUTOR`` is also a workspace ``VIEWER``, and any
scope ``OWNER`` is also a workspace ``OWNER``.

Roles may also be inferred from resource properties. For example, if
:py:attr:`debusine.db.models.Workspace.public` attribute is ``True`` then any
user will have the ``VIEWER`` role on the workspace.

Permissions
-----------

Permissions are checked by *permission predicates*, which are methods of
resource objects that take the current user as input and check if a permission
should be granted.

Permission checks
~~~~~~~~~~~~~~~~~

Permission predicates can be used to test permissions on an individual
resource: can the user display the contents of the collection being requested?
Can the user write to it?

Permission filters
~~~~~~~~~~~~~~~~~~

Permission predicates can be used to filter resources to display for UI
navigation or selection lists in forms. For example:

* list visible workspaces
* list executable workflows
* list writable collections as targets for a workflow

Extra groups for workflow templates
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

It is possible to add group names in the ``extra_groups`` attribute of a
workflow template, and all work requests of all workflows started with that
workflow template will behave, for permission checking purposes, as if the user
was also a member of those groups.

Note that once a workflow template grants extra groups, only members of those
groups can modify it.

Example of a permission check
=============================

Let's see, as an example, what happens if a user tries to display a workspace.

Workspaces have a ``Workspace.can_display`` method that is called by the web UI
or the server API whenever a workspace is accessed, and the method checks if
the current user has the ``VIEWER`` role.

Roles are not assigned directly to users, so Debusine will check roles on all
the groups that the current user is a member of. If the user is a member of any
group with a ``VIEWER`` role on the workspace, then the predicate will succeed,
otherwise it will fail and Debusine will deny the request.

The ``VIEWER`` role is defined so it can be:

* directly assigned to a group
* implied from the ``CONTRIBUTOR`` role
* implied by the workspace being public

The ``CONTRIBUTOR`` role is defined so it can be assigned directly or implied
by the ``OWNER`` role. In turn the ``OWNER`` role can be assigned directly or
implied by the ``OWNER`` role on the containing scope. The ``OWNER`` role on
the containing scope can only be assigned directly.

There can be complex sets of implications, but they are set up so that Debusine
is able to efficiently resolve them. In this example, the
``Workspace.can_display`` permission predicate will succeed if either the
workspace is public, or if the user is a member of any group for which any of
these condition is valid:

* The group has been assigned the ``VIEWER`` role on the workspace
* The group has been assigned the ``CONTRIBUTOR`` role on the workspace
* The group has been assigned the ``OWNER`` role on the workspace
* The group has been assigned the ``OWNER`` role on the containing scope


Further reading
===============

For more details on the permission system, see :ref:`permissions-reference`.
