minaremonshaker's avatar

How should I authorize viewing a user’s tickets (collection) in an index endpoint?

In a Laravel API controller, I return a collection of tickets that belong to a specific user (“author”). The authenticated user may have multiple tickets, and I’m unsure what the best-practice authorization approach is for this index endpoint.

Goal: Allow the request only if the authenticated user has the view_ticket permission and is the same user as the $author route parameter.

What I tried:

  • Looping through each ticket and calling authorize() / Gate::authorize() per ticket.

  • Creating a dedicated gate (show-users-tickets) that receives the $author and checks permission + ownership.

class AuthorTicketController extends Controller
{
    public function index(Request $request, User $author)
    {
        $filters = $request->only(['per_page']);

        Gate::authorize('show-users-tickets', $author);

        $tickets = TicketsService::getAuthorTickets($author, $filters);

        return TicketResource::collection($tickets);
    }
}
Gate::define('show-users-tickets', function (User $user, User $author) {
    if ($user->hasPermissionTo('view_ticket')) {
        return $user->id === $author->id;
    }

    return false;
});

Question: Is this gate-based approach correct for authorizing access to a list of tickets, or should I be using a policy (e.g., TicketPolicy@viewAny / view) and/or authorizing at the query level instead? (Laravel documentation describes policies as a way to organize authorization logic around a particular model or resource.)

0 likes
9 replies
LaryAI's avatar
Level 58

Your current gate-based approach is functionally correct: using a Gate and passing both the authenticated user and the $author to check if the requestor is allowed to see the tickets works and is safe. However, the best practice in Laravel is to use policies for model-related resource access, especially for index endpoints. Policies make your intent more explicit, use Laravel's conventions, and are designed for organizing model-related authorization logic.

Recommended Approach

  1. Create a Policy for the Ticket model, for example:
// app/Policies/TicketPolicy.php

public function viewAny(User $user, User $author)
{
    // Only allow if the user is requesting their own tickets
    // and has the proper permission
    return $user->hasPermissionTo('view_ticket') && $user->id === $author->id;
}

Note: The second parameter is custom; see usage below.

  1. Register the Policy for the Ticket model if you haven't already (in AuthServiceProvider):
// app/Providers/AuthServiceProvider.php

protected $policies = [
    \App\Models\Ticket::class => \App\Policies\TicketPolicy::class,
];
  1. Call the Policy in Your Controller (using authorize):
// app/Http/Controllers/AuthorTicketController.php

public function index(Request $request, User $author)
{
    // This will call TicketPolicy@viewAny($user, $author)
    $this->authorize('viewAny', [Ticket::class, $author]);

    $filters = $request->only(['per_page']);

    $tickets = TicketsService::getAuthorTickets($author, $filters);

    return TicketResource::collection($tickets);
}
  • authorize('viewAny', [Ticket::class, $author]); tells Laravel to use your custom signature for the policy.

Why?

  • Policies are made exactly for this kind of resource-based access.
  • You leverage the viewAny convention for listing resources.
  • If you ever need to introduce team/organization features or let admins view others' tickets, you can adapt the policy accordingly without touching scattered gates.
  • It keeps your authorization logic centralized and discoverable.

Summary

  • YES, a policy is preferred here over a Gate, because you’re authorizing access to a model collection.
  • Your index method should call authorize('viewAny', [Ticket::class, $author]) and put the logic in a viewAny method on the policy.
  • Only use Gates for "one-off" or non-model/global checks.

If you want, you can remove the Gate definition entirely and move all logic to your TicketPolicy.

Let me know if you want a complete example in one file!

Glukinho's avatar

For me, you're talking about two different authorizations:

  1. a user can see someone's tickets (any of them, in general), in other words, is a user allowed to use Tickets functionality at all - this is Gate or TicketPolicy::viewAny policy. I' d prefer Gate.
  2. a user can see a particular ticket - this is resolved based on who the user is (owner of the ticket? -> allow) - this is TicketPolicy::view policy.
    You can add there some additional logic like:
if ($ticket->isPublic()) return true;

I think you should implement both and use both separately.

Glukinho's avatar

No, no. I meant you could do that if you need some more complex authorization logic. If your logic is simple - keep it simple.

jlrdw's avatar

I agree with @rihulfaakbar I usually have a query scope where the authenticated users id is used in the WHERE clause for the child records.

Please or to participate in this conversation.