Permissions¶
This document describes the full organization permission system used by the app, including:
- Role defaults
- Membership-level permission overrides
- Runtime checks in Python views and templates
- DRF integration
- Permission sync behavior when new defaults are added
Permission Data Model¶
Permissions are represented by codename strings, such as organization.members.invite.
There are three layers:
- Code defaults:
DEFAULT_ROLE_PERMISSIONSinprismio.organizations.roles - Role defaults in DB:
RolePermissionrows (role,codename) - Membership permissions in DB:
OrganizationMembershipPermissionrows (membership,codename)
Core models:
RoleRolePermissionOrganizationMembershipOrganizationMembershipPermission
How Permission Resolution Works¶
prismio.organizations.permissions.user_has_permission(org_context, codename) evaluates in this order:
- If membership has explicit
OrganizationMembershipPermissionrows: only those rows are authoritative. - If membership has no explicit rows:
fallback to
RolePermissionfor that membership role.
This means membership-level permissions are the main source of truth after initialization.
Context Requirements¶
Most checks expect request.organization_context to be available.
OrganizationMiddleware resolves and attaches:
request.organization_contextrequest.organizationrequest.membership
from the active org in session.
Syncing Defaults Into the Database¶
DEFAULT_ROLE_PERMISSIONS is a Python constant. It is synced to DB through:
prismio.organizations.services.ensure_default_roles()- wrapper:
prismio.organizations.services.sync_default_role_permissions()
Sync creates missing Role and RolePermission rows and propagates newly added role permissions to memberships.
Propagation Rules¶
When a new permission is added to a role default:
- Non-owner roles: new permission is auto-added only for memberships whose current membership permission set exactly matches the old role defaults (unmodified memberships).
- Owner role: new permission is always auto-added to owner memberships, even if membership permissions were customized.
When Sync Runs¶
Sync runs automatically:
- via
post_migrateinprismio.organizations.apps.OrganizationsConfig
You can also run it manually:
Membership Permission Initialization¶
On membership creation, post_save signal on OrganizationMembership calls:
initialize_membership_permissions(membership)
which seeds membership permissions from current role defaults (unless permissions already exist).
Python Permission APIs¶
From prismio.organizations.permissions:
user_has_permission(org_context, codename) -> boolrequire_permission(org_context, codename) -> Nonehas_org_permission(request, codename) -> boolrequire_object_in_active_org(obj, org_context) -> None
has_org_permission is safe for UI gating and returns False when no valid org context exists.
Function-Based Views¶
Use @check_permissions from prismio.organizations.decorators.
Single permission¶
from django.http import HttpResponse
from prismio.organizations.decorators import check_permissions
@check_permissions("organization.projects.view")
def project_list(request, org_id):
return HttpResponse("ok")
Multiple permissions (match="all")¶
@check_permissions(
"organization.projects.view",
"organization.projects.update",
match="all",
)
def project_update(request, org_id):
...
Multiple permissions (match="any")¶
@check_permissions(
"organization.members.invite",
"organization.members.remove",
match="any",
)
def members_tools(request, org_id):
...
Notes:
- The decorator resolves org context from
request.organization_contextor fromorg_id/organization_idkwargs. - Invalid/missing permission access returns HTTP 403.
Class-Based Django Views¶
Prefer OrganizationPermissionRequiredMixin (or OrganizationAccessMixin) and set required permissions on the class.
from django.views.generic import TemplateView
from prismio.organizations.mixins import OrganizationPermissionRequiredMixin
class TeamAdminView(OrganizationPermissionRequiredMixin, TemplateView):
template_name = "organizations/team_admin.html"
required_permissions = (
"organization.members.invite",
"organization.members.remove",
)
permission_match = "any"
OrganizationScopedMixin.dispatch() validates active org context and permission requirements before dispatching.
DRF / ViewSet Usage¶
Use HasOrganizationContext in permission_classes and implement get_required_permission().
from prismio.organizations.permissions import HasOrganizationContext
class ExampleViewSet(ModelViewSet):
permission_classes = [HasOrganizationContext]
permission_map = {
"list": "resource.view",
"retrieve": "resource.view",
"create": "resource.create",
"update": "resource.update",
"partial_update": "resource.update",
"destroy": "resource.delete",
}
def get_required_permission(self):
return self.permission_map.get(self.action)
prismio.organizations.viewsets.OrganizationScopedViewSet already provides this pattern.
Template Tags¶
Load and use user_has_org_perm from prismio.organizations.templatetags.organization_tags.
{% load organization_tags %}
{% user_has_org_perm request "organization.members.invite" as can_invite %}
{% if can_invite %}
<a href="{% url 'organizations:invite' %}">Invite Members</a>
{% endif %}
Multiple permissions:
{% user_has_org_perm request "organization.members.invite" "organization.members.remove" match="any" as can_manage_members %}
Comma-separated values are also supported:
{% user_has_org_perm request "organization.members.invite,organization.members.remove" match="any" as can_manage_members %}
Typical Flows¶
- Define/adjust defaults in
DEFAULT_ROLE_PERMISSIONS. - Run sync (
post_migrateormanage.py sync_role_permissions). - New role codenames are inserted into
RolePermission. - Membership propagation applies according to propagation rules above.
- Runtime checks are performed via decorators, mixins, DRF permissions, or template tags.
Troubleshooting¶
- Permission check unexpectedly false in templates:
verify
OrganizationMiddlewareis active andrequest.organization_contextis set. - Added default permission but not visible yet:
run
python manage.py sync_role_permissions. - Membership has custom permissions and did not receive new non-owner default: this is expected behavior.
- Owner did not receive new owner default:
run sync and confirm membership role code is
owner.