Chapter 6. User Authorization and Access Control

Authenticating users is just half of the story. Even if you are writing some Web community hub or, God forbid, social network, and identifying users is crucial part of your business rules, you almost always need to control which users can access which part of the application's functionality. It can be said that there's no real point in authentication without access control accompanying it, which is, user authorization.

In this chapter, we'll see how Yii 2 can help us in preventing or allowing users to access the functionality of the web application. We will focus on the following four features of Yii:

  • Hook methods of the controller
  • Exception handling in Yii 2
  • Controller action filters
  • Role-based access control

As a code example, we'll implement a simple scheme of access control to our example CRM application based on the preceding four features.

Let's start simple.

Access control using the state of user authentication

We already learned about rudimentary access control in the previous chapter, when we wrote the code to render the authentication indicator like the following:

if (Yii::$app->user->isGuest)
    // render the indicator for guests
else
    // render the indicator for authenticated users

So, we are able to differentiate the content based on whether the user is authenticated or not.

Similarly, we cannot just differentiate the content, but totally prohibit visitors from entering some routes without authentication. For this we need to learn about two features of Yii: exception handling and the hook methods of the controller.

FEATURE – hook methods of the controller

Similar to the beforeSave() method we used in the previous chapter, Controller classes also have hook methods. They are as follows:

Method name

What it does

beforeAction($action)

This executes before the actual controller action executes. As with ActiveRecord.beforeSave(), it must return a Boolean value indicating whether the given action is allowed to be run.

afterAction($action, $result)

This executes after the actual controller action finishes executing, but before the result is sent to ViewRenderer. In fact, the $result argument is the result of the action, and we can do something with it. Of course, we need to return this argument or else the route will output nothing.

Both of these hooks get the $action argument, which represents the controller action being executed. This object is of little use, really, being just the container for the following:

  • Action ID, which can be useful for ultra-precise access control based on the action IDs.
  • Reference to the controller this action belongs to.
  • The run() method, which is why this action exists in the first place, as it's the actual route handler represented by the action.

Note

Please note that in the case where beforeAction() returns false, the client browser will get a blank page without any explanations. This in fact can happen quite often when writing these hooks and forgetting to return a Boolean value from them! The same will happen if you forget to return $result from afterAction().

While it's certainly interesting to know what this action really is, right now its brief description is perfectly sufficient. A close inspection of what role an action plays in route handling will be done in Chapter 12, Route Management. Most of the time, there's no need to care about the actions.

Note

All possible hooks, how they work, and how we can use them for our benefit will be discussed in the Chapter 10, Events and Behaviors.

Given the definition of the beforeAction() method, it is obvious that we can restrict access to some parts of the application in the simplest possible way:

public function beforeAction($action)
{
    $parentAllowed = parent::beforeAction($action);
    $meAllowed = !Yii::$app->user->isGuest;
    return $parentAllowed and $meAllowed;
}

First we check whether the parent implementation allows the execution of the action in question. We then check whether the current user is unauthenticated by querying the isGuest property of the user component. If both these conditions are true, then we allow the execution of the action.

Note

What checks does the yiiwebController.beforeAction() method do? It protects you from Cross-Site Request Forgery (CSRF) attacks. In Yii 2, each and every form generated by the ActiveForm widget, by default, includes a special token as the additional request parameter. Only if the expected token from the form is equal to the token included in the client request will the request be processed. Of course, this check is unnecessary (and is not being done) for the GET, HEAD, and OPTIONS requests (the last one being extremely rare). You can also disable this check (don't do this) via the enableCsrfValidation setting directly in the application configuration.

By the way, in the main layout file we made in Chapter 3, Automatically Generating the CRUD Code, and discussed in detail in Chapter 4, The Renderer, there is a call to the Html::csrfMetaTags() method that we haven't even mentioned yet. This call is here exactly to support the CSRF protection. More than that, with the enableCsrfValidation setting set to true, you must call this method or else you will lose the ability to submit any HTML form generated by the ActiveForm widget.

Most of the time you wouldn't do any serious harm if you don't call parent::beforeAction(), but you better do it, because if you forget to do this in the form-submitting action you'll lose an important line of security.

However, this way of blocking is not really interesting. First, it does not show the client any feedback about what really happened. Second, such a usage of beforeAction() is already anticipated in Yii 2 in the form of a special concept called filters.

Let's postpone the explanation of filters for a while and look at how we can show the client's browser what really happened with its request.

FEATURE – exception handling in Yii

Yii employs its own handler for unhandled exceptions, which is really usable right out of the box without any further improvements. Basically, if you throw an exception of any Exception class descendant somewhere in your controller action and not handle it, Yii will show the following error message to the client:

FEATURE – exception handling in Yii

However, here's what it will show if you throw the yiiwebNotFoundHttpException:

FEATURE – exception handling in Yii

Note that it correctly shows the status code and the error message according to the specification of HTTP status codes. The actual status code returned will also be 404.

In fact, yiiwebNotFoundHttpException is a wrapper around the base yiiwebHttpException, which you can throw to emit arbitrary status code and message to the client, as follows:

throw new HttpException(406, 'Pretty rare error, usually you should never see it.'),

Don't forget to write use yiiwebHttpException at the beginning of your script. Here is what will be shown:

FEATURE – exception handling in Yii

Note that the message provided in the exception constructor is shown underneath the main message of the 406 status code, which is autogenerated according to HTTP specification and is not changeable. The status code actually returned from the server will also be 406.

So, just by throwing the various kinds of HTTP exceptions you can block the user request with a descriptive message. Note, however, that nothing prevents someone from getting the following output when using yiiwebHttpException instead of its specialized subclasses:

FEATURE – exception handling in Yii

Well, indeed anyone will think this is a server error. Even while Yii really returns status code 200, the response will be the error page. In case anyone tries to throw the HttpException with the status code 302, for example, the status code will be 302 but no redirection will happen. This is just the static page error handler in all cases.

Here is the list of all descendants of HttpException that Yii 2 defines for its users' convenience:

Exception

HTTP status code shown

BadRequestHttpException

400 Bad Request

ConflictHttpException

409 Conflict

ForbiddenHttpException

403 Forbidden

GoneHttpException

410 Gone

MethodNotAllowedHttpException

405 Method Not Allowed

NotAcceptableHttpException

406 Not Acceptable

NotFoundHttpException

404 Not Found

TooManyRequestsHttpException

429 Too Many Requests (from RFC 6585 additional HTTP status codes, see http://tools.ietf.org/html/rfc6585)

UnauthorizedHttpException

401 Unauthorized

UnsupportedMediaTypeHttpException

415 Unsupported Media Type

Another good feature of Yii exception handling is its error reporting in case the debugging mode is enabled.

Inside the index.php entry point script, before a call to require() for Yii library, the constant YII_DEBUG will be defined with the value of true:

define('YII_DEBUG', true);

Tip

It's vital for this constant to be defined before the Yii library is loaded, as in its absence Yii will define it to be false. Note that YII_DEBUG also performs other tasks apart from enabling verbose error reporting.

If an unhandled exception like the following one is thrown in the code (this is different from the HttpException):

throw new LogicException('I am unhandled exception and I am proud of it'),

Then here's what will be shown instead of the generic "Exception" page:

FEATURE – exception handling in Yii

This is a really amazing stacktrace page. It's so long that to fit it here we had to tear the non-unique pieces out of it. It gives several layers of code before the failed line with code highlighting, filenames, line numbers, and links to the documentation pages for the Yii classes and methods mentioned (!). It shows the server runtime configuration and also cookies sent by the client.

Tip

This page should not be presented to visitors in the production environment. Always set YII_DEBUG to false there.

Okay, now here's the guard case that prevents unauthenticated users from using the controller action it is in:

if (Yii::$app->user->isGuest)
    throw new ForbiddenHttpException;

This is the simplest line of access control available for us in Yii 2. Of course, any check can be used as long as the HttpException descendant is being thrown. This is so fundamental that Yii has a special built-in AccessControl filter for it. We'll learn about the AccessControl filter later. Let's learn about what a filter is in Yii terminology.

FEATURE – controller action filters

An action filter, in short, is the beforeAction() and afterAction() methods packed into a single class, pluggable to the Controller instances through the Controller.behaviors() method.

The benefits of this approach are as follows:

  • You can define arbitrarily long and complex guard cases and/or post- or pre-process them, given that you now have an entire class dedicated for this purpose.
  • You can combine different filters in any combinations and order.

For the class to become the action filter for some controller, it needs to be:

  • A descendant of the ActionFilter class.
  • Referenced in the controller's behaviors() method.

The ActionFilter itself is the special case of the behaviors that are described in Chapter 10, Events and Behaviors. For now, it's sufficient to understand that a behavior is some class containing some methods that can be declared to be an integral part of some other class, extending its feature set. If you are familiar with the concept of "traits" from PHP 5.4, this is it.

We'll not look at how to create our own ActionFilter, as it's quite unnecessary given that we already know how the beforeAction() and afterAction() methods work. Let's briefly look at the built-in Yii 2 filters instead. Detailed information about their usage can be read from their corresponding documentation pages.

Filter class

What it does

yiifiltersVerbFilter

This filter prevents or allows access to controller actions based on the HTTP method used for request. For example, you can allow only POST requests for your log in and log out actions with this filter. This is the only action filter that doesn't inherit the ActionFilter class but directly inherits the Behavior class instead. It responds with the 405 Method Not Allowed message if the action is not to be executed. Visit http://www.yiiframework.com/doc-2.0/yii-filters-verbfilter.html for more information.

yiifiltersPageCache

This filter caches the rendering result of the given controller's actions. It has a sufficiently complex and flexible configuration to control where, for how long, and which particular routes should be cached. Visit http://www.yiiframework.com/doc-2.0/yii-filters-pagecache.html for more information.

yiifiltersHttpCache

This filter is conceptually equivalent to PageCache, but it uses the HTTP Last-Modified and ETag headers to perform caching. In effect, the client's browser will be responsible for keeping and presenting the cached version of the response to the user. Visit http://www.yiiframework.com/doc-2.0/yii-filters-httpcache.html for more information.

yiifiltersAccessControl

This filter prevents or allows access to the actions depending on the set of rules in a very flexible and expressive syntax. It is so powerful that often there's no need to implement any other form of access control in the controller at all. This filter can control access using the HTTP method, user authentication status, user role (to be defined later), action and/or controller ID, user IP, or even a custom callback provided. It responds with 403 Forbidden if the action is not to be executed. Visit http://www.yiiframework.com/doc-2.0/yii-filters-accesscontrol.html for more information, but we'll be discussing this filter in this chapter anyway.

yiifiltersContentNegotiator

This filter is a very handy filter for large applications. It automatically changes the application language and response format based on the HTTP headers and query parameters send by the client browser. Visit http://www.yiiframework.com/doc-2.0/yii-filters-contentnegotiator.html for more information.

yiifiltersRateLimiter

This filter prevents the access for users who exceeded their rate limit, that is, amount of page requests in some time range. The user identities (explained in the previous chapter) need to implement the yiifiltersRateLimitInterface to support specifying the allowed rates per user. Visit http://www.yiiframework.com/doc-2.0/yii-filters-ratelimiter.html for more information.

As this chapter is about access control, let's look at some examples of the usage of VerbFilter and AccessControl. The AccessControl filter is so important and powerful that we'll look at it later in more detail after we explore the user roles system.

Here's how Gii protects the controller autogenerated by the VerbFilter:

    public function behaviors()
    {
        return [
            'verbs' => [
                'class' => VerbFilter::className(),
                'actions' => [
                    'delete' => ['post'],
                ],
            ],
        ];
    }

Note

Note the VerbFilter::className() invocation. It is the Yii 2 idiom allowing us to easily get the fully qualified name of any class extending the base yiiaseObject, and the Yii 2 code uses this idiom extensively. In this case, it will always return the yiifiltersVerbFilter string.

The behaviors() method must return an array of configurations for the filters to be attached to the controller. In this case, we attach the VerbFilter at the arbitrarily chosen verbs key. This VerbFilter is configured so that the delete action can be accessed only by the POST request. All other actions are unguarded, because the update form request accepts the data being POSTed and renders the HTML page for the form.

Here's how we can quite naively protect our login and logout actions using the AccessControl filter:

    public function behaviors()
    {
        return [
            'access' => [ // 1
                'class' => AccessControl::className(), // 2
                'only' => ['login', 'logout'], // 3
                'rules' => [
                    [
                        'actions' => ['login'], // 4
                        'roles' => ['?'], // 5
                        'allow' => true, // 6
                    ],
                    [
                        'actions' => ['logout'], // 7
                        'roles' => ['@'], // 8
                        'allow' => true, // 9
                    ]
                ]
            ]
        ];
    }

In the preceding code, we are using the behaviors property of our Controller to attach the yiifiltersAccessControl class to it, which will be already configured as specified.

Let's understand the preceding code line-by-line from the top to bottom:

  1. An arbitrarily-selected name access is used to return back to the calling method from the behaviors() method.
  2. We register the AccessControl filter.
  3. The only variable denotes that only the login and logout actions, which translate to actionLogin() and actionLogout() methods of the controller, are used. The default for access control filter is to deny everything not explicitly allowed, so we need to restrict it here.
  4. For the login action.
  5. For unauthenticated users. The symbol ? means guest users.
  6. Allow request.
  7. For the logout action.
  8. For authenticated users. The symbol @ means authenticated users.
  9. Allow request.

The specific configuration parameters for each rule can be read in the documentation and/or source of the yiifiltersAccessRule class (http://www.yiiframework.com/doc-2.0/yii-filters-accessrule.html).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.187.62