07 — Roles & Permissions (RBAC)
Project: crmoffice
Implementation: spatie/laravel-permission
Last updated: 2026-05-30
Implementation note (Phase 1–6): 7 roles seeded (owner, admin, sales, pm, support, accountant, staff) with Spatie Permission — each role has granular resource-level permissions matching the navigation structure.
1. Roles (Seeded)
| Role Key | Display | Description |
|---|---|---|
owner |
Owner | Single owner (or co-owners). Full control + cannot be revoked by Admin. Mandatory 2FA. |
admin |
Admin | Full operational access. Cannot manage owner role. |
sales |
Sales | Leads, clients, estimates, proposals. No financial settings, limited invoice rights. |
pm |
Project Manager | Projects, tasks, milestones. View own clients/projects. |
support |
Support Agent | Tickets, KB authoring. Limited to support-related entities. |
accountant |
Accountant | All financial: invoices, payments, expenses, credit notes. No project/task editing. |
staff |
Staff | Generic team member. Has tasks assigned. Logs time. |
customer |
Customer | Portal access only. Different guard. Not in users table — uses contacts table. |
Multi-role allowed: a user may have pm + sales simultaneously. Permissions union.
Customer (contacts) uses separate guard (portal guard in auth.php). No spatie role assignment — portal access controlled by contacts.portal_access + per-contact email preferences.
2. Permission Naming Convention
Format: {action}.{resource} lower-snake.
Actions:
view— read singleview_any— list / indexcreateupdatedelete— soft delete (or void for financial)force_delete— hard delete (restricted)restoreexportimportmanage— admin-level configuration of the resource
Some custom actions for specific resources:
send.invoice— email sendmark_paid.invoice— manual paymentvoid.invoicerefund.paymentconvert.lead— lead → clientconvert.estimate— estimate → invoiceimpersonate.user— login asassign.ticketescalate.ticketreveal_secret.provider— see plaintext API keytest.provider— ping/probe credentials
3. Permission Catalog (Full List, Seeded)
3.1 CRM
view.client,view_any.client,create.client,update.client,delete.client,restore.client,force_delete.client,export.client,import.clientview.contact,view_any.contact,create.contact,update.contact,delete.contactview.lead,view_any.lead,create.lead,update.lead,delete.lead,convert.lead,export.lead,import.leadview.activity,create.activity,update.activity,delete.activity
3.2 Sales
view.estimate,view_any.estimate,create.estimate,update.estimate,delete.estimate,send.estimate,convert.estimateview.proposal,view_any.proposal,create.proposal,update.proposal,delete.proposal,send.proposalview.contract,view_any.contract,create.contract,update.contract,delete.contract,send.contractview.invoice,view_any.invoice,create.invoice,update.invoice,delete.invoice,send.invoice,mark_paid.invoice,void.invoice,apply_credit_note.invoiceview.payment,view_any.payment,create.payment,refund.payment,delete.paymentview.credit_note,view_any.credit_note,create.credit_note,update.credit_note,delete.credit_noteview.item,manage.itemview.tax_rate,manage.tax_rateview.currency,manage.currencyview.expense,view_any.expense,create.expense,update.expense,delete.expense
3.3 Projects & Tasks
view.project,view_any.project,create.project,update.project,delete.project,manage_members.projectview.milestone,create.milestone,update.milestone,delete.milestoneview.task,view_any.task,create.task,update.task,delete.task,assign.taskview.time_entry,view_any.time_entry,create.time_entry,update.time_entry,delete.time_entry,view_others.time_entryview.discussion,create.discussion,update.discussion,delete.discussion
3.4 Support
view.ticket,view_any.ticket,create.ticket,update.ticket,delete.ticket,assign.ticket,escalate.ticket,reply.ticket,internal_note.ticketview.department,manage.departmentview.ticket_priority,manage.ticket_priorityview.ticket_status,manage.ticket_statusview.sla_policy,manage.sla_policyview.kb_category,manage.kb_categoryview.kb_article,view_any.kb_article,create.kb_article,update.kb_article,delete.kb_article,publish.kb_article
3.5 Cross-Cutting
view.calendar,manage_own.calendarview.notification,manage_own.notificationview.goal,view_any.goal,create.goal,update.goal,delete.goal,view_others.goalview.survey,view_any.survey,create.survey,update.survey,delete.surveyview.announcement,manage.announcementview.report.sales,view.report.leads,view.report.project_profitability,view.report.time,view.report.tickets,view.report.expenses
3.6 Platform / Admin
view.user,view_any.user,create.user,update.user,delete.user,impersonate.user,disable.userview.role,manage.roleview.permissionview.custom_field,manage.custom_fieldview.provider,manage.provider,reveal_secret.provider,test.providerview.setting,manage.settingview.audit_logview.webhook,manage.webhook,replay.webhook_deliveryview.file,delete.file,delete_others.file
4. Role-Permission Matrix
✅ = granted · ⛔ = not granted · 🔸 = scoped (own/assigned only)
4.1 CRM
| Permission | Owner | Admin | Sales | PM | Support | Accountant | Staff |
|---|---|---|---|---|---|---|---|
*.client |
✅ | ✅ | ✅ | 🔸 own client | 🔸 own ticket's client | ✅ | 🔸 view only |
*.contact |
✅ | ✅ | ✅ | 🔸 | 🔸 | ✅ | 🔸 view |
*.lead |
✅ | ✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ |
*.activity |
✅ | ✅ | ✅ | 🔸 own | 🔸 own | 🔸 own | 🔸 own |
4.2 Sales
| Permission | Owner | Admin | Sales | PM | Support | Accountant | Staff |
|---|---|---|---|---|---|---|---|
*.estimate |
✅ | ✅ | ✅ | 🔸 view | ⛔ | ✅ | ⛔ |
*.proposal |
✅ | ✅ | ✅ | 🔸 view | ⛔ | 🔸 view | ⛔ |
*.contract |
✅ | ✅ | ✅ | 🔸 view | ⛔ | ✅ | ⛔ |
view.invoice |
✅ | ✅ | 🔸 created_by_self | 🔸 own project | ⛔ | ✅ | ⛔ |
create.invoice |
✅ | ✅ | ✅ | ✅ project-billing | ⛔ | ✅ | ⛔ |
update.invoice |
✅ | ✅ | 🔸 status=draft | 🔸 own project | ⛔ | ✅ | ⛔ |
mark_paid.invoice |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ✅ | ⛔ |
void.invoice |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ✅ | ⛔ |
*.payment |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ✅ | ⛔ |
*.credit_note |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ✅ | ⛔ |
*.expense |
✅ | ✅ | 🔸 own | 🔸 own project | ⛔ | ✅ | 🔸 own |
manage.item |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ✅ | ⛔ |
manage.tax_rate |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ✅ | ⛔ |
manage.currency |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ✅ | ⛔ |
4.3 Projects & Tasks
| Permission | Owner | Admin | Sales | PM | Support | Accountant | Staff |
|---|---|---|---|---|---|---|---|
view.project |
✅ | ✅ | 🔸 own client | ✅ | 🔸 linked | 🔸 invoicing | 🔸 member |
create.project |
✅ | ✅ | ⛔ | ✅ | ⛔ | ⛔ | ⛔ |
update.project |
✅ | ✅ | ⛔ | 🔸 own | ⛔ | ⛔ | ⛔ |
manage_members.project |
✅ | ✅ | ⛔ | 🔸 own | ⛔ | ⛔ | ⛔ |
*.milestone |
✅ | ✅ | ⛔ | 🔸 own project | ⛔ | ⛔ | ⛔ |
view.task |
✅ | ✅ | ⛔ | 🔸 project | ⛔ | ⛔ | 🔸 assigned |
create.task |
✅ | ✅ | ⛔ | ✅ | ⛔ | ⛔ | ⛔ |
update.task |
✅ | ✅ | ⛔ | 🔸 project | ⛔ | ⛔ | 🔸 assigned |
assign.task |
✅ | ✅ | ⛔ | 🔸 project | ⛔ | ⛔ | ⛔ |
delete.task |
✅ | ✅ | ⛔ | 🔸 project | ⛔ | ⛔ | ⛔ |
create.time_entry |
✅ | ✅ | ⛔ | ✅ | ⛔ | ⛔ | ✅ |
view_others.time_entry |
✅ | ✅ | ⛔ | 🔸 project | ⛔ | ✅ | ⛔ |
update.time_entry |
✅ | ✅ | ⛔ | 🔸 own project | ⛔ | ⛔ | 🔸 own |
*.discussion |
✅ | ✅ | ⛔ | 🔸 own project | 🔸 linked | ⛔ | 🔸 member |
4.4 Support
| Permission | Owner | Admin | Sales | PM | Support | Accountant | Staff |
|---|---|---|---|---|---|---|---|
view.ticket |
✅ | ✅ | 🔸 own client | 🔸 own project | ✅ | ⛔ | 🔸 assigned |
create.ticket |
✅ | ✅ | ✅ | ✅ | ✅ | ⛔ | ✅ |
update.ticket |
✅ | ✅ | ⛔ | ⛔ | ✅ | ⛔ | 🔸 assigned |
assign.ticket |
✅ | ✅ | ⛔ | ⛔ | ✅ | ⛔ | ⛔ |
escalate.ticket |
✅ | ✅ | ⛔ | ⛔ | ✅ | ⛔ | ⛔ |
internal_note.ticket |
✅ | ✅ | ⛔ | ⛔ | ✅ | ⛔ | ⛔ |
delete.ticket |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |
manage.department |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |
manage.sla_policy |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |
*.kb_article |
✅ | ✅ | ⛔ | ⛔ | ✅ | ⛔ | ⛔ |
publish.kb_article |
✅ | ✅ | ⛔ | ⛔ | ✅ | ⛔ | ⛔ |
4.5 Cross-Cutting
| Permission | Owner | Admin | Sales | PM | Support | Accountant | Staff |
|---|---|---|---|---|---|---|---|
manage_own.calendar |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
view.goal |
✅ | ✅ | 🔸 own | 🔸 own | 🔸 own | 🔸 own | 🔸 own |
view_others.goal |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |
create.goal |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |
*.survey |
✅ | ✅ | 🔸 view | 🔸 view | 🔸 view | ⛔ | ⛔ |
manage.announcement |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |
view.report.sales |
✅ | ✅ | 🔸 own deals | ⛔ | ⛔ | ✅ | ⛔ |
view.report.leads |
✅ | ✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ |
view.report.project_profitability |
✅ | ✅ | ⛔ | 🔸 own | ⛔ | ✅ | ⛔ |
view.report.time |
✅ | ✅ | ⛔ | 🔸 project | ⛔ | ✅ | 🔸 own |
view.report.tickets |
✅ | ✅ | ⛔ | ⛔ | ✅ | ⛔ | ⛔ |
view.report.expenses |
✅ | ✅ | ⛔ | 🔸 project | ⛔ | ✅ | ⛔ |
4.6 Platform / Admin
| Permission | Owner | Admin | Sales | PM | Support | Accountant | Staff |
|---|---|---|---|---|---|---|---|
*.user |
✅ | ✅ (no owner) | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |
impersonate.user |
✅ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |
manage.role |
✅ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |
manage.custom_field |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |
manage.provider |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |
reveal_secret.provider |
✅ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |
test.provider |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |
manage.setting |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |
view.audit_log |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |
manage.webhook |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |
replay.webhook_delivery |
✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |
5. Scoped Access Rules ("🔸 own")
Permission alone isn't enough — Laravel Policies enforce ownership/scope:
ProjectPolicy
viewallowed if:user->can('view.project')AND (user->id == project.project_manager_idORproject.members->contains(user)ORuser->hasAnyRole(['owner','admin'])ORproject.client.account_manager_id == user->id)
TaskPolicy
view/updateallowed if: assigned to user OR can manage project
InvoicePolicy
view: account_manager of client OR billing role (accountant,owner,admin)update: only if status =draftfor sales role
ClientPolicy
view: account_manager_id == user->id OR user has full role
LeadPolicy
view/update: assigned_to == user->id OR user has full role
TimeEntryPolicy
view_others: only ifview_others.time_entrypermission
ProviderPolicy
- Reading credentials masked unless
reveal_secret.providerAND owner role
6. Implementation Notes
Default Permission Cache
spatie/laravel-permissioncaches via Redis (configured inconfig/permission.php)- Cache cleared on permission/role change
Adding a New Role
- Create role in admin UI → POST
/api/v1/roles - Assign permission subset
- Use Policies to scope further
Adding a New Permission
- Add to
PermissionSeederarray (code-defined; not user-creatable arbitrary) - Run
php artisan db:seed --class=PermissionSeeder - Update Policies/Filament resource visibility
Filament Resource Visibility
- Each Resource has
public static function canViewAny() { return auth()->user()->can('view_any.{resource}'); } - Forms/actions guarded similarly
Customer Portal Authorization
- Customer guard =
portal - No spatie role on contacts
- Per-action access derived from:
contacts.portal_access = truecontacts.receives_*_emailsfor notification preferences- Direct ownership: a contact can see invoices of
client_id = self.client_id, tickets they opened, projects of their client (ifproject.is_visible_to_customer), discussions markedis_visible_to_customer
- Customer cannot see internal notes (
ticket_replies.is_internal = true)
7. Two-Factor Authentication Policy
| Role | 2FA Requirement |
|---|---|
| Owner | Mandatory (cannot be disabled) |
| Admin | Mandatory after first login (force setup) |
| Sales / PM / Support / Accountant | Optional, encouraged |
| Staff | Optional |
| Customer | Optional (per contact preference) |
Enforcement via RequiresTwoFactor middleware on /admin routes when user role demands it. Recovery codes (10 single-use) generated at setup.
8. Audit Log Triggers (Mandatory)
Every action logs to audit_log:
- All login/logout (success + failure)
- Role assignment / removal
- Permission grant / revoke
- Provider credential change (value masked)
- Setting change
- Invoice: create, send, mark_paid, void
- Payment: record, refund
- Credit Note: create, apply, refund
- User: create, disable, delete, impersonate
- Webhook: create, delete, replay
- Data export (CSV/PDF of any entity list)
- Custom field create/delete
Implementation: domain event subscribers append to audit_log. Cannot be edited from UI (only view.audit_log permission).
9. Owner Role Special Rules
- At least 1 user with
ownerrole must exist at all times - An owner cannot delete themselves if they are the last owner
- An admin cannot manage
ownerrole (assign, revoke, delete owner-tagged user) - Owner role configurable to require quorum (e.g., 2 owners must approve provider credential change) — Phase 4 feature