Книга: Laravel 4 Cookbook
Назад: Authentication
Дальше: Deployment

Access Control List

Previously we looked at how to set up a basic authentication system. In this chapter; we’re going to continue to improve the authentication system by adding what’s called ACL (Access Control List) to the authentication layer.

The code for this chapter can be found at: https://github.com/formativ/tutorial-laravel-4-acl

Managing Groups

We’re going to be creating an interface for adding, modifying and deleting user groups. Groups will be the containers to which we add various users and resources. We’ll do that by create a migration and a model for groups, but we’re also going to optimise the way we create migrations.

Refactoring Migrations

We’ve got a few more migrations to create in this tutorial; so it’s a good time for us to refactor our approach to creating them…

 1 <?php  2   3 use Illuminate\Database\Migrations\Migration;  4 use Illuminate\Database\Schema\Blueprint;  5   6 class BaseMigration  7 extends Migration  8 {  9     protected $table; 10  11     public function getTable() 12     { 13         if ($this->table == null) 14         { 15             throw new Exception("Table not set."); 16         } 17  18         return $this->table; 19     } 20  21     public function setTable(Blueprint $table) 22     { 23         $this->table = $table; 24         return $this; 25     } 26  27     public function addNullable($type, $key) 28     { 29         $types = [ 30             "boolean", 31             "dateTime", 32             "integer", 33             "string", 34             "text" 35         ]; 36  37         if (in_array($type, $types)) 38         { 39             $this->getTable() 40                 ->{$type}($key) 41                 ->nullable() 42                 ->default(null); 43         } 44  45         return $this; 46     } 47  48     public function addTimestamps() 49     { 50         $this->addNullable("dateTime", "created_at"); 51         $this->addNullable("dateTime", "updated_at"); 52         $this->addNullable("dateTime", "deleted_at"); 53         return $this; 54     } 55  56     public function addPrimary() 57     { 58         $this->getTable()->increments("id"); 59         return $this; 60     } 61  62     public function addForeign($key) 63     { 64         $this->addNullable("integer", $key); 65         $this->getTable()->index($key); 66         return $this; 67     } 68  69     public function addBoolean($key) 70     { 71         return $this->addNullable("boolean", $key); 72     } 73  74     public function addDateTime($key) 75     { 76         return $this->addNullable("dateTime", $key); 77     } 78  79     public function addInteger($key) 80     { 81         return $this->addNullable("integer", $key); 82     } 83  84     public function addString($key) 85     { 86         return $this->addNullable("string", $key); 87     } 88  89     public function addText($key) 90     { 91         return $this->addNullable("text", $key); 92     } 93 } 

This file should be saved as app/database/migrations/BaseMigration.php.

We’re going to base all of our models off of a single BaseModel class. This will make it possible for us to reuse a lot of the repeated code we had before.

The BaseModel class has a single protected $table property, for storing the current Blueprint instance we are giving inside our migration callbacks. We have a typical setter for this; and an atypical getter (which throws an exception if $this->table hasn’t been set). We do this as we need a way to validate that the methods which require a valid Blueprint instance have one or throw an exception.

Our BaseMigration class also has a factory method for creating fields of various types. If the type provided is one of those defined; a nullable field of that type will be created. This significantly shortens the code we used previously to create nullable fields.

Following this; we have addPrimary(), addForeign() and addTimestamps(). The addPrimary() method is a bit clearer than the increments() method, the addForeign() method adds both a nullable integer field and an index for the foreign key. The addTimestamps() method is similar to the Blueprint’s timestamps() method; except that it also adds the deleted_at timestamp field.

Finally; there are a handful of methods which proxy to the addNullable() method.

Using these methods, the amount of code required for the migrations we will create (and have already created) is drastically reduced.

 1 <?php  2   3 use Illuminate\Database\Schema\Blueprint;  4   5 class CreateGroupTable  6 extends BaseMigration  7 {  8     public function up()  9     { 10         Schema::create("group", function(Blueprint $table) 11         { 12             $this 13                 ->setTable($table) 14                 ->addPrimary() 15                 ->addString("name") 16                 ->addTimestamps(); 17         }); 18     } 19  20     public function down() 21     { 22         Schema::dropIfExists("group"); 23     } 24 } 

This file should be saved as app/database/migrations/0000_00_00_000000_CreateGroupTable.php. Yours may be slightly different as the 0’s are replaced with other numbers.

The group table has a primary key, timestamp fields (including created_at, updated_at and deleted_at) as well as a name field.

If you’re skipping migrations; the following SQL should create the same table structure as the migration:

1 CREATE TABLE `group` ( 2  `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 3  `name` varchar(255) DEFAULT NULL, 4  `created_at` datetime DEFAULT NULL, 5  `updated_at` datetime DEFAULT NULL, 6  `deleted_at` datetime DEFAULT NULL, 7  PRIMARY KEY (`id`) 8 ) ENGINE=InnoDB CHARSET=utf8; 

Listing Groups

We’re going to be creating views to manage group records; more comprehensive than those we created for users previously, but much the same in terms of complexity.

 1 @extends("layout")  2 @section("content")  3     @if (count($groups))  4         <table>  5             <tr>  6                 <th>name</th>  7             </tr>  8             @foreach ($groups as $group)  9                 <tr> 10                     <td>{{ $group->name }}</td> 11                 </tr> 12             @endforeach 13         </table> 14     @else 15         <p>There are no groups.</p> 16     @endif 17     <a href="{{ URL::route("group/add") }}">add group</a> 18 @stop 

This file should be saved as app/views/group/index.blade.php.

The first view is the index view. This should list all the groups that are in the database. We extend the layout as usual, defining a content block for the markup specific to this page.

The main idea is to iterate over the group records, but before we do that we first check if there are any groups. After all, we don’t want to go to the trouble of showing a table if there’s nothing to put in it.

If there are groups, we create a (rough) table and iterate over the groups; creating a row for each. We finish off the view by adding a link to create a new group.

1 Route::any("/group/index", [ 2     "as"   => "group/index", 3     "uses" => "GroupController@indexAction" 4 ]); 

This was extracted from app/routes.php.

 1 <?php  2   3 class Group  4 extends Eloquent  5 {  6     protected $table = "group";  7    8     protected $softDelete = true;  9  10     protected $guarded = [ 11         "id", 12         "created_at", 13         "updated_at", 14         "deleted_at" 15     ]; 16 } 

This file should be saved as app/models/Group.php.

 1 <?php  2   3 class GroupController  4 extends Controller  5 {  6    public function indexAction()  7    {   8         return View::make("group/index", [  9             "groups" => Group::all() 10         ]); 11     } 12 } 

This file should be saved as app/controllers/GroupController.php.

In order to view the index page; we need to define a route to it. We also need to define a model for the group table. Lastly; we render the index view, having passed all the groups to the view. Navigating to this route should now display the message “There are no groups.” as we have yet to add any.

You can test out how the index page looks by adding a group to the database directly.

An important thing to note is the use of $softDelete. Laravel 4 provides a new method of ensuring that no data is hastily deleted via Eloquent; so long as this property is set. If true; any calls to the $group->delete() method will set the deleted_at timestamp to the date and time on which the method was invoked. Records with a deleted_at timestamp (which is not null) will not be returned in normal QueryBuilder (including Eloquent) queries.

You can find out more about soft deleting at: http://laravel.com/docs/eloquent#soft-deleting

Another important thing to note is the use of $guarded. Laravel 4 provides mass assignment protection. What we’re doing by specifying this list of fields; is telling Eloquent which fields should not be settable when providing an array of data in the creation of a new Group instance.

You can find out more about this mass assignment protection at: http://laravel.com/docs/eloquent#mass-assignment

Adding Groups

We’re going to be abstracting much of the validation out of the controllers and into new form classes.

 1 <?php  2   3 use Illuminate\Support\MessageBag;  4   5 class BaseForm  6 {  7     protected $passes;  8     protected $errors;  9  10     public function __construct() 11     { 12         $errors = new MessageBag(); 13  14         if ($old = Input::old("errors")) 15         { 16             $errors = $old; 17         } 18  19         $this->errors = $errors; 20     } 21  22     public function isValid($rules) 23     { 24         $validator = Validator::make(Input::all(), $rules); 25         $this->passes = $validator->passes(); 26         $this->errors = $validator->errors(); 27         return $this->passes; 28     } 29  30     public function getErrors() 31     { 32         return $this->errors; 33     } 34  35     public function setErrors(MessageBag $errors) 36     { 37         $this->errors = $errors; 38         return $this; 39     } 40  41     public function hasErrors() 42     { 43         return $this->errors->any(); 44     } 45  46     public function getError($key) 47     { 48         return $this->getErrors()->first($key); 49     } 50  51     public function isPosted() 52     { 53         return Input::server("REQUEST_METHOD") == "POST"; 54     } 55 } 

This file should be saved as app/forms/BaseForm.php.

The BaseForm class checks for the error messages we would normally store to flash (session) storage. We would typically pull this data in each action, and now it will happen when each form class instance is created.

The validation takes place in the isValid() method, which gets all the input data and compares it to a set of provided validation rules. This will be used later, in BaseForm subclasses.

BaseForm also has a few methods for managing the $errors property, which should always be a MessageBag instance. They can be used to set and get the MessageBag instance, get an individual message and even tell whether there are any error messages present.

There’s also a method to determine whether the request method, for the current request, is POST.

 1 <?php  2   3 class GroupForm  4 extends BaseForm  5 {  6     public function isValidForAdd()  7     {  8         return $this->isValid([  9             "name" => "required" 10         ]); 11     } 12  13     public function isValidForEdit() 14     { 15         return $this->isValid([ 16             "id"   => "exists:group,id", 17             "name" => "required" 18         ]); 19     } 20  21     public function isValidForDelete() 22     { 23         return $this->isValid([ 24             "id" => "exists:group,id" 25         ]); 26     } 27 } 

This file should be saved as app/forms/GroupForm.php.

The first implementation of BaseForm is the GroupForm class. It’s quite simply by comparison; defining three validation methods. These will be used in their respective actions.

We also need a way to generate not only validation error message markup but also a quicker way to create form markup. Laravel 4 has great utilities for creating form and HTML markup, so let’s see how these can be extended.

1 {{ Form::label("name", "Name") }} 2 {{ Form::text("name", Input::old("name"), [ 3     "placeholder" => "new group" 4 ]) }} 

We’ve already seen this type of Blade template syntax before. The label and text helpers are great for programatically creating the markup we would otherwise have to create; but sometimes it is nice to be able to create our own markup generators for commonly repeated patterns.

What if we, for instance, often use a combination of label, text and error message markup? It would then be ideal for us to create what’s called a macro to generate that markup.

  1 <?php   2    3 Form::macro("field", function($options)   4 {   5     $markup = "";   6    7     $type = "text";   8    9     if (!empty($options["type"]))  10     {  11         $type = $options["type"];  12     }  13   14     if (empty($options["name"]))  15     {  16         return;  17     }  18   19     $name = $options["name"];  20   21     $label = "";  22   23     if (!empty($options["label"]))  24     {  25         $label = $options["label"];  26     }  27   28     $value = Input::old($name);  29   30     if (!empty($options["value"]))  31     {  32         $value = Input::old($name, $options["value"]);  33     }  34   35     $placeholder = "";  36   37     if (!empty($options["placeholder"]))  38     {  39         $placeholder = $options["placeholder"];  40     }  41   42     $class = "";  43   44     if (!empty($options["class"]))  45     {  46         $class = " " . $options["class"];  47     }  48   49     $parameters = [  50         "class"       => "form-control" . $class,  51         "placeholder" => $placeholder  52     ];  53   54     $error = "";  55   56     if (!empty($options["form"]))  57     {  58         $error = $options["form"]->getError($name);  59     }  60   61     if ($type !== "hidden")  62     {  63         $markup .= "<div class='form-group";  64         $markup .= ($error ? " has-error" : "");  65         $markup .= "'>";  66     }  67   68     switch ($type)  69     {  70         case "text":  71         {  72             $markup .= Form::label($name, $label, [  73                 "class" => "control-label"  74             ]);  75   76            $markup .= Form::text($name, $value, $parameters);  77   78            break;  79         }  80   81         case "password":  82         {  83             $markup .= Form::label($name, $label, [  84                 "class" => "control-label"  85             ]);  86   87             $markup .= Form::password($name, $parameters);  88   89             break;  90         }  91   92         case "checkbox":  93         {  94             $markup .= "<div class='checkbox'>";  95             $markup .= "<label>";  96             $markup .= Form::checkbox($name, 1, !!$value);  97             $markup .= " " . $label;  98             $markup .= "</label>";  99             $markup .= "</div>"; 100  101             break; 102         } 103  104         case "hidden": 105         { 106             $markup .= Form::hidden($name, $value); 107             break; 108         } 109     } 110  111     if ($error) 112     { 113         $markup .= "<span class='help-block'>"; 114         $markup .= $error; 115         $markup .= "</span>"; 116     } 117  118     if ($type !== "hidden") 119     { 120         $markup .= "</div>"; 121     } 122  123     return $markup; 124 }); 

This file should be saved as app/macros.php.

This macro evaluates an $options array, generating a label, input element and validation error message. There’s white a lot of checking involved to ensure that all the required data is there, and that optional data affects the generated markup correctly. It supports text inputs, password inputs, checkboxes and hidden fields; but more types can easily be added.

The markup this macro generates is Bootstrap friendly. If you haven’t already heard of Bootstrap (where have you been?) then you can find out more about it at: http://getbootstrap.com/

To see this in action, we need to include it in the startup processes of the application and then modify the form views to use it:

1 require app_path() . "/macros.php"; 

This was extracted from app/start/global.php.

 1 @extends("layout")  2 @section("content")  3     {{ Form::open([  4         "route"        => "group/add",  5         "autocomplete" => "off"  6     ]) }}  7         {{ Form::field([  8             "name"        => "name",  9             "label"       => "Name", 10             "form"        => $form, 11             "placeholder" => "new group" 12         ])}} 13         {{ Form::submit("save") }} 14     {{ Form::close() }} 15 @stop 16 @section("footer") 17     @parent 18     <script src="//polyfill.io"></script> 19 @stop 

This file should be saved as app/views/group/add.blade.php.

You’ll notice how much neater the view is; thanks to the form class handling the error messages for us. This view happens to be relatively short since there’s only a single field (name) for groups.

 1 .help-block  2 {  3     float : left;  4     clear : left;  5 }  6   7 .form-group.has-error .help-block  8 {  9     color : #ef7c61; 10 } 

This was extracted from public/css/layout.css.

One last thing we have to do, to get the error messages to look the same as they did before, is to add a bit of CSS to target the Bootstrap-friendly error messages.

With the add view complete; we can create the addAction() method:

 1 public function addAction()  2 {  3     $form = new GroupForm();  4   5     if ($form->isPosted())  6     {  7         if ($form->isValidForAdd())  8         {  9             Group::create([ 10                 "name" => Input::get("name") 11             ]); 12  13             return Redirect::route("group/index"); 14         } 15  16         return Redirect::route("group/add")->withInput([ 17             "name"   => Input::get("name"), 18             "errors" => $form->getErrors() 19         ]); 20     } 21  22     return View::make("group/add", [ 23         "form" => $form 24     ]); 25 } 

This was extracted from app/controllers/GroupController.php.

You can also see how much simpler our addAction() method is; now that we’re using the GroupForm class. It takes care of retrieving old error messages and handling validation so that we can simply create groups and redirect.

Editing Groups

The view and action for editing groups is much the same as for adding groups.

 1 @extends("layout")  2 @section("content")  3     {{ Form::open([  4         "url"          => URL::full(),  5         "autocomplete" => "off"  6     ]) }}  7         {{ Form::field([  8             "name"        => "name",  9             "label"       => "Name", 10             "form"        => $form, 11             "placeholder" => "new group", 12             "value"       => $group->name 13         ]) }} 14         {{ Form::submit("save") }} 15     {{ Form::close() }} 16 @stop 17 @section("footer") 18     @parent 19     <script src="//polyfill.io"></script> 20 @stop 

This file should be saved as app/views/group/edit.blade.php.

The only difference here is the form action we’re setting. We need to take into account that a group id will be provided to the edit page, so the URL must be adjusted to maintain this id even after the form is posted. For that; we use the URL::full() method which returns the full, current URL.

 1 public function editAction()  2 {  3     $form = new GroupForm();  4   5     $group = Group::findOrFail(Input::get("id"));  6     $url   = URL::full();  7   8     if ($form->isPosted())  9     { 10         if ($form->isValidForEdit()) 11         { 12             $group->name = Input::get("name"); 13             $group->save(); 14             return Redirect::route("group/index"); 15         } 16  17         return Redirect::to($url)->withInput([ 18             "name"   => Input::get("name"), 19             "errors" => $form->getErrors(), 20             "url"    => $url 21         ]); 22     }  23  24     return View::make("group/edit", [ 25         "form"  => $form, 26         "group" => $group 27     ]); 28 } 

This was extracted from app/controllers/GroupController.php.

In the editAction() method; we’re still create a new instance of GroupForm. Because we’re editing a group, we need to get that group to display its data in the view. We do this with Eloquent’s findOrFail() method; which will cause a 404 error page to be displayed if the id is not found within the database.

The rest of the action is much the same as the addAction() method. We’ll also need to add the edit route to the routes.php file…

1 Route::any("/group/edit", [ 2     "as"   => "group/edit", 3     "uses" => "GroupController@editAction" 4 ]); 

This was extracted from app/routes.php.

Deleting Groups

There are a number of options we can explore when creating the delete interface, but we’ll go with the quickest which is just to present a link on the listing page.

 1 @extends("layout")  2 @section("content")  3     @if (count($groups))  4         <table>  5             <tr>  6                 <th>name</th>  7                 <th>&nbsp;</th>  8             </tr>  9             @foreach ($groups as $group) 10                 <tr> 11                     <td>{{ $group->name }}</td> 12                     <td> 13                         <a href="{{ URL::route("group/edit") }}?id={{ $group->id \ 14 }}">edit</a> 15                         <a href="{{ URL::route("group/delete") }}?id={{ $group->i\ 16 d }}" class="confirm" data-confirm="Are you sure you want to delete this group?">\ 17 delete</a> 18                     </td> 19                 </tr> 20             @endforeach 21         </table> 22     @else 23         <p>There are no groups.</p> 24     @endif 25     <a href="{{ URL::route("group/add") }}">add group</a> 26 @stop 

This file should be saved as app/views/group/index.blade.php.

We’ve modified the group/index view to include two links; which will redirect users either to the edit page or the delete action. Notice the class=”confirm” and data-confirm=”…” attributes we’ve added to the delete link — we’ll use these shortly. We’ll also need to add the delete route to the routes.php file…

1 Route::any("/group/delete", [ 2     "as"   => "group/delete", 3     "uses" => "GroupController@deleteAction" 4 ]); 

This was extracted from app/routes.php.

Since we’ve chosen such an easy method of deleting groups, the action is pretty straightforward:

 1 public function deleteAction()  2 {  3     $form = new GroupForm();  4   5     if ($form->isValidForDelete())  6     {  7         $group = Group::findOrFail(Input::get("id"));  8         $group->delete();  9     } 10  11     return Redirect::route("group/index"); 12 } 

This was extracted from app/controllers/GroupController.php.

We simply need to find a group with the provided id (using the findOrFail() method we saw earlier) and delete it. After that; we redirect back to the listing page. Before we take this for a spin, let’s add the following JavaScript:

1 (function($){ 2     $(".confirm").on("click", function() { 3         return confirm($(this).data("confirm")); 4     }); 5 }(jQuery)); 

This file should be saved as public/js/layout.js.

1 @section("footer") 2     @parent 3     <script src="/js/jquery.js"></script> 4     <script src="/js/layout.js"></script> 5 @stop 

This was extracted from app/views/group/index.blade.php.

You’ll notice I have linked to jquery.js (any recent version will do). The code in layout.js adds a click event handler on to every element with class=”confirm” to prompt the user with the message in data-confirm=”…”. If “OK” is clicked; the callback returns true and the browser will redirect to the page on the other end (in this case the deleteAction() method on our GroupController class). Otherwise the click will be ignored.

Adding Users And Resources

Next on our list is making a way for us to specify resource information and add users to our groups. Both of these thing will happen on the group edit page; but before we get there we will need to deal with migrations, models and relationships…

Adding Migrations, Models And Relationships

 1 <?php  2   3 use Illuminate\Database\Schema\Blueprint;  4   5 class CreateResourceTable  6 extends BaseMigration  7 {  8     public function up()  9     { 10         Schema::create("resource", function(Blueprint $table) 11         { 12             $this 13                 ->setTable($table) 14                 ->addPrimary() 15                 ->addString("name") 16                 ->addString("pattern") 17                 ->addString("target") 18                 ->addBoolean("secure") 19                 ->addTimestamps(); 20         }); 21     } 22  23     public function down() 24     { 25         Schema::dropIfExists("resource"); 26     } 27 } 

This file should be saved as app/database/migrations/0000_00_00_000000_CreateResourceTable.php. Yours may be slightly different as the 0’s are replaced with other numbers.

We are calling them resources to avoid the name collision with the existing Route class.

If you’re skipping migrations; the following SQL should create the same table structure as the migration:

 1 CREATE TABLE `resource` (  2  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,  3  `name` varchar(255) DEFAULT NULL,  4  `pattern` varchar(255) DEFAULT NULL,  5  `target` varchar(255) DEFAULT NULL,  6  `secure` tinyint(1) DEFAULT NULL,  7  `created_at` datetime DEFAULT NULL,  8  `updated_at` datetime DEFAULT NULL,  9  `deleted_at` datetime DEFAULT NULL, 10  PRIMARY KEY (`id`) 11 ) ENGINE=InnoDB CHARSET=utf8; 

The resource table has fields for the things we usually store in our routes file. The idea is that we keep the route information in the database so we can both programatically generate the routes for our application; and so that we can link various routes to groups for controlling access to various parts of our application.

 1 <?php  2   3 class Resource  4 extends Eloquent  5 {  6     protected $table = "resource";  7   8     protected $softDelete = true;  9  10     protected $guarded = [ 11         "id", 12         "created_at", 13         "updated_at", 14         "deleted_at" 15     ]; 16  17     public function groups() 18     { 19         return $this->belongsToMany("Group")->withTimestamps(); 20     } 21 } 

This file should be saved as app/models/Resource.php.

The Resource model is similar to those we’ve seen before; but it also specifies a many-to-many relationship (in the groups() method). This will allows us to return related groups with $this->groups. We’ll use that later!

The withTimestamps() method will tell Eloquent to update the timestamps of related groups when resources are updated. You can find out more about it at: http://laravel.com/docs/eloquent#working-with-pivot-tables

We also need to add the reverse relationship to the Group model:

1 public function resources() 2 { 3     return $this->belongsToMany("Resource")->withTimestamps(); 4 } 

This was extracted from app/models/Group.php.

There really is a lot to relationships in Eloquent; more than we have time to cover now. I will be going into more detail about these relationships in future tutorials; exploring the different types and configuration options. For now, this is all we need to complete this chapter.

We can also define relationships for users and groups, as in the following examples:

1 public function users() 2 { 3     return $this->belongsToMany("User")->withTimestamps(); 4 } 

This was extracted from app/models/Group.php.

1 public function groups() 2 { 3     return $this->belongsToMany("Group")->withTimestamps(); 4 } 

This was extracted from app/models/User.php.

Before we’re quite done with the database work; we’ll also need to remember to set up the pivot tables in which the relationship data will be stored.

 1 <?php  2   3 use Illuminate\Database\Schema\Blueprint;  4   5 class CreateGroupUserTable  6 extends BaseMigration  7 {  8     public function up()  9     { 10         Schema::create("group_user", function(Blueprint $table) 11         { 12             $this 13                 ->setTable($table) 14                 ->addPrimary() 15                 ->addForeign("group_id") 16                 ->addForeign("user_id") 17                 ->addTimestamps(); 18         }); 19     } 20  21     public function down() 22     { 23         Schema::dropIfExists("group_user"); 24     } 25 } 

This file should be saved as app/database/migrations/0000_00_00_000000_CreateGroupUserTable.php. Yours may be slightly different as the 0’s are replaced with other numbers.

 1 <?php  2   3 use Illuminate\Database\Schema\Blueprint;  4   5 class CreateGroupResourceTable  6 extends BaseMigration  7 {  8     public function up()  9     { 10         Schema::create("group_resource", function(Blueprint $table) 11         { 12             $this 13                 ->setTable($table) 14                 ->addPrimary() 15                 ->addForeign("group_id") 16                 ->addForeign("resource_id") 17                 ->addTimestamps(); 18         }); 19     } 20  21     public function down() 22     { 23         Schema::dropIfExists("group_resource"); 24     } 25 } 

This file should be saved as app/database/migrations/0000_00_00_000000_CreateGroupResourceTable.php. Yours may be slightly different as the 0’s are replaced with other numbers.

We now have a way to manage the data relating to groups; so let’s create the views and actions through which we can capture this data.

If you’re skipping migrations; the following SQL should create the same table structures as the migrations:

 1 CREATE TABLE `group_resource` (  2  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,  3  `group_id` int(11) DEFAULT NULL,  4  `resource_id` int(11) DEFAULT NULL,  5  `created_at` datetime DEFAULT NULL,  6  `updated_at` datetime DEFAULT NULL,  7  `deleted_at` datetime DEFAULT NULL,  8  PRIMARY KEY (`id`),  9  KEY `group_resource_group_id_index` (`group_id`), 10  KEY `group_resource_resource_id_index` (`resource_id`) 11 ) ENGINE=InnoDB CHARSET=utf8; 12  13 CREATE TABLE `group_user` ( 14  `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 15  `group_id` int(11) DEFAULT NULL, 16  `user_id` int(11) DEFAULT NULL, 17  `created_at` datetime DEFAULT NULL, 18  `updated_at` datetime DEFAULT NULL, 19  `deleted_at` datetime DEFAULT NULL, 20  PRIMARY KEY (`id`), 21  KEY `group_user_group_id_index` (`group_id`), 22  KEY `group_user_user_id_index` (`user_id`) 23 ) ENGINE=InnoDB CHARSET=utf8; 

Adding Views

The views we need to create are those in which we will select which users and resources should be assigned to a group.

1 <div class="assign"> 2     @foreach ($resources as $resource) 3         <div class="checkbox"> 4             {{ Form::checkbox("resource_id[]", $resource->id, $group->resources->\ 5 contains($resource->id)) }} 6             {{ $resource->name }} 7         </div> 8     @endforeach 9 </div> 

This file should be saved as app/views/resource/assign.blade.php.

1 <div class="assign"> 2     @foreach ($users as $user) 3         <div class="checkbox"> 4             {{ Form::checkbox("user_id[]", $user->id, $group->users->contains($us\ 5 er->id)) }} 6             {{ $user->username }} 7         </div> 8     @endforeach 9 </div> 

This file should be saved as app/views/user/assign.blade.php.

These views similarly iterate over resources and users (passed to the group edit view) and render markup for checkboxes. It’s important to note the names of the checkbox inputs ending in [] — this is the recommended way to passing array-like data in HTML forms.

The first parameter of the Form::checkbox() method is the input’s name. The second is its value. The third is whether of not the checkbox should initially be checked. Eloquent models provide a useful contains() method which searches the related rows for those matching the provided id(s).

 1 @extends("layout")  2 @section("content")  3     {{ Form::open([  4         "url"          => URL::full(),  5         "autocomplete" => "off"  6     ]) }}  7         {{ Form::field([  8             "name"        => "name",  9             "label"       => "Name", 10             "form"        => $form, 11             "placeholder" => "new group", 12             "value"       => $group->name 13         ])}} 14         @include("user/assign") 15         @include("resource/assign") 16         {{ Form::submit("save") }} 17     {{ Form::close() }} 18 @stop 19 @section("footer") 20     @parent 21     <script src="//polyfill.io"></script> 22 @stop 

This file should be saved as app/views/group/edit.blade.php.

We’ve modified the group/edit view to include the new assign views. If you try to edit a group, at this point, you might see an error. This is because we still need to pass the users and resources to the view…

1 return View::make("group/edit", [ 2     "form"      => $form, 3     "group"     => $group, 4     "users"     => User::all(), 5     "resources" => Resource::where("secure", true)->get() 6 ]); 

This was extracted from app/controllers/GroupController.php.

Seeding Resources

We return all the users (so that any user can be in any group) and the resources that need to be secure. Right now, that database table is empty, but we can easily create a seeder for it:

 1 <?php  2   3 class ResourceSeeder  4 extends DatabaseSeeder  5 {  6     public function run()  7     {  8         $resources = [  9             [ 10                 "pattern" => "/", 11                 "name"    => "user/login", 12                 "target"  => "UserController@loginAction", 13                 "secure"  => false 14             ], 15             [ 16                 "pattern" => "/request", 17                 "name"    => "user/request", 18                 "target"  => "UserController@requestAction", 19                 "secure"  => false 20             ], 21             [ 22                 "pattern" => "/reset", 23                 "name"    => "user/reset", 24                 "target"  => "UserController@resetAction", 25                 "secure"  => false 26             ], 27             [ 28                 "pattern" => "/logout", 29                 "name"    => "user/logout", 30                 "target"  => "UserController@logoutAction", 31                 "secure"  => true 32             ], 33             [ 34                 "pattern" => "/profile", 35                 "name"    => "user/profile", 36                 "target"  => "UserController@profileAction", 37                 "secure"  => true 38             ], 39             [ 40                 "pattern" => "/group/index", 41                 "name"    => "group/index", 42                 "target"  => "GroupController@indexAction", 43                 "secure"  => true 44             ], 45             [ 46                 "pattern" => "/group/add", 47                 "name"    => "group/add", 48                 "target"  => "GroupController@addAction", 49                 "secure"  => true 50             ], 51             [ 52                 "pattern" => "/group/edit", 53                 "name"    => "group/edit", 54                 "target"  => "GroupController@editAction", 55                 "secure"  => true 56             ], 57             [ 58                 "pattern" => "/group/delete", 59                 "name"    => "group/delete", 60                 "target"  => "GroupController@deleteAction", 61                 "secure"  => true 62             ] 63         ]; 64  65         foreach ($resources as $resource) 66         { 67             Resource::create($resource); 68         } 69     } 70 } 

This file should be saved as app/database/seeds/ResourceSeeder.php.

We should also add this seeder to the DatabaseSeeder class so that the Artisan commands which deal with seeding pick it up:

 1 <?php  2   3 class DatabaseSeeder  4 extends Seeder  5 {  6     public function run()  7     {  8         Eloquent::unguard();  9  10         $this->call("ResourceSeeder"); 11         $this->call("UserSeeder"); 12     } 13 } 

This file should be saved as app/database/seeds/DatabaseSeeder.php.

Saving Relationships

Now you should be seeing the lists of resources and users when you try to edit a group. We need to save selections when the group is saved; so that we can successfully assign both users and resources to groups.

 1 if ($form->isValidForEdit())  2 {  3     $group->name = Input::get("name");  4     $group->save();  5   6     $group->users()->sync(Input::get("user_id", []));  7     $group->resources()->sync(Input::get("resource_id", []));  8   9     return Redirect::route("group/index"); 10 } 

This was extracted from app/controllers/GroupController.php.

Laravel 4 provides and excellent method for synchronising related database records — the sync() method. You simply provide it with the id(s) of the related records and it makes sure there is a record for each relationship. It couldn’t be easier!

Finally, we will add a bit of CSS to make the lists less of a mess…

 1 .assign  2 {  3     padding     : 10px 0 0 0;  4     line-height : 22px;  5 }  6 .checkbox, .assign  7 {  8     float : left;  9     clear : left; 10 } 11 .checkbox input[type='checkbox'] 12 { 13     margin : 0 10px 0 0; 14     float  : none; 15 } 

This was extracted from public/css/layout.css.

Take it for a spin! You will find that the related records are created (in the pivot) tables, and each time you submit it; the edit page will remember the correct relationships and show them back to you.

Advanced Routes

The final thing we need to do is manage how resources are translated into routes and how the security behaves in the presence of our simple ACL.

 1 <?php  2   3 Route::group(["before" => "guest"], function()  4 {  5     $resources = Resource::where("secure", false)->get();  6   7     foreach ($resources as $resource)  8     {  9         Route::any($resource->pattern, [ 10             "as"   => $resource->name, 11             "uses" => $resource->target 12         ]); 13     } 14 }); 15  16 Route::group(["before" => "auth"], function() 17 { 18     $resources = Resource::where("secure", true)->get(); 19  20     foreach ($resources as $resource) 21     { 22         Route::any($resource->pattern, [ 23             "as"   => $resource->name, 24             "uses" => $resource->target 25         ]); 26     } 27 }); 

This file should be saved as app/routes.php.

There are some significant changes to the routes file. Firstly, all the routes are being generated from resources. We no longer need to hard-code routes in this file because we can save them in the database.

It’s more efficient hard-coding them, and we really should be caching them if we have to read them from the database; but that’s the subject of future tutorials — we’ve running out of time here!

All the “insecure” routes are rendered in the first block — the block in which routes are subject to the guest filter. All the “secure” routes are rendered in the secure; where they are subject to the auth filter.

 1 Route::filter("auth", function()  2 {  3     if (Auth::guest())  4     {  5         return Redirect::route("user/login");  6     }  7     else  8     {  9         foreach (Auth::user()->groups as $group) 10         { 11             foreach ($group->resources as $resource) 12             { 13                 $path = Route::getCurrentRoute()->getPath(); 14  15                 if ($resource->pattern == $path) 16                 { 17                     return; 18                 } 19             } 20         } 21  22         return Redirect::route("user/login"); 23     } 24 }); 

This was extracted from app/filters.php.

The new auth filter needs not only to make sure the user is authenticated, but also that one of the group to which they are assigned has the current route assigned to it also. Users can belong to multiple groups and so can resources; so this is the only (albeit inefficient way) to filter allowed resources from those which the user is not allowed access to.

To test this out; alter the group to which your user account belongs to disallow access to the group/add route. When you try to visit it you will be redirected first to the user/login route and the not the user/profile route.

You need to make sure you’re not disallowing access to a route specified in the auth filter. That will probably lead to a redirect loop!

Lastly, we need a way to hide links to disallowed resources…

 1 <?php  2   3 if (!function_exists("allowed"))  4 {  5     function allowed($route)  6     {  7         if (Auth::check())  8         {  9             foreach (Auth::user()->groups as $group) 10             { 11                 foreach ($group->resources as $resource) 12                 { 13                     if ($resource->name == $route) 14                     { 15                         return true; 16                     } 17                 } 18             } 19         } 20  21         return false; 22     } 23 } 

This file should be saved as app/helpers.php.

1 require app_path() . "/helpers.php"; 

This was extracted from app/start/global.php.

Once we’ve included that helpers.php file in the startup processes of our application; we can check whether the authenticated user is allowed access to resources simply by passing the resource name to the allowed() method.

The first time you run the migrations (if you’re installing from GitHub); you may see errors relating to the resource table. This is likely caused by the routes.php file trying to load routes from an empty database table before seeding takes place. Try commenting out the code in routes.php until you’ve successfully migrated and seeded your database. There are nicer ways to do this; all of which I will not cover now.

Try this out by wrapping the links of your application in a condition which references this method.

For the sake of brevity; I have not included any examples of this, though many can be found, in the source code, on GitHub.

Назад: Authentication
Дальше: Deployment