# How Workspace-Level Permissions and Role-Based Access Control Work in Plane's Django API

> Learn how Plane's Django API implements workspace-level permissions and role-based access control using integer-based role storage and DRF BasePermission subclasses for secure API access.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: deep-dive
- Published: 2026-06-22

---

**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`](https://github.com/makeplane/plane/blob/main/apps/api/plane/db/models/workspace.py). Here, the `WorkspaceMember` model links users to workspaces and stores their authorization level as an integer field.

```python

# 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`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/permissions/workspace.py). This module defines role constants that mirror the model choices and implements reusable `BasePermission` subclasses.

```python

# 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 `POST` requests 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 `Admin` role (20) on the workspace.
- **WorkSpaceAdminPermission**: Allows access to active users with either `Admin` or `Member` roles.
- **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`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/views/workspace/label.py), read-only access is granted to all workspace members:

```python

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

```python

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

1. **URL Resolution**: DRF extracts `workspace_slug` from the URL pattern and attaches it to the view instance.
2. **Authentication**: Middleware populates `request.user` via JWT or session authentication.
3. **Permission Evaluation**: DRF iterates through `permission_classes`, calling `has_permission(request, view)` on each.
4. **Database Validation**: The permission method queries `WorkspaceMember.objects.filter()` for a row matching the user, workspace slug, and required role constraints.
5. **Access Decision**: If the query returns a match, the view executes; otherwise, DRF returns `403 Forbidden` using the standardized response format defined in [`plane/utils/openapi/responses.py`](https://github.com/makeplane/plane/blob/main/plane/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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.role` in [`apps/api/plane/db/models/workspace.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/db/models/workspace.py) define the Admin/Member/Guest hierarchy.
- **Permission classes**: Reusable DRF `BasePermission` subclasses in [`apps/api/plane/app/permissions/workspace.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/permissions/workspace.py) centralize 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 `WorkspaceMember` against `workspace_slug` and 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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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.