Skip to content

Authorizing controller actions

Tortue Torche edited this page Mar 17, 2015 · 9 revisions

You can use the authorize method to manually handle authorization in a controller action. This will raise a Efficiently\AuthorityController\Exceptions\AccessDenied exception when the user does not have permission. See Exception Handling for how to react to this.

public function show($id)
{
    $this->project = Project::find($id);
    $this->authorize('show', $this->project);
}

However that can be tedious to apply to each action. Instead you can use the loadAndAuthorizeResource() method in your controller to load the resource into an instance variable and authorize it automatically for every action in that controller.

class ProductsController extends BaseController
{
    public function __construct() 
    {
        $this->loadAndAuthorizeResource();
    }
}

This is the same as calling loadResource() and authorizeResource() because they are two separate steps and you can choose to use one or the other.

class ProductsController extends BaseController
{
    public function __construct() 
    {
        $this->loadResource();
        $this->authorizeResource();
    }
}

Also see Controller Authorization Example, Ensure Authorization and Non RESTful Controllers.

Choosing Actions

By default this will apply to every action in the controller even if it is not one of the 7 RESTful actions. The action name will be passed in when authorizing. For example, if we have a discontinue action on ProductsController it will have this behavior.

class ProductsController extends BaseController
{
    public function __construct() 
    {
        $this->loadAndAuthorizeResource();
    }

    public function discontinue($id)
    {
        // Automatically does the following:
        // $this->product = Product::find($id);
        // $this->authorize('discontinue', $this->product);
    }
}

You can specify which actions to affect using the except and only options, just like a beforeFilter.

$this->loadAndAuthorizeResource(['only' => ['index', 'show']]);

loadResource

index action

The index action will automatically set $this->products to Product::get() or Product::$options['collectionScope']()->get().

public function index()
{
    // $this->products automatically set to Product::get(); which is equivalent to Product::all();
}

If you want custom find options such as join or with or pagination, you can create a scope in your model:

For Laravel 5.0

// app/Product.php
class Product extends Model
{
    public function scopePopular($query)
    {
        return $query->where('votes', '>', 100);
    }

}

// app/Http/Controllers/ProductsController.php
class ProductsController extends Controller
{
    public function __construct() 
    {
        $this->loadAndAuthorizeResource(['collectionScope' => 'scopePopular']);
    }

    public function index()
    {
        // $this->products automatically set to Product::popular()->get();
    }
}

For Laravel 4.*

// app/models/Product.php
class Product extends Eloquent
{
    public function scopePopular($query)
    {
        return $query->where('votes', '>', 100);
    }

}

// app/controllers/ProductsController.php
class ProductsController extends BaseController
{
    public function __construct() 
    {
        $this->loadAndAuthorizeResource(['collectionScope' => 'scopePopular']);
    }

    public function index()
    {
        // $this->products automatically set to Product::popular()->get();
    }
}

You can pass parameters in collectionScope option with an array. For example:

$this->loadAndAuthorizeResource(['collectionScope' => ['scopeOfType', 'published']]); // will use Product::ofType('published')->get();

The $this->products variable will not be set initially if you are only using a conditional callback in the allow definitions because there is no way to determine which records to fetch from the database.

show, edit, update and destroy actions

These member actions simply fetch the record directly.

public function show($id)
{
    // $this->product; automatically set to Product::find($id);
}

create and store actions

These builder actions will initialize the resource with the attributes in the conditional callback. For example, if we have this allow definition.

$this->authorize('manage', 'Product', function ($self, $project) {
    return $project->discontinued === false;
});

Then the product will be built with that attribute in the controller.

$this->product = new Product(['discontinued' => false]);

This way it will pass authorization when the user accesses the create action.

The attributes are then overridden by whatever is passed by the user in $this->params['product'];.

Custom class

If the model is named differently than the controller than you may explicitly name the model that should be loaded; however, you must specify that it is not a parent in a nested routing situation, ie:

class ArticlesController extends BaseController
{
    public function __construct() 
    {
        $this->loadAndAuthorizeResource('post', ['parent' => false]);
    }
}

If the model class is namespaced differently than the controller you will need to specify the class option.

class ArticlesController extends BaseController
{
    public function __construct() 
    {
        $this->loadAndAuthorizeResource(['class' => "Store\Product"]);
    }
}

Custom find

If you want to fetch a resource by something other than id it can be done so using the findBy option.

class ArticlesController extends BaseController
{
    public function __construct() 
    {
        $this->loadResource(['findBy' => 'permalink']); // will use Article::where('permalink', $this->params['id'])->firstOrFail();
        $this->authorizeResource();
    }
}

Custom primary key

If you want to fetch a resource who has a custom primary key, other than id, it can be done so using the idParam option.

class ArticlesController extends BaseController
{
    public function __construct() 
    {
        $this->loadAndAuthorizeResource(['idParam' => 'url']); // will use Article::findOrFail($this->params['url'])
    }
}

Override loading

The resource will only be loaded into an instance variable if it hasn't been already. This allows you to easily override how the loading happens in a separate beforeFilter.

class BooksController extends BaseController
{
    public function __construct() 
    {
        $this->beforeFilter('findPublishedBook', ['only' => 'show']);
        $this->loadAndAuthorizeResource();
    }

    protected function findPublishedBook($route)
    {
        $this->book = Book::released()->find($route->getParameters()['books']);
    }
}

It is important that any custom loading behavior happens before the call to loadAndAuthorizeResource. If you have authorizeResource in your BaseController then you need to use prependBeforeFilter to do the loading in the controller subclasses so it happens before authorization.

authorizeResource

Adding authorizeResource will make a before filter which calls authorize, passing the resource instance variable if it exists. If the instance variable isn't set (such as in the index action) it will pass in the class name. For example, if we have a ProductsController it will do this before each action.

$this->authorize(params['action'], ($this->product ? $this->product : 'Product'));

More info

For additional information see the PHPDoc blocks of the loadResource() and authorizeResource() methods.

Also see Nested Resources and Non RESTful Controllers.

Resetting Current Authority

If you ever update a User record which may be the current user, it will make the current authority for that request stale. This means any can checks will use the user record before it was updated. You will need to reset the currentAuthority instance so it will be reloaded. Do the same for the currentUser if you are caching that too.

if ($this->user->update(params['user'])) {
    $this->getCurrentAuthority()->setCurrentUser(null);
    $this->setCurrentAuthority(null);
    // ...
}