How Workspace-Level Permissions and Role-Based Access Control Work in Plane's Django API
Plane implements workspace-level RBAC through integer-based role storage on the WorkspaceMember model and a hierarchy of DRF BasePermission subclasses that query membership status before allowing API access.
The open-source project management platform Plane relies on Django REST Framework (DRF) to secure its multi-tenant workspace architecture. Understanding how workspace-level permissions and role-based access control are enforced requires examining the interaction between the membership data model, centralized permission classes, and view-level declarations. This architecture ensures that only authenticated users with appropriate roles—Admin (20), Member (15), or Guest (5)—can access or modify workspace resources.
Role Definitions in the Data Model
The foundation of Plane’s RBAC system lives in apps/api/plane/db/models/workspace.py. Here, the WorkspaceMember model links users to workspaces and stores their authorization level as an integer field.
# apps/api/plane/db/models/workspace.py
ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest"))
class WorkspaceMember(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_member"
)
member = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="member_workspace",
)
# Role field drives all permission decisions
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=5)
is_active = models.BooleanField(default=True)
The integer values provide a hierarchical comparison mechanism. Higher values indicate greater privileges, allowing the permission layer to use role__gte (greater than or equal) queries when checking for minimum access levels.
DRF Permission Classes
All workspace-level authorization logic is centralized in apps/api/plane/app/permissions/workspace.py. This module defines role constants that mirror the model choices and implements reusable BasePermission subclasses.
# apps/api/plane/app/permissions/workspace.py
Admin = 20
Member = 15
Guest = 5
Core Permission Classes
Each class implements a specific access pattern by querying the WorkspaceMember table against the workspace_slug extracted from the view:
- WorkSpaceBasePermission: Grants
POSTrequests to any authenticated user, allows safe methods (GET,HEAD,OPTIONS) for active members, permits updates (PUT/PATCH) to Admins and Members, and restricts deletes to Admins only. - WorkspaceOwnerPermission: Restricts access to users with an explicit
Adminrole (20) on the workspace. - WorkSpaceAdminPermission: Allows access to active users with either
AdminorMemberroles. - WorkspaceEntityPermission: Permits read access to any active member, but limits write operations to Admins and Members.
- WorkspaceViewerPermission: Read-only access for any active workspace member regardless of role.
- WorkspaceUserPermission: Identical to
WorkspaceViewerPermission, used for endpoints requiring user-specific membership validation.
Every class first checks request.user.is_anonymous to reject unauthenticated traffic before executing database queries.
Integrating Permissions into API Views
Workspace-related viewsets import these classes and declare them in the permission_classes list. DRF automatically enforces these checks before executing view logic.
In apps/api/plane/app/views/workspace/label.py, read-only access is granted to all workspace members:
# apps/api/plane/app/views/workspace/label.py
from plane.app.permissions import WorkspaceViewerPermission
class WorkspaceLabelsEndpoint(BaseAPIView):
permission_classes = [WorkspaceViewerPermission]
def get(self, request, workspace_slug):
# Only authenticated, active workspace members reach this code
labels = Label.objects.filter(workspace__slug=workspace_slug)
return Response(LabelSerializer(labels, many=True).data)
For destructive operations, stricter permissions apply. The member management endpoint uses WorkspaceEntityPermission to ensure only Admins and Members can modify memberships:
# apps/api/plane/app/views/workspace/member.py
from plane.app.permissions import WorkspaceEntityPermission
class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
permission_classes = [WorkspaceEntityPermission]
def delete(self, request, workspace_slug, member_id):
# Permission class guarantees role >= Member (15) and is_active=True
WorkspaceMember.objects.filter(
workspace__slug=workspace_slug,
member_id=member_id
).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
Runtime Permission Flow
The enforcement mechanism follows DRF’s standard request lifecycle:
- URL Resolution: DRF extracts
workspace_slugfrom the URL pattern and attaches it to the view instance. - Authentication: Middleware populates
request.uservia JWT or session authentication. - Permission Evaluation: DRF iterates through
permission_classes, callinghas_permission(request, view)on each. - Database Validation: The permission method queries
WorkspaceMember.objects.filter()for a row matching the user, workspace slug, and required role constraints. - Access Decision: If the query returns a match, the view executes; otherwise, DRF returns
403 Forbiddenusing the standardized response format defined inplane/utils/openapi/responses.py.
This design centralizes security logic, preventing authorization bugs that could arise from implementing checks individually in each view.
Extending the RBAC System
Adding new roles requires only two changes. First, append the role to ROLE_CHOICES in the model file with a unique integer value. Second, update the constants in apps/api/plane/app/permissions/workspace.py to reflect the new level.
For endpoint-specific logic, developers can subclass BasePermission or compose existing classes. The allow_permission utilities in plane/app/permissions/__init__.py provide helper methods for complex conditional checks without duplicating query logic.
Summary
- Role storage: Integer values (20, 15, 5) on
WorkspaceMember.roleinapps/api/plane/db/models/workspace.pydefine the Admin/Member/Guest hierarchy. - Permission classes: Reusable DRF
BasePermissionsubclasses inapps/api/plane/app/permissions/workspace.pycentralize all workspace-level access checks. - View integration: API views declare permissions in
permission_classes, allowing DRF to automatically reject unauthorized requests before business logic executes. - Query pattern: All permissions verify active membership by querying
WorkspaceMemberagainstworkspace_slugand the required role bitmask. - Extensibility: New roles require updates only to the model choices and permission constants, maintaining consistency across the API surface.
Frequently Asked Questions
What database query checks workspace permissions in Plane?
The permission classes execute WorkspaceMember.objects.filter(member=request.user, workspace__slug=view.workspace_slug, role__in=[Admin, Member], is_active=True).exists() to validate access. This query runs for every request to workspace-scoped endpoints, verifying both the user's role and active status.
Can Guests (role 5) write data to a workspace?
No. According to the permission implementations in apps/api/plane/app/permissions/workspace.py, Guests are excluded from write operations. Classes like WorkspaceEntityPermission and WorkSpaceAdminPermission explicitly filter for role__in=[Admin, Member], blocking Guests from POST, PUT, PATCH, and DELETE requests while allowing read access through WorkspaceViewerPermission.
How does Plane handle unauthenticated API requests?
All workspace permission classes check request.user.is_anonymous as their first operation. If the user is anonymous, the method returns False immediately, causing DRF to return a 403 Forbidden response before any database queries execute. This prevents unnecessary load and ensures consistent error responses defined in plane/utils/openapi/responses.py.
Where should I add a new "Editor" role between Member and Admin?
Add the new role to ROLE_CHOICES in apps/api/plane/db/models/workspace.py with a value between 15 and 20 (e.g., 10 or 12). Then update apps/api/plane/app/permissions/workspace.py to include the new constant. Existing permission classes can be updated to include the new role in their role__in filters, or you can create a new permission class specifically for Editor-level access.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →