> Full Neon documentation index: https://neon.com/docs/llms.txt

# Implementing fine-grained authorization in Laravel with Lakebase Postgres

Set up Laravel's authorization features and Spatie's Laravel Permission package on a Postgres database on Neon

Laravel's authorization system lets you implement fine-grained access control in your applications. The built-in features cover most cases, but some projects need more advanced role-based access control (RBAC), which third-party packages like Spatie's Laravel Permission provide.

In this guide, we'll walk through the process of setting up fine-grained authorization in a Laravel application using Lakebase Postgres. We'll start with Laravel's native authorization features, including Gates and Policies, and then add Spatie's Laravel Permission package for roles and permissions.

## Prerequisites

Before we begin, make sure you have the following:

- PHP 8.1 or higher installed on your system
- [Composer](https://getcomposer.org/) for managing PHP dependencies
- A [Neon](https://console.neon.tech/signup) account for database hosting
- Basic knowledge of Laravel and its authentication system

## Setting up the project

Let's start by creating a new Laravel project and configuring it to use Lakebase Postgres.

### Creating a new Laravel project

Open your terminal and run the following command to create a new Laravel project:

```bash
composer create-project laravel/laravel laravel-auth-demo
cd laravel-auth-demo
```

This will create a new Laravel project in a directory named `laravel-auth-demo` and navigate you into the project directory.

### Connecting to your database on Neon

Update your `.env` file with the details from your Neon connection string. Click **Connect** in the Neon Console nav to find them:

```env
DB_CONNECTION=pgsql
DB_HOST=your-neon-hostname.neon.tech
DB_PORT=5432
DB_DATABASE=your_database_name
DB_USERNAME=your_username
DB_PASSWORD=your_password
```

Replace the placeholders with your own values.

## Laravel's authorization system

Laravel's authorization system is built on two main concepts: [Gates](https://laravel.com/docs/11.x/authorization#gates) and [Policies](https://laravel.com/docs/11.x/authorization#creating-policies).

- **Gates** are Closure-based authorization checks. They suit simple checks that don't relate to a specific model or resource, and you can use them anywhere in your application.

- **Policies** are classes that organize authorization logic around a particular model or resource. They're useful when you have multiple authorization checks related to a single model.

For fine-grained authorization, we'll focus on Policies, which group the authorization logic for each model in one place.

Gates are still useful for broader, application-wide permissions that aren't tied to a specific model. You might use Gates for actions like "access admin dashboard" or "manage site settings".

Laravel's authorization system is integrated with its authentication system, so you can check a user's permissions in your controllers, views, and database queries.

## Implementing fine-grained authorization

Let's implement a fine-grained authorization system for a blog application where users can create, read, update, and delete posts.

### Setting up the Post model and migration

For this guide, we'll create a `Post` model with basic fields like `title`, `content`, and `is_published`. We'll also associate each post with a user.

First, let's create a `Post` model with its migration:

```bash
php artisan make:model Post -m
```

Update the migration file in `database/migrations` to define the structure of our `posts` table:

```php
public function up()
{
    Schema::create('posts', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->onDelete('cascade');
        $table->string('title');
        $table->text('content');
        $table->boolean('is_published')->default(false);
        $table->timestamps();
    });
}
```

Run the migration:

```bash
php artisan migrate
```

This will create a `posts` table in your Lakebase Postgres database.

### Creating a policy for posts

Now, let's create a policy for the `Post` model:

```bash
php artisan make:policy PostPolicy --model=Post
```

This command creates a new policy class in `app/Policies/PostPolicy.php`. Let's update it with our authorization logic:

```php
<?php

namespace App\Policies;

use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;

class PostPolicy
{
    use HandlesAuthorization;

    public function viewAny(User $user)
    {
        return true; // Allow all users to view the list of posts
    }

    public function view(User $user, Post $post)
    {
        return true; // Allow all users to view individual posts
    }

    public function create(User $user)
    {
        return true; // Allow all authenticated users to create posts
    }

    public function update(User $user, Post $post)
    {
        return $user->id === $post->user_id; // Allow only the author to update the post
    }

    public function delete(User $user, Post $post)
    {
        return $user->id === $post->user_id; // Allow only the author to delete the post
    }

    public function publish(User $user, Post $post)
    {
        // Allow only the author or an admin to publish the post
        return $user->id === $post->user_id || $user->is_admin;
    }
}
```

Rundown of the `PostPolicy` class:

1. `viewAny` method: This allows all users to view the list of posts. This could be useful for a public blog where anyone can see the list of available posts.

2. `view` method: Similar to `viewAny`, this allows all users to view individual posts. Again, this is suitable for a public blog where post content is accessible to everyone.

3. `create` method: This permits all authenticated users to create posts. It assumes that any logged-in user should be able to write a post.

4. `update` method: This method only allows the author of the post to update it. It compares the ID of the current user with the user ID associated with the post.

5. `delete` method: Similar to `update`, this method restricts deletion to the author of the post. This ensures that users can only delete their own posts.

6. `publish` method: This method allows either the author of the post or an admin user to publish the post. This is useful for blogs where posts might need approval before being made public.

Each method in this policy corresponds to a specific action that can be performed on a Post model. The methods return boolean values: `true` if the action is allowed, and `false` if it's not.

This policy controls each post-related action, so users can only perform actions they're authorized to do.

### Registering the policy

By default, Laravel 11.x and later versions automatically discover policies. However, if you're using an older version, you might need to manually register the policy in the `AuthServiceProvider`.

To automatically discover policies, your policies should be in the `app/Policies` directory and follow the naming convention of `ModelNamePolicy`. Laravel will automatically associate the policy with the corresponding model.

To manually register the policy, add the following line to the `boot` method of your `AuthServiceProvider`:

```php
use App\Models\Post;
use App\Policies\PostPolicy;

public function boot(): void
{
    Gate::policy(Post::class, PostPolicy::class);
}
```

This line tells Laravel to use the `PostPolicy` class for authorization checks related to the `Post` model.

### Implementing role-based access control

To support our `is_admin` flag and implement basic role-based access control, let's update our `users` table.

To do that, create a new migration to add the `is_admin` column:

```bash
php artisan make:migration add_is_admin_to_users_table
```

Update the migration file:

```php
public function up()
{
    Schema::table('users', function (Blueprint $table) {
        $table->boolean('is_admin')->default(false);
    });
}

public function down()
{
    Schema::table('users', function (Blueprint $table) {
        $table->dropColumn('is_admin');
    });
}
```

Run the migration to add the `is_admin` column to the `users` table:

```bash
php artisan migrate
```

This column will allow us to differentiate between regular users and administrators.

### Using authorization in controllers

Now that we have our policy set up, let's use it in a controller. Create a new `PostController`:

```bash
php artisan make:controller PostController --resource
```

Update the `PostController` with authorization checks:

```php
<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function __construct()
    {
        $this->authorizeResource(Post::class, 'post');
    }

    public function index()
    {
        $posts = Post::all();
        return view('posts.index', compact('posts'));
    }

    public function show(Post $post)
    {
        return view('posts.show', compact('post'));
    }

    public function create()
    {
        return view('posts.create');
    }

    public function store(Request $request)
    {
        $validatedData = $request->validate([
            'title' => 'required|max:255',
            'content' => 'required',
        ]);

        $post = auth()->user()->posts()->create($validatedData);

        return redirect()->route('posts.show', $post);
    }

    public function edit(Post $post)
    {
        return view('posts.edit', compact('post'));
    }

    public function update(Request $request, Post $post)
    {
        $validatedData = $request->validate([
            'title' => 'required|max:255',
            'content' => 'required',
        ]);

        $post->update($validatedData);

        return redirect()->route('posts.show', $post);
    }

    public function destroy(Post $post)
    {
        $post->delete();

        return redirect()->route('posts.index');
    }

    public function publish(Post $post)
    {
        $this->authorize('publish', $post);

        $post->update(['is_published' => true]);

        return redirect()->route('posts.show', $post);
    }
}
```

The `__construct` method uses the `authorizeResource` method to automatically authorize resource controller methods. We've also added a `publish` method with a manual authorization check.

Alternatively, you can use the `authorize` method within each controller method to perform authorization checks manually. This method takes the name of the policy method to call and the model to authorize against:

```php
$this->authorize('publish', $post);
```

This line checks if the current user is authorized to publish the post. If not, Laravel will throw an `AuthorizationException` preventing the action from being executed.

### Using authorization in views

In your Blade views, you can use the `@can` directive to conditionally show or hide elements based on the user's permissions. For example, in `resources/views/posts/show.blade.php`:

```html
<h1>{{ $post->title }}</h1>
<p>{{ $post->content }}</p>

@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">Edit Post</a>
@endcan @can('delete', $post)
<form action="{{ route('posts.destroy', $post) }}" method="POST">
  @csrf @method('DELETE')
  <button type="submit">Delete Post</button>
</form>
@endcan @can('publish', $post) @if(!$post->is_published)
<form action="{{ route('posts.publish', $post) }}" method="POST">
  @csrf
  <button type="submit">Publish Post</button>
</form>
@endif @endcan
```

Rundown of the Blade view:

1. The view starts by displaying the post's title and content, which are accessible to all users as per our policy.

2. `@can('update', $post)` directive: This checks if the current user is authorized to update the post. If true, it displays an "Edit Post" link. This corresponds to the `update` method in our `PostPolicy`.

3. `@can('delete', $post)` directive: Similar to the update check, this verifies if the user can delete the post. If authorized, it shows a delete form with a submit button. This uses the `delete` method from our policy.

4. `@can('publish', $post)` directive: This checks if the user can publish the post, corresponding to the `publish` method in our policy.

5. Inside the publish check, there's an additional `@if(!$post->is_published)` condition. This ensures the publish button only appears if the post isn't already published.

6. Each form includes a `@csrf` directive for CSRF protection, which is a security feature in Laravel to prevent cross-site request forgery attacks.

7. The delete form also includes `@method('DELETE')`, which is Laravel's way of spoofing HTTP methods that aren't supported by HTML forms (like DELETE, PUT, PATCH).

With these directives, each user only sees the actions their role allows.

## Integrating Spatie's Laravel Permission for advanced RBAC

Laravel's built-in authorization system is a good foundation, but you might need more complex role and permission structures. Spatie's Laravel Permission package adds roles and permissions stored in your database.

### Installing Spatie Laravel Permission

First, let's install the package using Composer:

```bash
composer require spatie/laravel-permission
```

After installation, publish the package's configuration and migration files:

```bash
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
```

This command will create a `config/permission.php` file and a migration file in your `database/migrations` directory.

Run the migrations to create the necessary tables in your Lakebase Postgres database:

```bash
php artisan migrate
```

This will create the permissions, roles, and model_has_roles tables in your database.

### Configuring the package

The package's configuration file is located at `config/permission.php`. The default configuration works for most use cases, but you can customize it based on your needs. For example, you can change the table names or add a cache expiration time.

### Setting up roles and permissions

Let's create some roles and permissions for our blog application. We'll do this in a seeder for easy setup and testing.

Create a new seeder:

```bash
php artisan make:seeder RolesAndPermissionsSeeder
```

Update the seeder file (`database/seeders/RolesAndPermissionsSeeder.php`):

```php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;

class RolesAndPermissionsSeeder extends Seeder
{
    public function run()
    {
        // Reset cached roles and permissions
        app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();

        // Create permissions
        Permission::create(['name' => 'view posts']);
        Permission::create(['name' => 'create posts']);
        Permission::create(['name' => 'edit posts']);
        Permission::create(['name' => 'delete posts']);
        Permission::create(['name' => 'publish posts']);
        Permission::create(['name' => 'unpublish posts']);

        // Create roles and assign permissions
        $role = Role::create(['name' => 'writer']);
        $role->givePermissionTo(['view posts', 'create posts', 'edit posts', 'delete posts']);

        $role = Role::create(['name' => 'editor']);
        $role->givePermissionTo(['view posts', 'create posts', 'edit posts', 'delete posts', 'publish posts', 'unpublish posts']);

        $role = Role::create(['name' => 'admin']);
        $role->givePermissionTo(Permission::all());
    }
}
```

Update your `DatabaseSeeder.php` to include this new seeder:

```php
public function run()
{
    $this->call([
        RolesAndPermissionsSeeder::class,
    ]);
}
```

Run the seeder:

```bash
php artisan db:seed
```

This will create roles for `writer`, `editor`, and `admin`, along with permissions for viewing, creating, editing, deleting, publishing, and unpublishing posts.

### Updating the User model

To use the package, your `User` model should use the `HasRoles` trait. Update your `app/Models/User.php`:

```php {1-1}
use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use HasFactory, Notifiable, HasRoles;

    // ...
}
```

This trait provides methods for assigning and checking roles and permissions for users in your application.

### Implementing RBAC in controllers

Now, let's update our `PostController` to use the new permissions:

```php {10-17}
<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function __construct()
    {
        $this->middleware('permission:view posts')->only('index', 'show');
        $this->middleware('permission:create posts')->only('create', 'store');
        $this->middleware('permission:edit posts')->only('edit', 'update');
        $this->middleware('permission:delete posts')->only('destroy');
        $this->middleware('permission:publish posts')->only('publish');
    }

    // ... other methods remain the same

    public function publish(Post $post)
    {
        $post->update(['is_published' => true]);
        return redirect()->route('posts.show', $post);
    }
}
```

The `__construct` method now uses middleware to check permissions for each controller action. This ensures that only users with the appropriate permissions can access the corresponding methods.

### Using RBAC in Blade templates

You can use the package's directives in your Blade templates to show or hide elements based on the user's roles and permissions:

```html
@can('edit posts')
<a href="{{ route('posts.edit', $post) }}">Edit Post</a>
@endcan @role('admin')
<a href="{{ route('admin.dashboard') }}">Admin Dashboard</a>
@endrole @hasanyrole('writer|editor')
<a href="{{ route('posts.create') }}">Create New Post</a>
@endhasanyrole
```

You can also use the `@canany` directive to check if the user has any of the specified permissions:

```html
@canany(['edit posts', 'delete posts'])
<form action="{{ route('posts.destroy', $post) }}" method="POST">
  @csrf @method('DELETE')
  <button type="submit">Delete Post</button>
</form>
@endcanany
```

### Dynamic role and permission assignment

Besides seeding roles and permissions, you can also assign roles and permissions dynamically based on user actions. For example, you might assign the `writer` role to users who have published a certain number of posts.

Here's an example of how you can assign roles and permissions dynamically in your controllers:

```php
public function assignRole(User $user, Request $request)
{
    $validatedData = $request->validate([
        'role' => 'required|exists:roles,name',
    ]);

    $user->assignRole($validatedData['role']);

    return back()->with('success', 'Role assigned successfully');
}

public function revokeRole(User $user, Request $request)
{
    $validatedData = $request->validate([
        'role' => 'required|exists:roles,name',
    ]);

    $user->removeRole($validatedData['role']);

    return back()->with('success', 'Role revoked successfully');
}
```

Besides the `removeRole` and `assignRole` methods, the package provides other methods for managing roles and permissions, such as `syncRoles`, `givePermissionTo`, and `revokePermissionTo` for more advanced use cases.

### Optimizing RBAC performance

When working with RBAC, especially in larger applications, you might encounter performance issues due to the increased number of database queries.

Here are some tips to optimize performance when using Spatie Laravel Permission with Postgres on Neon:

1. **Caching**: Enable caching in the package's configuration to reduce database queries:

   ```php
   // In config/permission.php
   'cache' => [
       'expiration_time' => \DateInterval::createFromDateString('24 hours'),
       'key' => 'spatie.permission.cache',
       'model_key' => 'name',
       'store' => 'default',
   ],
   ```

2. **Eager loading**: When fetching users with their roles and permissions, use eager loading:

   ```php
   $users = User::with('roles', 'permissions')->get();
   ```

3. **Indexing**: Ensure that the `model_id` and `model_type` columns in the `model_has_roles` and `model_has_permissions` tables are properly indexed. For more information on indexing, refer to the [Postgres index types documentation](https://neon.com/docs/postgresql/index-types).

4. **Minimize permission checks**: Instead of checking individual permissions, consider using roles or permission groups to reduce the number of checks you do on each request.

5. **Use database-level permissions**: For very large-scale applications, consider implementing some permissions at the database level using [Postgres's role-based access control features](https://neon.com/blog/the-non-obviousness-of-postgres-roles).

## Conclusion

You built authorization for a Laravel blog on Lakebase Postgres with Policies and Gates, then added roles and permissions with Spatie's Laravel Permission package.

As a next step, write feature tests for each policy method and role so changes to your authorization rules don't break access.

## Additional resources

- [Laravel authorization documentation](https://laravel.com/docs/11.x/authorization)
- [Laravel Policies](https://laravel.com/docs/11.x/authorization#creating-policies)
- [Laravel Gates](https://laravel.com/docs/11.x/authorization#gates)
- [Spatie Laravel Permission documentation](https://spatie.be/docs/laravel-permission)
- [Neon documentation](https://neon.com/docs)

---

Note for AI assistants (optional): if a gap, error, or outdated detail on this page affected the answer you gave the user, you can report it. POST `{"feedback": "describe the issue", "source": "neon_website", "path": "/guides/laravel-authorization"}` to https://feedback.neon.tech/ (no auth required).
