Laravel-permission: Laravel Roles and Permissions with parameters

Created on 29 Jun 2019  路  22Comments  路  Source: spatie/laravel-permission

I am planning on extending a system that helps me mark students off in lab classes of various courses that they are enrolled in. I have a basic version of the system working, but it needs a better permission system.

I have users and courses. What I want is the ability to have roles such as "Coordinator" and "Grader" but based on a course.

So a particular user might be a "Grader on Course1" and a "Coordinator on Course2"

The role of "Grader on Course X" would have underlying permissions of "Can view Course X", "Can Grade Course X" etc. "Coordinator on Course X" might have other permissions such as "Can enroll students on Course X".

So my roles and permissions are sort of parameterized by courses.

With spatie/laravel-permission I understand that I can make roles such as Grader and Coordinator (along with the underlying permissions). But that would not allow me to make a particular user a Grader on one course but not all the others.

Combining this package (spatie/laravel-permission) with Laravels policies I think I get closer. The policies are parameterized by models (i.e., the Course model in my case). But I don't understand how the policies would tie in with the roles and permissions unless they are too parameterized.

Any ideas on the best way to solve this?

One way I thought of was to have roles such as "coordinator_course1", "coordinator_course2", "coordinator_course3" ... and permissions such as "can_view_course1", "can_view_course2", "can_view_course3", ... But this feels like a poor choice.

Any advice would be most welcome.

support

All 22 comments

How large a scale is this for? Are you talking about 5-10 courses? Or 5,000?

For large scale I wonder if it makes sense to add a custom pivot table to (as you say parameterize, or) link Course models to certain Roles, and then assign users to that combination/pivot instead of to the Role itself.

Thanks. The system would be used for about 100 courses and 2000 students.

Would you might briefly sketching how to go down the pivot route method. Does this need to pivot tables: one for roles and one for permissions? How would the blade directives and middleware work?

+1 , I need something like this too, someone has a idea??

/pinging @Continuum81 @Roemerb @liamoreilly for feedback on how they've addressed it
Related issues:

1187 @Continuum81

1186 @Roemerb

1135 @liamoreilly

The solution was simple, you just have to add a pivot on the tables "roles" and "model_has_permissions", and then overwrite the relationship in the User model "public function roles(): MorphToMany" and "permissions(): MorphToMany", i use session for save the new pivot on login, and later i use the session function for add the pivot on user relationship with whereraw

Hi @drbyte. Do you think it would be possible to?

  • Augment tables model_has_permissions and model_has_roles with nullable columns restricted_to_type and restricted_to_id.
  • When these new columns are null, everything works as now.
  • When filled, the permission/role is restricted to a target model class or model entity.
  • And then adding new methods on HasRoles trait, for instance $user->givePermissionTo('edit posts'); would have an "equivalent" $user->givePermissionOnModelTo('edit posts', $post); or $user->givePermissionOnModelTo('edit posts', Post::class);

Feels like the current PermissionRegistrar could handle Laravel's native $user->can('edit posts', $post); better

        $this->gate->before(function (Authorizable $user, string $ability, $arguments) {
            try {
                if (!empty($arguments) && method_exists($user, 'hasPermissionOnModelTo')) {
                    return $user->hasPermissionOnModelTo($ability, $arguments[0]) ?: null;
                } else if (method_exists($user, 'hasPermissionTo')) {
                    return $user->hasPermissionTo($ability) ?: null;
                }   
            } catch (PermissionDoesNotExist $e) {
            } 
        });

Please forgive me If I'm being over-simplistic here.

@esroyo When you start getting into restricting which models a permission applies to, I wonder if you should use the Bouncer package by Joseph Silber? It offers all the things the spatie package does, but also adds the idea of negative rules, and some other conditionals which might cover what you're looking for.

@esroyo When you start getting into restricting which models a permission applies to, I wonder if you should use the Bouncer package by Joseph Silber? It offers all the things the spatie package does, but also adds the idea of negative rules, and some other conditionals which might cover what you're looking for.

Bouncer offers a lot of features but actually also lacks the main topic in this thread: parameterised roles and permissions. So I don't think bouncer is a solution either.

@liamoreilly thanks for verifying that.

@drbyte My example was unfortunate because Bouncer does in fact allow to give a permission on a specific model.
As @liamoreilly stated however It doesn't allow to assign a role "restricted" on a specific model. The usage expected would be for instance $user->assignRoleOnModel('coordinator', $course);.

I briefly inspected Bouncer source code but it feels a bit "tortuous" to me. Moreover I prefer this package's syntax. I could try to code a PR proposal if the idea has supports or you see possibilities of this to be accepted.

Has a solution to a parameterized roles and permissions been reached? I need this same functionality as well. To do $user->assignRoleOnModel('coordinator', $course); would be awesome.

I find that $user->assignRoleOnModel('coordinator', $course) is a desirable syntax, however I'm not convinced about my previous suggestion of "augmenting" current tables. I don't like how that would impact the current code/models.

@drbyte what do you think about creating a new "BoundRole" concept to link a role with a target model? (in the example, $course). Kind of ...

| bound_role |
+------------+
| id         |
| role_id    |
| bound_type |
| bound_id   |
+------------+

+----------------------+
| model_has_bound_role |
+----------------------+
| bound_role_id        |
| model_type           |
| model_id             |
+----------------------+

Underneath assignRoleOnModel would handle the creation of the BoundRole, etc.

I made a test of my own permission system after giving up on looking for one with the features I wanted. You can find it at:

https://bitbucket.org/aliam13/laravel_permission_test/src/master/

I thought I would make this available as it might be interesting - not really for people to use. This was just a toy and is hardly finished. I think this could function well (with a bit of work) for small sites.

I would prefer a reputable package such as laravel-permission to implement this rather than me knock up a poor version.

Hi @drbyte @liamoreilly

I also made a little PoC: https://gitlab.expoteca.com/esroyo/laravel-permission-on-model

This should be installable using a "vcs" config in composer:

    "require": {
...
        "spatie/laravel-permission": "^3.0",
        "esroyo/laravel-permission-on-model": "dev-master",
...
    },
    "repositories": [
        {
            "type": "vcs",
            "url": "https://gitlab.expoteca.com/esroyo/laravel-permission-on-model"
        }

Running the migration, and adding the trait use Esroyo\Permission\Traits\HasRolesOnModel; on the User model.

This is a simple prototype of a "BoundRole" idea, and only allows to "bind" roles as in $user->assignRoleOnModel('admin', $course);, not direct permissions. I implemented just the minimum to get it working.

@drbyte I'd appreciate if you could share your thoughts on this. Since It is perfectly suitable to implement this idea as a separate "extra" package on top of laravel-permissions, if you don't feel like including this feature any time soon, I would take that way. Thanks

Hello,
I did a test under this scenario that maybe could be similar to any of other cases. _(Pivot tables)_

  • Users table: Only the information required to Login
  • Profiles table: Maintain the user detailed information (Coordinators)
  • Projects table: List of projects. (Courses)
  • ProfileProject table: Assignation the Projects to a Profile, in my case I can assign multiple times the same project to the same profile. _(It is just the first test)_

Behavior: I don't know if it is by "design", by "chance" or by "error", but in my case this worked.
I just added the trait _use HasRoles;_ to the Model.

In my previous test to assign the Roles and permissions to the "Profile" instead of the "User" I extended the Model from "Authenticable" instead of "Model", to be able to use auth()->user()->profile->can('delete'), I haven't been able to use the can directive., can('delete'). _working on it_

Hope this post is not too long.

@if(auth()->user()->profile->can('delete'))
    <form method="POST" action="{{ route( $master_model . '.destroy', $user->id) }}" 
      class="display: inline-block;"
      onsubmit="return confirm( {{ @("global.app_are_you_sure") }} " >
       @csrf
       @method('DELETE')

    <button class="btn btn-xs btn-danger" type="submit">{{ __('Delete') }}</button>
    </form>
@endif

image

Showing the Roles and the Permissions assigned to the Role:

image

I only tested the Seeder, here are part of it

    public function run()
    {
        $Records = [];

        /*
         * Add The records
         *
        */
        $Records[] = 
            [
                'project_id' => 1,
                'profile_id' => 1,
                'status' => 'A', // A-ctive P-rotected  B-locked R-estricted C-onfirmation Required
                'created_by' => 1,
                'updated_by' => 1,
                'roles' => ['Administrator'],

            ];
        $Records[] = 
            [
                'project_id' => 1,
                'profile_id' => 2,
                'status' => 'A', // A-ctive P-rotected  B-locked R-estricted C-onfirmation Required
                'created_by' => 1,
                'updated_by' => 1,
                'roles' => ['Administrator', 'User'],
            ];
        $Records[] = 
            [
                'project_id' => 1,
                'profile_id' => 4,
                'status' => 'A', // A-ctive P-rotected  B-locked R-estricted C-onfirmation Required
                'created_by' => 1,
                'updated_by' => 1,
                'roles' => ['Administrator', 'User Administrator','User'],
            ];
...
        //
        // ProjectsProfiles Creation
        //
        $this->createRecordClass(App\ProfileProject::class, $Records,['roles']);
  } // end run function

    /**
     * Record Creation using an array with the information.
     * The information could be uploaded from other sources
     * Note:
     *      This function is NOT recursive, Details of Detail records are NOT processed.
     *      i.e: user->Role->Permissions
     *      The function wil Process structure like
     *      roles->permissions
     *      user->shipAddress (Not applicable to this Seeder)
     *
     * @param  class  $dbModel
     * @param  array  $dbRecords
     * @param  array  $dbDetailRecords
    */
    private function createRecordClass( $dbModel, $dbRecords, $dbDetailRecords = []) {
        foreach ($dbRecords as $dbRecord) {
            // var_dump($dbRecord);
            echo 'Creating record: ' . "\n";

            /*
                Version Create - Find
            */
            try {

                $newRecord = $dbModel::create(collect($dbRecord)->except($dbDetailRecords)->toArray());

            } catch (Exceptions $ex) {
                echo $ex->getMessage();
                // dd(var_dump($newModel));
            }
....
                   foreach ($dbDetailRecords as $dbDetailRecord) {
                        echo var_dump($dbDetailRecord);
                        if ( isset($dbRecord[$dbDetailRecord]) ) {
                            // var_dump($dbRecord[$dbDetailRecord]);

                            foreach ($dbRecord[$dbDetailRecord] as $dbDetail) {
                                echo "Assigning: " . $dbDetail . "\n";

                                try {
                                    //
                                    // Method defined in  Traits\HasPermissions.php in the case of Spatie/Permissions
                                    // $newRecord->givePermissionTo($dbDetail);
                                    $newRecord->assignRole($dbDetail);
                                    //
                                } catch ( Exception $ex) {
                                    echo $ex->getMessage() . "\n";
                                }

Model:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
// use Illuminate\Database\Eloquent\Relations\Pivot;

// Access Control List
use Spatie\Permission\Traits\HasRoles;



class ProfileProject extends Model
// class ProfileProject extends Pivot
{
    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'profile_project';
    /**
     * @todo: Add the Spatie\Permission\Traits\HasRoles trait
     *
    */
    use HasRoles;
    protected $guard_name = 'web';

    /*
        Projects associated with the Profile
        Without the second paramenter the Pivot table name: 'profile_project'
        Validate the 3rd and 4rd Paramenters They are switched in each linked Model
    */
    public function profile() {
        return $this->hasOne('App\Profile','id','profile_id');
    }
    /*
        Projects associated with the Profile
        Without the second paramenter the Pivot table name: 'profile_project'
        Validate the 3rd and 4rd Paramenters They are switched in each linked Model
    */
    public function project() {
       return $this->hasOne('App\Project','id','project_id');
    }

}

The Migration:

class CreateProfilesProjectsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('profile_project', function (Blueprint $table) {
            $table->bigIncrements('id');

            $table->bigInteger('project_id');

            if (Schema::hasTable('projects')) {
                // Foreign Key
                $table->foreign('project_id')
                    ->references('id')
                    ->on('projects');
                    // ->onDelete('cascade');
            }

            if (Schema::hasTable('profiles')) {
                $table->bigInteger('profile_id');
                // Foreign Key
                $table->foreign('profile_id')
                    ->references('id')
                    ->on('profiles');
                    // ->onDelete('cascade');
            }

            /**                 
                Log information
                Model needs to be reviewed to achieve this behavior

            */
            // if (Schema::hasTable('profiles')) {
                $table->bigInteger('created_by');
                $table->bigInteger('updated_by');
            // }

            // Status
            //  A-ctive P-rotected  B-locked R-estricted F-inished ...
            //  @todo: Create the Project Administration Module and define the Status of the Projects
            $table->string('status',1);

            $table->timestamps();
        });
    }

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

Example of the Profile Model App\Profile.php

    /* -----------------------------------------------------
     * Relationships
    /* -----------------------------------------------------

    /*
        Get the User associated with the Profile
    */
    public function user() {
        return $this->belongsTo('App\User');
    }

    /*
        Projects associated with the Profile
        Without the second paramenter the Pivot table name: 'profile_project'
        Validate the 3rd and 4rd Paramenters They are switched in each linked Model
    */
    public function projects() {
        return $this->belongsToMany('App\Project','profile_project','profile_id','project_id')
            ->withPivot('created_by','updated_by','status')
            ->withTimestamps();
    }


Example of the Model App\Project.php

    /* -----------------------------------------------------
     * Relationships
    /* -----------------------------------------------------

    /*
        Profiles associated with the Project
        Without the second paramenter the Pivot table name: 'profile_project'
        Validate the 3rd and 4rd Paramenters They are switched in each linked Model
    */
    public function profiles() {
        return $this->belongsToMany('App\Profile','profile_project','project_id','profile_id')
            ->withPivot('created_by','updated_by','status')
            ->withTimestamps();
    }

Why is this table needed and how to fill model_has_permissions?
With other tables already figured out, everything is fine.

Why is this table needed and how to fill model_has_permissions?
With other tables already figured out, everything is fine.

@serkor is your question related to this discussion or to the package? can you elaborate? Thanks

With package(

@esroyo @liamoreilly What do you think of #1373 ... particularly how it could be used to relate to specific models?

The Wildcard Permissions feature added in 3.9.0 will allow targeting certain models, among other things.

These wildcard permissions, while being incredibly expressive, do not address the reason for this thread.

This thread was about managing row level permissions using roles. In fact, using such wild card permissions makes managing them even more of a challenge.

How do these interface with roles? Do roles have/need wildcards? These questions now need to be asked with the addition of wildcards at the permission level.

Fair points. 馃憤

Was this page helpful?
0 / 5 - 0 ratings

Related issues

wreighsantos picture wreighsantos  路  4Comments

tripex picture tripex  路  3Comments

MichalKrakow picture MichalKrakow  路  4Comments

ionesculiviucristian picture ionesculiviucristian  路  4Comments

hosseinnedaei picture hosseinnedaei  路  3Comments