Skip to content

Authorization

What the Odden Filament plugin checks before showing or changing data, and how to restrict access with panel access and Laravel policies.

The plugin ships no policies, gates, permissions or roles. It checks your Laravel policies the same way Filament resources do: when a policy is registered for an Odden model, the plugin's resources, custom pages, boards and actions respect it. When there is no policy, everything is allowed, so an app without policies works out of the box and any user who can get into the panel can see and change every Odden record.

Panel access

Filament decides who can sign in to the panel. Outside the local environment, your user model must implement Filament\Models\Contracts\FilamentUser:

php
<?php

namespace App\Models;

use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable implements FilamentUser
{
    public function canAccessPanel(Panel $panel): bool
    {
        return str_ends_with($this->email, '@yourcompany.com');
    }
}

Policies on Odden models

Odden's resources are standard Filament resources. When a Laravel policy is registered for a resource's model, Filament checks it for that resource's navigation item, list, create, view, edit and delete pages, and for the built-in create, edit, view, delete, restore and force-delete actions. The plugin uses the same policies for its custom pages, boards and actions (see below).

Odden models live in the packages, so register policies for them explicitly, for example in AppServiceProvider::boot():

php
use App\Policies\ContactPolicy;
use Odden\Core\Models\Contact;
use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::policy(Contact::class, ContactPolicy::class);
}
php
<?php

namespace App\Policies;

use App\Models\User;
use Odden\Core\Models\Contact;

class ContactPolicy
{
    public function viewAny(User $user): bool
    {
        return true;
    }

    public function view(User $user, Contact $contact): bool
    {
        return $contact->owner_id === $user->id;
    }

    public function create(User $user): bool
    {
        return true;
    }

    public function update(User $user, Contact $contact): bool
    {
        return $contact->owner_id === $user->id;
    }

    public function delete(User $user, Contact $contact): bool
    {
        return false;
    }
}

With this policy, users can only open the view and edit pages of contacts they own (other contacts return a 403), and the delete actions are hidden. The Playbook, Auto-Route and Merge row actions only show on contacts the user owns, and merging is refused because the duplicate would be deleted. The Sales Cockpit's touches and call and meeting logs return a 403 for contacts the user doesn't own.

If viewAny() returns false, Contacts disappears from the navigation and /admin/contacts returns a 403. So do the pages that show contact data: Executive Overview, Data Quality, the Sales Cockpit and the Marketing Cockpit.

Policies don't filter the table. With the policy above, users still see every contact in the list. To scope the query, you need your own resource subclass that overrides getEloquentQuery() (see Customizing and extending).

If you want Filament to fail loudly when a model has no policy, enable Filament's ->strictAuthorization() on the panel. You then need a policy for every Odden model the plugin registers, and for Odden\Core\Models\Activity and Odden\Sales\Models\SalesSequenceEnrollment, which the Sales Cockpit and the contact relation managers check.

Each resource's model is listed in Resources.

How the plugin checks policies

Outside Filament's built-in actions, the plugin checks abilities with Odden\Filament\Support\OddenAuthorization, which follows Filament's rules for resources:

  • If a policy with a method for the ability is registered for the model, the policy decides.
  • If there is no policy, or the policy has no method for that ability, the ability is allowed. A Gate::before() callback that returns false still denies it, and strict authorization throws instead.
  • A user must be signed in. Nothing falls back to a default user: with no authenticated user, every check fails with a 403.

Records that a page or action loads by ID (from a Livewire call or a form field) are looked up inside the plugin resource's getEloquentQuery(), which applies Filament's tenant scoping. An ID outside that query returns a 404. The lookups use the plugin's own resource classes in Odden\Filament\Resources, so a getEloquentQuery() override in your own resource subclass isn't applied to them.

You can use the same helper in your own pages and actions:

php
use Filament\Actions\Action;
use Odden\Core\Enums\LeadStatus;
use Odden\Core\Models\Contact;
use Odden\Filament\Resources\ContactResource;
use Odden\Filament\Support\OddenAuthorization;

// A row action that is hidden, and refused, unless the user may update the contact.
Action::make('flag')
    ->authorize(OddenAuthorization::forRecord('update', ContactResource::class))
    ->action(fn (Contact $record) => $record->update(['lead_status' => LeadStatus::Unqualified]));

// In a Livewire method: 404 outside the resource's query, 403 if the policy denies.
$contact = OddenAuthorization::findAndAuthorize(ContactResource::class, Contact::class, $id, 'update');

Custom pages

Each custom page requires viewAny on every resource whose data it shows. If any of them is denied, the page is hidden from the navigation, opening it returns a 403, and Livewire requests to an already-open page return a 403. Resources of modules that aren't installed are skipped.

Page Requires viewAny on
Executive Overview ContactResource, CompanyResource, DealResource, SalesQuotaResource, CampaignResource, TicketResource
Data Quality ContactResource, CompanyResource
Sales Cockpit ContactResource, DealResource, QuoteResource, SalesSequenceResource
Support Cockpit, Service Analytics TicketResource
Marketing Cockpit CampaignResource, MarketingFormResource, MarketingWorkflowResource, ContactResource
ABM Cockpit CompanyResource
Attribution & ROI, Campaign Benchmarking, Campaign Calendar, Domain Health (SPF/DKIM) CampaignResource
UTM Link Builder CampaignResource, LandingPageResource
Pipeline Board (/admin/deals/board) DealResource
Tickets Board (/admin/tickets/board) TicketResource

The page classes implement this with the Odden\Filament\Pages\Concerns\AuthorizesPageAccess trait, which overrides canAccess().

Page and board actions

The Livewire methods that change data authorize the record they change:

Page Method Checks
Data Quality mergeContacts(), mergeCompanies() update on the primary record and delete on the duplicate, which the merge soft-deletes
Pipeline Board moveDeal() update on the deal
Tickets Board moveTicket() update on the ticket
Support Cockpit claimTicket(), claimAndOpen(), openReplyModal(), sendQuickReply(), openResolveModal(), quickResolveTicket() update on the ticket
Sales Cockpit logQuickTouch(), openCallModal(), saveCallLog(), openMeetingModal(), saveMeetingLog() update on the contact; a meeting without a contact needs create on Activity
Sales Cockpit advanceEnrollment() update on the contact and on the SalesSequenceEnrollment
Sales Cockpit completeActivity() update on the Activity
ABM Cockpit recalculateCompany() update on the company
ABM Cockpit recalculateAll() update on every target account it would recalculate; if any is denied, nothing is recalculated
Marketing Cockpit sendCampaignNow() update on the campaign

A denied check returns a 403, and an ID outside the resource's query returns a 404. In the Support Cockpit, Claim assigns the ticket to the signed-in user, and canned responses are limited to shared ones and the user's own (the ticket's Add Reply / Note action applies the same limit).

Resource actions

The custom row and header actions on the resources use Filament's ->authorize(). An action the user isn't allowed to run is hidden, and Filament refuses to run it if it is called anyway.

Resource or page Actions Checks
Contacts Playbook, Auto-Route update on the contact. Hidden when getodden/crm-sales isn't installed.
Contacts, Companies Merge update on the record; delete on the selected duplicate (403 if denied)
Contacts, Companies AI Briefing view on the record
Companies Recalculate Health, Recalculate Intent update on the company
Contact relation managers Enroll in Cadence, Adjust Score update on the contact
Contact relation managers Advance Step, Unenroll update on the contact and on the enrollment
Activities relation manager Complete update on the activity
Deals Playbook, Auto-Route; on the view page Generate Quote, Run Playbook, Mark Won, Mark Lost update on the deal
Quotes Accept & Sign update on the quote (and on the deal, in the deal's Quotes relation manager)
Deal Quotes relation manager Generate from Products update on the deal
Sales Sequences Enroll Contact update on the sequence and on the selected contact
Sales Sequences Process Due Cadences update on every active sequence, because it processes all of them
Tickets Resolve, Auto-Route update on the ticket
Tickets Merge update on the ticket and on the destination ticket
Ticket Messages relation manager Add Reply / Note update on the ticket
Campaigns Send Now, Pick Winner & Deploy, AI Copy Assistant, Send Test (also on the edit page) update on the campaign
Campaigns Preview, Spam Audit view on the campaign
Campaigns Duplicate view on the campaign and create for campaigns (create only on the edit page)
Marketing Templates Send Test (also on the edit page), Apply Layout Preset, Evaluate A/B update on the template
CRM Lists Sync (table) and Sync Members (view page) update on the list
Marketing Subscriptions Restore / Resubscribe, Suppress update on the subscription
Ad Audience Syncs Sync Now update on the sync

What isn't covered

  • Data visibility on cockpits and dashboards. Policies decide who can open a page, not which records it shows. Once a user can open a cockpit or dashboard, its counts, charts and lists cover all records. For example, the Sales Cockpit lets any user who can open it switch to another rep's view. The Support Cockpit, the board columns and the lookups by ID are limited to the resource's getEloquentQuery(), but view isn't checked per record.
  • Read-only and link actions, such as Portal, the marketing template Preview, Revisions, Export HTML, Export MJML and Download ZIP, Embed Code, Embed Snippet, Copy URL, Visual Journey and the deal health analysis. They are available to anyone who can open the page they're on.

If some panel users must not see everything, don't give them access to a panel that has OddenPlugin registered. Either limit who can access the panel with canAccessPanel(), or build a separate panel from only the resources you want, with your own subclasses. See Customizing and extending.