[Перевод] Простой контроль доступа ACR в Laravel 10 (инструкция шаг за шагом)
В этой статье мы рассмотрим, как реализовать 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, мы начнём с:
User Management — Мы создадим управление пользователями с помощью Laravel 10. Это позволит легче применять права доступа в Laravel.
Rules Management — Кроме того, мы реализуем управление правилами, чтобы ограничить доступ к контенту, определив список правил для проекта.
Permits and inheritance Management — Управление разрешениями может использоваться для добавления ролей в учетные записи пользователей и назначения прав доступа в Laravel.
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-приложения. Ниже вы найдете полный код, необходимый для определения структуры таблицы, включая различные поля и их соответствующие типы:
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
Пример кода для модели новостей:
Шаг 7: Создание Seeder
Теперь, когда у нас есть все необходимые таблицы в базе данных, настало время заполнить их тестовыми данными и настроить правила для них.
Для этого мы создадим Seeder — класс, которые позволят нам заполнить таблицы начальными данными.
1. Создадим несколько пользователей:
php artisan make:seeder CreateUserSeeder
Исходник файла database\seeders\CreateUserSeeder.php:
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:
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:
'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:
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:
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:
Что происходит в результате:
Пример 2
Давайте попробуем проверить разрешение в самом контроллере.
Добавим в файл routes\web.php:
Route::get('/example2', [Example2Controller::class, 'show']);
Исходник файла …Example2Controller.php:
toArray(), 200);
}
}
Пример, как можно использовать фасад Laravel Gate.
Пример 3
Этот пример очень похож на предыдущий, но с использованием параметра опции.
Добавим в файл routes\web.php:
Route::any('/example3/{frm}', [Example3Controller::class, 'update']);
Исходник файла …Example3Controller.php:
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:
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:
'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):
authorizeResource(News::class, 'News');
}
...
Просмотр уже дает другую ошибку:
Таким образом, с минимальными изменениями в существующем коде
можно легко включить возможности динамического управления доступом.
Пример 6
Ниже представлен достаточно простой пример, похожий на второй.
Здесь присутствует использование »магиеской» проверки.
Добавим в файл routes\web.php:
Route::any('/example6/{news}', [Example6Controller::class, 'update']);
Исходник файла …Example6Controller.php:
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:
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:
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 с минимальными изменениями кода, обеспечивая безопасность и легкость обслуживания приложения.