Простой контроль доступа ACR в Laravel 10 (инструкция шаг за шагом)

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

В этой статье мы рассмотрим, как реализовать Role-Based Access Control (RBAC) в Laravel 10 для эффективного управления доступом пользователей.

RBAC - это модель безопасности, в которой пользователям назначаются роли на основе их должностных обязанностей, а доступ к ресурсам приложения предоставляется этим ролям.
Этот подход гарантирует, что только авторизованные пользователи имеют доступ к определенным функциям и данным в приложении.
Для реализации RBAC мы будем использовать пакет "wnikk/laravel-access-rules" с Github, который упрощает создание ролей и разрешений.

Мы рассмотрим шаги по созданию ролей и разрешений, назначению их пользователям и защите конфиденциальной информации от несанкционированного доступа.

Одним из основных преимуществ реализации RBAC в Laravel является возможность ограничения и контроля доступа.

С помощью RBAC вы можете определить роли для различных должностей, которые ограничивают доступ к функциям и данным на сайте.

Например, вы можете создать роль "администратор" с полным доступом к приложению, в то время как роль "гость" может иметь доступ только к определенным страницам.
Вы также можете создавать настраиваемые роли, которые имеют доступ к определенным функциям, таким как "менеджер" или "модератор".

Таким образом, пользователи имеют доступ только к функциям, которые необходимы для выполнения их целевых задач.

Для создания RBAC в Laravel мы будем использовать пакет "wnikk/laravel-access-rules" из composer, который предоставляет простой и гибкий способ создания ролей и разрешений.
Этот пакет позволяет нам назначать роли пользователям, назначать разрешения ролям и назначать разрешения напрямую пользователям.

Мы рассмотрим шаги, необходимые для настройки пакета, определения ролей и разрешений, а также назначения их пользователям.

Следуя этому пошаговому руководству, вы сможете легко реализовать RBAC в своем приложении Laravel и обеспечить безопасность учетных записей ваших пользователей.

С чего начнём?

Чтобы упростить процесс реализации прав доступа в Laravel, мы начнём с:

  1. User Management - Мы создадим управление пользователями с помощью Laravel 10. Это позволит легче применять права доступа в Laravel.

  2. Rules Management - Кроме того, мы реализуем управление правилами, чтобы ограничить доступ к контенту, определив список правил для проекта.

  3. Permits and inheritance Management - Управление разрешениями может использоваться для добавления ролей в учетные записи пользователей и назначения прав доступа в Laravel.

  4. News Management - Наконец, мы можем реализовать управление новостями и применять права доступа в Laravel для каждой роли, назначенной пользователю.

Мы также будем использовать функциональность CRUD, которая обеспечивает возможности: создание, чтение, обновление и удаление правил.

Если вы ищете примеры концепций, обсуждаемых в этой статье, вы можете найти их в соответствующем репозитории на GitHub. Просто перейдите по ссылке в репозиторий и просмотрите исходный код, чтобы увидеть реализацию примера. Это позволит вам лучше понять, как работают концепции на практике и как применить их в своих собственных проектах.

Шаг 1: Создание приложения на Laravel

Для начала реализации Laravel 10 первым шагом создадим новое веб-приложение. Для этого откройте терминал или командную строку и инициируйте создание нового приложения на Laravel:

composer create-project laravel/laravel rules-example

Шаг 2: Установка пакетов

Далее нам нужно установить необходимый пакет Wnikk для Access Control Rules (ACR) и пакет визуального контроля. Это можно легко сделать, открыв терминал и выполнить указанные ниже команды:

composer require wnikk/laravel-access-rules
composer require wnikk/laravel-access-ui

Чтобы внести изменения в пакет Wnikk, нам нужно выполнить команду, которая создаст конфигурационные файлы, файлы миграции и файлы представлений. Следуя этому шагу, вы сможете настроить пакет, чтобы он соответствовал конкретным требованиям вашего приложения:

php artisan vendor:publish --provider="Wnikk\\LaravelAccessRules\\AccessRulesServiceProvider"
php artisan vendor:publish --provider="Wnikk\\LaravelAccessUi\\AccessUiServiceProvider"

Шаг 3: Обновление модели User

Теперь мы будем интегрировать ACR с нашей существующей моделью пользователя. Этот шаг важен, чтобы гарантировать, что в нашем приложении правильно настроены механизмы контроля доступа. Нам нужно только добавить trait HasPermissions в модель:

use Wnikk\LaravelAccessRules\Traits\HasPermissions;

class User extends Model {
    // The User model requires this trait
    use HasPermissions;

Шаг 4: Настройка соединения с базой данных

Для целей этого примера мы будем использовать файловую базу данных SQLite. Чтобы начать, создайте пустой файл с именем "./database/database.sqlite" и настройте соединение с базой данных, как показано в приведенном примере.
Файл .env:

DB_CONNECTION=sqlite

На этом этапе мы готовы выполнить команду миграции. Выполнение этой команды позволит создать необходимые таблицы в нашей SQLite-базе данных.

php artisan migrate

Теперь, когда мы собрали работающую систему контроля доступа с помощью пакета ACR, следующим шагом будет добавление разрешений (permissions) для моделей в нашем приложении. Разрешения определяют, какие действия пользователь может выполнять для определенного ресурса.

Шаг 5: Создание миграции для новостей

Переходим к следующему шагу: созданию миграции для таблицы news. Чтобы выполнить это задание, выполните следующую команду, которая сгенерирует необходимый файл и позволит определить схему таблицы:

php artisan make:migration create_news_table

Это создаст новый файл миграции в директории database/migrations web-приложения. Ниже вы найдете полный код, необходимый для определения структуры таблицы, включая различные поля и их соответствующие типы:

<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('news', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->integer('user_id');
            $table->string('name', 70);
            $table->string('description', 320)->nullable();
            $table->text('body');
            $table->softDeletes();
        });
    }
    public function down(): void
    {
        Schema::dropIfExists('news');
    }
};

Теперь запустим миграцию повторно:

php artisan migrate

Шаг 6: Создание модели

Теперь создадим модель для новостей. Чтобы сгенерировать модель новостей, необходимо выполнить следующую команду Artisan. Это создаст модель новостей в каталоге app\Models.

php artisan make:model News

Пример кода для модели новостей:

<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

/**
 * Class News
 *
 * @property $id
 * @property $user_id
 * @property $name
 * @property $description
 * @property $body
 */
class News extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = [
        'user_id',
        'name',
        'description',
        'body',
    ];
}

Шаг 7: Создание Seeder

Теперь, когда у нас есть все необходимые таблицы в базе данных, настало время заполнить их тестовыми данными и настроить правила для них.

Для этого мы создадим Seeder - класс, которые позволят нам заполнить таблицы начальными данными.

1. Создадим несколько пользователей:

php artisan make:seeder CreateUserSeeder

Исходник файла database\seeders\CreateUserSeeder.php:

<?php
namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

class CreateUserSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        DB::table('users')->insert([
            'id' => 1,
            'name' => 'Test user 1',
            'email' => 'root@mail.com',
            'password' => Hash::make('12345'),
        ]);
        DB::table('users')->insert([
            'id' => 2,
            'name' => 'Test user 2',
            'email' => 'test@mail.com',
            'password' => Hash::make('password'),
        ]);
        DB::table('users')->insert([
            'name' => 'Test user 3',
            'email' => Str::random(10).'@mail.com',
            'password' => Hash::make(Str::random(10)),
        ]);
        DB::table('users')->insert([
            'name' => 'Test user 4',
            'email' => Str::random(10).'@mail.com',
            'password' => Hash::make(Str::random(10)),
        ]);
        DB::table('users')->insert([
            'name' => 'Test user 5',
            'email' => Str::random(10).'@mail.com',
            'password' => Hash::make(Str::random(10)),
        ]);
    }
}

2. Создаем несколько записей новостей.

php artisan make:seeder NewsTableSeeder

Исходник файла database\seeders\NewsTableSeeder.php:

<?php
namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\News;

class NewsTableSeeder extends Seeder
{
    public function run(): void
    {
        News::create([
            'user_id' => 1,
            'name' => 'First news',
            'description' => 'Description of first news',
            'body' => 'Body content 1...',
        ]);
        News::create([
            'user_id' => 1,
            'name' => 'Second news',
            'description' => 'Description of second test news',
            'body' => 'Body content 2...',
        ]);
        News::create([
            'user_id' => 2,
            'name' => 'News of test user',
            'body' => 'Body content 3...',
        ]);
    }
}

3. Множество правил для тестирования.

php artisan make:seeder CreateRulesSeeder

Правила сами по себе:
Исходник файла database\seeders\CreateRulesSeeder.php:

<?php
namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Wnikk\LaravelAccessRules\AccessRules;

class CreateRulesSeeder extends Seeder
{
    public function run(): void
    {
        // example #1 - route middleware
        AccessRules::newRule('example1.viewAny', 'View all users on example1');

        // example #2 - check in action
        AccessRules::newRule('example2.view', 'View data of user on example2');

        // example #3 - check on action options
        AccessRules::newRule([
            'guard_name' => 'example3.update',
            'title' => 'Changing different user data on example3',
            'options' => 'required|in:name,email,password'
        ]);

        // example #4 - global resource
        AccessRules::newRule('viewAny', 'Global rule "viewAny" for example4');
        AccessRules::newRule('view', 'Global rule "view" for example4');
        AccessRules::newRule('create', 'Global rule "create" for example4');
        AccessRules::newRule('update', 'Global rule "update" for example4');
        AccessRules::newRule('delete', 'Global rule "delete" for example4');

        // example #5 - resource for controller
        AccessRules::newRule('Examples.Example5.viewAny', 'Rule for one Controller his action "viewAny" example5');
        AccessRules::newRule('Examples.Example5.view', 'Rule for one Controller his action "view" example5');
        AccessRules::newRule('Examples.Example5.create', 'Rule for one Controller his action "create" example5');
        AccessRules::newRule('Examples.Example5.update', 'Rule for one Controller his action "update" example5');
        AccessRules::newRule('Examples.Example5.delete', 'Rule for one Controller his action "delete" example5');

        // example #6 - magic self
        AccessRules::newRule(
            'example6.update',
            'Rule that allows edit all news',
        'An example of how to use a magic suffix ".self" on example6'
        );
        AccessRules::newRule('example6.update.self', 'Rule that allows edit only where user is author');

        // example #7 - Policy
        AccessRules::newRule('Example7News.test', 'Rule event "test" example7');

        // Final example, add control to the Access user interface
        $id = AccessRules::newRule('Examples.UserRules.main', 'View all rules, permits and inheritance');
        AccessRules::newRule('Examples.UserRules.rules', 'Working with Rules', null, $id, 'nullable|in:index,store,update,destroy');
        AccessRules::newRule('Examples.UserRules.roles', 'Working with Roles', null, $id, 'nullable|in:index,store,update,destroy');
        AccessRules::newRule('Examples.UserRules.inherit', 'Working with Inherit', null, $id, 'nullable|in:index,store,destroy');
        AccessRules::newRule('Examples.UserRules.permission', 'Working with Permission', null, $id, 'nullable|in:index,update');
    }
}

4. Мы теперь создадим роль супер-администратора.

От которой будут наследоваться другие пользовательские роли. В этом шаге установлено три типа моделей, которые могут иметь разрешения в файле настроек по умолчанию (config/access.php): группы (groups), роли (roles) и пользователи (users). Для супер-администратора мы будем использовать роли.

php artisan make:seeder CreateRootAdminRoleSeeder

Вся логика заложена в том что у разрешений есть владелец, владелец это абстракция, которая закрепляется за любой моделью.

Исходник файла database\seeders\CreateRootAdminRoleSeeder.php:

<?php
namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Wnikk\LaravelAccessRules\AccessRules;

class CreateRootAdminRoleSeeder extends Seeder
{
    public function run(): void
    {
        $acr = new AccessRules;
        $acr->newOwner('Role', 'root', 'RootAdmin role');

        // For example #1
        $acr->addPermission('example1.viewAny');

        // For example #2
        $acr->addPermission('example2.view');

        // For example #3
        $acr->addPermission('example3.update', 'name');
        $acr->addPermission('example3.update', 'email');
        $acr->addPermission('example3.update', 'password');

        // For example #4
        $acr->addPermission('viewAny');
        $acr->addPermission('view');
        $acr->addPermission('create');
        $acr->addPermission('update');
        $acr->addPermission('delete');

        // For example #5
        $acr->addPermission('Examples.Example5.viewAny');
        $acr->addPermission('Examples.Example5.view');
        $acr->addPermission('Examples.Example5.create');
        $acr->addPermission('Examples.Example5.update');
        $acr->addPermission('Examples.Example5.delete');

        // For example #6
        //For all - $acr->addPermission('example6.update');
        $acr->addPermission('example6.update.self');

        // For example #7
        $acr->addPermission('Example7News.test');

        // For final example
        $acr->addPermission('Examples.UserRules.index');
        $acr->addPermission('Examples.UserRules.rules');
        $acr->addPermission('Examples.UserRules.rules', 'index');
        $acr->addPermission('Examples.UserRules.rules', 'store');
        $acr->addPermission('Examples.UserRules.rules', 'update');
        $acr->addPermission('Examples.UserRules.rules', 'destroy');
        $acr->addPermission('Examples.UserRules.roles');
        $acr->addPermission('Examples.UserRules.roles', 'index');
        $acr->addPermission('Examples.UserRules.roles', 'store');
        $acr->addPermission('Examples.UserRules.roles', 'update');
        $acr->addPermission('Examples.UserRules.roles', 'destroy');
        $acr->addPermission('Examples.UserRules.inherit');
        $acr->addPermission('Examples.UserRules.inherit', 'index');
        $acr->addPermission('Examples.UserRules.inherit', 'store');
        $acr->addPermission('Examples.UserRules.inherit', 'destroy');
        $acr->addPermission('Examples.UserRules.permission');
        $acr->addPermission('Examples.UserRules.permission', 'index');
        $acr->addPermission('Examples.UserRules.permission', 'update');
    }
}

5. И, наконец-то, добавим наследование прав от супер-администратора ко всем пользователям.

php artisan make:seeder AddRoleToAllUserSeeder

Исходник файла database\seeders\AddRoleToAllUserSeeder.php:

<?php
namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\User;

class AddRoleToAllUserSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        $all = User::all();
        foreach ($all as $one) $one->inheritPermissionFrom('Role', 'root');

        // or
        // $acr = new AccessRules;
        // $acr->setOwner('Role', 'root');
        // foreach ($all as $one) $one->inheritPermissionFrom($acr);

        // or
        // $mainUser = User::find(1);
        // foreach ($all as $one) $one->inheritPermissionFrom($mainUser);
    }
}

Перейдем к импорту всех инструкций, созданных на этом шаге:

php artisan db:seed --class=CreateUserSeeder
php artisan db:seed --class=NewsTableSeeder
php artisan db:seed --class=CreateRulesSeeder
php artisan db:seed --class=CreateRootAdminRoleSeeder
php artisan db:seed --class=AddRoleToAllUserSeeder

Все те же манипуляции с контролем доступа и наследованием, можно выполнить используя интерфейс, который был добавлен в начале этой статьи.
Вы можете получить доступ к интерфейсу, открыв адрес "/accessui/" в вашем проекте, при базовых настройках когда он включен:

Список всех ролей, групп и пользователей:

Список всех правил:

Давайте перейдем к самой интересной части ☕

Различным методам проверки доступа и связанных с ними правил.
В дальнейшем контроллеры, использованные в примерах, будут возвращать JSON данные, как для SPA Frontend.
Таким образом, не требуется в этих примерах создавать еще шаблоны.

Пример 1

В этом примере мы используем middleware в маршрутизации, чтобы ограничить доступ к контроллеру.

Добавим в файл routes\web.php:

Route::get('/example1', [Example1Controller::class, 'index'])->middleware('can:example1.viewAny');

Контроллер стандартный и не используется для проверок, в данном примере.
Вариация файла ...Example1Controller.php:

<?php
namespace App\Http\Controllers\Examples;

use Illuminate\Support\Facades\Response;
use App\Http\Controllers\Controller;
use App\Models\User;

class Example1Controller extends Controller
{
    public function index()
    {
        return Response::json(User::all(), 200);
    }
}

Что происходит в результате:

Пример 2

Давайте попробуем проверить разрешение в самом контроллере.

Добавим в файл routes\web.php:

Route::get('/example2', [Example2Controller::class, 'show']);

Исходник файла ...Example2Controller.php:

<?php
namespace App\Http\Controllers\Examples;

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Gate;
use App\Http\Controllers\Controller;

class Example2Controller extends Controller
{
    public function show()
    {
        Gate::authorize('example2.view');
        
        return Response::json(Auth::user()->toArray(), 200);
    }
}

Пример, как можно использовать фасад Laravel Gate.

Пример 3

Этот пример очень похож на предыдущий, но с использованием параметра опции.
Добавим в файл routes\web.php:

Route::any('/example3/{frm}', [Example3Controller::class, 'update']);

Исходник файла ...Example3Controller.php:

<?php
namespace App\Http\Controllers\Examples;

use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Hash;
use App\Http\Controllers\Controller;
use App\Enum\UserProfileFormEnum;

class Example3Controller extends Controller
{
    public function update(UserProfileFormEnum $frm, Request $request)
    {
        // Add the check by indicating after the point of the [Option] field
        Gate::authorize('example3.update.'.$frm->value);

        $user = Auth::user();
        switch ($frm)
        {
            case(UserProfileFormEnum::Name):

                if($request->name) $user->fill( $request->only(['name']) );

            break;
            case(UserProfileFormEnum::Password):

                if($request->password) $user->password = Hash::make($request->password);

            break;
            case(UserProfileFormEnum::Email):

                $validator = Validator::make($request->all(), [
                    'email' => 'required|email',
                ]);
                if ($validator->fails()) abort('403', $validator->messages());

                $user->email = $request->email;

            break;
        }

        return Response::json($user->save(), 200);
    }
}

Почему возникает такое поведение и в чем отличие поля "Option" от стандартного определения правил?

Стоит отметить, что поле "Option" не предназначено не для правила, а к самим разрешением.
Это сделано для того, чтобы позволить создавать несколько разрешений в рамках одного правила.
Например, можно получить отдельный список записей по ID, к которым необходимо организовать доступ, не создавая отдельных таблиц или полей.

Пример 4

В этом примере мы воспользуем встроенную функцию $this->authorizeResource(), которая поставляется вместе с функцией ресурсов (resource). Эта функция очень удобна, так как автоматически создает проверки для следующих правил: "viewAny", "view", "create", "update" и "delete".
Добавим в файл routes\web.php:

Route::apiResource('example4', Example4Controller::class)->parameters([
    'example4' => 'news'
]);

Исходник файла ...Example4Controller.php:

<?php
namespace App\Http\Controllers\Examples;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Response;
use App\Http\Controllers\Controller;
use App\Models\News;

class Example4Controller extends Controller
{
    /**
     * Create the controller instance.
     */
    public function __construct()
    {
        $this->authorizeResource(News::class, 'News');
    }

    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        return Response::json(News::all());
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request)
    {
        $news = News::create($request->toArray());

        return Response::json($news->id, 201);
    }

    /**
     * Display the specified resource.
     */
    public function show(News $news)
    {
        return Response::json($news->toArray());
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, News $news)
    {
        $news->fill($request->toArray());

        return Response::json($news->save);
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(News $news)
    {
        return Response::json($news->delete());
    }
}

Пример 5

В предыдущем примере мы использовали глобальные правила, что не очень удобно. Чтобы это исправить, мы можем создать trait который позволит создавать правила для каждого контроллера отдельно.

Исходник файла App\Http\Traits\GuardedController.php:

<?php
namespace App\Http\Traits;

use App\Http\Controllers\Controller;

trait GuardedController
{
    /**
     * Map of resource methods to ability names
     * @example ['index' => 'viewAny']
     *
     * @var string[]
     */
    //abstract protected $guardedMethods = [];

    /**
     * Do not automatically scan all available methods.
     *
     * @var bool
     */
    //abstract protected $disableAutoScanGuard = true;

    /**
     * List of resource methods which do not have model parameters.
     * @example ['index']
     *
     * @var string[]
     */
    //abstract protected $methodsWithoutModels = ['index'];

    /**
     * Get the map of resource methods to ability names.
     *
     * @return array
     */
    protected function resourceAbilityMap()
    {
        if (empty($this->disableAutoScanGuard)) {
            $methods = array_diff(
                get_class_methods($this),
                get_class_methods(Controller::class)
            );
            $map = array_combine($methods, $methods);
        } else {
            $map = [];
        }

        $map = array_merge($map, parent::resourceAbilityMap());
        $map = array_merge($map, $this->guardedMethods??[]);

        // Replace name for class App\Http\Controllers\Examples\Example1Controller
        // to guard prefix "Examples.Example1."
        $name = $this->getClassNameGate();

        // Replace standard rule "viewAny" to "Examples.Example1.viewAny"
        foreach ($map as &$item) {$item = $name.$item;}
        unset($item);

        return $map;
    }

    /**
     * Get the list of resource methods which do not have model parameters.
     *
     * @return array
     */
    protected function resourceMethodsWithoutModels()
    {
        $base = parent::resourceMethodsWithoutModels();

        return array_merge($base, $this->methodsWithoutModels??[]);
    }

    /**
     * Get name off class witch namespace for guard
     *
     * @param string|null $action
     * @return string
     */
    protected static function getClassNameGate(?string $action = null): string
    {
        // Replace name for class App\Http\Controllers\Examples\Example1Controller
        // to guard prefix "Examples.Example1."
        $name = str_replace([
            'App\\Http\\Controllers\\',
            '\\',
            'Controller'
        ], [
            '', '.', '.'
        ], static::class);

        return $name.$action;
    }
}

Контроллер и его пример остаются точно такими же, как в предыдущем примере.
Только добавляется трейт (файл ...Example5Controller.php):

<?php
namespace App\Http\Controllers\Examples;
...
use App\Http\Traits\GuardedController;

class Example5Controller extends Controller
{
    use GuardedController;

    public function __construct()
    {
        $this->authorizeResource(News::class, 'News');
    }
...

Просмотр уже дает другую ошибку:

Таким образом, с минимальными изменениями в существующем коде
можно легко включить возможности динамического управления доступом.

Пример 6

Ниже представлен достаточно простой пример, похожий на второй.
Здесь присутствует использование "магиеской" проверки.
Добавим в файл routes\web.php:

Route::any('/example6/{news}', [Example6Controller::class, 'update']);

Исходник файла ...Example6Controller.php:

<?php
namespace App\Http\Controllers\Examples;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Gate;
use App\Http\Controllers\Controller;
use App\Models\News;

class Example6Controller extends Controller
{
    public function update(Request $request, News $news)
    {
        Gate::authorize('example6.update', $news);

        $news->fill($request->toArray());

        return Response::json($news->save?1:0);
    }
}

Давайте внимательнее рассмотрим это. Если у нас есть правило "example6.update.self",
нам необходимо проверять правило "example6.update", система сама добавит ".self",
если есть объект записи для проверки внутри ACR.

Другии словами работа ACR будет выглядеть так:

if (
    'example6.update' === $ability
    && Gate::allows('example6.update.self')
    && $user->id === $news->user_id
) {
    return true;
}

Кроме того, стоит отметить, что если мы не проверяем пользователя, а другую модель, например, модератор.
ACR отслеживает это, и проверка будет выглядеть, примерно так, внутри системы:

$moderator = App\Models\Moderator::find('...');
if (
    'example6.update' === $ability
    && Gate::forUser($moderator)->allows('example6.update.self')
    && $moderator->uuid === $news->moderator_uuid
) {
    return true;
}

За счет таких возможностей больше не нужно выполнять проверки на владельца, поскольку магический метод будет обрабатывать их автоматически.

Пример 7

Хотя это не ABAC, необходимая функциональность контроля доступа на основе атрибутов, может быть достигнута, путем механизма политик Laravel.

Все предыдущие примеры сосредоточены на проверке доступа к контроллеру, но мы можем использовать тот же подход в "Policy" для реализации контроля доступа через атрибуты со всеми их вариациями.
Для этого необходимо сгенерировать политику для нашей модели:

php artisan make:policy NewsPolicy –model=News

Исходник файла ...NewsPolicy.php:

<?php
namespace App\Policies;

use App\Models\News;
use App\Models\User;
use Illuminate\Auth\Access\Response;

class NewsPolicy
{
    public function availableUpdateOnSomeTime(User $user, News $news): ?bool
    {
        if(
            $user->can('Example7News.allowedEditLast24Hours', $news)
            && stripos($user->name, 'author') !== false
            && ($news->created_at->isToday() || $news->created_at->isYesterday())
        ) {
            return true;
        }
        return null;
    }
}

После этого необходимо обновить значение $policies в классе AuthServiceProvider, как написано ниже:

protected $policies = [
    News::class => NewsPolicy::class,
];

Теперь проверим политику в контроллере:
Исходник файла ...Example7Controller.php:

<?php
namespace App\Http\Controllers\Examples;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Response;
use App\Http\Controllers\Controller;
use App\Models\News;

class Example7Controller extends Controller
{

    public function index(News $news)
    {
        $this->authorize('availableUpdateOnSomeTime', $news);

        return Response::json($news->toArray());
    }
}

Таким образом, теперь мы можем проверять не только правила индивидуально, но также проверять атрибуты модели или пользователя.

Важно отметить, что если вы добавите правило "availableUpdateOnSomeTime" и разрешение для пользователя, то политика не будет проверена.

Финальный пример

По умолчанию интерфейс AccessUi не включает проверку уровня предоставленного доступа.
Чтобы исправить это, мы можем создать прокси-контроллер, который будет проверять все разрешения перед любой манипуляцией с данными.

Сначала отключим стандартные маршруты AccessUi.
Для этого нужно отредактировать файл config/accessUi.php:

    /**
     * Panel Register
     *
     * This manages if routes used for the admin panel should be registered.
     * Turn this value to false if you don't want to use admin panel
     */
    'register' => false,

Затем мы создадим 2 контроллера: "UserRulesController" и "UserProfileController", которые используют трейт "RunsAnotherController" для запуска других контроллеров от AccessUi.
Также добавим представление в файлах "user-rules.blade.php" и "user-profile.blade.php".
Файлы немного длинные для статьи, но их можно просмотреть отдельно в репозитории.

Как результат, у нас будет отдельные страницы в нашем стиле с проверкой прав доступа

Страница профиля авторизованного пользователя:

Список правил и только ролей (скрыт список пользователей и групп):

Заключение, с помощью "wnikk/laravel-access-rules" (ACR, ACL, RBAC) в проекте на Laravel - можно создать, мощный способ обеспечения доступа, пользователей только к тем ресурсам, к которым они авторизованы.

С помощью встроенных в Laravel функций middleware и авторизации, можно легко создавать и управлять сложными политиками контроля доступа как на глобальном, так и на уровне конкретного контроллера или модели.

Используя Access-Control-Rules, разработчик может добавлять возможности динамического контроля доступа в свои приложения Laravel с минимальными изменениями кода, обеспечивая безопасность и легкость обслуживания приложения.

Источник: https://habr.com/ru/articles/729414/


Интересные статьи

Интересные статьи

У нас был сложный сайт с личным кабинетом клиентов, устаревшая, переписанная 1С-ка, десяток маркетинговых сервисов, и телефония на Asterisk.Единственное, что вызывало у меня опасение — это учётная сис...
Всем привет! Хочу рассказать о том, как в мире хаоса и бардака упорядочить жизнь,  сохранить/повысить продуктивность, успеть сделать многое, не разочароваться, оглядываясь назад, систематизироват...
Привет, менеджер!Сегодня я расскажу о самом простом плане работы с проектом от его начала до его завершения. Этот план отлично подойдет для новичков. Если вы еще не освоили Agile, не знае...
Одна из основных проблем разработчиков, когда они создают приложение с ORM — это N+1 запрос в их приложениях. Проблема N+1 запроса — это не эффективный способ обращения к базе данных,...
С 10 июня 2019 (а технически с ноября 2019), Яндекс прекратил поддержку анонимного использования сервисов JS API & HTTP Geocoder — тарифицируемые запросы к API (поиск, геокодирование, панорам...