Only allow owner to update their user profile in Laravel with a policy

April 10, 2020 • Last updated: April 10, 2019

The problem: Only owners should be allowed to edit their profiles

The solution: Use a policy for the User model

The step by step explanation

Step 1: Write a failing test user_can_update_own_profile

// 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);
    }
}

Step 2: Fix the test to assert that you can update your own profile

// 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());
    }
}

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);
    }

Step 4: Add a policy to disallow changing someone else's profile

// 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();
    }
}
// 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

Send me your feedback on
"Only allow owner to update their user profile in Laravel with a policy"

April 10, 2020 • Last updated: April 10, 2019

tech