Authorization¶
Aide provides a lightweight, object-oriented authorization system based heavily on the popular Ruby gem, Pundit.
Policies¶
At the core of Aide’s authorization is the notion of policy classes. Policy classes must extend Deefour\Aide\Authorization\AbstractPolicy
. Each method should return a boolean. For example
use Deefour\Aide\Authorization\AbstractPolicy;
class ArticlePolicy extends AbstractPolicy {
public function edit() {
return $this->user->id === $this->record->author_id;
}
}
When a policy class is instantiated through Aide, a $record to authorize is passed along with a $user to authorize against. Using the helper methods provided in Deefour\Aide\Authorization\PolicyTrait
is optional; you could instantiate a policy and check authorization for the $user yourself
$user = User::find(1); // find some User with id = 1
$article = $user->articles()->first(); // get the first Article authored by the User
$policy = new ArticlePolicy($user, $article);
$policy->edit(); // true
Assumptions¶
When generating a policy class for an object via Aide’s helpers, the following assumptions are made.
- The policy class has the same name as the object being authorized, suffixed with “Policy” (though this can be overridden)
- The first argument is the user to authorize for the action. When using Aide’s helpers, this requires you create a currentUser method on the class using
Deefour\Aide\Authorization\PolicyTrait
.- The second argument is the object you wish to check the authorization against.
Scopes¶
Aide also provides support for policy scopes. A policy scope will typically generate an iterable collection of objects the current user is able to access
For example, the scope against the Article model below will return a collection of only published articles unless the current user is an administrator, in which case a collection of all articles will be returned.
use Deefour\Aide\Authorization\AbstractScope;
class ArticleScope extends AbstractScope {
public function resolve() {
if ($this->user->isAdmin()) {
return $this->scope->all();
} else {
return $this->scope->where('published', true)->get();
}
}
}
When a policy scope is instantiated through Aide, the current $user and a $scope object are passed into the derived policy scope. By default, the policy scope is determined based on the name of the $scope object.
$user = User::find(1);
$policyScope = new ArticleScope($user, Article::newQuery());
$articles = $policyScope->resolve(); // collection of Articles
Assumptions¶
When generating a policy scope via Aide’s helpers, the following assumptions are made.
- The policy scope has the same name as the object being authorized, suffixed with “Scope” (though this can be overridden)
- The first argument is the user to filter the scope for. When using Aide’s helpers, this requires you create a currentUser method on the class using
Deefour\Aide\Authorization\PolicyTrait
.- The second argument is the scope object you wish to modify based on the state/details of the $user.
An Example in the Context of an Application¶
Using Laravel, the following could be added to the BaseController.
class BaseController extends Controller {
use Deefour\Aide\Authorization\PolicyTrait;
protected function currentUser() {
return Auth::user() ?: new User;
}
}
Now, for some ArticleController, to authorize the current user against the ability to edit a specific Article, the edit() method would like this
public function edit($id) {
$article = Article::find($id);
$this->authorize($article); // if NOT authorized, exception will be thrown
return View::make('articles.edit'); // display the form
}
The $this->authorize($article); line will generate a fresh ArticlePolicy instance through Aide, passing the current user and the fetched $article into it. The ArticlePolicy::edit() method will be called, and if the user is authorized to edit the article, the view for the action will render as expected.
Usage Within Laravel¶
Aide provides a service provider and facade for the Policy class to make interacting with it very simple inside of a Laravel application.
Service Provider¶
In Laravel’s app/config/app.php file, add the class:Deefour\Aide\Authorization\PolicyServiceProvider class to the list of providers
'providers' => array(
// ...
'Deefour\Aide\Support\Facades\PolicyServiceProvider',
),
// ...
The IoC container is responsible for instantiating a single, shared instance of the Deefour\Aide\Authorization\Policy
class. This is done outside the scope of a controller method, meaning the IoC container has no access to or knowledge of the currentUser method that may exist within a base controller. Further, the API provided by the Policy facade does not expect a user to be passed. This means instead of calling
$app['policy']->policy(Auth::user(), new Article);
the following is actually correct
$app['policy']->policy(new Article);
To accomplish this, the service provider looks for configuration in an app/config/policy.php file. At a minimum, the following is required when using the policy service provider.
<?php
return array(
'user' => function() {
return Auth::user() ?: new User; // this logic can be replaced with anything you like.
},
);
To keep things DRY, the currentUser method in the base controller could be modified to take advantage of this same Closure.
public function currentUser() {
return call_user_func(Config::get('policy.user'));
}
Policy Facade¶
With the service provider in place, the instantiated policy class can be accessed via the main application container.
$articlePolicy = $app['policy']->policy(new Article);
Remember, you can also instantiate the policy provider yourself.
use Deefour\Aide\Authorization\Policy;
$policyProvider = new Policy(Auth::user());
$articlePolicy = $policyProvider->policy(new Article);
This can be simplified through the use of a Policy facade. Add the following to app/config/app.php
'aliases' => array(
// ...
'Policy' => 'Deefour\Aide\Support\Facades\Policy',
),
// ...
The same functionality above is now as simple as this
$articlePolicy = Policy::policy(new Article);
Policies in Views¶
The facade makes working with policies in views simple too. For example, to conditionally show an ‘Edit’ link for a specific $article based on the current user’s ability to edit that article, the following could be used in a blade template
@if (Policy::policy($article)->edit())
<a href="{{ URL::route('articles.edit', [ 'id' => $article->id ]) }}">Edit</a>
@endif
Handling Unauthorized Exceptions¶
If false is returned by the authorize() call, a Deefour\Aide\Authorization\NotAuthorizedException
will be thrown. This exception can be caught by Laravel with the following in app/start/global.php.
use Deefour\Aide\Authorization\NotAuthorizedException;
App::error(function(NotAuthorizedException $exception) {
// Handle the exception...
});
Note
There is nothing Laravel-specific about Aide’s authorization component. The Deefour\Aide\Authorization\PolicyTrait
trait can be used in any class.
Ensuring Policies Are Used¶
Again using Laravel as an example, an after filter can be configured to prevent accidentally unauthorized actions from being wide open by default. A filter in the constructor of the ArticleController could look like this
public function __construct() {
$this->afterFilter(function() {
$this->verifyAuthorized();
}, [ 'except' => 'index' ]);
}
There is a similar method to ensure a scope is used, which is particularly useful for index actions where a collection of objects is rendered and is dependent on each user.
public function __construct() {
$this->afterFilter(function() {
$this->requirePolicyScoped();
}, [ 'only' => 'index' ]);
}
Policy/Scope Instantiation Without Trait Methods¶
Policies and scopes can easily be retrieved using static or instance methods on the Deefour\Aide\Authorization\Policy
class.
Static Instantiation¶
The following methods are statically exposed:
- Policy::policy()
- Policy::policyOrFail()
- Policy::scope()
- Policy::scopeOrFail()
For example:
use Deefour\Aide\Authorization\Policy;
$user = User::find(1);
$article = $user->articles()->first();
Policy::policy($user, $article);
Policy::policyOrFail($user, $article);
Policy::scope($user, new Article);
Policy::scopeOrFail($user, new Article);
The ...OrFail version of each method will throw a Deefour\Aide\Authorization\NotDefinedException
exception if the policy class Aide tries to instantiate doesn’t exist.
Instance Instantiation¶
A limited version of the above API is available when creating an instance of the Policy class.
- Policy::policy()
- Policy::scope()
- Policy::authorize()
use Deefour\Aide\Authorization\Policy;
$user = User::find(1);
$article = $user->articles()->first();
$policy = new Policy($user);
$policy->policy($article);
$policy->scope($article);
$policy->authorize($article, 'edit');
Note
The authorize method in this case requires an action/method be passed as the second argument.
The policy() and scope() methods are pass-through’s to the ...OrFail() methods on the PolicyTrait; exceptions will be thrown if a policy or scope cannot be found.
Manually Specifying Policy Classes¶
The policy class Aide tries to instantiate for an object can be overridden. Given the following scenario
use Deefour\Aide\Authorization\Policy;
class ArticlePolicy {}
class Article { }
class NewsArticle extends Article { }
Policy::policyOrFail(new Article); // returns fresh ArticlePolicy instance
Policy::policyOrFail(new NewsArticle); // throws Deefour\Aide\Authorization\NotDefinedException
Aide can be instructed to instantiate an ArticlePolicy
class for the NewsArticle
through a policyClass() method on Article
(since :class:`NewsArticle` extends it).
use Deefour\Aide\Authorization\Policy;
class ArticlePolicy {}
class Article {
public function policyClass() {
return 'ArticlePolicy';
}
}
class NewsArticle extends Article { }
Policy::policyOrFail(new Article); // returns fresh ArticlePolicy instance
Policy::policyOrFail(new NewsArticle); // returns fresh ArticlePolicy instance
Similarly, if a name() method is provided on the object, the string returned will be used as the class prefix for the policy class Aide tries to instantiate.
use Deefour\Aide\Authorization\Policy;
class PostPolicy {}
class Article {
public function name() {
return 'Post';
}
}
Policy::policyOrFail(new Article); // returns fresh PostPolicy instance
Closed System¶
Many apps only allow authenticated users to perform most actions. Instead of verifying on every policy action that the current user is not null, unpersisted in the database, or similarly not a legitimately authenticated user, this can be done through a special ApplicationPolicy
that your other policy classes extend.
use Deefour\Aide\Authorization\AbstractPolicy;
use Deefour\Aide\Authorization\NotAuthorizedException;
class ApplicationPolicy extends AbstractPolicy {
public function __construct($user, $record) {
if (is_null($user) or ! $user->exists) {
throw new NotAuthorizedException;
}
parent::__construct($user, $record);
}
}
class ArticlePolicy extends ApplicationPolicy { }
Mass Assignment Protection¶
A special permittedAttributes method can be created on a policy to conditionally provide a whitelist of attributes for a given request by a user to create or modify a record.
use Deefour\Aide\Authorization\AbstractPolicy;
class ArticlePolicy extends AbstractPolicy {
public function permittedAttributes() {
$attributes = [ 'title', 'body', ];
// prevent the author and slug from being modified after the article
// has been persisted to the database.
if ( ! $this->record->exists) {
return array_merge($attributes, [ 'user_id', 'slug', ]);
}
return $attributes;
}
}
This policy method can be used within a controller to filter unauthorized attributes from being set on a model via mass assignment. Again, in a Laravel controller action (`Repository` below comes from a facade provided by Aide for Laravel)
$article = Article::find(1);
$repository = Repository::make($article);
$policy = Policy::make($article);
$repository->update(
$policy->permittedAttributes(Input::get('article'))
);