Lets Do This! Part 2

You are Authorized

Posted by lagbox on April 27, 2016 laravel tutorial

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 and users database tables
  • More use of artisan tinker
  • Some adjustments to our DataController and views to use the Gate

Git hub repository for Part 2

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