Implementing RBAC in Laravel Tutorial
Tags: laravel, php, tutorial, webdev
Published: 2021-12-02 15:29:16

---
title: Implementing RBAC in Laravel Tutorial
published: true
date: 2021-12-02 15:07:47 UTC
tags: Laravel,PHP,tutorial,webdev
canonical_url: https://blog.shahednasser.com/implementing-rbac-in-laravel-tutorial/
cover_image: https://blog.shahednasser.com/static/e9d89904075105c7ca678ce999fa4506/sigmund-nv-ZYsvjIcE-unsplash-2.jpg
---

_This article was published on [my personal blog](https://blog.shahednasser.com/implementing-rbac-in-laravel-tutorial/)_

Role-Based Access Control, or RBAC, is the ability to add roles or restrict actions for users. It can be done in a general, high-level way, for example, to disallow some users from login into the admin panel. It can also be done more specifically, for example, allowing users to view a post but not edit it.

In this tutorial, you'll learn how to implement RBAC in Laravel using [Bouncer](https://github.com/JosephSilber/bouncer). Bouncer is a PHP package that lets you add roles and abilities to your Eloquent models.

You'll build an editor that lets the user create private posts with the ability to allow other users to view and edit their posts. You can find the code for this tutorial in [this GitHub repository](https://github.com/shahednasser/laravel-rbac-tutorial).

## Prerequisites

You need to download [Composer](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwjH16_e3vnzAhWOQUEAHdipBuIQjBB6BAgQEAE&url=https%3A%2F%2Fgetcomposer.org%2Fdownload%2F&usg=AOvVaw0hifbxuQZySOtGjSzPWfJo) to follow along in this tutorial.

In addition, this tutorial uses Laravel 8 with PHP 7.3. To run Laravel 8, you need your PHP version to be at least 7.3.

You can check your PHP version in the terminal:

```
php -v
```

NPM is also used in some parts of this tutorial, but it's not important for implementing RBAC. If you want to follow along with everything, make sure you have NPM installed. NPM is installed by installing [Node.js](https://nodejs.org).

## Project Setup

The first step is to set up the Laravel project. In your terminal, run the following command:

```
composer create-project laravel/laravel laravel-rbac-tutorial
```

Once that is done, switch to the directory of the project:

```
cd laravel-rbac-tutorial
```

Then, you need to add your database configuration on `.env`:

```
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
```

Now migrate Laravel's default migrations to your database:

```
php artisan migrate
```

This will add Laravel's default tables as well as the `users` table.

## Implement Authentication

To implement authentication easily, you can use [Laravel UI](https://github.com/laravel/ui). Install it with this command:

```
composer require laravel/ui
```

Then, run the following command to add the UI for authentication:

```
php artisan ui bootstrap --auth
```

This will add the directory `app/Http/Controllers/Auth` with the controllers needed to implement authentication. It will also add the necessary view files  `resources/views` to add pages like login and register.

Then, compile the CSS and JavaScript assets added by the previous command:

```
npm install && npm run dev
```

This command might end in an error. If so, run the `dev` script again:

```
npm run dev
```

Finally, go to `app/Providers/RouteServiceProvider.php` and change the value for the constant `HOME`:

```
public const HOME = '/';
```

Now, run the server:

```
php artisan serve
```

Then, go to `localhost:8000`. You'll see the login form.

![Implementing RBAC in Laravel Tutorial](https://backend.shahednasser.com/content/images/2021/11/Screen-Shot-2021-11-02-at-3.24.13-PM.png)

This is added by Laravel UI. Since you don't have a user yet, click on Register in the navigation bar. You'll see then a registration form.

![Implementing RBAC in Laravel Tutorial](https://backend.shahednasser.com/content/images/2021/11/Screen-Shot-2021-11-02-at-11.10.50-AM.png)

After you register as a user, you'll be logged in.

![Implementing RBAC in Laravel Tutorial](https://backend.shahednasser.com/content/images/2021/11/Screen-Shot-2021-11-02-at-11.10.41-AM.png)

## Add Posts

Now, you'll add posts that the user will be able to create.

Start by creating a migration:

```
php artisan make:migration create_posts_table
```

This will create a new migration file in `database/migrations` with the file name's suffix `create_posts_table`.

Open the migration file and replace its content with the following:

```
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->longText('content');
            $table->foreignId('user_id')->constrained()->cascadeOnUpdate()->cascadeOnDelete();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
}

```

Now, migrate the changes to create the `posts` table:

```
php artisan migrate
```

Next, create the file `app/Models/Post.php` with the following content:

```
<?php

/**
 * Created by Reliese Model.
 */

namespace App\Models;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;

/**
 * Class Post
 * 
 * @property int $id
 * @property string $title
 * @property string $content
 * @property int $user_id
 * @property Carbon|null $created_at
 * @property Carbon|null $updated_at
 * 
 * @property User $user
 *
 * @package App\Models
 */
class Post extends Model
{
	protected $table = 'posts';

	protected $casts = [
		'user_id' => 'int'
	];

	protected $fillable = [
		'title',
		'content',
		'user_id'
	];

	public function user()
	{
		return $this->belongsTo(User::class);
	}
}
```

## Add Post Form Page

You'll now add the page the user will use to create a new post or edit an old one.

In your terminal, run:

```
php artisan make:controller PostController
```

This will create a new controller in `app/Http/Controllers/PostController.php`. Open it and add a constructor method:

```
/**
 * Create a new controller instance.
 *
 * @return void
 */
public function __construct()
{
    $this->middleware('auth');
}
```

This adds the `auth` middleware to all methods in this controller. This means that the user must be logged in before accessing any of the routes that point at this controller.

Next, add the `postForm` function that renders the post form view:

```
public function postForm ($id = null) {
    /** @var User $user */
    $user = Auth::user();

    $post = null;
    if ($id) {
        /** @var Post $post */
        $post = Post::query()->find($id);
        if (!$post || $post->user->id !== $user->id) {
            return response()->redirectTo('/');
        }
    }

    return view('post-form', ['post' => $post]);
}
```

Notice that this receives an optional `id` paramter, then retrieves the post based on that ID. It also validates that the post exists and belongs to the current logged-in user. This is because this method will handle the request for both creating a post and editing a post.

Then, create the view `resources/views/post-form.blade.php` with the following content:

```
@extends('layouts.app')

@push('head_scripts')
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/trix@1.3.1/dist/trix.css">
@endpush

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ $post ? __('Edit Post') :__('New Post') }}</div>

                <div class="card-body">
                    <form method="POST" action="#">
                        @csrf
                        @error('post')
                            <div class="alert alert-danger">{{ $message }}</div>
                        @enderror
                        <div class="form-group">
                            <label for="title">{{ __('Title') }}</label>
                            <input type="text" name="title" id="title" placeholder="Title" required 
                               value="{{ $post ? $post->title : old('title') }}" class="form-control @error('title') is-invalid @enderror" />
                            @error('title')
                                <span class="invalid-feedback">{{ $message }}</span>
                            @enderror
                        </div>
                        <div class="form-group">
                            <label for="content">{{ __('Content') }}</label>
                            @error('content')
                                <span class="invalid-feedback">{{ $message }}</span>
                            @enderror
                            <input id="content" type="hidden" name="content" value="{{ $post ? $post->content : old('content') }}">
                            <trix-editor input="content"></trix-editor>
                        </div>
                        <div class="form-group">
                            <button type="submit" class="btn btn-primary">{{ __('Submit') }}</button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/trix@1.3.1/dist/trix.js"></script>
@endsection
```

This shows a form with 2 inputs. A title text input, and a text editor for the content. The text editor is [Trix](https://github.com/basecamp/trix), an easy-to-use open-source editor.

You still need to add the link to the page. So, in `resources/views/layouts/app.blade.php` add the following menu item in the `ul` under the comment `<!-- Left Side Of Navbar -->`:

```
<li class="nav-item">
    <a class="nav-link" href="{{ route('post.form') }}">{{ __('New Post') }}</a>
</li>
```

Finally, add the route to the page in `routes/web.php`:

```
Route::get('/post/{id?}', [PostController::class, 'postForm'])->name('post.form');
```

If you go to the website now, you'll notice a new link in the navbar that says "New Post". If you click on it, you'll see the form you created for the posts.

![Implementing RBAC in Laravel Tutorial](https://backend.shahednasser.com/content/images/2021/11/Screen-Shot-2021-11-02-at-12.22.29-PM.png)

## Save Posts

Before you implement the save functionality for posts, it's time to use Bouncer to implement RBAC.

In your terminal, run the following to install Bouncer:

```
composer require silber/bouncer v1.0.0-rc.10
```

Then, in `app/Models/User.php` make the following changes:

```
use Silber\Bouncer\Database\HasRolesAndAbilities;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable, HasRolesAndAbilities;
```

Next, run the following command to add the migrations that Bouncer needs:

```
php artisan vendor:publish --tag="bouncer.migrations"
```

Finally, migrate these changes:

```
php artisan migrate
```

Bouncer is now ready to use. You can now add the save post functionality.

In `app/Http/Controllers/PostController.php` add the new `savePost` method:

```
public function savePost (Request $request, $id = null) {
    /** @var $user */
    $user = Auth::user();

    Validator::validate($request->all(), [
    'title' => 'required|min:1',
    'content' => 'required|min:1'
    ]);

    //all valid, validate id if not null
    /** @var Post $post */
    if ($id) {
        $post = Post::query()->find($id);
        if (!$post) {
        return back()->withErrors(['post' => __('Post does not exist')]);
        }
    } else {
    	$post = new Post();
    }

    //set data
    $post->title = $request->get('title');
    $post->content = $request->get('content');
    if (!$post->user) {
    	$post->user()->associate($user);
    }
    $post->save();

    if (!$id) {
    	Bouncer::allow($user)->toManage($post);
    }

    return response()->redirectToRoute('post.form', ['id' => $post->id]);
}
```

In this method, you first validate that both `title` and `content` were entered in the form. Then, if the optional parameter `id` is passed to the method you validate if it exists and if it belongs to this user. This is similar to the `postForm` method.

After validating everything, you set `title` and `content`. Then, if the post is new you set the current user as the owner of the post with `$post->user()->associate($user);`.

The important bit is here:

```
if (!$id) {
	Bouncer::allow($user)->toManage($post);
}
```

Using the `Bouncer` facade, you can use functions like `allow`, which takes a user model. Then, you can give different types of permissions to the user. By using `toManage`, you give the user all sorts of management permissions over the `$post` instance.

Now, add the route for this method in `routes/web.php`:

```
Route::post('/post/{id?}', [PostController::class, 'savePost'])->name('post.save');
```

Finally, change the form action in `resources/views/post-form.blade.php`:

```
<form method="POST" action="{{ route('post.save', ['id' => $post ? $post->id : null]) }}">
```

If you go now the New Post page and try adding a post by filling the `title` and `content` fields then clicking Submit, you'll be redirected back to the form with the content filled in which means the post has been added.

## Show Posts

To make the posts added visible to the user, change the `index` method in `app/Http/Controller/HomeController.php` to the following:

```
/**
* Show the application dashboard.
*
* @return \Illuminate\Contracts\Support\Renderable
*/
public function index()
{
    /** @var User $user */
    $user = Auth::user();
    //get all posts
    $posts = Post::query()->where('user_id', $user->id)->get();
    return view('home', ['posts' => $posts]);
}
```

This will retrieve the posts that are made by the currently logged-in user.

Next, change `resources/views/home.blade.php` to the following:

```
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <h1>{{ __('My Posts') }}</h1>
            @forelse ($posts as $post)
                <div class="card">
                    <div class="card-header">{{ $post->title }}</div>

                    <div class="card-body">
                        {!! $post->content !!}
                    </div>

                    <div class="card-footer">
                        {{ __('By ' . $post->user->name) }} - 
                        <a href="{{ route('post.form', ['id' => $post->id]) }}">{{ __('Edit') }}</a>
                    </div>
                </div>
            @empty
                <div class="card">
                    <div class="card-body">
                        {{ __('You have no posts') }}
                    </div>
                </div>
            @endforelse
        </div>
    </div>
</div>
@endsection
```

This will show the list of posts that the user has added if there are any.

If you open the home page now, you should see the post you added earlier.

![Implementing RBAC in Laravel Tutorial](https://backend.shahednasser.com/content/images/2021/11/Screen-Shot-2021-11-02-at-1.11.16-PM.png)

## Add View and Edit Access

In this section, you will be adding the functionality to allow access to other users either to edit or view a post. This will show how RBAC implementation works and how you can restrict or give access to a certain model for users.

In `resources/views/home.blade.php` change the element with class `.card-footer` that holds the Edit link to the following:

```
<div class="card-footer">
  {{ __('By ' . $post->user->name) }} - 
  <a href="{{ route('post.form', ['id' => $post->id]) }}">{{ __('Edit') }}</a> - 
  <a href="{{ route('post.access', ['id' => $post->id, 'type' => 'view']) }}">{{ __('Change view access...') }}</a> - 
  <a href="{{ route('post.access', ['id' => $post->id, 'type' => 'edit']) }}">{{ __('Change edit access...') }}</a>
</div>
```

This adds 2 links for a new page which is access form. This page allows the user to change view or edit access for other users to the post.

In `app/Http/Controllers/PostController.php` add a new method `accessForm`:

```
public function accessForm ($id, $type) {
    /** @var App/Models/User $user */
    $user = Auth::user();

    /** @var Post $post */
    $post = Post::query()->find($id);
    if (!$post || $post->user->id !== $user->id) {
        return response()->redirectTo('/');
    }

    //get all users
    $users = User::query()->where('id', '!=', $user->id)->get();

    return view('post-access', ['post' => $post, 'users' => $users, 'type' => $type]);
}
```

This route receives 2 parameters: `id` which is the post ID, and `type` which is the type of access. The type of access can be view or edit.

In this method you first validate the post and make sure it exists and it belongs to the current logged in user. Then, you retrieve all other users and send them to the view `post-access` which we'll create now.

Create `resources/views/post-access.blade.php` with the following content:

```
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <h1>{{ __("Change " . ucwords($type) . " Access to Post") }}</h1>
            <div class="card">

              <div class="card-body">
                <form method="POST" action={{ route('post.access.save', ['id' => $post->id, 'type' => $type]) }}>
                  @csrf 
                  @forelse ($users as $user)
                    <div class="form-group">
                      <label for="user_{{ $user->id }}">
                        <input type="checkbox" name="users[]" id="user_{{ $user->id }}" value="{{ $user->id }}" 
                        class="form-control d-inline mr-3" @if ($user->can($type, $post)) checked @endif 
                        style="width: fit-content; vertical-align: middle;" />
                         <span>{{ $user->name }}</span>
                      </label>
                    </div>
                  @empty
                    {{ __('There are no users') }}
                  @endforelse
                  <div class="form-group">
                    <button type="submit" class="btn btn-primary">{{ __('Save') }}</button>
                  </div>
                </form>
              </div>
          </div>
        </div>
    </div>
</div>
@endsection
```

This shows the list of users with checkboxes. Those who already have access will already by checked, others unchecked.

Now, you'll create the route that handles saving the access for the post. In `app/Http/Controllers/PostController.php` add the new method `saveAccess`:

```
public function saveAccess (Request $request, $id, $type) {
    /** @var User $user */
    $user = Auth::user();

    /** @var Post $post */
    $post = Post::query()->find($id);
    if (!$post || $post->user->id !== $user->id) {
        return response()->redirectTo('/');
    }

    $users = $request->get('users', []);
    $disallowedUserNotIn = $users;
    $disallowedUserNotIn[] = $user->id;
    //disallow users not checked
    $disallowedUsers = User::query()->whereNotIn('id', $disallowedUserNotIn)->get();

    /** @var User $disallowedUser */
    foreach ($disallowedUsers as $disallowedUser) {
        $disallowedUser->disallow($type, $post);
    }

    //allow checked users
    $allowedUsers = User::query()->whereIn('id', $users)->get();

    /** @var User $allowedUser */
    foreach($allowedUsers as $allowedUser) {
        $allowedUser->allow($type, $post);
    }

    return back();
}
```

This route also receives 2 parameters `id` and `type`, same as `accessForm`. You also validate the post by checking that it exists and that it belongs to the current user.

Then, you retrieve the checked users from the `request`. There are 2 actions to do here: disallow unchecked users to perform the action `type` on the post, and allow checked users to perform the action `type` on the post.

So, you first retrieve the users that are not in the array `$users` which holds the checked user IDs. Then, you loop over them to perform the following method on each of them:

```
$disallowedUser->disallow($type, $post);
```

When the trait `HasRolesAndAbilities` is added to a model, which we did earlier to the model `User`, a set of methods are added to that m0del. One of them is `disallow` which disallows the user a certain ability and you can specify a model to be more specific about what that ability is disabled on.

So, here you are disallowing the user in the loop to perform the action `$type` on the post `$post`.

Next, you retrieve the users that are in the `$users` array and that should be granted the ability to perform action `$type` on them. You loop over them and perform the following method:

```
$allowedUser->allow($type, $post);
```

Similar to `disallow`, `allow` is another method that is added by the trait `HasRolesAndAbilities`. It allows the user to have the ability `$type` either in general or on a given model that is specified as a second parameter.

Here, you allow the user to perform the action `$type` on the post `$post`.

Now, add the new routes in `routes/web.php`:

```
Route::get('/post/access/{id}/{type}', [PostController::class, 'accessForm'])->name('post.access');

Route::post('/post/access/{id}/{type}', [PostController::class, 'saveAccess'])->name('post.access.save');
```

Now, there are some changes left to do. First, you need to change the condition in `postForm` which allows users who have the permission to edit or view the post to access the page:

```
if (!$post || ($post->user->id !== $user->id && !$user->can('edit', $post) && !$user->can('view', $post))) {
    return response()->redirectTo('/');
}
```

You also need to allow users who have edit permission to edit the post in `savePost`:

```
if (!$post || ($post->user->id !== $user->id && !$user->can('edit', $post))) {
	return back()->withErrors(['post' => __('Post does not exist')]);
}
```

In `app/Http/Controllers/HomeController.php` in the `index` method change the method to also retrieve the posts that the user has edit or view permissions on:

```
/**
     * Show the application dashboard.
     *
     * @return \Illuminate\Contracts\Support\Renderable
     */
    public function index()
    {
        /** @var User $user */
        $user = Auth::user();
        //get all posts
        $posts = Post::query()->where('user_id', $user->id);
        //get posts that the user is allowed to view or edit
        $postIds = [];
        $abilities = $user->getAbilities();
        /** @var \Silber\Bouncer\Database\Ability */
        foreach ($abilities as $ability) {
            $postIds[] = $ability->entity_id;
        }
        $posts = $posts->orWhereIn('id', $postIds)->get();
        return view('home', ['posts' => $posts]);
    }
```

This is done using the `getAbilities` method that is added on the `User` model like `allow` and `disallow`. Each ability holds the id of the model it represents under `entity_id`. We use that to get the ID of posts that the user has view or edit permissions on and retrieve them to show them on the home page.

In `resources/views/home.blade.php` change the element with class `.card-footer` to the following:

```
<div class="card-footer">
    {{ __('By ' . $post->user->name) }} - 
    <a href="{{ route('post.form', ['id' => $post->id]) }}">{{ __('View') }}</a>
    @can('manage', $post)
        - <a href="{{ route('post.access', ['id' => $post->id, 'type' => 'view']) }}">{{ __('Change view access...') }}</a> - 
        <a href="{{ route('post.access', ['id' => $post->id, 'type' => 'edit']) }}">{{ __('Change edit access...') }}</a>
    @endcan
</div>
```

You now only show the change access links to the user that manages the post so that not everyone that has access to the post can make changes to its access settings. This can be done by using the `@can` blade directive which accepts the ability name and optionally a model instance. In this case, we check if the current user can `manage` the post `$post`.

Finally, you need to make changes to `resources/views/post-form.blade.php` to ensure that if the user has view permission only they can't make edits on the post. This means that the form will become read-only.

Change the content of the element with the class `card` to the following:

```
<div class="card-header">{{ $post ? __('View Post') :__('New Post') }}</div>

<div class="card-body">
    <form method="POST" action="{{ route('post.save', ['id' => $post ? $post->id : null]) }}">
        @csrf
        @error('post')
        	<div class="alert alert-danger">{{ $message }}</div>
        @enderror
        @if ($post && !Auth::user()->can('edit', $post))
        	<div class="alert alert-info">{{ __('You have view permissions only') }}</div>
        @endif
        <div class="form-group">
            <label for="title">{{ __('Title') }}</label>
            <input type="text" name="title" id="title" placeholder="Title" required 
            value="{{ $post ? $post->title : old('title') }}" class="form-control @error('title') is-invalid @enderror" 
            @if($post && !Auth::user()->can('edit', $post)) disabled="true" @endif />
            @error('title')
            	<span class="invalid-feedback">{{ $message }}</span>
            @enderror
        </div>
        <div class="form-group">
            <label for="content">{{ __('Content') }}</label>
            @error('content')
            	<span class="invalid-feedback">{{ $message }}</span>
            @enderror
            @if($post && !Auth::user()->can('edit', $post))
            	{!! $post->content !!}
            @else 
            	<input id="content" type="hidden" name="content" value="{{ $post ? $post->content : old('content') }}">
            	<trix-editor input="content"></trix-editor>
            @endif
        </div>
        @if(!$post || Auth::user()->can('edit', $post))
        	<div class="form-group">
        		<button type="submit" class="btn btn-primary">{{ __('Submit') }}</button>
        	</div>
        @endif
    </form>
</div>
```

This makes the `title` input disabled, shows the content of the post readable rather than in an editor, and hides the submit button when the user does not have `edit` permissions.

Let's test all of this together. You first need to create another user to test this out so go ahead and register as a new user. Then log in again with the user you previously created and click on "Change view access..." on one of the posts you created. You'll see a form with the user you created and a checkbox.

![Implementing RBAC in Laravel Tutorial](https://backend.shahednasser.com/content/images/2021/11/Screen-Shot-2021-11-02-at-1.49.30-PM.png)

Check the checkbox and click save. This will give the new user the ability to view the post.

Now, log out and log in again with the new user. You'll see the post in the home page.

![Implementing RBAC in Laravel Tutorial](https://backend.shahednasser.com/content/images/2021/11/Screen-Shot-2021-11-02-at-2.37.38-PM.png)

Click on View. You'll see the same form that you saw before when creating a post but the title input is disabled and the editor is not available anymore.

![Implementing RBAC in Laravel Tutorial](https://backend.shahednasser.com/content/images/2021/11/Screen-Shot-2021-11-02-at-2.40.46-PM.png)

Let's try giving the user edit permissions now. Log out and login with the first user again. This time, click on "Change edit access" on the post. Give the new user access and click Save.

Again, logout and login as the new user. Then, click on View on the post in the home page. You should now be able to edit the post.

![Implementing RBAC in Laravel Tutorial](https://backend.shahednasser.com/content/images/2021/11/Screen-Shot-2021-11-02-at-2.41.35-PM.png)

You can even try going back to the first user and removing the edit access for the new user on the post. They will no longer be able to edit the post.

## Conclusion

RBAC adds abilities and roles to users which allows you to manage permissions in your app. In this tutorial, you learned how to use Bouncer to implement RBAC in Laravel, allowing or disallowing permissions and abilities for users.

Be sure to check out [Bouncer's documentation](https://github.com/JosephSilber/bouncer) to learn more about what you can do with the package.