Skip to content

Role Management

The Settings page provides role CRUD, member-role assignment, member removal, and phone assignment. All endpoints enforce ABAC permissions via the Role and Member resources — org owners and super admins bypass checks automatically.

For how permissions work at runtime, see ABAC Authorization. For a walkthrough of role assignment effects, see Assigning Roles & Limiting Permissions.

role — stores role definitions with granular permissions:

ColumnTypeNotes
idUUIDPrimary key
organization_idUUIDFK → organization, cascade delete
nameStringRole display name
descriptionString?Optional description
permissionsJSONBArray of permission strings

member_role — junction table linking members to roles:

ColumnTypeNotes
idUUIDPrimary key
member_idUUIDFK → member, cascade delete
role_idUUIDFK → role, cascade delete

Unique constraint on (member_id, role_id). Deleting a role cascades to remove all its assignments.

member_phone_number — junction table linking members to phone numbers:

ColumnTypeNotes
idUUIDPrimary key
member_idUUIDFK → member, cascade delete
phone_number_idUUIDFK → phone_number, cascade delete
is_defaultboolOne phone per member can be default
created_attimestampAuto-set on insert
// Role definition
pub struct Role {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub permissions: Vec<String>,
}
// Create/update payload
pub struct RoleData {
pub name: String,
pub description: Option<String>,
pub permissions: Vec<String>,
}
// Member with their assigned roles
pub struct OrgMemberWithRoles {
pub member_id: Uuid,
pub user_id: Uuid,
pub name: String,
pub is_owner: bool,
pub roles: Vec<MemberRoleSummary>,
}
pub struct MemberRoleSummary {
pub role_id: Uuid,
pub role_name: String,
}
// Replaces ALL role assignments for a member
pub struct AssignRolesData {
pub role_ids: Vec<Uuid>,
}
// Phone assignment types
pub struct AssignPhonesData {
pub phone_number_ids: Vec<Uuid>,
pub default_phone_number_id: Option<Uuid>,
}
pub struct PhoneAssignmentInfo {
pub phone_number_id: Uuid,
pub number: String,
pub friendly_name: Option<String>,
pub assigned_to: Vec<PhoneAssignee>,
}
pub struct PhoneAssignee {
pub member_id: Uuid,
pub member_name: String,
pub is_default: bool,
}
pub struct MemberPhoneSummary {
pub phone_number_id: Uuid,
pub number: String,
pub friendly_name: Option<String>,
pub is_default: bool,
}

All endpoints enforce ABAC permissions. Super admins and org owners bypass all checks. Other members need the specific Role:* or Member:* permissions assigned via their roles.

MethodRoutePermission RequiredBodyReturns
POST/api/rolesRole:Collection:CreateRoleDataRole
GET/api/rolesRole:Collection:ListVec<Role>
GET/api/roles/{id}Role:Instance:ViewRole
PUT/api/roles/{id}Role:Instance:UpdateRoleData()
DELETE/api/roles/{id}Role:Instance:Delete()

On create and update, the server validates permission strings against the catalog via validate_permission_strings(). Invalid strings are rejected with an error listing the bad keys.

MethodRoutePermission RequiredBodyReturns
GET/api/org/members-with-rolesMember:Collection:ListVec<OrgMemberWithRoles>
POST/api/members/{member_id}/rolesMember:Instance:UpdateAssignRolesData()
DELETE/api/members/{member_id}/roles/{role_id}Member:Instance:Update()
DELETE/api/members/{member_id}Member:Instance:Remove()

The POST /api/members/{member_id}/roles endpoint uses a replace-all strategy: it deletes all existing assignments for that member and inserts the new set. Send the complete list of role IDs the member should have.

The members endpoint sorts owners first in the response. You cannot remove the org owner — the API returns 400.

MethodRoutePermission RequiredBodyReturns
GET/api/org/phone-assignmentsMember:Collection:ListVec<PhoneAssignmentInfo>
POST/api/members/{member_id}/phonesMember:Instance:UpdateAssignPhonesData()

Phone assignment also uses replace-all. If only one phone is assigned and no default_phone_number_id is provided, that phone becomes the default automatically. The API validates that all phone IDs belong to the current org before assigning.

all_permission_groups() in permission_catalog_type.rs returns the full catalog of available permissions, grouped by resource. Each permission follows the Resource:Level:Variant format.

pub struct PermissionGroup {
pub resource: String, // "Contact", "Agent", etc.
pub permissions: Vec<PermissionEntry>,
}
pub struct PermissionEntry {
pub key: String, // "Contact:Instance:View"
pub label: String, // "View"
pub level: String, // "Instance" or "Collection"
pub display_name: String, // "View any contact"
}

There are 19 resource groups: Contact, Agent, Settings, Role, Member, ContactNote, Analyzer, Call, Dashboard, Knowledge, Message, Phone, TextAgent, Task, Plan, PlanTemplate, Twilio, Notification, Report, and Assistant.

Each permission entry includes a display_name — a human-readable label like "View any contact" or "List all agents". Display names follow these patterns:

LevelPatternExample
Instance"<Action> any <resource>""View any contact"
Assigned"<Action> assigned <resources>""View assigned contacts"
Collection"List all <resources>" or "Create <resources>""List all contacts"

The role management UI lives in two new owner-only tabs on the Settings page: Roles and Members.

A grouped, collapsible permission selector with a sticky search bar. Each resource group shows a header with the resource name and a selected count badge (e.g., “3/9”). Expand a group to see individual permission checkboxes showing human-readable display names (e.g., “View any contact”) instead of raw labels. Each group has a “Select all” toggle.

Search bar — a sticky input at the top of the picker lets you filter permissions by keyword. The search supports multiple terms separated by spaces — all terms must match against either the resource name or the permission display name. Matching groups auto-expand; non-matching groups are hidden. A clear button (X) appears while searching.

// Multi-term filtering logic
let terms: Vec<&str> = raw_query.split_whitespace().collect();
terms.iter().all(|term| {
resource_lower.contains(term) || display_lower.contains(term)
})

Example searches:

  • "contact" → shows Contact and ContactNote groups
  • "update contact" → filters to Update/UpdateAssigned/UpdateOwn permissions in the Contact group
  • "view assigned" → shows ViewAssigned permissions across all resources

Dual-mode form for creating or editing a role. Fields: name (required), description, and a PermissionPicker for selecting permissions. Submit is disabled while the name is empty or a save is in progress.

Fetches all roles via GET /api/roles and renders each as a card showing the name, description, and a permission count badge. Click ”+ New Role” to show a create form above the list. Click the pencil icon on a card to edit inline — the card is replaced with RoleForm. Click the trash icon to delete (with confirmation).

Fetches all org members with their roles and all available roles. Each member shows a card with their name and role badges. Click “Manage Roles” to expand a checkbox list of available roles — toggling a checkbox immediately calls the assign endpoint with the updated full role list. The owner is always listed first and cannot have roles managed.

Displays a member’s assigned phone numbers with a picker to add or remove phones. Shows each phone’s number and friendly name, with a radio button to set the default. Uses the PhoneAssignmentInfo data to show which phones are available and who else has them assigned.

Confirmation dialog for removing a member from the organization. Requires Member:Instance:Remove permission. Prevents removing the org owner.

All mutation components use the Toaster context for error feedback. When a server call fails (permission denied, validation error, network issue), the component calls toaster.mutation_error(&e.to_string()) to display a toast notification instead of failing silently.

// Standard pattern in mutation handlers
match assign_member_roles_api(member_id.to_string(), data).await {
Ok(_) => on_roles_changed.call(()),
Err(e) => toaster.mutation_error(&e.to_string()),
}

This pattern is consistent across the settings, contact, and phone modules.

PR #628 added two new resources to the ABAC system for settings module enforcement:

Role resource — controls who can manage role definitions:

PermissionDescription
Role:Instance:ViewView a single role
Role:Instance:UpdateEdit role name, description, permissions
Role:Instance:DeleteDelete a role (cascades to assignments)
Role:Collection:ListList all org roles
Role:Collection:CreateCreate new roles

Member resource — controls who can manage org members:

PermissionDescription
Member:Instance:ViewView member details
Member:Instance:UpdateAssign/unassign roles and phones
Member:Instance:RemoveRemove a member from the org
Member:Collection:ListList all org members

Both resources use org-scoped instance checks via RoleInstance { org_id } and MemberInstance { org_id }.

src/mods/settings/
├── types/
│ ├── role_type.rs # Role, RoleData
│ ├── role_resource.rs # RoleInstance, Resource impl for RoleResource
│ ├── member_resource.rs # MemberInstance, Resource impl for MemberResource
│ ├── org_member_with_roles_type.rs # OrgMemberWithRoles, AssignRolesData
│ ├── member_phone_type.rs # MemberPhoneSummary, AssignPhonesData
│ ├── phone_assignment_info_type.rs # PhoneAssignmentInfo, PhoneAssignee
│ └── permission_catalog_type.rs # PermissionGroup, all_permission_groups()
├── api/
│ ├── create_role_api.rs
│ ├── get_roles_api.rs
│ ├── get_role_api.rs
│ ├── update_role_api.rs
│ ├── delete_role_api.rs
│ ├── get_org_members_api.rs
│ ├── get_org_members_with_roles_api.rs
│ ├── assign_member_roles_api.rs
│ ├── unassign_member_role_api.rs
│ ├── remove_member_api.rs
│ ├── assign_member_phones_api.rs
│ └── get_phone_assignments_api.rs
└── components/
├── permission_picker_component.rs # PermissionPicker
├── role_form_component.rs # RoleForm
├── role_list_component.rs # RoleList
├── member_role_manager_component.rs # MemberRoleManager
├── member_phone_picker_component.rs # MemberPhonePicker
├── member_detail_panel_component.rs # MemberDetailPanel
└── confirm_remove_member_component.rs # ConfirmRemoveMember