tetranyble's avatar

Authorization from scratch

I attempted implement this toturial https://laracasts.com/series/whats-new-in-laravel-5-1/episodes/16 in laravel 10 and it turned breaking. these are all section of the code:

  1. AuthServiceProvider
<?php

namespace App\Providers;

use App\Models\Permission;
use Exception;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The model to policy mappings for the application.
     *
     * @var array<class-string, class-string>
     */
    protected $policies = [
 
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @param  GateContract  $gate
     * @return void
     *
     * @throws Exception
     */
    public function boot(GateContract $gate)
    {
        $this->registerPolicies();

//        $gate->before(function ($user) {
//            return $user->isSuperAdmin();
//        });
//        $gate->define('course_edit',function ($user){
//            return true;
//        });

        try {
            if (Schema::hasTable('permissions')) {
                foreach ($this->getPermissions() as $permission) {
                    $gate->define($permission->name, function ($user) use ($permission) {
                        return $user->hasPermissions($permission);
                    });
                }
            }
        } catch (Exception $e) {
            Log::error($e->getMessage());
        }
    }

    protected function getPermissions()
    {
        return Permission::with('roles')->get();
    }
}

  1. The test Route
Route::get('authorization', function (){
    if(\Illuminate\Support\Facades\Gate::denies('course_edit', \auth()->user())){
        return abort(403);
    }
    return \auth()->user();
})->name('authorization');
  1. Feature Test
<?php

namespace Tests\Feature;

use App\Models\Permission;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\Supports\AuthorizationSupport;
use Tests\TestCase;

class AuthorizationTest extends TestCase
{
    use RefreshDatabase, AuthorizationSupport, WithFaker;

    /** @test */
    public function user_has_permission_to_access_resource(){
        $user = User::factory()->create();

        $roles = $this->makeRoles($this->makePermissions());

        $user = User::factory()->create();

        $user->assignRoles($roles);

        //Special permission
        $permission = Permission::factory()->create([
            'name' => $this->faker->randomElement(['editor_right', 'admin_right'])
        ]);

        $user->assignPermissions($permission);

        $this->actingAs($user)
            ->get(route('authorization'))
            ->assertOk();
    }
}
  1. The result
		Expected response status code [200] but received 403.
		Failed asserting that 200 is identical to 403.
  1. My findings When i use a string, for example "course_edit" in the gate
       $gate->define('course_edit',function ($user){
            return true;
        });

it worked. But when I get the permission from the database as show below, it will fail.

if (Schema::hasTable('permissions')) {
                foreach (Permission::with('roles')->get() as $permission) {
                    $gate->define($permission->name, function ($user) use ($permission) {
                        return $user->hasPermissions($permission);
                    });
                }
            }

What i figured out is that the boot method called even before the database is been created and seeded which makes the permission table not to exist and everything crashed. So i guide the foreach statement with Schema::hasTable('permissions'). However, this prevent the definition of all the permissions.

I need help on this, specifically:

if (Schema::hasTable('permissions')) {
                foreach (Permission::with('roles')->get() as $permission) {
                    $gate->define($permission->name, function ($user) use ($permission) {
                        return $user->hasPermissions($permission);
                    });
                }
            }

Hope someone come through for me. Thank you in advance.

0 likes
2 replies
LaryAI's avatar
Level 58

The issue you're facing is that the boot method in the AuthServiceProvider is being called before the database is created and seeded, causing the permissions table to not exist. To solve this, you can delay the registration of the permissions until after the database has been migrated and seeded.

Here's a possible solution:

  1. In your AuthServiceProvider, remove the try-catch block and the getPermissions method.
public function boot(GateContract $gate)
{
    $this->registerPolicies();

    $gate->before(function ($user) {
        return $user->isSuperAdmin();
    });

    $this->registerPermissions($gate);
}

protected function registerPermissions(GateContract $gate)
{
    if (Schema::hasTable('permissions')) {
        $permissions = Permission::with('roles')->get();

        foreach ($permissions as $permission) {
            $gate->define($permission->name, function ($user) use ($permission) {
                return $user->hasPermissions($permission);
            });
        }
    }
}
  1. In your TestCase, override the setUp method to run the migrations and seed the database before running the tests.
use Illuminate\Foundation\Testing\RefreshDatabase;

class AuthorizationTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();

        $this->artisan('migrate');
        $this->artisan('db:seed');
    }

    // Rest of your tests...
}

By delaying the registration of permissions until after the migrations and seeding have been performed, you ensure that the permissions table exists before attempting to retrieve the permissions from the database.

Note: Make sure you have the necessary migrations and seeders set up correctly for the permissions table and its data.

tetranyble's avatar

So i figured it out after about 24 hours. I know about this Spatie package https://github.com/spatie/laravel-permission/blob/main/src/PermissionRegistrar.php and really curious on how things work under the hood. I dug ged through the code base and find this section between 123 to 132:

   /**
     * Register the permission check method on the gate.
     * We resolve the Gate fresh here, for benefit of long-running instances.
     */
    public function registerPermissions(Gate $gate): bool
    {
        $gate->before(function (Authorizable $user, string $ability) {
            if (method_exists($user, 'checkPermissionTo')) {
                return $user->checkPermissionTo($ability) ?: null;
            }
        });

        return true;
    }

in the class PermissionRegistrar.php. So what makes this function different is that it did not load the permission in the PermissionServiceProvider.php service provider instead it differed to the incoming request like so, 'can:academicsession_create' and the full code below:

Route::get('authorization', function (){

    return \auth()->user();

})->name('authorization')
    ->middleware(['auth', 'can:academicsession_create']);

Also this was possible through switching from $gate->define to $gate->before.` the latter taking on annonymous function as the only parameter.

my final code that works like charm:

<?php

namespace App\Providers;

use App\Models\Article;
use App\Models\Permission;
use App\Policies\ArticlePolicy;
use Illuminate\Contracts\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Access\Gate;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;


class AuthServiceProvider extends ServiceProvider
{
    /**
     * The model to policy mappings for the application.
     *
     * @var array<class-string, class-string>
     */
    protected $policies = [
        Article::class => ArticlePolicy::class,
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @param Gate $gate
     * @return void
     */
    public function boot(Gate $gate)
    {
        $this->registerPolicies();
        $this->callAfterResolving(Gate::class, function (Gate $gate, Application $app){
            $this->registerPermissions($gate);
        });

    }

    protected function registerPermissions(Gate $gate)
    {
        $gate->before(function (Authorizable $user, string $permission) {
            return $user->hasPermissions($permission) ?: null;
        });

        return true;
    }

}

Thanks to @laryai that gave me the clue to differing registration.

1 like

Please or to participate in this conversation.