Multi-Tenancy and Authorization¶
Goal¶
Prismio enforces tenant isolation at the application layer with secure defaults:
- A user can belong to many organizations.
- Each membership has a role and status.
- Each request runs in exactly one active organization.
- UI and API share the same authorization path.
- Organization scoping is server-side and automatic where possible.
Core Components¶
Models¶
Defined in prismio/organizations/models.py:
Organization: tenant entity.Role: role record (owner,admin, etc).RolePermission: codename permissions attached to each role.OrganizationMembership:user + organization + role + status.MembershipStatus:active,invited,disabled.OrganizationOwnedModel(abstract): base class for tenant-owned data.Project: reference tenant-owned model for API examples.
Query Scoping¶
Defined in prismio/organizations/managers.py:
OrganizationQuerySet.for_organization(organization)
Tenant-owned models now use a scoped default manager during requests with an
active organization context. In those requests, MyModel.objects.all() is
automatically scoped to the active organization.
For defense in depth, keep explicit scoping in sensitive paths:
When truly cross-organization access is required (for example, internal admin and maintenance tasks), use the explicit unscoped manager:
OrganizationOwnedModel uses this manager so tenant-owned models can do:
Request Context¶
Defined in prismio/organizations/services.py:
OrganizationContextdataclass:organizationmembershiproleresolve_organization_context(user=..., organization_id=...)
The resolver validates:
- Authenticated user
- Organization exists and is active
- Membership exists
- Membership status is
active - Role exists and is active
Permission Service¶
Defined in prismio/organizations/permissions.py:
user_has_permission(org_context, codename)require_permission(org_context, codename)require_object_in_active_org(obj, org_context)
Suggested codename style:
projects.viewprojects.createprojects.updateprojects.deletemembers.invitebilling.manage
Middleware and Active Organization¶
prismio/organizations/middleware.py:
- Reads
request.session["active_organization_id"]. - Resolves context with
resolve_organization_context. - Attaches to request:
request.organization_contextrequest.organizationrequest.membership- Sets request-local organization context used by scoped managers.
- Clears invalid organization session state automatically.
The only supported way to change the active organization is the organization
switch endpoint handled by prismio/organizations/views.py::OrganizationAuthRedirectView.
That view is used both after login and when the user selects a different
organization in the UI.
UI Authorization Path¶
prismio/organizations/mixins.py:
OrganizationScopedMixin- Requires the URL org to match the already-active organization.
- Redirects to
organizations:organization_authwhen the active org does not match. - Enforces optional
required_permission. - Scopes queryset in
get_queryset()when.for_organization()exists.
prismio/organizations/views.py:
OrganizationAuthRedirectView- Sets
request.session["active_organization_id"]and the matching organization payload. - Runs on login redirect and on explicit org switching in the UI.
Use in class-based views:
class ExampleView(LoginRequiredMixin, OrganizationScopedMixin, ListView):
model = Project
required_permission = "projects.view"
API Authorization Path¶
prismio/organizations/permissions.py:
HasOrganizationContextDRF permission:- Requires the request to already have an active organization context.
- Requires the active organization to match
organization_idfrom the route. - Enforces action permission via
view.get_required_permission(). - Validates object organization in
has_object_permission.
prismio/organizations/viewsets.py:
OrganizationScopedViewSet:permission_classes = [HasOrganizationContext]- Declarative
permission_map - Scoped
get_queryset() - Server-side
perform_create(serializer.save(organization=request.organization))
API requests cannot switch organizations by changing the URL alone. A user must first switch the active organization through the UI auth path.
Example endpoint pattern:
Creating a New Tenant-Owned Model¶
1. Define model¶
from django.db import models
from prismio.organizations.models import OrganizationOwnedModel
class Report(OrganizationOwnedModel):
title = models.CharField(max_length=255)
2. Serializer must not accept organization from client¶
from rest_framework import serializers
class ReportSerializer(serializers.ModelSerializer):
organization = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Report
fields = ["id", "title", "organization"]
3. API ViewSet¶
from prismio.organizations.viewsets import OrganizationScopedViewSet
class ReportViewSet(OrganizationScopedViewSet):
queryset = Report.objects.all()
serializer_class = ReportSerializer
permission_map = {
"list": "reports.view",
"retrieve": "reports.view",
"create": "reports.create",
"update": "reports.update",
"partial_update": "reports.update",
"destroy": "reports.delete",
}
4. URL shape¶
Use org-scoped URLs:
5. UI View (if needed)¶
Use OrganizationScopedMixin and set required_permission.
6. Querying rules¶
Do:
Also acceptable in request-scoped code:
Prefer explicit for_organization(...) for defense in depth on critical read
and write paths.
Avoid:
Report.objects.get(pk=pk)
Report.unscoped_objects.all() # unless explicitly needed for cross-org work
Role and Permission Bootstrap¶
ensure_default_roles() seeds baseline roles and permissions.
Call this in setup paths where role records may be needed before user actions (for example, organization auth/create flows and selected tests).
Required Test Coverage¶
At minimum, include tests for:
- Cross-organization object access is denied.
- Inactive membership is denied.
- Wrong role is denied for mutating actions.
- Client cannot force
organizationon create. - UI and API enforce the same policy.
- Switching active organization changes visible tenant data.
Reference tests:
prismio/organizations/tests/test_multitenancy_security.py
Security Defaults Summary¶
- Tenant-owned models inherit
OrganizationOwnedModel. - API uses
OrganizationScopedViewSet. - UI uses
OrganizationScopedMixin. - Permissions are declared and centralized.
- Organization assignment is server-side.
- Object-level organization checks are explicit.