Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

jvazquez's avatar

ROLES AND PERMISSIONS

Hi guys, I am developing a new application where I need to define how to implement a roles-permissions structure. The client wants to define different access levels L1,L2,L3, ... to access different kind of information and different kind of table fields. For example, if I have a L1 access level, I can search on "Person" table and get access to some fields on that table. If I have a L2 access level, I can search on " Person" table too, but I can access more fields then with L1. If I have L3 access level, more tables and more fields, ... and that way.

Which is the best way to implement this kind of solution?

Is it enough if I use Entrust or Sentinel ?

I appreciate all the help you can give me.

Thanks, Jorge

0 likes
3 replies
ohffs's avatar

If it's really just a simple access level like 1, 2, 3 I'd just have an 'access_level' int field on the user model and use that.

jekinney's avatar

@jvazquez For your example you're asking how to implement a different query for each level?

Roles and permissions doesn't care about that. Establish your roles and/or permissions first. Depending on the complexity of the query you can implement in different ways:

  1. Possibly run a bunch of if statements
  2. Switch statement
  3. Each way either builds the query or runs a function with the query
  4. sure there are many other ways too

I would probably lean towards a switch statement that executes a function based on permission level. The function, maybe in a helper or repository would then either have the query logic for the specific level, or build the query by calling other smaller functions. Each has pros and cons, for searching you may want the fastest way, which would be just calling the query within a function versus building it up even though you may be repeating yourself rather than microseconds to build the query.

If you set your role as L1, L2 etc.. your permissions could include which fields/columns to include for searching, I don't know your setup though. Then you could build the search query by looping through the permissions and adding to the query's where statements.

Keep in mind a user can have many roles too, so setting up search level role with associated permissions just for searching shouldn't be hard at all. But once again it all depends.

Obviously a lot to consider, pencil and paper and write/sketch it out and see where it leads you.

As far as packages, IMO it is easier just to write my own after set up time etc.. Entrust is great and simple, made for Laravel that I have used in L4 days for quick projects.

1 like
Connor-S-Parks's avatar
Level 8

I don't have much time so I'm sort of just going to dump this here... Simple 'Role' -> 'Permission' system! A user may have one role, a role may have many permissions and permissions may belong to many roles.

You'll need two new models and their relations set up (User belongsTo Role, Role belongsToMany Permission): 'Role'

class Role extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
    ];

    /**
     * The role's permissions.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function permissions()
    {
        return $this->belongsToMany(Permission::class);
    }

    /**
     * The role's users.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function users()
    {
        return $this->hasMany(User::class);
    }
}

'Permission' (I usually include a description of what it matches)

class Permission extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'identifier', 'description',
    ];

    /**
     * The permission's roles.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function roles()
    {
        return $this->belongsToMany(Role::class);
    }
}

The User model will need this relation adding:

/**
 * The user's role.
 *
 * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
 */
public function role()
{
    return $this->belongsTo(Role::class);
}

You'll also need some migrations: 'roles'

Schema::create('roles', function (Blueprint $table) {
    $table->increments('id');

    $table->string('name');

    $table->timestamps();
});

'permissions'

Schema::create('permissions', function (Blueprint $table) {
    $table->increments('id');

    $table->string('identifier')->unique()->index();
    $table->string('description');

    $table->timestamps();
});

Don't forget to add role_id to the users table!

You'll need this adding to a provider's boot method (I advise App\Providers\AuthServiceProvider), $gate = Illuminate\Contracts\Auth\Access\Gate. This can probably be done a bit more efficiently but I don't have the time to do so right now.

$gate->before(function ($user, $ability) {
    $abilities = explode('|', $ability);

    foreach ($abilities as $ability) {
        if ($user->role->permissions->first(function ($k, $permission) use ($ability) {
                return strpos($permission->identifier, '!') !== false && str_is($permission->identifier, '!'.$ability);
            })) {
            return false;
        }
    }

    foreach ($abilities as $ability) {
        if ($user->role->permissions->first(function ($k, $permission) use ($ability) {
                return str_is($permission->identifier, $ability);
            })) {
            return true;
        }
    }

    return false;
});

This basically means you can now do $user->can('some.permission') and it'll return true if the user has any permissions matching that (some.*, some.permission etc) but only if they don't have a negating permission (used to block out features specifically, ie: !some.permission). The way this is built is in a way that expects the user to always have a role set. This is because I usually seed a default role_id of 1 and an initial role of User with no permissions. I then upgrade them as they go User Manager, Administrator etc.

Because of it going through gate, you can also use blade's @can methods too, eg something like:

@can('debug.queries')
    <table class="table table-bordered" data-expanding-widget-data="debug-queries" style="margin-top:16px;margin-bottom:8px;">
        <tbody>
            <tr>
                <th style="width:15px">#</th>
                <th>Query</th>
                <th style="width:80px">Time (ms)</th>
            </tr>
        @foreach ($queries as $i => $query)
            <tr>
                <td>{{ number_format($i + 1) }}</td>
                <td><code>{{ $query->sql }}</code></td>
                <td>{{ number_format($query->time, 2) }}</td>
            </tr>
        @endforeach
            <tr>
                <td colspan="2" class="text-right"><strong>Total</strong></code></td>
                <td>{{ number_format($totalTime, 2) }}</td>
            </tr>
        </tbody>
    </table>
@endcan

If you want middleware, you can use this:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpKernel\Exception\HttpException;

class Permiss
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|array|null  $permiss
     * @return mixed
     */
    public function handle($request, Closure $next, $permiss = null)
    {
        if (!empty($permiss) && !Auth::user()->can($permiss)) {
            throw new HttpException(403);
        }

        return $next($request);
    }
}

Register the middleware in App\Http\Kernel's routeMiddleware parameter like so:

protected $routeMiddleware = [
    'permiss' => \App\Http\Middleware\Permiss::class,

    // ...
];

Now you can have route specific permissions such as permiss:users.index will throw a 403 if the user doesn't have the permission users.index.

If you wish to OR check permissions, you can do so using pipes (|). permiss:perm.one|perm.two would check if you the user either has perm.one OR perm.two.

NOTE: Had to rewrite this... I had a smilie and it seemingly removed all content after the smilie upon save... Hm*

2 likes

Please or to participate in this conversation.