Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

jaspercreel's avatar

Global policy or gate for every resource

I'm building an app that is team based and pretty much every resource is tied back to a team in some way, whether as a direct child or a more distant relationship. Teams should only ever see the resources they own. Every way I can think to do this the "Laravel" way seems cumbersome. I don't want to write up a policy for every resource that has the same logic in it. I was thinking of doing it via middleware and a gate, but it seems hard to access the resource in question via a gate unless I write a lot of repeated route logic.

Ideally, what I am looking for is a way to wrap a group of resource routes in middleware that would know how to parse out the resource in each route and authorize. This seems like something that should be doable in Laravel, but I can't figure out how. Any ideas?

0 likes
7 replies
LaryAI's avatar
Level 58

To achieve a global policy or gate for every resource in a Laravel application, you can use a combination of route model binding, middleware, and a single gate or policy method that checks the team ownership of the bound model. Here's a step-by-step solution:

  1. Define a Gate or Policy Method: First, define a gate or policy method that checks if the authenticated user is a member of the team that owns the resource.

    // In AuthServiceProvider or any service provider you prefer
    
    use Illuminate\Support\Facades\Gate;
    
    public function boot()
    {
        // ...
    
        Gate::define('view-team-resource', function ($user, $resource) {
            return $user->team_id === $resource->team_id;
        });
    }
    
  2. Create a Middleware: Create a middleware that will extract the resource from the route and check the gate for the given resource.

    // Run `php artisan make:middleware CheckTeamOwnership`
    
    namespace App\Http\Middleware;
    
    use Closure;
    use Illuminate\Support\Facades\Gate;
    
    class CheckTeamOwnership
    {
        public function handle($request, Closure $next)
        {
            $routeParameters = $request->route()->parameters();
    
            foreach ($routeParameters as $parameter) {
                if (is_object($parameter) && method_exists($parameter, 'team')) {
                    if (Gate::denies('view-team-resource', $parameter)) {
                        abort(403);
                    }
                    break;
                }
            }
    
            return $next($request);
        }
    }
    

    In this middleware, we assume that your resources have a team() relationship method or a team_id property that can be used to check ownership.

  3. Register the Middleware: Register the middleware in your app/Http/Kernel.php file.

    protected $routeMiddleware = [
        // ...
        'team.ownership' => \App\Http\Middleware\CheckTeamOwnership::class,
    ];
    
  4. Apply Middleware to Routes: Apply the middleware to your resource routes. You can do this in your routes/web.php or routes/api.php file.

    Route::middleware(['auth', 'team.ownership'])->group(function () {
        // Define your resource routes here
        Route::resource('projects', 'ProjectController');
        // ... other resources
    });
    

    Ensure that your controllers use route model binding so that the middleware can automatically inject the model instance into the route.

  5. Route Model Binding: If you haven't already, make sure to use route model binding in your controller methods. Laravel will automatically resolve the model instances for you.

    public function show(Project $project)
    {
        // $project is automatically resolved and injected by Laravel
        return view('projects.show', compact('project'));
    }
    

With this setup, you have a single gate that checks team ownership, and a middleware that applies this check to all routes within the group. This approach avoids the need to write repetitive logic for each resource and keeps your authorization logic centralized.

martinbean's avatar
Level 80

@jaspercreel You can use a combination of scoping and a gate:

Route::middleware('can:view,team')
    ->prefix('teams/{team:slug}')
    ->group(function (): void {
        // Team Project Routes...
        Route::get(
            uri: 'projects/{project:slug}',
            action: [ProjectController::class, 'index'],
        );
    });
  • A user will only be able to view the requested project if it also includes the correct owning team in the URI. So if project 1 belongs to team 1, they will only be able to view it via /teams/{1 slug}/projects/{1 slug}. If they try /teams/{2 slug}/projects/{1 slug} then the scoping will fail and they will get a 404.
  • A user can only view the route if they have permission to view the team.

This will protect your routes from users trying to view resources belonging to other teams, and also stop them viewing resources for teams that they do not belong to.

1 like
jaspercreel's avatar

EDIT: Never mind, figured it out, I needed to use scopeBindings() on the routes

@martinbean Can you elaborate on how this would stop users from viewing other team's resources? I don't see any logic there for stopping that, because this middleware only checks if a user can view the team. So nothing is stopping a user from visiting /teams/1/project/team-2-project and seeing it if they belong to team 1.

martinbean's avatar

Can you elaborate on how this would stop users from viewing other team's resources? I don't see any logic there for stopping that, because this middleware only checks if a user can view the team. So nothing is stopping a user from visiting /teams/1/project/team-2-project and seeing it if they belong to team 1.

@jaspercreel Say Project 2 belongs to Team 1. The only URL the project can be viewed via is /teams/1/projects/2. But there’s authorisation checking the user can view the team in the URL. So, if the user can view Team 1’s resources, they can in turn view Project 2. However, if they change the URL to something like /teams/2/projects/2 then they’ll get a 404, because the scoped bindings won’t resolve. And if the user uses the correct URL but doesn’t have permission to view Team 1, then they’ll get a 403 Forbidden response instead.

So, it’s impossible to view resources for another team by URL if the user does not have permission to view the “root” team in the first place.

1 like
jaspercreel's avatar

@martinbean That is such a cool way of handling the security. Thanks for suggesting. It wasn't working for me before because I am using ids in the url and needed to call "->scopeBindings()" at the end. Once I did, it worked.

I ended up needing to create a custom middleware that basically did the same thing as using "can" because I wanted to return a 404 instead of a 403 for obfuscation.

martinbean's avatar

@jaspercreel You don’t need to add that method if you specify a binding key in the route definition, like I did in my examples:

Route::get('teams/{team:slug}/projects/{project:slug}', []);

From the Laravel docs:

When using a custom keyed implicit binding as a nested route parameter, Laravel will automatically scope the query to retrieve the nested model by its parent using conventions to guess the relationship name on the parent.

--

I ended up needing to create a custom middleware that basically did the same thing as using "can" because I wanted to return a 404 instead of a 403 for obfuscation.

You also don’t need to create custom middleware to do that. You can just return responses directly from policies:

use Illuminate\Auth\Access\Response;

class TeamPolicy
{
    public function view(User $user, Team $team): Response
    {
        return $user->teams->contains($team)
            ? Response::allow();
            ? Response::denyAsNotFound();
    }
}

So using can:view,team, if the user does not have permission, then a 404 Not Found response is returned instead.

1 like

Please or to participate in this conversation.