|

Setting Permissions With Policy Objects

Setting Permissions With Policy Objects

We might have created a problem by letting any user in the system to delete any thread in the system. It would be better if a registered user could only delete threads that he or she created. This tutorial will look at how we can assign permissions to users in the system that will decide whether or not they are authorized to take an action such as delete a thread. First we’ll look at how to complete this task by hand so to speak, and then we’ll look at creating a dedicated Policy Object instead to streamline the process.


Without Policies, Users May Have Too Much Power

In the last tutorial, we gave users the ability to delete any threads they have created. Well it turns out that not only can a user delete their own threads, they are able to delete any thread in the system, no matter who created it. Surely we don’t want this to be the case so we will need to consider how to limit the powers of individual users. For example if we log in as Nikola Tesla, we should only be able to delete his threads. Watch as Nikola goes ahead and deletes one of Tom’s threads. Not Good!
user has too many permissions


Refactoring The CreateThreadsTest Class

As we start adding permissions for what a user can and can not do on the site, we can start with a test. We will not create a new test, but refactor one we have in place already. There is already a test of test_guests_can_not_delete_threads() in the class. This test makes sure that a non logged in user can not delete a thread. Now let’s think about that for a second. Not only do we not want guests of the site deleting threads, we also do not want logged in users to delete other people’s threads. So we will change this test to test_unauthorized_users_can_not_delete_threads(). This reflects the fact that we want to protect against guests, as well as authenticated users which do not have the required permissions.

With these thoughts in mind, here is a rough mock up of that test.

Of course, it is failing to start, but we will update the code as we go to fix this.

vagrant@homestead:~/Code/forumio$ phpunit --filter test_unauthorized_users_can_not_delete_threads
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 892 ms, Memory: 10.00MB

There was 1 failure:

1) Tests\Feature\CreateThreadsTest::test_unauthorized_users_can_not_delete_threads
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'http://localhost/login'
+'http://localhost/threads'

/home/vagrant/Code/forumio/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:97
/home/vagrant/Code/forumio/tests/Feature/CreateThreadsTest.php:62

FAILURES!
Tests: 1, Assertions: 4, Failures: 1.

Using Your Own Logic In destroy()
The first way we can tackle this is to put in place our own logic that does the permission checking on the user. We can do this in the destroy() method of the ThreadsController class. Highlighted in the snippet below shows a little bit of logic that can be placed in the controller to make sure that if a user tries to delete a thread that they do not own, then they will not be allowed to do complete the delete function.

Sure enough, that allows the test to pass.

vagrant@homestead:~/Code/forumio$ phpunit --filter test_unauthorized_users_can_not_delete_threads
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 821 ms, Memory: 10.00MB

OK (1 test, 4 assertions)

We can now run the full suite of tests and make sure all is good.

vagrant@homestead:~/Code/forumio$ phpunit
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.

......F......................                                     29 / 29 (100%)

Time: 3.23 seconds, Memory: 12.00MB

There was 1 failure:

1) Tests\Feature\CreateThreadsTest::test_a_thread_can_be_deleted
Expected status code 204 but received 403.
Failed asserting that false is true.

/home/vagrant/Code/forumio/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:78
/home/vagrant/Code/forumio/tests/Feature/CreateThreadsTest.php:74

FAILURES!
Tests: 29, Assertions: 50, Failures: 1.

Uh oh. All is not good it appears. The test test_a_thread_can_be_deleted() is now failing. What is happening in that test? Let’s have a look.

This test here is good, but we’re finding maybe it is a bit to generalized as we begin fleshing things out. In order to fix this, we can make the test more specific, such as test_authorized_users_can_delete_threads(). In addition, we will need to fix the code in the test to reflect that fact that when a user is deleting a thread, their user id must match the user id associated with that thread. Otherwise, that means they are trying to delete a thread that does not belong to them, which we can not allow. The highlighted code shows where we make this explicit connection.

Now we are back to all tests in a passing state which is good.
phpunit all tests passing


Testing In The Browser

Really quickly, we can just test this out in the browser now. We are logged in as Nikola Tesla and if we try to delete a thread by say user asdf, we are immediately redirected, and the thread continues to exist in the database. It was *not* deleted, which is what we want.
no permission to delete model


Dropping The Hammer

Come to think of it, maybe redirecting the user is a bit too nice if they are trying to do something they really shouldn’t be doing. If that’s the case, we can drop the hammer and simple abort() the request altogether like so.

You can see the user is given a good smack down when trying to do anything nefarious now.
symfony httpexception

Do note that if you are using this approach, you need to update the corresponding test to make sure it is accounting for the 403 status code. This update below does allow for that.

update destroy() method on threadsController
add logic in destroy to handle this

fix any tests that now fail


Better Permissions With Policy Objects

So we had a problem with users taking actions they should not have. Much like users have permissions on an operating system like Linux, we can also assign permissions to users in our application. We took the long route at first and created a solution to the problem by hand so to speak. Now, we will use a dedicated Policy Object to more easily solve our problem. A Policy Object is like a guard for a Model. It determines permissions. Like most features in Laravel, you can use artisan to create the boilerplate for you, which is so nice. We will create the Policy Object now.

vagrant@homestead:~/Code/forumio$ php artisan make:policy ThreadPolicy --model=Thread
Policy created successfully.

Just above, we entered the command to create our new Policy Object. Also notice the flag of –model. Here we are specifying which Model this Policy is associated with. By specifying this flag, you get all of this boilerplate here.

Note that this action has created a new directory and file. Policies is the directory and ThreadPolicy.php is your file.
policy object class in laravel

We will start with filling out the update() method of the class. We can set up a scenario where the method checks if the threads user id is equal to that of the authenticated user’s id like so:


Configuring AuthServiceProvider

Now that we have a dedicated Policy Object, it must be registered properly in the AuthServiceProvider class. Below we simply comment out the existing line of code in the $policies property. Then we create a new mapping between our Thread Model and the ThreadPolicy we have created. We could also just delete the commented out code, but it helps demonstrate how the mapping works so we’ll leave it be.


Making Use of authorize() in the Controller

The legwork and boilerplate is now all set up for us. That means we can make use of the authorize() method associated with a Policy Object in our controller. For example consider this code:

The highlighted code above leans on that policy object we created to do it’s work. It looks at the update() method on ThreadPolicy, and checks to see if the thread’s user id and the authenticated user’s id match. If they match, the user is authorized to continue. If they do not match, an unauthorized exception is automatically thrown. Once again we can be logged in to the application as the user of Nikola Tesla and when we try to delete a post made by user asdf – we get that exception.
access denied http exception


How To Use the @can blade directive

With policies set up, you can now make use of a really cool @can directive in your blade views if you like. We want to make use of the @can directive to selectively show the delete thread link to users. In other words, if the user is not authorized to perform a delete on the given thread, then we should not display the delete thread link to them. Here is how we can now update the view to handle this. In the threads/show.blade.php file, find the delete thread link, and wrap it in the @can directive like you see here:

This is so slick. The markup is saying, if the logged in user is authorized to ‘update’ (remember the method we filled in) a $thread, then display the link. If they are not authorized, then that link will not be displayed.


Logged in user does not see delete link for other persons thread

Logged in user does not see delete link for other persons thread


Logged in user does see delete link for their own thread

Logged in user does see delete link for their own thread


Granting Admin Super Powers

While we are on the topic of authorization, permissions, and policy objects, let’s see how we can give a user admin powers. In any application, there needs to be a user who has the ability to manage things with elevated privileges. For example, how can we make Nikola Tesla the administrator. How can you implement this?


Policy Specific before() method

In any given Policy class, you can create a before() method which is a way to grant authorization for all other methods based on a given criteria. The following snippet will give Nikola Tesla authorization for all things in the Policy object.

Since we have now given Nikola super admin powers, he can delete other people’s threads.
admin powers can delete other people threads


AuthServiceProvider Global

Don’t want to deal with granting authorization on a per policy basis? No problem. In that case, go ahead and visit AuthServiceProvider.php and invoke the before() method of the Gate class.

Now, Nikola Tesla is a Super User no matter what.


Setting Permissions With Policy Objects Summary

In this tutorial we had a nice introduction to creating Policy classes in Laravel to help manage permissions for users in an application. Policy Objects work on a one to one relation between a given Model class and a given Policy class. Once the Policy is set up, we can make use of simple methods in the controller to determine when actions should be taken or not. In addition, we learned about that cool @can directive in your blade views which can selectively display UI elements based on the permissions of the logged in user. Fantastic!

|