Files
lasuite-messages/docs/permissions.md
Jean-Baptiste PENRATH 21255a4c4a (global) allow thread assignation (#645)
As a follow-up of the mention feature, we build upon ThreadEvent & UserEvent
models a feature to assign users to a thread.
We allow to filter mailboxe's inbox through assignation state (assigned to me, unassigned).
The thread share modal has been forked from ui-kit to be able to list users of each
mailbox and add a cta to assign them to the thread. A section above shows assigned users.
2026-05-19 14:44:26 +02:00

7.7 KiB

Permissions & Data Model

Core Data Model

Entity Relationships

User
├── MailboxAccess (role: VIEWER|EDITOR|SENDER|ADMIN)
│   └── Mailbox
│       ├── ThreadAccess (role: VIEWER|EDITOR)
│       │   └── Thread
│       │       ├── Message
│       │       │   ├── sender → Contact
│       │       │   ├── recipients → MessageRecipient → Contact
│       │       │   ├── parent → Message (reply chain)
│       │       │   ├── blob → Blob (raw MIME)
│       │       │   ├── draft_blob → Blob (JSON draft content)
│       │       │   └── attachments → Attachment → Blob (only for drafts)
│       │       ├── events → ThreadEvent (im / assign / unassign)
│       │       │   └── user_events → UserEvent (mention / assign)
│       │       ├── accesses → ThreadAccess (multiple mailboxes)
│       │       └── labels → Label (M2M)
│       ├── contacts → Contact
│       ├── labels → Label
│       └── blobs → Blob
├── user_events → UserEvent (per-user notifications, thread-scoped)
└── MailDomainAccess (role: ADMIN)
    └── MailDomain
        └── mailboxes → Mailbox (via domain FK)

Key Models

Model Purpose Key Fields
User Identity (OIDC) sub, email, full_name
Mailbox Email account local_part, domain (FK)
MailboxAccess User→Mailbox permission user, mailbox, role (unique together)
Thread Message thread subject, denormalized flags (has_trashed, is_spam, etc.)
ThreadAccess Mailbox→Thread permission thread, mailbox, role (unique together)
Message Email message thread, sender, parent, flags (is_draft, is_trashed, etc.)
ThreadEvent Timeline entry on a thread (comment, assign, unassign) thread, type, author, data (JSON, schema-validated per type)
UserEvent Per-user notification derived from a ThreadEvent user, thread, thread_event, type, read_at
Contact Email address entity email, mailbox, name
Label Folder/tag (hierarchical) name, slug, mailbox, threads (M2M)

Role Hierarchies

MailboxRoleChoices (User access to Mailbox)

VIEWER = 1   # Read-only: view mailbox threads/messages
EDITOR = 2   # Edit: create drafts, flag, delete, manage thread access
SENDER = 3   # Send: EDITOR + can send messages
ADMIN  = 4   # Admin: SENDER + manage mailbox accesses, labels, templates, import

Role groups defined in enums.py:

  • MAILBOX_ROLES_CAN_EDIT = [EDITOR, SENDER, ADMIN]
  • MAILBOX_ROLES_CAN_SEND = [SENDER, ADMIN]

ThreadAccessRoleChoices (Mailbox access to Thread)

VIEWER = 1   # Read-only: view thread messages and events
EDITOR = 2   # Edit: create replies, flag messages, manage thread sharing, assign

Role group:

  • THREAD_ROLES_CAN_EDIT = [EDITOR]

Event Types

# ThreadEvent.type (stored on the thread timeline)
IM       = "im"        # Internal comment, may embed mentions in data
ASSIGN   = "assign"    # User(s) newly assigned to the thread
UNASSIGN = "unassign"  # User(s) removed from the thread

# UserEvent.type (per-user notification, derived from ThreadEvent)
MENTION  = "mention"   # One per (user, message mention); read_at tracks ack
ASSIGN   = "assign"    # At most one per (user, thread); source of truth for "assigned"

UserEvent is not mailbox-scoped: a user reachable through several mailboxes sees the same notification everywhere.

Permission Classes

Defined in core/api/permissions.py. The table below lists the main ones and the rule they enforce.

Class Rule
IsAuthenticated Baseline — user is logged in.
IsAllowedToAccess Read access to a Mailbox/Thread/Message/ThreadEvent via any MailboxAccessThreadAccess path.
HasThreadEditAccess Full edit rights: ThreadAccess.role == EDITOR AND MailboxAccess.role ∈ MAILBOX_ROLES_CAN_EDIT on the same mailbox.
HasThreadCommentAccess Allowed to author internal comments: any ThreadAccess (viewer or editor) on a mailbox where the user has MAILBOX_ROLES_CAN_EDIT.
HasThreadEventWriteAccess Type-aware: im events follow the comment rule; every other ThreadEvent type requires full edit rights. Update/destroy is author-only.
IsAllowedToCreateMessage User must have MAILBOX_ROLES_CAN_EDIT on the sender mailbox (plus EDITOR ThreadAccess when replying).
IsAllowedToManageThreadAccess Managing a ThreadAccess requires full edit rights on the thread.
IsMailboxAdmin / IsMailDomainAdmin Admin paths for mailbox and maildomain management.
HasChannelScope Scope check for Channel-authenticated calls; CHANNEL_API_KEY_SCOPES_GLOBAL_ONLY further requires scope_level=global.

Shared ORM helpers (core/models.py):

  • ThreadAccess.objects.editable_by(user, mailbox_id=None) — rows matching the full-edit-rights rule.
  • ThreadAccess.objects.editor_user_ids(thread_id, user_ids=None) — user ids with full edit rights on a thread.

Key Design Principles

  1. Two-level permission model. User→Mailbox (MailboxAccess) and Mailbox→Thread (ThreadAccess) are independent and composed for every access check. Edit-level actions always verify both sides.
  2. ThreadAccess is per-mailbox. Each mailbox has its own role on a given thread, enabling selective sharing and per-mailbox read_at / starred_at state.
  3. Flags are shared state. Message flags (is_trashed, is_spam, is_unread, etc.) live on the Message and mutate the thread for everyone — they require EDITOR ThreadAccess.
  4. Thread stats are denormalized. Thread has boolean fields (has_trashed, is_spam, …) updated by thread.update_stats() after message flag changes. Mention/assignment stats (has_mention, has_unread_mention, has_assigned_to_me, has_unassigned) are not stored: they are computed per request via Exists(UserEvent...) annotations in ThreadViewSet.
  5. Comments relax the thread role. Posting or editing an im ThreadEvent only requires VIEWER ThreadAccess + mailbox edit rights. Assign/unassign and any other event type keep the stricter full-edit-rights policy.
  6. Event mutations are author-only. Update and destroy of a ThreadEvent are refused for non-authors, regardless of role. A configurable window (settings.MAX_THREAD_EVENT_EDIT_DELAY) can close the edit/delete path entirely after creation.
  7. Assignment is derived from the event log. UserEvent(type=ASSIGN) is the source of truth for "who is assigned"; there is no denormalized field on Thread. A partial UniqueConstraint enforces at most one active ASSIGN per (user, thread) and absorbs races between concurrent ASSIGN requests.
  8. Undo window for assignments. An UNASSIGN within UNDO_WINDOW_SECONDS (120s) of the matching ASSIGN, by the same author, is absorbed: the original ASSIGN ThreadEvent is trimmed or deleted, the UserEvent ASSIGN is removed, and no UNASSIGN event is emitted.
  9. Access changes cascade to assignments. Downgrading or removing a ThreadAccess / MailboxAccess triggers cleanup_invalid_assignments, which emits a single system ThreadEvent(type=UNASSIGN, author=None) for any assignee who lost full edit rights (re-evaluated across all their mailboxes).
  10. Mentions survive edits idempotently. Editing an im event diffs the mentions payload and reconciles UserEvent(MENTION) rows; unchanged mentions keep their read_at, removed ones disappear from the user's "Mentioned" view, new ones are created.