Skip to content

Permissions System

Bifolk implements Role-Based Access Control (RBAC) where permissions are granted based on a user's role within an organization.

Target Audience: Software developers


Core Concepts

  1. Organizations: All business data belongs to an organization
  2. Membership: Users belong to organizations through OrganizationMembership
  3. Roles: Each membership has a role (Owner, Admin, Member, Viewer)
  4. Data Isolation: Users only see data from their organizations
User → OrganizationMembership (with Role) → Organization → Resources

Role Hierarchy

Owner > Admin > Member > Viewer
Role View Create Edit Own Edit All Delete Manage Members Delete Org
Owner Yes Yes Yes Yes Yes Yes Yes
Admin Yes Yes Yes Yes Yes Yes No
Member Yes Yes Yes No No No No
Viewer Yes No No No No No No

Permission Utility Functions

All centralized in organizations/utils.py.

Core Functions

from organizations.utils import (
    get_user_role,
    get_user_organizations,
    user_is_member,
)
Function Returns Description
get_user_role(user, organization) str or None User's role: 'owner', 'admin', 'member', 'viewer'
get_user_organizations(user) QuerySet All organizations user belongs to
user_is_member(user, organization) bool Active membership check

Resource Permission Functions

from organizations.utils import (
    user_can_view,
    user_can_create,
    user_can_edit,
    user_can_delete,
)
Function Parameters Rules
user_can_view(user, resource) Resource with organization attr Any member can view
user_can_create(user, organization) Organization Member+ can create
user_can_edit(user, resource) Resource with organization, created_by Owner/Admin: all; Member: own only
user_can_delete(user, resource) Resource with organization Owner/Admin only

Organization Management Functions

from organizations.utils import (
    user_can_manage_members,
    user_can_delete_org,
    user_can_change_org_settings,
)
Function Rules
user_can_manage_members(user, org) Owner/Admin
user_can_delete_org(user, org) Owner only
user_can_change_org_settings(user, org) Owner/Admin

Query Filtering

from organizations.utils import filter_by_user_organizations

user_hives = filter_by_user_organizations(Hive.objects.all(), request.user)

Model-Level Permissions

All business models implement permission methods:

class Hive(models.Model):
    organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
    created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)

    def user_can_view(self, user):
        # Any active member of the organization
        ...

    def user_can_edit(self, user):
        # Owner/Admin: all; Member: own only; Viewer: no
        ...

    def user_can_delete(self, user):
        # Owner/Admin only
        ...

Models with these methods: Hive, Queen, HiveInspection, HarvestRecord, all BeekeepingOperation subclasses, QueenBreeding, ColonySplit, InventoryItem, InventoryTransaction, ProductSale.


View-Level Permissions

In Views

from django.http import HttpResponseForbidden
from organizations.utils import user_can_edit

def edit_hive(request, hive_id):
    hive = get_object_or_404(Hive, id=hive_id)
    if not user_can_edit(request.user, hive):
        return HttpResponseForbidden()
    # proceed with edit...

Filtering Querysets

Always filter by current organization:

def hive_list(request):
    if request.current_organization:
        hives = Hive.objects.filter(organization=request.current_organization)
    else:
        hives = Hive.objects.none()
    return render(request, 'hives/hive_list.html', {'hives': hives})

Template-Level Permissions

{% load organization_tags %}

{% if hive.user_can_edit user %}
    <a href="{% url 'hives:edit' hive.id %}">Edit</a>
{% endif %}

{% if hive.user_can_delete user %}
    <a href="{% url 'hives:delete' hive.id %}">Delete</a>
{% endif %}

Organization Middleware

OrganizationMiddleware (organizations/middleware.py) runs after AuthenticationMiddleware and provides:

  • request.current_organization - Active organization (from session or default)
  • request.user_organizations - All organizations user belongs to

Switching: Users switch organizations via the sidebar selector. The selected organization is stored in the session.


Best Practices

  1. Always check permissions in views before modifying data
  2. Always filter querysets by request.current_organization
  3. Set organization on create: obj.organization = request.current_organization
  4. Use the utility functions instead of reimplementing permission logic
  5. Handle None organization for users without organizations
  6. Hide non-existent resources: Filter querysets to prevent revealing resource existence
# Prefer this (hides existence)
hive = get_object_or_404(Hive, id=hive_id, organization__in=request.user_organizations)

# Over this (reveals existence)
hive = get_object_or_404(Hive, id=hive_id)
if not hive.user_can_view(request.user):
    return HttpResponseForbidden()