Authorization
Following up on part 1, we will now bring in some authorization into our starter application. We already have authentication setup, so this is a natural next step.
- We will create a Policy for our Data model
- Adjust the
data
andusers
database tables - More use of
artisan tinker
- Some adjustments to our DataController and views to use the Gate
Git hub repository for Part 2
- Authorization
- Policies
- Create
- Register
- Fill in the Policy methods
- Adjust User Model for relationship and admin flag
- Migrations
- User Model
- Data Model
- Make an Admin
- Assign users to the Data records
- Using the Gate in our index view
- Adjust the controller to add the user_id to the records on creation
- Check your Data index page
- Finish
- Notes
Policies
Create
⚡ artisan make:policy DataPolicy
Policy created successfully.
Register
Now lets register this policy with the Authorization system (Gate)
AuthServiceProvider
protected $policies = [
App\Data::class => App\Policies\DataPolicy::class,
];
Fill in the Policy methods
We really only need to create 1 method as we will use the same 'rules' to decide if a user can edit/update/delete a Data record. You don't have to define all these methods, but I am doing this to have them ready to use, and maybe you will want to adjust how these work down the road.
For our example here, we want the policy to be that only the owner of a Data
record can edit, update and delete that record. We will also allow an admin user to have no restrictions.
namespace App\Policies;
use App\Data;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class DataPolicy
{
use HandlesAuthorization;
// We will allow the admin to have full access.
public function before($user, $ability)
{
if ($user->isAdmin()) {
return true;
}
}
// We will only allow the owner to change their data.
public function edit(User $user, Data $data)
{
return $this->owner($user, $data);
}
public function update(User $user, Data $data)
{
return $this->edit($user, $data);
}
public function delete(User $user, Data $data)
{
return $this->edit($user, $data);
}
// Owner check
protected function owner(User $user, Data $data)
{
return $data->user_id == $user->id;
}
}
Adjust User Model for relationship and admin flag
We will now need to assign the data records to a user. This will be done when we create the Data record in the controller. For now we will need to create a new migration to alter the users
table to add the admin
field and to alter the data
table to add a user_id
field.
Migrations
You can do your migrations how you wish. For the sake of keeping things explicit, I am creating separate migration files for the changes to each table.
⚡ artisan make:migration adjust_user_table_to_add_admin_migration
Created Migration: 2016_04_17_053053_adjust_user_table_to_add_admin_migration
⚡ artisan make:migration adjust_data_table_to_add_user_id_migration
Created Migration: 2016_04_17_053401_adjust_data_table_to_add_user_id_migration
2016_04_17_053053_adjust_user_table_to_add_admin_migration.php
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('admin')->default(false);
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumns('admin');
});
}
2016_04_17_053401_adjust_data_table_to_add_user_id_migration.php
public function up()
{
Schema::table('data', function (Blueprint $table) {
$table->integer('user_id')->unsigned()->nullable();
$table->foreign('user_id')
->references('id')->on('users');
});
}
public function down()
{
Schema::table('data', function (Blueprint $table) {
$table->dropColumns('user_id');
});
}
User Model
To avoid any odd complications, we will just cast this to a boolean.
protected $casts = [
'admin' => 'boolean'
];
Lets add the relationship method, data
, for our Has Many relationship to Data
.
public function data()
{
return $this->hasMany(Data::class);
}
We will make a simple isAdmin
method to check if the admin field we added is true.
public function isAdmin()
{
return $this->admin == true;
}
Data Model
Add user
relationship.
As previously, we will just cast this field to an integer to avoid any comparison complications, just in case.
protected $casts = [
'user_id' => 'int'
];
Last, how about a relationship, user
, to represent the Belongs To relationship with User
.
public function user()
{
return $this->belongsTo(User::class);
}
Make an Admin
⚡ php artisan tinker
Psy Shell v0.7.2 (PHP 7.0.5-3+donate.sury.org~wily+1 — cli) by Justin Hileman
>>> $admin = App\User::first();
=> App\User {#654
id: "1",
name: "admin",
email: "admin@admin",
created_at: "2016-04-17 04:15:56",
updated_at: "2016-04-17 04:15:56",
admin: "0",
}
>>> $admin->admin = 1;
=> 1
>>> $admin->save();
=> true
>>>
Our admin User is now assigned as an admin.
Lets create another user to test our system.
>>> App\User::create(['name' => 'Bob', 'email' => 'bob@bob', 'password' => bcrypt('test')]);
=> App\User {#663
name: "Bob",
email: "bob@bob",
updated_at: "2016-04-17 05:44:43",
created_at: "2016-04-17 05:44:43",
id: 2,
}
>>>
Assign users to the Data records
If you have Data records atm, we should assign them to some users. We will blanket assign these to Bob.
>>> App\Data::query()->update(['user_id' => 2]);
Now lets create a new Data record and assign it to the admin.
>>> App\Data::create([
... 'name' => 'Admin owns this',
... 'value' => 'more testing data',
... 'user_id' => 1
... ]);
=> App\Data {#665
name: "Admin owns this",
value: "more testing data",
user_id: 1,
updated_at: "2016-04-17 05:51:07",
created_at: "2016-04-17 05:51:07",
id: 2,
}
>>>
Now we have all the Data records assigned to users and our new one assigned to the admin user.
Using the Gate in our index view
We only have to make a small adjustment to our index view in data folder. We can check abilities using @can
blade directive.
@if (Auth::check())
<td>
@can ('edit', $item)
<a href="{{ route('data.edit', [$item->id]) }}" class="btn btn-primary">Edit</a>
@endcan
@can ('delete', $item)
<form method="POST" action="{{ route('data.destroy', [$item->id]) }}" style="display:inline">
<input type="hidden" name="_method" value="DELETE">
{{ csrf_field() }}
<input type="submit" value="Delete" class="btn btn-danger">
</form>
@endcan
</td>
@endif
Adjust the controller to add the user_id to the records on creation
We can adjust the store method easily to make sure we assign any created Data
record to the current user. Since we are allowing any authenticated user to create new Data
records we won't do any authorization in this method.
public function store(Request $request)
{
$this->validate($request, $this->rules);
// only take the variables we need, because why not
$data = $request->only(['name', 'value']);
// get the currently authed user's id
$data['user_id'] = $request->user()->id;
Data::create($data);
...
}
For the methods we want authorization, we can simply use the authorize
method on the controller to do our ability check. By passing in $data
, a Data
model, it will know to use the policy we have created, DataPolicy
.
public function edit(Data $data)
{
$this->authorize($data);
...
}
public function update(Request $request, Data $data)
{
$this->authorize($data);
...
}
public function destroy(Data $data)
{
$this->authorize('delete', $data);
...
}
In these example you might notice we are only passing 2 arguments to the authorize
method on the controller. When we only pass in a resource (Model) into the method it will use the name of the calling method as the 'ability' we are authorizing. So for the update
method in our controller the call to authorize
is going to use the update
ability on the DataPolicy
we created.
Check your Data index page
Depending on which user you are logged in as, you will see different things. If you are logged in as Bob you will be able to edit and delete all the records except for the one owned by the admin user.
If you are logged in as the admin you will have full access.
Finish
Again, you have done a great job and have been a lovely audience. With not much code, we have now added authorization to this application.
Notes
As always, this is one way to go about these things. This is all meant to be a quick run through and is using what I think to be the simplest ways, to get these things done fast. There are other features that Laravel provides that can replace parts of what we are doing in the controller and move them into other places. We could also be using repositories to deal with our database layer instead of directly interacting with the Model to do queries.
Just a quick way to do some simple things.
Link to Part 3