Only allow owners to update their user profile in Laravel with a policy
Tags: laravel, policy, authorization
Published: 2020-04-10 11:33:43

---
title: "Only allow owners to update their user profile in Laravel with a policy"
description: "Only allow owners to update their user profile in Laravel with a policy"
published: true
tags: laravel, policy, authorization
canonical_url: https://martinbetz.eu/articles/only-allow-owner-to-update-their-user-profile-in-laravel-with-a-policy/
---

## 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](https://laravel.com/docs/7.x/authorization#creating-policies) 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_

```php
// 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 a `POST` request to
  - We do not have a `experience_years` property on the `User` model
  - We do not have a User controller that actually updates the model
- 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 a `UserController` with an `update` 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

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

```

```php
// 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',
    ];
}

```

```php
// Routes: routes/web.php

<?php

use Illuminate\Support\Facades\Route;

Route::post('/{user}/profile/update', 'UserController@update')->name('user.profile.update');

```

```php
// 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

```php
/** @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`

```php
// 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

```php
// 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](https://github.com/minthemiddle/200410-listing).
- If you want to extend this functionality, try to add an exception for admins: They should be able to change every profile