• Час читання ~4 хв
  • 02.06.2022

У Laravel ролі та дозволи були однією з найбільш заплутаних тем протягом багатьох років. Здебільшого тому, що про це немає жодної документації: ті самі речі «ховаються» під іншими термінами у фреймворку, як-от «ворота», «поліси», «охорони» тощо. У цій статті я спробую пояснити їх усе в «людська мова».

Ворота – те саме, що і дозвіл

Одною з найбільших плутанини, на мою думку, є термін «ворота». Я думаю, що розробники б уникли багато плутанини, якби їх називали такими, якими вони є.

Ворота — це Дозволи, які називаються іншим словом.

Які типові дії нам потрібно виконувати з дозволами?

  • Define the permission, ex. "manage_users"
  • Check the permission on the front-end, ex. show/hide the button
  • Check the permission on the back-end, ex. can/can't update the data

Так, замініть слово "дозвіл" на "ворота", і ви все зрозумієте.

Простим прикладом Laravel може бути такий:

app/Providers/AppServiceProvider.php:

use App\Models\User;
use Illuminate\Support\Facades\Gate;
 
class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
    	// Should return TRUE or FALSE
        Gate::define('manage_users', function(User $user) {
            return $user->is_admin == 1;
        });
    }
}

resources/views/navigation.blade.php:

<ul>
    <li>
        <a href="{{ route('projects.index') }}">Projects</a>
    </li>
    @can('manage_users')
    <li>
        <a href="{{ route('users.index') }}">Users</a>
    </li>
    @endcan
</ul>

routes/web.php:

Route::resource('users', UserController::class)->middleware('can:manage_users');

Тепер я знаю, що технічно Gate може означати більше одного дозволу.Отже, замість «manage_users» ви можете визначити щось на кшталт «admin_area». Але в більшості прикладів, які я бачив, ворота є синонімом дозволу.

Також, в деяких випадках, дозволи називаються "здібностями", як у пакеті Bouncer. Це також означає те саме – здатність/дозвіл на якусь дію.Ми розглянемо пакети пізніше в цій статті.


Різні способи перевірки дозволу воріт

Іншим джерелом плутанини є те, як/де перевірити ворота. Він настільки гнучкий, що ви можете знайти дуже різні приклади. Давайте пройдемося по них:

Варіант 1. Маршрути: проміжне програмне забезпечення('can:xxxxxx')

Це приклад зверху.Безпосередньо на маршруті/групі ви можете призначити проміжне програмне забезпечення:

Route::post('users', [UserController::class, 'store'])
    ->middleware('can:create_users');

Варіант 2.

Варіант 3.Gate::allows() або Gate::denies()

public function store(Request $request)
{
	if (!$request->user()->can('create_users'))
        abort(403);
    }
}

Інший спосіб – використовувати фасад воріт:

public function store(Request $request)
{
	if ($request->user()->cannot('create_users'))
        abort(403);
    }
}

Або навпаки:

public function create()
{
	if (!auth()->user()->can('create_users'))
        abort(403);
    }
}

Або коротший спосіб переривання за допомогою помічників:

Варіант 4. Контролер: authorize()

public function store(Request $request)
{
	if (!Gate::allows('create_users')) {
        abort(403);
    }
}

Ще коротший і мій улюблений варіант – використовувати authorize() у контролерах.У разі невдачі він автоматично повертає сторінку 403.

public function store(Request $request)
{
    if (Gate::denies('create_users')) {
        abort(403);
    }
}

Варіант 5. Клас запиту форми:

public function store(Request $request)
{
    abort_if(Gate::denies('create_users'), 403);
}

Я помітив, що багато розробників створюють класи запиту форми лише для визначення правила перевірки, повністю ігноруючи перший метод цього класу, який є authorize().

Ви також можете використовувати його для перевірки воріт. Таким чином, ви досягаєте поділу проблем, що є гарною практикою для твердого коду, тому контролер не піклується про перевірку, оскільки це зроблено у спеціальному класі Form Request.

public function store(Request $request)
{
    $this->authorize('create_users');
}

А потім у запиті форми:

Політика: набір дозволів на основі моделі

Якщо ваші дозволи можна призначити моделі Eloquent, у типовому контролері CRUD ви можете створити навколо них клас політики.

public function store(StoreUserRequest $request)
{
	// No check is needed in the Controller method
}

Якщо ми запустимо цю команду:

class StoreProjectRequest extends FormRequest
{
    public function authorize()
    {
        return Gate::allows('create_users');
    }
 
    public function rules()
    {
        return [
            // ...
        ];
    }
}

Це створить файл app/Policies/UserPolicy.php із методами за замовчуванням, які мають коментар, що пояснює їх призначення:

У кожному з цих методів ви визначаєте умову повернення істини/неправди. Отже, якщо ми наслідуємо ті самі приклади, що й раніше Гейтс, ми зможемо зробити це:

Тоді ви можете перевірити Політику дуже подібно до Gates:

php artisan make:policy ProductPolicy --model=Product

Отже, ви вказуєте назву методу та назву класу політики.

use App\Models\Product;
use App\Models\User;
 
class ProductPolicy
{
    use HandlesAuthorization;
 
    /**
     * Determine whether the user can view any models.
     */
    public function viewAny(User $user)
    {
        //
    }
 
    /**
     * Determine whether the user can view the model.
     */
    public function view(User $user, Product $product)
    {
        //
    }
 
    /**
     * Determine whether the user can create models.
     */
    public function create(User $user)
    {
        //
    }
 
    /**
     * Determine whether the user can update the model.
     */
    public function update(User $user, Product $product)
    {
        //
    }
 
    /**
     * Determine whether the user can delete the model.
     */
    public function delete(User $user, Product $product)
    {
        //
    }
 
    /**
     * Determine whether the user can restore the model.
     */
    public function restore(User $user, Product $product)
    {
        //
    }
 
    /**
     * Determine whether the user can permanently delete the model.
     */
    public function forceDelete(User $user, Product $product)
    {
        //
    }
}

Іншими словами, політики – це лише інший спосіб групувати дозволи замість Gates. Якщо ваші дії здебільшого стосуються CRUD моделей, тоді політики, ймовірно, є більш зручним і краще структурованим варіантом, ніж Gates.

class ProductPolicy
{
    public function create(User $user)
    {
        return $user->is_admin == 1;
    }

Роль: універсальний набір дозволів

public function store(Request $request)
{
    $this->authorize('create', Product::class);
}

Давайте обговоримо ще одну плутанину: у документах Laravel ви не знайдете жодного розділу про ролі користувачів.

З точки зору платформи, не існує «ролей», є лише ворота/політики, за якими ви можете групувати будь-яким способом.


Іншими словами, роль є об’єктом ПОЗА межами фреймворка Laravel, тож нам потрібно створити рольову структуру самостійно.Це може бути частиною загальної плутанини з аутентифікацією, але це цілком сенс, оскільки ми повинні контролювати, як визначаються ролі:

Отже, функція Role – це ще один рівень вашої програми Laravel. Ось де ми потрапляємо до пакетів Laravel, які можуть допомогти. Але ми також можемо створити ролі без жодного пакету:

Останній біт є найважливішим.

Отже, замість:

  • Is it one role or multiple roles?
  • Can a user have one role or multiple roles?
  • Who can manage roles in the system?
  • etc.

Ви б зробили щось на кшталт:

  1. Create "roles" DB table and Role Eloquent Model
  2. Add a relationship from User to Role: one-to-many or many-to-many
  3. Seed the default Roles and assign them to the existing Users
  4. Assign a default Role at the registration
  5. Change Gates/Policies to check the Role instead

Знову ж таки, тут у вас є кілька варіантів перевірки ролей. У наведеному вище прикладі ми припускаємо, що від користувача до ролі існує відношення belongsTo, а також є константи в моделі ролі, наприклад < code>ADMIN = 1, як EDITOR = 2, щоб уникнути зайвих запитів до бази даних.

Але якщо ви віддаєте перевагу бути гнучким, ви можете щоразу запитувати базу даних:

class ProductPolicy
{
    public function create(User $user)
    {
        return $user->is_admin == 1;
    }

Але не забудьте завантажити "рольові" відносини, інакше ви можете легко зіткнутися з N Проблема із запитом +1 тут.

class ProductPolicy
{
    public function create(User $user)
    {
        return $user->role_id == Role::ADMIN;
    }

Гнучкість: дозволи, збережені в БД

З мого особистого досвіду, звичайна модель побудови всього разом така:

class ProductPolicy
{
    public function create(User $user)
    {
        return $user->role->name == 'Administrator';
    }

Іншими словами, ми не перевіряємо доступ за ролями. Роль — це лише «штучний» рівень, набір дозволів, який трансформується у Gates протягом життєвого циклу програми.


О, ці. Вони викликають так багато плутанини протягом багатьох років.Багато розробників подумали, що Guards — це ролі, і почали створювати окремі таблиці БД, як-от «адміністратори», а потім призначати їх як Guards. Частково тому, що в документації ви можете знайти фрагменти коду, наприклад Auth::guard('admin') ->спроба($credentials))

Отже, охоронці – це більш глобальне поняття, ніж ролі.

  • All permissions and roles are saved in the database, managed with some admin panel;
  • Relationships: roles many-to-many permissions, User belongs to Role (or many-to-many roles);
  • Then, in AppServiceProvider you make a foreach loop from all permissions from DB, and run a Gate::define() statement for each of them, returning true/false based on the role;
  • And finally, you check the permissions with @can('permission_name') and $this->authorize('permission_name'), like in the examples above.
$roles = Role::with('permissions')->get();
$permissionsArray = [];
foreach ($roles as $role) {
    foreach ($role->permissions as $permissions) {
        $permissionsArray[$permissions->title][] = $role->id;
    }
}
 
// Every permission may have multiple roles assigned
foreach ($permissionsArray as $title => $roles) {
    Gate::define($title, function ($user) use ($roles) {
    	// We check if we have the needed roles among current user's roles
        return count(array_intersect($user->roles->pluck('id')->toArray(), $roles)) > 0;
    });
}

Looks complicated?


$user->givePermissionTo('edit articles');
$user->assignRole('writer');
$role->givePermissionTo('edit articles');
$user->can('edit articles');
Bouncer::allow($user)->to('create', Post::class);
Bouncer::allow('admin')->to('ban-users');
Bouncer::assign('admin')->to($user);

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Про автора CrazyBoy49z
WORK EXPERIENCE
Контакти
Ukraine, Lutsk
+380979856297