.. _internal-api-resources:

=========
Resources
=========

Resources are a subset of database models in Debusine which have a common
structure around user display and permission management.

Resource metadata
=================

When a resource can have a user-provided description, it is in a
``description``  ``TextField`` whose contents can use Markdown formatting.

Resource roles
==============

Groups can have roles on resources, which are used for permission management.

The set of roles for a resource is defined as an enumerated type derived from
:py:class:`permissions.RoleBase`, and made available as a ``Roles`` attribute
in the model class. For example::

    class WorkspaceRoles(permissions.Roles, WorkspaceRoleBase, enum.ReprEnum):
        """Available roles for a Workspace."""

        OWNER = ...
        CONTRIBUTOR = ...
        VIEWER = ...


    WorkspaceRoles.setup()


    class Workspace(models.Model):
        """Workspace model."""

        Roles: TypeAlias = WorkspaceRoles


Roles can be entirely implied by roles on other resources, as is currently the
case with :py:class:`debusine.db.models.Collection`, or explicitly assigned to
groups.

When they are explicitly assigned to groups, it is done via a model with
``resource``, ``group``, and ``role`` members. For example::

    class WorkspaceRole(models.Model):
        """Role assignments for workspaces."""

        Roles: TypeAlias = WorkspaceRoles

        resource = models.ForeignKey(
            Workspace,
            on_delete=models.CASCADE,
            related_name="roles",
        )

        group = models.ForeignKey(
            "Group",
            on_delete=models.CASCADE,
            related_name="workspace_roles",
        )

        role = models.CharField(max_length=16, choices=Roles.choices)

        class Meta(TypedModelMeta):
            constraints = [
                UniqueConstraint(
                    fields=["resource", "group", "role"],
                    name="%(app_label)s_%(class)s_unique_resource_group_role",
                ),
            ]

The role assignment model is made accessible via the ``get_roles_model`` method
in the manager of the resource model::

    class WorkspaceManager(models.Manager["Workspace"]):
        ...

        def get_roles_model(self) -> type["WorkspaceRole"]:
            """Get the model used for role assignment."""
            return WorkspaceRole

Consistency is important in order to make use of structural typing to maintain
code that can operate on any type of resource.

Managers for resource models have at least these well defined methods::

    class WorkspaceManager(models.Manager["Workspace"]):
        ...

        def has_role(self, pc: PermissionContext, role: WorkspaceRoles) -> bool:
            """Check if the user has the given role on this Workspace."""

        def get_roles(self, pc: PermissionContext) -> frozenset[WorkspaceRoles]:
            """Get the effective roles of the user on this workspace."""

Querysets for resource roles have at least these well defined methods::

    class WorkspaceQuerySet[A](QuerySet["Workspace", A]):
        ...

        def with_role(self, pc: PermissionContext, role: WorkspaceRoles) -> Self:
            """Keep only resources where the user has the given role."""

The resource model itself has at least these well defined methods::

    class Workspace(models.Model):
        """Workspace model."""

        Roles: TypeAlias = WorkspaceRoles

        def has_role(self, pc: PermissionContext, role: WorkspaceRoles) -> bool:
            """Check if the user has the given role on this Workspace."""

        def get_roles(self, pc: PermissionContext) -> frozenset[WorkspaceRoles]:
            """Get the effective roles of the user on this workspace."""


List of resources
=================

* :py:class:`debusine.db.models.Scope`
* :py:class:`debusine.db.models.Workspace`
* :py:class:`debusine.db.models.Collection`
* :py:class:`debusine.db.models.Artifact`
* :py:class:`debusine.db.models.Asset`
* :py:class:`debusine.db.models.AssetUsage`
* :py:class:`debusine.db.models.Group`
* :py:class:`debusine.db.models.WorkRequest`
* :py:class:`debusine.db.models.WorkflowTemplate`
