Policy and agent scope
Policies\TicketPolicy is the authority of the API — the PHP API never asks it. It is registered for the configured ticket model (tickets.authorization.register_policy) and swappable through tickets.authorization.policy with a subclass. Its abilities rest on Gate abilities you define (tickets.agent, tickets.assign, tickets.manage; merge defaults to tickets.agent); an undefined one denies:
| Ability | Who |
|---|---|
viewAny | Anyone signed in (the list is scoped). |
view | An agent the agent scope allows, the requester, a watcher (watchers.can_view); anyone else gets 404. |
create | Anyone signed in (self_service), agents always. |
update | Agents; the requester only subject / description (requester_can_edit) and requester-editable fields, while unresolved. |
delete, restore | The manage ability, held by an agent the agent scope lets work on the ticket. |
manageCategories, manageFields, manageTags | The manage ability. |
transition | Agents; the requester (a watcher too with participants_can_transition) only requester_transitions. |
assign | Agents with the assign ability; any agent for themselves (claim). |
unassign | Agents with the assign ability, or the agent holding the ticket. |
reply | Whoever may view, until Closed (409 ticket_closed). |
note, viewInternal, tag, link | Agents. |
updateComment, deleteComment | Agents; anyone else only their own public reply. |
manageWatchers | Agents; anyone who may view, for themselves. |
merge | The merge ability, held by an agent the agent scope lets work on the ticket (asked for both tickets). |
viewTags | Agents. |
Asked without attributes (@can('update', $ticket)), update answers whether the user may edit anything: the subject and description, or an active requester-editable field of the ticket’s field set.
Managers are scoped agents. Deleting or restoring a ticket needs tickets.manage and being an agent for that ticket (tickets.agent within the AgentScope); a manager without tickets.agent, or outside the scope, gets the same 404 as for a hidden ticket. Grant both abilities to people who manage tickets.
register_policy off is not deny-all: Laravel’s policy discovery maps RoundlyConsulting\Tickets\Models\Ticket to RoundlyConsulting\Tickets\Policies\TicketPolicy by name and finds the packaged policy anyway. To use your own, register it with Gate::policy(Ticket::class, MyTicketPolicy::class), or simply name a subclass in tickets.authorization.policy. An invalid policy registers nothing and tickets:doctor reports it.
Agent scope
By default every agent works on every ticket. Narrow it with a Contracts\AgentScope:
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Tickets\Contracts\AgentScope;
use RoundlyConsulting\Tickets\Models\Ticket;
final readonly class ProjectMembersOnly implements AgentScope
{
public function allows(Model $agent, Ticket $ticket): bool
{
return $ticket->project_id === null
|| $agent->projects()->whereKey($ticket->project_id)->exists();
}
public function constrain(Builder $query, Model $agent): void
{
// where clauses only — they are grouped and OR-ed with the requester and watcher branches
$query->where(fn (Builder $q) => $q
->whereNull('project_id')
->orWhereIn('project_id', $agent->projects()->select('projects.id')));
}
}// config/tickets.php
'authorization' => ['agent_scope' => App\Tickets\ProjectMembersOnly::class, /* … */],allows() decides one ticket; constrain() narrows a list (visibleTo, the index). They must agree — test yours as the package tests its own. constrain() also runs inside a key subquery for the agent-only filters and sort, so keep it to where clauses on the ticket table.
The scope is the authority on an agent for this ticket everywhere — the API and the PHP API alike: the policy and the resources, notifications and mentions, assignment eligibility, the author role of a message, internal notes, agent-only custom fields, the agent-only index filters and the last_activity_at sort, merging (both tickets), the other end of a link (reference only), and deleting and restoring. An agent outside the scope still sees a ticket they requested or watch, as requester or participant.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.