Skip to content

Multi-tenant SaaS Application

This example shows how to implement permissions for a multi-tenant SaaS application with organizations, teams, and role-based access.

Models

# models.py
from django.db import models
from django.contrib.auth import get_user_model

User = get_user_model()

class Organization(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='owned_orgs')
    plan = models.CharField(max_length=20, default='free')  # free, pro, enterprise
    created_at = models.DateTimeField(auto_now_add=True)

class Membership(models.Model):
    ROLE_CHOICES = [
        ('owner', 'Owner'),
        ('admin', 'Admin'),
        ('member', 'Member'),
        ('viewer', 'Viewer'),
    ]

    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='memberships')
    organization = models.ForeignKey(Organization, on_delete=models.CASCADE, related_name='memberships')
    role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='member')
    joined_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ['user', 'organization']

class Project(models.Model):
    organization = models.ForeignKey(Organization, on_delete=models.CASCADE, related_name='projects')
    name = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    is_archived = models.BooleanField(default=False)
    created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)

class Task(models.Model):
    project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='tasks')
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    assignee = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
    is_completed = models.BooleanField(default=False)
    created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='created_tasks')

class Document(models.Model):
    project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='documents')
    title = models.CharField(max_length=200)
    content = models.TextField()
    is_public = models.BooleanField(default=False)
    created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)

Permissions

# permissions.py
from django_shield import rule
from .models import Membership

def get_membership(user, organization):
    """Helper to get user's membership in an organization."""
    try:
        return Membership.objects.get(user=user, organization=organization)
    except Membership.DoesNotExist:
        return None

def get_user_role(user, organization):
    """Get user's role in organization."""
    membership = get_membership(user, organization)
    return membership.role if membership else None

# Organization permissions
@rule
def is_org_member(user, org):
    return Membership.objects.filter(user=user, organization=org).exists()

@rule
def is_org_admin(user, org):
    role = get_user_role(user, org)
    return role in ['owner', 'admin']

@rule
def is_org_owner(user, org):
    return org.owner == user

@rule
def can_manage_org(user, org):
    return org.owner == user or user.is_superuser

@rule
def can_invite_members(user, org):
    role = get_user_role(user, org)
    return role in ['owner', 'admin']

@rule
def can_remove_members(user, org):
    role = get_user_role(user, org)
    return role in ['owner', 'admin']

# Project permissions
@rule
def can_view_project(user, project):
    if project.is_archived:
        role = get_user_role(user, project.organization)
        return role in ['owner', 'admin']
    return is_org_member.predicate(user, project.organization)

@rule
def can_edit_project(user, project):
    role = get_user_role(user, project.organization)
    return role in ['owner', 'admin', 'member']

@rule
def can_delete_project(user, project):
    role = get_user_role(user, project.organization)
    return role in ['owner', 'admin']

@rule
def can_archive_project(user, project):
    role = get_user_role(user, project.organization)
    return role in ['owner', 'admin']

# Task permissions
@rule
def can_view_task(user, task):
    return is_org_member.predicate(user, task.project.organization)

@rule
def can_edit_task(user, task):
    role = get_user_role(user, task.project.organization)
    if role == 'viewer':
        return False
    return True

@rule
def can_assign_task(user, task):
    role = get_user_role(user, task.project.organization)
    return role in ['owner', 'admin', 'member']

@rule
def is_task_assignee(user, task):
    return task.assignee == user

# Document permissions
@rule
def can_view_document(user, document):
    if document.is_public:
        return True
    return is_org_member.predicate(user, document.project.organization)

@rule
def can_edit_document(user, document):
    role = get_user_role(user, document.project.organization)
    return role in ['owner', 'admin', 'member']

@rule
def can_delete_document(user, document):
    if document.created_by == user:
        return True
    role = get_user_role(user, document.project.organization)
    return role in ['owner', 'admin']

# Plan-based permissions
@rule
def has_pro_plan(user, org):
    return org.plan in ['pro', 'enterprise']

@rule
def has_enterprise_plan(user, org):
    return org.plan == 'enterprise'

@rule
def can_use_advanced_features(user, org):
    return org.plan in ['pro', 'enterprise'] and is_org_member.predicate(user, org)

Views

# views.py
from django.shortcuts import render, redirect, get_object_or_404
from django_shield import guard
from .models import Organization, Project, Task, Document, Membership

# Organization views
@guard('is_org_member', model=Organization, lookup='org_slug', lookup_field='slug', inject='org')
def org_dashboard(request, org_slug, org):
    projects = org.projects.filter(is_archived=False)
    return render(request, 'saas/org_dashboard.html', {
        'org': org,
        'projects': projects
    })

@guard('can_manage_org', model=Organization, lookup='org_slug', lookup_field='slug', inject='org')
def org_settings(request, org_slug, org):
    if request.method == 'POST':
        org.name = request.POST['name']
        org.save()
        return redirect('org_dashboard', org_slug=org.slug)
    return render(request, 'saas/org_settings.html', {'org': org})

@guard('can_invite_members', model=Organization, lookup='org_slug', lookup_field='slug', inject='org')
def member_invite(request, org_slug, org):
    if request.method == 'POST':
        # Invite logic here
        ...
    return render(request, 'saas/member_invite.html', {'org': org})

@guard('is_org_admin', model=Organization, lookup='org_slug', lookup_field='slug', inject='org')
def member_list(request, org_slug, org):
    members = org.memberships.select_related('user')
    return render(request, 'saas/member_list.html', {
        'org': org,
        'members': members
    })

# Project views
@guard('can_view_project', model=Project, inject='project')
def project_detail(request, pk, project):
    tasks = project.tasks.all()
    documents = project.documents.all()
    return render(request, 'saas/project_detail.html', {
        'project': project,
        'tasks': tasks,
        'documents': documents
    })

@guard('is_org_member', model=Organization, lookup='org_slug', lookup_field='slug', inject='org')
def project_create(request, org_slug, org):
    role = get_user_role(request.user, org)
    if role == 'viewer':
        raise PermissionDenied("Viewers cannot create projects")

    if request.method == 'POST':
        project = Project.objects.create(
            organization=org,
            name=request.POST['name'],
            description=request.POST.get('description', ''),
            created_by=request.user
        )
        return redirect('project_detail', pk=project.pk)
    return render(request, 'saas/project_form.html', {'org': org})

@guard('can_edit_project', model=Project, inject='project')
def project_edit(request, pk, project):
    if request.method == 'POST':
        project.name = request.POST['name']
        project.description = request.POST.get('description', '')
        project.save()
        return redirect('project_detail', pk=project.pk)
    return render(request, 'saas/project_form.html', {'project': project})

@guard('can_archive_project', model=Project, inject='project')
def project_archive(request, pk, project):
    project.is_archived = True
    project.save()
    return redirect('org_dashboard', org_slug=project.organization.slug)

# Task views
@guard('can_view_task', model=Task, inject='task')
def task_detail(request, pk, task):
    return render(request, 'saas/task_detail.html', {'task': task})

@guard('can_edit_project', model=Project, lookup='project_pk', inject='project')
def task_create(request, project_pk, project):
    if request.method == 'POST':
        task = Task.objects.create(
            project=project,
            title=request.POST['title'],
            description=request.POST.get('description', ''),
            created_by=request.user
        )
        return redirect('task_detail', pk=task.pk)
    return render(request, 'saas/task_form.html', {'project': project})

@guard('can_edit_task', model=Task, inject='task')
def task_edit(request, pk, task):
    if request.method == 'POST':
        task.title = request.POST['title']
        task.description = request.POST.get('description', '')
        task.save()
        return redirect('task_detail', pk=task.pk)
    return render(request, 'saas/task_form.html', {'task': task})

@guard.any('is_task_assignee', 'can_assign_task', model=Task, inject='task')
def task_complete(request, pk, task):
    task.is_completed = True
    task.save()
    return redirect('task_detail', pk=task.pk)

# Document views
@guard('can_view_document', model=Document, inject='document')
def document_detail(request, pk, document):
    return render(request, 'saas/document_detail.html', {'document': document})

@guard('can_edit_document', model=Document, inject='document')
def document_edit(request, pk, document):
    if request.method == 'POST':
        document.title = request.POST['title']
        document.content = request.POST['content']
        document.save()
        return redirect('document_detail', pk=document.pk)
    return render(request, 'saas/document_form.html', {'document': document})

@guard('can_delete_document', model=Document, inject='document')
def document_delete(request, pk, document):
    project_pk = document.project.pk
    document.delete()
    return redirect('project_detail', pk=project_pk)

# Plan-based feature gates
@guard.all('is_org_member', 'has_pro_plan', model=Organization, lookup='org_slug', lookup_field='slug')
def advanced_analytics(request, org_slug):
    # Pro feature
    ...

@guard.all('is_org_admin', 'has_enterprise_plan', model=Organization, lookup='org_slug', lookup_field='slug')
def sso_settings(request, org_slug):
    # Enterprise feature
    ...

Expression Syntax Examples

# Role-based checks with helper
@guard('is_org_admin', model=Organization, lookup='org_slug', lookup_field='slug')
def admin_panel(request, org_slug):
    ...

# Owner check
@guard('obj.owner == user', model=Organization, lookup='org_slug', lookup_field='slug')
def org_billing(request, org_slug):
    ...

# Plan-based feature with membership check
@guard.all('is_org_member', 'has_pro_plan', model=Organization)
def pro_feature(request, pk):
    ...

URLs

# urls.py
from django.urls import path
from . import views

urlpatterns = [
    # Organization
    path('orgs/<slug:org_slug>/', views.org_dashboard, name='org_dashboard'),
    path('orgs/<slug:org_slug>/settings/', views.org_settings, name='org_settings'),
    path('orgs/<slug:org_slug>/members/', views.member_list, name='member_list'),
    path('orgs/<slug:org_slug>/members/invite/', views.member_invite, name='member_invite'),

    # Projects
    path('orgs/<slug:org_slug>/projects/new/', views.project_create, name='project_create'),
    path('projects/<int:pk>/', views.project_detail, name='project_detail'),
    path('projects/<int:pk>/edit/', views.project_edit, name='project_edit'),
    path('projects/<int:pk>/archive/', views.project_archive, name='project_archive'),

    # Tasks
    path('projects/<int:project_pk>/tasks/new/', views.task_create, name='task_create'),
    path('tasks/<int:pk>/', views.task_detail, name='task_detail'),
    path('tasks/<int:pk>/edit/', views.task_edit, name='task_edit'),
    path('tasks/<int:pk>/complete/', views.task_complete, name='task_complete'),

    # Documents
    path('documents/<int:pk>/', views.document_detail, name='document_detail'),
    path('documents/<int:pk>/edit/', views.document_edit, name='document_edit'),
    path('documents/<int:pk>/delete/', views.document_delete, name='document_delete'),

    # Plan-gated features
    path('orgs/<slug:org_slug>/analytics/', views.advanced_analytics, name='advanced_analytics'),
    path('orgs/<slug:org_slug>/sso/', views.sso_settings, name='sso_settings'),
]

Role Hierarchy

Role Permissions
Owner Full access, billing, delete org
Admin Manage members, all projects/tasks
Member Create/edit projects and tasks
Viewer Read-only access

Permission Summary

Action Required Role/Condition
View org dashboard Any member
Manage org settings Owner only
Invite members Owner, Admin
Remove members Owner, Admin
Create project Owner, Admin, Member
Edit project Owner, Admin, Member
Delete/archive project Owner, Admin
View task Any member
Edit task Owner, Admin, Member
Complete task Assignee, or Admin+
View document Member, or public
Edit document Owner, Admin, Member
Delete document Creator, or Admin+
Advanced analytics Pro plan members
SSO settings Enterprise Admins