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 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 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