mirror of
https://github.com/suitenumerique/messages.git
synced 2026-08-29 03:09:37 +02:00
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.
121 lines
7.7 KiB
Markdown
121 lines
7.7 KiB
Markdown
|
|
# Permissions & Data Model
|
|
|
|
## Core Data Model
|
|
|
|
### Entity Relationships
|
|
|
|
```text
|
|
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)
|
|
|
|
```python
|
|
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)
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
# 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 `MailboxAccess` → `ThreadAccess` 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.
|