Skip to content

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:

  1. Code defaults: DEFAULT_ROLE_PERMISSIONS in prismio.organizations.roles
  2. Role defaults in DB: RolePermission rows (role, codename)
  3. Membership permissions in DB: OrganizationMembershipPermission rows (membership, codename)

Core models:

  • Role
  • RolePermission
  • OrganizationMembership
  • OrganizationMembershipPermission

How Permission Resolution Works

prismio.organizations.permissions.user_has_permission(org_context, codename) evaluates in this order:

  1. If membership has explicit OrganizationMembershipPermission rows: only those rows are authoritative.
  2. If membership has no explicit rows: fallback to RolePermission for 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_context
  • request.organization
  • request.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_migrate in prismio.organizations.apps.OrganizationsConfig

You can also run it manually:

python manage.py sync_role_permissions

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) -> bool
  • require_permission(org_context, codename) -> None
  • has_org_permission(request, codename) -> bool
  • require_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_context or from org_id / organization_id kwargs.
  • 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

  1. Define/adjust defaults in DEFAULT_ROLE_PERMISSIONS.
  2. Run sync (post_migrate or manage.py sync_role_permissions).
  3. New role codenames are inserted into RolePermission.
  4. Membership propagation applies according to propagation rules above.
  5. Runtime checks are performed via decorators, mixins, DRF permissions, or template tags.

Troubleshooting

  • Permission check unexpectedly false in templates: verify OrganizationMiddleware is active and request.organization_context is 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.