Contact Access Control
Every contact API endpoint calls check_contact_access before reading or mutating data. This function enforces assignment-based permissions — members with *Assigned permissions only access contacts where they appear in the contact_user table.
How It Works
Section titled “How It Works”API request hits a contact endpoint → check_contact_access(db, session, contact_id, action) 1. Load contact WHERE id = ? AND organization_id = session.org_id 2. Not found? → 404 3. Query contact_user for assigned_member_ids 4. Build ContactInstance { org_id, assigned_member_ids } 5. ContactResource::has_instance_permission(session, action, &instance) 6. Denied? → 403 "You do not have permission to access this contact" 7. Granted? → return contact model (saves a second DB query)The check_contact_access Function
Section titled “The check_contact_access Function”File: src/mods/contact/services/check_contact_access_service.rs
pub async fn check_contact_access( db: &DatabaseConnection, session: &Session, contact_id: Uuid, action: ContactInstanceAction,) -> Result<contact::Model, AppError>The function returns the contact model on success. This eliminates redundant queries — most endpoints need the contact data after verifying access.
Permission Resolution
Section titled “Permission Resolution”The ABAC engine resolves permissions in this order:
- Super admin → allow (cross-org access)
- Wrong org → deny
- Org owner → allow
View/Update/Delete→ allow (org-wide access)ViewAssigned/UpdateAssigned/DeleteAssigned→ allow only ifassigned_member_idscontainssession.member_id- No match → deny (403)
Contact Permissions
Section titled “Contact Permissions”| Permission | Scope | What it grants |
|---|---|---|
Contact:Instance:View | Org-wide | View any contact |
Contact:Instance:ViewAssigned | Assigned | View contacts assigned to you |
Contact:Instance:Update | Org-wide | Edit any contact |
Contact:Instance:UpdateAssigned | Assigned | Edit contacts assigned to you |
Contact:Instance:Delete | Org-wide | Delete any contact |
Contact:Instance:DeleteAssigned | Assigned | Delete contacts assigned to you |
Contact:Collection:List | Org-wide | List all contacts |
Contact:Collection:ListAssigned | Assigned | List only assigned contacts |
Contact:Collection:Create | Org-wide | Create new contacts |
Contact List Filtering
Section titled “Contact List Filtering”The list endpoint uses a two-tier check:
let show_all = ContactResource::has_collection_permission( &session, ContactCollectionPermission::List,);if !show_all { ContactResource::has_collection_permission( &session, ContactCollectionPermission::ListAssigned, ).or_forbidden("Not allowed to list contacts")?;}When show_all is false, the query adds a subquery filter:
let assigned_subquery = contact_user::Entity::find() .select_only() .column(contact_user::Column::ContactId) .filter(contact_user::Column::MemberId.eq(session.member_id)) .into_query();
condition = condition.add(contact::Column::Id.in_subquery(assigned_subquery));Members with ListAssigned only see contacts in their contact_user rows.
Cascading to Related Resources
Section titled “Cascading to Related Resources”Access control cascades through contact access. Notes, messages, and attachments all verify contact access first:
| Endpoint | Contact check | Additional check |
|---|---|---|
GET /api/contacts/{id}/notes | View | ContactNote:Collection:List |
POST /api/contacts/{id}/notes | View | ContactNote:Collection:Create |
PUT /api/contacts/{id}/notes/{note_id} | View | ContactNote:Instance:Update or UpdateOwn |
DELETE /api/contacts/{id}/notes/{note_id} | View | ContactNote:Instance:Delete or DeleteOwn |
GET /api/contacts/{id}/tasks | View | Task:Collection:List |
GET /api/messages?contact_id | View | Message:Collection:List |
POST /api/messages/send | View | Message:Collection:Create |
GET /api/messages/attachments/{id} | View (if linked) | Message:Collection:ViewAttachment |
Note Permissions
Section titled “Note Permissions”Contact assignment is the sole access boundary for viewing notes. If you can view the contact, you can see all its notes. Authorship only affects edit and delete:
| Permission | Grants |
|---|---|
ContactNote:Instance:Update | Edit any note |
ContactNote:Instance:UpdateOwn | Edit notes you authored |
ContactNote:Instance:Delete | Delete any note |
ContactNote:Instance:DeleteOwn | Delete notes you authored |
The server returns can_edit and can_delete flags with each note, computed via ABAC against ContactNoteInstance { org_id, author_id }.
Error Handling
Section titled “Error Handling”When a 403 reaches the UI, the toast notification system displays a user-friendly message:
// In any componentlet mut toaster = use_context::<Toaster>();toaster.mutation_error(&error_string);// Detects 403 → shows "Access denied" title// Strips HTTP status prefix → shows clean message bodyKey utilities:
is_forbidden_error()— checks if error starts with"403"(src/shared/utils/is_forbidden_error.rs)friendly_server_error()— strips"403 Forbidden: "prefix (src/shared/utils/friendly_server_error.rs)
Affected Endpoints
Section titled “Affected Endpoints”All contact CRUD, detail mutations (phones, emails, addresses, tags, field values), notes, messages, and attachment serving now call check_contact_access. The full list:
get_contact_api,update_contact_api,delete_contact_api- All
create_/update_/delete_contact_{phone,email,address}_api assign_contact_tag_api,unassign_contact_tag_apiupsert_contact_field_value_api,delete_contact_field_value_apiget_contact_tasks_api,get_contact_calls_api,get_contacts_list_apiget_contact_notes_api,create_contact_note_api,update_contact_note_api,delete_contact_note_apiget_contact_messages_api,send_message_api,serve_attachment_api