<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://johnwick.cc/index.php?action=history&amp;feed=atom&amp;title=Multitenant_Permissions_in_Django%3A_Client-Based_Access_Control</id>
	<title>Multitenant Permissions in Django: Client-Based Access Control - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://johnwick.cc/index.php?action=history&amp;feed=atom&amp;title=Multitenant_Permissions_in_Django%3A_Client-Based_Access_Control"/>
	<link rel="alternate" type="text/html" href="https://johnwick.cc/index.php?title=Multitenant_Permissions_in_Django:_Client-Based_Access_Control&amp;action=history"/>
	<updated>2026-05-07T02:33:42Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.44.1</generator>
	<entry>
		<id>https://johnwick.cc/index.php?title=Multitenant_Permissions_in_Django:_Client-Based_Access_Control&amp;diff=3172&amp;oldid=prev</id>
		<title>PC: Created page with &quot;650px  In multitenant SaaS applications, isolating data access per tenant (e.g. client, company, or organization) is crucial. This article explores how to implement client-based access control in Django, ensuring users can only access data belonging to their organization.  We will walk through real-world modeling, query restrictions, permission enforcement, middleware, and role-based layering with practical examples to sec...&quot;</title>
		<link rel="alternate" type="text/html" href="https://johnwick.cc/index.php?title=Multitenant_Permissions_in_Django:_Client-Based_Access_Control&amp;diff=3172&amp;oldid=prev"/>
		<updated>2025-12-13T22:55:53Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;&lt;a href=&quot;/index.php?title=File:Multitenant_Permissions_in_Django.jpg&quot; title=&quot;File:Multitenant Permissions in Django.jpg&quot;&gt;650px&lt;/a&gt;  In multitenant SaaS applications, isolating data access per tenant (e.g. client, company, or organization) is crucial. This article explores how to implement client-based access control in Django, ensuring users can only access data belonging to their organization.  We will walk through real-world modeling, query restrictions, permission enforcement, middleware, and role-based layering with practical examples to sec...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;[[file:Multitenant_Permissions_in_Django.jpg|650px]]&lt;br /&gt;
&lt;br /&gt;
In multitenant SaaS applications, isolating data access per tenant (e.g. client, company, or organization) is crucial. This article explores how to implement client-based access control in Django, ensuring users can only access data belonging to their organization.&lt;br /&gt;
&lt;br /&gt;
We will walk through real-world modeling, query restrictions, permission enforcement, middleware, and role-based layering with practical examples to secure your SaaS platform.&lt;br /&gt;
&lt;br /&gt;
Use Case: SaaS with Multiple Clients&lt;br /&gt;
Imagine a platform where companies (clients) sign up, and each company has multiple users. These users should only access data owned by their respective companies.&lt;br /&gt;
Example:&lt;br /&gt;
* 		Company A and Company B both use your SaaS.&lt;br /&gt;
* 		A user from Company A should not see Company B’s resources, even if they share the same resource type.&lt;br /&gt;
Multitenancy in Django can be achieved in two major ways:&lt;br /&gt;
* 		Shared Database with Discriminator Field (e.g. client_id) — simpler and widely used.&lt;br /&gt;
* 		Separate schemas or databases per tenant — more isolation is used in advanced setups.&lt;br /&gt;
This article focuses on the shared database pattern for its simplicity and ease of maintenance.&lt;br /&gt;
Core Concepts&lt;br /&gt;
* 		Tenant Isolation Every model must be scoped by a foreign key to Client, which serves as the tenant identifier. This prevents cross-client data leakage.&lt;br /&gt;
* 		Request Context Awareness Every request is associated with an authenticated user, and all data access should be scoped to request.user.client.&lt;br /&gt;
* 		Role-Based Access Permissions can be fine-tuned by assigning roles within a tenant, e.g. Admin, Manager, Staff. Roles determine the scope of access within the tenant’s data.&lt;br /&gt;
* 		Security-First Validation Do not rely on frontend submissions for client IDs. Enforce permissions and data ownership at the backend.&lt;br /&gt;
* 		Scalability through Middleware and Reusability Encapsulate tenant extraction logic in middleware, and permission logic in reusable classes.&lt;br /&gt;
Step-by-Step Implementation&lt;br /&gt;
&lt;br /&gt;
1. Define the Models&lt;br /&gt;
At the core of a multitenant system is the relationship between Client, User, and domain models such as Project, Invoice, Report, etc.&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
class Client(models.Model):&lt;br /&gt;
    name = models.CharField(max_length=255)&lt;br /&gt;
    created_at = models.DateTimeField(auto_now_add=True)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
class User(AbstractUser):&lt;br /&gt;
    client = models.ForeignKey(Client, on_delete=models.CASCADE)&lt;br /&gt;
    role = models.CharField(&lt;br /&gt;
        max_length=20,&lt;br /&gt;
        choices=[(&amp;#039;admin&amp;#039;, &amp;#039;Admin&amp;#039;), (&amp;#039;manager&amp;#039;, &amp;#039;Manager&amp;#039;), (&amp;#039;staff&amp;#039;, &amp;#039;Staff&amp;#039;)],&lt;br /&gt;
        default=&amp;#039;staff&amp;#039;&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
class Project(models.Model):&lt;br /&gt;
    client = models.ForeignKey(Client, on_delete=models.CASCADE)&lt;br /&gt;
    name = models.CharField(max_length=255)&lt;br /&gt;
    owner = models.ForeignKey(User, on_delete=models.CASCADE)&lt;br /&gt;
    created_at = models.DateTimeField(auto_now_add=True)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add client to every critical model to tie them to their owner organization.&lt;br /&gt;
&lt;br /&gt;
2. Use Custom Permissions&lt;br /&gt;
Start by creating a basic permission class that ensures the object being accessed belongs to the same client as the requesting user.&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
from rest_framework.permissions import BasePermission&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
class IsSameClient(BasePermission):&lt;br /&gt;
    def has_object_permission(self, request, view, obj):&lt;br /&gt;
        return hasattr(obj, &amp;#039;client&amp;#039;) and obj.client == request.user.client&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Apply this permission class to views or override the queryset in viewsets to restrict access:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
class ProjectViewSet(viewsets.ModelViewSet):&lt;br /&gt;
    serializer_class = ProjectSerializer&lt;br /&gt;
    permission_classes = [IsAuthenticated, IsSameClient]&lt;br /&gt;
&lt;br /&gt;
    def get_queryset(self):&lt;br /&gt;
        return Project.objects.filter(client=self.request.user.client)&lt;br /&gt;
    &lt;br /&gt;
    def perform_create(self, serializer):&lt;br /&gt;
        serializer.save(client=self.request.user.client, owner=self.request.user)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Admin and Role Logic&lt;br /&gt;
Use roles to control privilege within the same tenant.&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
class UserRole(models.TextChoices):&lt;br /&gt;
    ADMIN = &amp;#039;admin&amp;#039;, &amp;#039;Admin&amp;#039;&lt;br /&gt;
    MANAGER = &amp;#039;manager&amp;#039;, &amp;#039;Manager&amp;#039;&lt;br /&gt;
    STAFF = &amp;#039;staff&amp;#039;, &amp;#039;Staff&amp;#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
class User(AbstractUser):&lt;br /&gt;
    client = models.ForeignKey(Client, on_delete=models.CASCADE)&lt;br /&gt;
    role = models.CharField(max_length=20, choices=UserRole.choices, default=UserRole.STAFF)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Create a compound permission class:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
class IsClientAdminOrOwner(BasePermission):&lt;br /&gt;
    def has_object_permission(self, request, view, obj):&lt;br /&gt;
        return (&lt;br /&gt;
            obj.client == request.user.client and (&lt;br /&gt;
                request.user.role == UserRole.ADMIN or&lt;br /&gt;
                obj.owner == request.user&lt;br /&gt;
            )&lt;br /&gt;
        )&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Use role checks in serializers to allow certain fields to be modified only by admins:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
class ProjectSerializer(serializers.ModelSerializer):&lt;br /&gt;
    class Meta:&lt;br /&gt;
        model = Project&lt;br /&gt;
        fields = [&amp;#039;id&amp;#039;, &amp;#039;name&amp;#039;, &amp;#039;owner&amp;#039;, &amp;#039;created_at&amp;#039;]&lt;br /&gt;
        read_only_fields = [&amp;#039;owner&amp;#039;]&lt;br /&gt;
    &lt;br /&gt;
    def validate(self, attrs):&lt;br /&gt;
        if self.context[&amp;#039;request&amp;#039;].user.role != UserRole.ADMIN:&lt;br /&gt;
            raise serializers.ValidationError(&amp;quot;Only admins can create projects.&amp;quot;)&lt;br /&gt;
        return super().validate(attrs)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Middleware for Tenant Access&lt;br /&gt;
For large applications, separating out the logic to determine tenant context improves code clarity.&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
class ClientMiddleware:&lt;br /&gt;
    def __init__(self, get_response):&lt;br /&gt;
        self.get_response = get_response&lt;br /&gt;
&lt;br /&gt;
    def __call__(self, request):&lt;br /&gt;
        if request.user.is_authenticated:&lt;br /&gt;
            request.tenant = request.user.client&lt;br /&gt;
        return self.get_response(request)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Serializers: Injecting Context&lt;br /&gt;
Avoid exposing client in the serializer:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
class ProjectSerializer(serializers.ModelSerializer):&lt;br /&gt;
    class Meta:&lt;br /&gt;
        model = Project&lt;br /&gt;
        fields = [&amp;#039;id&amp;#039;, &amp;#039;name&amp;#039;, &amp;#039;created_at&amp;#039;]&lt;br /&gt;
&lt;br /&gt;
    def create(self, validated_data):&lt;br /&gt;
        validated_data[&amp;#039;client&amp;#039;] = self.context[&amp;#039;request&amp;#039;].user.client&lt;br /&gt;
        validated_data[&amp;#039;owner&amp;#039;] = self.context[&amp;#039;request&amp;#039;].user&lt;br /&gt;
        return super().create(validated_data)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Best Practices&lt;br /&gt;
* 		Never trust user-submitted client_id — always derive it from the authenticated user.&lt;br /&gt;
* 		Centralize permission logic — avoid duplicating access checks across views.&lt;br /&gt;
* 		Write shared mixins and base viewsets to encapsulate multitenant behavior.&lt;br /&gt;
* 		Document your permission model for better team collaboration.&lt;br /&gt;
Common Mistakes&lt;br /&gt;
* 		❌ Forgetting to filter queryset by client.&lt;br /&gt;
* 		❌ Accepting client_id in POST bodies.&lt;br /&gt;
* 		❌ Missing role-based logic where needed (e.g. staff modifying others’ resources).&lt;br /&gt;
* 		❌ Not testing cross-tenant access violations.&lt;br /&gt;
Testing Access Control&lt;br /&gt;
Example test cases:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
def test_user_cannot_access_others_project(api_client, user_a, project_b):&lt;br /&gt;
    api_client.force_authenticate(user=user_a)&lt;br /&gt;
    response = api_client.get(f&amp;#039;/api/projects/{project_b.id}/&amp;#039;)&lt;br /&gt;
    assert response.status_code == 404&lt;br /&gt;
&lt;br /&gt;
def test_admin_can_create_project(api_client, admin_user):&lt;br /&gt;
    api_client.force_authenticate(user=admin_user)&lt;br /&gt;
    data = {&amp;#039;name&amp;#039;: &amp;#039;New Project&amp;#039;}&lt;br /&gt;
    response = api_client.post(&amp;#039;/api/projects/&amp;#039;, data)&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Also, consider integration tests using tenant fixtures.&lt;br /&gt;
Multitenant permission management in Django ensures strong data isolation and secure access control across client organizations. Implementing it correctly requires:&lt;br /&gt;
* 		Consistent tenant-scoping of all data models&lt;br /&gt;
* 		Permission classes that validate client ownership&lt;br /&gt;
* 		Role-based access is layered on top of tenant logic&lt;br /&gt;
* 		Middleware and reusable patterns to reduce repetition&lt;br /&gt;
* 		Strong testing to verify proper isolation&lt;br /&gt;
With these patterns, you can confidently scale your SaaS platform across multiple clients while maintaining privacy, security, and compliance.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Read the full article here: https://medium.com/django-unleashed/multitenant-permissions-in-django-client-based-access-control-3af2fec01ebb&lt;/div&gt;</summary>
		<author><name>PC</name></author>
	</entry>
</feed>