The problem: Only owners should be allowed to edit their profiles
- You have a website that lists the profile of Laravel developers
- The profile is just an extension of the
User
model, so you have extra fields - For sake of simplicity, you have only one field in your profile that is the numbers of years you've been coding with Laravel
- Developers listed should only be able to edit their own profile and not foreign profiles
The solution: Use a policy for the User model
- We will write a policy that will be triggered when a user updates a profile
- The policy checks whether you are the owner of the user profile
- If you are, you can update the profile
- If you are not the owner, your request will be denied
The step by step explanation
- I will show you how to add this policy test-driven
- I assume that you already created a fresh Laravel app and set up your database
Step 1: Write a failing test user_can_update_own_profile
- Create a test for the User model:
php artisan make:test Http/Controllers/UserControllerTest
- We will write a test with how we wish our app should work if we are the owner of a profile
- This is called the Happy Path
// tests/Feature/Http/Controllers/UserControllerTest.php
<?php
namespace Tests\Feature\Http\Controllers;
use App\User;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class UserControllerTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function user_can_update_own_profile()
{
$user = factory(User::class)->create();
$response = $this->actingAs($user)->post(route('user.profile.update', $user), [
'experience_years' => 5,
]);
$response->assertSuccessful();
$user->refresh();
$this->assertEquals(5, $user->experience_years);
}
}
- This test will fail for many reasons
- We do not have a route named
user.profile.update
that we can send aPOST
request to - We do not have a
experience_years
property on theUser
model - We do not have a User controller that actually updates the model
- We do not have a route named
- In a normal test-driven process, I would solve this error by error
- As this tutorial is about policies, I will just solve all problems at once to get to the policy part
Step 2: Fix the test to assert that you can update your own profile
- The solution: Update your migration,
User
model,web.php
routes file and create aUserController
with anupdate
method that updates with everything passed with a request - I marked all new files with a
// ADD:
comment - I deleted all comments and unneccessary lines
// Migration: database/migrations/2014_10_12_000000_create_users_table.php
// Your date in the filename should differ
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUsersTable extends Migration
{
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->integer('experience_years')->nullable(); // ADD: nullable integer
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('users');
}
}
// User Model: app/User.php
<?php
namespace App;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use Notifiable;
protected $fillable = [
'name', 'email', 'password', 'experience_years' // ADD: 'experience_years'
];
protected $hidden = [
'password', 'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
];
}
// Routes: routes/web.php
<?php
use Illuminate\Support\Facades\Route;
Route::post('/{user}/profile/update', 'UserController@update')->name('user.profile.update');
// Controller: app/Http/Controllers/UserController.php
<?php
namespace App\Http\Controllers;
use App\User;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function update(Request $request, User $user)
{
$user->update($request->all());
}
}
- You can now run the tests with
php artisan test
. - You should see two passing example tests and a passing test:
✓ user can update own profile
- Let's try to update someone else's years of experience:
Step 3: Try to update someone else's profile – and fail
/** @test */
public function user_cannot_update_foreign_profile()
{
$user = factory(User::class)->create();
$foreign_user = factory(User::class)->create([
'experience_years' => 2,
]);
$response = $this->actingAs($user)->post(route('user.profile.update', $foreign_user), [
'experience_years' => 5,
]);
$response->assertForbidden();
$foreign_user->refresh();
$this->assertEquals(2, $foreign_user->experience_years);
}
- This test will fail
Asserting that 5 is 2
- This means: Yes, we can change someone else's profile
- Not good!
Step 4: Add a policy to disallow changing someone else's profile
- Solution: Add a policy to stopp this
- Create the policy:
php artisan make:policy UserPolicy
// Policy: app/Policies/UserPolicy.php
<?php
namespace App\Policies;
use App\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Auth\Access\HandlesAuthorization;
class UserPolicy
{
use HandlesAuthorization;
public function update(User $user, User $user_model)
{
return $user->id === $user_model->id ? Response::allow() : Response::deny();
}
}
- Policies always have the current user passed via
User $user
- The second argument is the model that we want to protect
- Because we want to protect the user model, we need to pass it twice with differing names
- Interpretation of the command: If the ID of the currently logged in user is the same as the id of the user model that is being tried to update, allow the update. If not, reject the update
- Because we named the policy like the model, Laravel automatically finds it
- We only need to apply the policy on the route
// Routes with policy: routes/web.php
<?php
use Illuminate\Support\Facades\Route;
Route::post('/{user}/profile/update', 'UserController@update')->name('user.profile.update')->middleware('can:update,user');
The ->middleware('can:update,user)
means: Authorize the update()
action and pass the user
URL parameter to the policy (that's our $user_model
in the policy).
Repo and extension
- If you have problems following the code, check out the repo on Github.
- If you want to extend this functionality, try to add an exception for admins: They should be able to change every profile
Top comments (2)
Thanks to Reddit user reddit.com/user/Re-Infected/, here's an simpler
update()
method:In plain English: If the logged in user is equal to the user passed then return true (= pass this request), if not deny the update process.
Github url triggers a 404 error.