Блог на Laravel 7, часть 4. Роли и Права пользователей, трейт HasRolesAndPermissions

09.12.2020

Теги: LaravelMySQLPHPАвторизацияБазаДанныхБлогПользовательПраваДоступаПрактикаФреймворк

Роли и Права пользователей

Продолжаем разбираться с Правами и Ролями пользователей. Нам нужны методы, которые позволяют выяснить, что может делать текущий пользователь, а что не может. Кроме того, нам нужно иметь возможность назначать пользователю Права и Роли, а при необходимости — отнимать ранее назначенные Права и Роли.

5. Права и Роли пользователя

Добавим в трейт методы, которые позволяют выяснить роли и права текущего пользователя:

namespace App\Traits;

use App\Role;
use App\Permission;

trait HasRolesAndPermissions {

    /**
     * Связь модели User с моделью Role, позволяет получить все роли пользователя
     */
    public function roles() {
        return $this
            ->belongsToMany(Role::class, 'user_role')
            ->withTimestamps();
    }

    /**
     * Связь модели User с моделью Permission, позволяет получить все права пользователя
     */
    public function permissions() {
        return $this
            ->belongsToMany(Permission::class, 'user_permission')
            ->withTimestamps();
    }

    /**
     * Имеет текущий пользователь роль $role?
     */
    public function hasRole($role) {
        return $this->roles->contains('slug', $role);
    }

    /**
     * Имеет текущий пользователь право $permission?
     */
    public function hasPerm($permission) {
        return $this->permissions->contains('slug', $permission);
    }

    /**
     * Имеет текущий пользователь право $permission через одну
     * из своих ролей?
     */
    public function hasPermViaRoles($permission) {
        // смотрим все роли пользователя и ищем в них нужное право
        foreach ($this->roles as $role) {
            if ($role->permissions->contains('slug', $permission)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Имеет текущий пользователь право $permission либо напрямую,
     * либо через одну из своих ролей?
     */
    public function hasPermAnyWay($permission) {
        return $this->hasPermViaRoles($permission) || $this->hasPerm($permission);
    }

    /**
     * Имеет текущий пользователь все права из $permissions либо
     * напрямую, либо через одну из своих ролей?
     */
    public function hasAllPerms(...$permissions) {
        foreach ($permissions as $permission) {
            $condition = $this->hasPermViaRoles($permission) || $this->hasPerm($permission);
            if ( ! $condition) {
                return false;
            }
        }
        return true;
    }

    /**
     * Имеет текущий пользователь любое право из $permissions либо
     * напрямую, либо через одну из своих ролей?
     */
    public function hasAnyPerms(...$permissions) {
        foreach ($permissions as $permission) {
            if ($this->hasPermViaRoles($permission) || $this->hasPerm($permission)) {
                return true;
            }
        }
        return false;
    }
}

Еще несколько полезных методов, которые могут потребоваться:

trait HasRolesAndPermissions {

    /* ... */

    /**
     * Возвращает массив всех прав текущего пользователя
     */
    public function getAllPerms() {
        return $this->permissions->pluck('slug')->toArray();
    }

    /**
     * Возвращает массив всех прав текущего пользователя,
     * которые у него есть через его роли
     */
    public function getAllPermsViaRoles() {
        // перебираем все роли пользователя, для каждой роли
        // получаем права, все права записываем в массив
        $permissions = [];
        foreach ($this->roles as $role) {
            $perms = $role->permissions;
            foreach ($perms as $perm) {
                $permissions[] = $perm->slug;
            }
        }
        return array_values(array_unique($permissions));
    }

    /**
     * Возвращает массив всех прав текущего пользователя,
     * либо напрямую, либо через одну из своих ролей
     */
    public function getAllPermsAnyWay() {
        $perms = array_merge(
            $this->getAllPerms(),
            $this->getAllPermsViaRoles()
        );
        return array_values(array_unique($perms));
    }

    /**
     * Возвращает массив всех ролей текущего пользователя
     */
    public function getAllRoles() {
        return $this->roles->pluck('slug')->toArray();
    }
}

Мы добавили связи модели User с моделями Permission и Role. Давайте сразу добавми аналогичные связи для моделей Pernission и Role.

namespace App;

use Illuminate\Database\Eloquent\Model;

class Permission extends Model {
    /**
     * Связь модели Permission с моделью Role, позволяет получить
     * все роли, куда входит это право
     */
    public function roles() {
        return $this
            ->belongsToMany(Role::class,'role_permission')
            ->withTimestamps();
    }
    /**
     * Связь модели Permission с моделью User, позволяет получить
     * всех пользователей с этим правом
     */
    public function users() {
        return $this
            ->belongsToMany(User::class,'user_permission')
            ->withTimestamps();
    }
}
namespace App;

use Illuminate\Database\Eloquent\Model;

class Role extends Model {
    /**
     * Связь модели Role с моделью Permission, позволяет получить
     * все права для этой роли
     */
    public function permissions() {
        return $this
            ->belongsToMany(Permission::class,'role_permission')
            ->withTimestamps();
    }
    /**
     * Связь модели Role с моделью Usrer, позволяет получить
     * всех пользователей с этой ролью
     */
    public function users() {
        return $this
            ->belongsToMany(User::class,'user_role')
            ->withTimestamps();
    }
}

Теперь добавим трейт HasRolesAndPermissions в модель User:

namespace App;

use App\Traits\HasRolesAndPermissions;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable {

    use Notifiable, HasRolesAndPermissions;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password', 'email_verified_at'
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

6. Выдача Прав и назначение Ролей

Добавим еще несколько методов в трейт HasRolesAndPermissions:

  • Метод assignPermissions() выдает пользователю новые права (к тем, что были выданы ранее)
  • Метод unassignPermissions() удаляет права пользователя (из тех, что уже были выданы ранее)
  • Метод refreshPermissions() удаляет все текущие права пользователя и назначает новые права
  • Метод assignRoles() назначает пользователю новые роли (к тем, что были назначены ранее)
  • Метод unassignRoles() удаляет роли пользователя (из тех, что уже были назначены ранее)
  • Метод refreshRoles() удаляет все текущие роли пользователя и назначает новые роли
namespace App\Traits;

use App\Role;
use App\Permission;

trait HasRolesAndPermissions {

    /* ... */

    /**
     * Добавить текущему пользователю права $permissions
     * (в дополнение к тем, что уже были назначены ранее)
     */
    public function assignPermissions(...$permissions) {
        $permissions = Permission::whereIn('slug', $permissions)->get();
        if ($permissions->count() === 0) {
            return $this;
        }
        $this->permissions()->syncWithoutDetaching($permissions);
        return $this;
    }

    /**
     * Отнять у текущего пользователя права $permissions
     * (из числа тех, что были назначены ранее)
     */
    public function unassignPermissions(...$permissions) {
        $permissions = Permission::whereIn('slug', $permissions)->get();
        if ($permissions->count() === 0) {
            return $this;
        }
        $this->permissions()->detach($permissions);
        return $this;
    }

    /**
     * Назначить текущему пользователю права $permissions
     * (отнять при этом все ранее назначенные права)
     */
    public function refreshPermissions(...$permissions) {
        $permissions = Permission::whereIn('slug', $permissions)->get();
        if ($permissions->count() === 0) {
            return $this;
        }
        $this->permissions()->sync($permissions);
        return $this;
    }

    /**
     * Добавить текущему пользователю роли $roles
     * (в дополнение к тем, что уже были назначены)
     */
    public function assignRoles(...$roles) {
        $roles = Role::whereIn('slug', $roles)->get();
        if ($roles->count() === 0) {
            return $this;
        }
        $this->roles()->syncWithoutDetaching($roles);
        return $this;
    }

    /**
     * Отнять у текущего пользователя роли $roles
     * (из числа тех, что были назначены ранее)
     */
    public function unassignRoles(...$roles) {
        $roles = Role::whereIn('slug', $roles)->get();
        if ($roles->count() === 0) {
            return $this;
        }
        $this->roles()->detach($roles);
        return $this;
    }

    /**
     * Назначить текущему пользователю роли $roles
     * (отнять при этом все ранее назначенные роли)
     */
    public function refreshRoles(...$roles) {
        $roles = Role::whereIn('slug', $roles)->get();
        if ($roles->count() === 0) {
            return $this;
        }
        $this->roles()->sync($roles);
        return $this;
    }
}

7. Добавляем сидеры

Класс для заполнения таблицы roles:

> php artisan make:seeder RoleTableSeeder
use Illuminate\Database\Seeder;

class RoleTableSeeder extends Seeder {
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run() {
        $roles = [
            ['slug' => 'root', 'name' => 'Супер-админ'],
            ['slug' => 'admin', 'name' => 'Администратор'],
            ['slug' => 'user', 'name' => 'Пользователь'],
        ];
        foreach ($roles as $item) {
            $role = new App\Role();
            $role->name = $item['name'];
            $role->slug = $item['slug'];
            $role->save();
        }
    }
}

Класс для заполнения таблицы permissions:

> php artisan make:seeder PermissionTableSeeder
use Illuminate\Database\Seeder;

class PermissionTableSeeder extends Seeder {
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run() {
        $permissions = [
            ['slug' => 'manage-users', 'name' => 'Управление пользователями'],
            ['slug' => 'create-user', 'name' => 'Создание пользователя'],
            ['slug' => 'edit-user', 'name' => 'Редактирование пользователя'],
            ['slug' => 'delete-user', 'name' => 'Удаление пользователя'],

            ['slug' => 'manage-roles', 'name' => 'Управление ролями пользователей'],
            ['slug' => 'create-role', 'name' => 'Создание роли пользователя'],
            ['slug' => 'edit-role', 'name' => 'Редактирование роли пользователя'],
            ['slug' => 'delete-role', 'name' => 'Удаление роли пользователя'],

            ['slug' => 'assign-role', 'name' => 'Назначение роли для пользователя'],
            ['slug' => 'assign-permission', 'name' => 'Назначение права для пользователя'],

            ['slug' => 'manage-posts', 'name' => 'Управление постами блога'],
            ['slug' => 'create-post', 'name' => 'Создание поста блога'],
            ['slug' => 'edit-post', 'name' => 'Редактирование поста блога'],
            ['slug' => 'publish-post', 'name' => 'Публикация поста блога'],
            ['slug' => 'delete-post', 'name' => 'Удаление поста блога'],

            ['slug' => 'manage-categories', 'name' => 'Управление категориями блога'],
            ['slug' => 'create-category', 'name' => 'Создание категории блога'],
            ['slug' => 'edit-category', 'name' => 'Редактирование категории блога'],
            ['slug' => 'delete-category', 'name' => 'Удаление категории блога'],

            ['slug' => 'manage-comments', 'name' => 'Управление комментариями блога'],
            ['slug' => 'create-comment', 'name' => 'Создание комментария к посту'],
            ['slug' => 'edit-comment', 'name' => 'Редактирование комментария к посту'],
            ['slug' => 'publish-comment', 'name' => 'Публикация комментария к посту'],
            ['slug' => 'delete-comment', 'name' => 'Удаление комментария к посту'],
        ];
        foreach ($permissions as $item) {
            $permission = new App\Permission();
            $permission->name = $item['name'];
            $permission->slug = $item['slug'];
            $permission->save();
        }
    }
}

Класс для заполнения таблицы user_permission:

> php artisan make:seeder UserPermissionTableSeeder
use Illuminate\Database\Seeder;

class UserPermissionTableSeeder extends Seeder {
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run() {
        // создать связи между пользователями и правами
        foreach(App\User::all() as $user) {
            foreach(App\Permission::all() as $perm) {
                if (rand(1, 20) == 10) {
                    // $user->permissions()->attach($perm->id);
                }
            }
        }
    }
}

Класс для заполнения таблицы user_role:

> php artisan make:seeder UserRoleTableSeeder
use Illuminate\Database\Seeder;

class UserRoleTableSeeder extends Seeder {
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run() {
        // создать связи между пользователями и ролями
        foreach(App\User::all() as $user) {
            foreach(App\Role::all() as $role) {
                if ($user->id == 1 && $role->slug == 'root') { // один супер-админ
                    $user->roles()->attach($role->id);
                }
                if (in_array($user->id, [2,3]) && $role->slug == 'admin') { // два админа
                    $user->roles()->attach($role->id);
                }
                if ($user->id > 3 && $role->slug == 'user') { // обычные пользователи
                    $user->roles()->attach($role->id);
                }
            }
        }
    }
}

Класс для заполнения таблицы role_permission:

> php artisan make:seeder RolePermissionTableSeeder
use Illuminate\Database\Seeder;

class RolePermissionTableSeeder extends Seeder {
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run() {
        // создать связи между ролями и правами
        foreach(App\Role::all() as $role) {
            if ($role->slug == 'root') { // для роли супер-админа все права
                foreach (App\Permission::all() as $perm) {
                    $role->permissions()->attach($perm->id);
                }
            }
            if ($role->slug == 'admin') { // для роли администратора поменьше
                $slugs = [
                    'create-post', 'edit-post', 'publish-post', 'delete-post',
                    'create-comment', 'edit-comment', 'publish-comment', 'delete-comment'
                ];
                foreach ($slugs as $slug) {
                    $perm = App\Permission::where('slug', $slug)->first();
                    $role->permissions()->attach($perm->id);
                }
            }
            if ($role->slug == 'user') { // для обычного пользователя совсем чуть-чуть
                $slugs = ['create-post', 'create-comment'];
                foreach ($slugs as $slug) {
                    $perm = App\Permission::where('slug', $slug)->first();
                    $role->permissions()->attach($perm->id);
                }
            }
        }
    }
}

8. Наполнение базы данных

Редактируем класс DatabaseSeeder:

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder {
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run() {
        $this->call(UserTableSeeder::class);
        $this->command->info('Таблица пользователей загружена данными!');

        $this->call(CategoryTableSeeder::class);
        $this->command->info('Таблица категорий загружена данными!');

        $this->call(TagTableSeeder::class);
        $this->command->info('Таблица тегов загружена данными!');

        $this->call(PostTableSeeder::class);
        $this->command->info('Таблица постов загружена данными!');

        $this->call(PostTagTableSeeder::class);
        $this->command->info('Таблица пост-тег загружена данными!');

        $this->call(CommentTableSeeder::class);
        $this->command->info('Таблица комментариев загружена данными!');

        $this->call(RoleTableSeeder::class);
        $this->command->info('Таблица ролей загружена данными!');

        $this->call(PermissionTableSeeder::class);
        $this->command->info('Таблица прав загружена данными!');

        $this->call(RolePermissionTableSeeder::class);
        $this->command->info('Таблица роль-право загружена данными!');

        $this->call(UserPermissionTableSeeder::class);
        $this->command->info('Таблица пользователь-право загружена данными!');

        $this->call(UserRoleTableSeeder::class);
        $this->command->info('Таблица пользователь-роль загружена данными!');
    }
}

Все готово, запускаем миграцию:

> php artisan migrate:fresh --seed

9. Новые blade-директивы

Для начала создадим два сервис-провайдера — RoleServiceProvider и PermissionServiceProvider

> php artisan make:provider RoleServiceProvider
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Blade;

class RoleServiceProvider extends ServiceProvider {
    /**
     * Register services.
     *
     * @return void
     */
    public function register() {
        // ...
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot() {
        Blade::directive('role', function ($role) {
            return "<?php if(auth()->check() && auth()->user()->hasRole({$role})): ?>";
        });
        Blade::directive('endrole', function ($role) {
            return "<?php endif; ?>";
        });
    }
}
> php artisan make:provider PermissionServiceProvider
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Blade;

class PermissionServiceProvider extends ServiceProvider {
    /**
     * Register services.
     *
     * @return void
     */
    public function register() {
        // ...
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot() {
        Blade::directive('perm', function ($perm) {
            return "<?php if(auth()->check() && auth()->user()->hasPermAnyWay({$perm})): ?>";
        });
        Blade::directive('endperm', function ($perm) {
            return "<?php endif; ?>";
        });
        Blade::directive('allperms', function ($perms) {
            return "<?php if(auth()->check() && auth()->user()->hasAllPerms({$perms})): ?>";
        });
        Blade::directive('endallperms', function ($perms) {
            return "<?php endif; ?>";
        });
        Blade::directive('anyperms', function ($perms) {
            return "<?php if(auth()->check() && auth()->user()->hasAnyPerms({$perms})): ?>";
        });
        Blade::directive('endanyperms', function ($perms) {
            return "<?php endif; ?>";
        });
    }
}

Добавим сервис-провайдеров в файл конфигурации config/app.php:

return [
    /* ... */
    'providers' => [
        /* ... */
        App\Providers\RoleServiceProvider::class,
        App\Providers\PermissionServiceProvider::class,
        /* ... */
    ],
    /* ... */
];

Теперь в шаблонах нам доступны новые blade-директивы:

@role('root')
    <p>Есть роль супер-администратора</p>
@else
    <p>Нет роли супер-администратора</p>
@endrole
@perm('edit-post')
    <p>Есть право редактировать посты</p>
@else
    <p>Нет права редактировать посты</p>
@endperm
@allperms('edit-post', 'delete-post')
    <p>Есть право редактировать и удалять посты</p>
@endallperms
@anyperms('edit-post', 'delete-post')
    <p>Есть по крайней мере одно право</p>
@endanyperms

10. Новые посредники

Для удобства создадим двух посредников — CheckUserRole и CheckUserPermission:

> php artisan make:middleware CheckUserRole
namespace App\Http\Middleware;

use Closure;

class CheckUserRole {
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next, $role) {
        if ( ! auth()->user()->hasRole($role)) {
            abort(404);
        }
        return $next($request);
    }
}
> php artisan make:middleware CheckUserPermission
namespace App\Http\Middleware;

use Closure;

class CheckUserPermission {
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next, $perm) {
        if ( ! auth()->user()->hasPermAnyWay($perm)) {
            abort(404);
        }
        return $next($request);
    }
}

Перед использованием посредники нужно добавить в app/Http/Kernel.php:

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel {
    /* ... */
    protected $routeMiddleware = [
        /* ... */
        'role' => \App\Http\Middleware\CheckUserRole::class,
        'perm' => \App\Http\Middleware\CheckUserPermission::class,
    ];
}

Теперь мы можем защитить маршруты, проверяя роль и/или право пользователя:

Route::group(['middleware' => 'role:admin'], function() {
   Route::get('/admin/index', function() {
      return 'Это панель управления сайта';
   });
});

Страница 404 Not Found

Разместим картинку 404.jpg в директории public/img, где она будет доступна из веб. Создадим директорию resources/views/errors и внутри нее — шаблон 404.blade.php.

@extends('layout.site', ['title' => 'Страница не найдена'])

@section('content')
    <h1>Страница не найдена</h1>
    <img src="{{ asset('img/404.jpg') }}" alt="" class="img-fluid">
    <p class="mt-3 mb-0">Запрошенная страница не найдена.</p>
@endsection

Главная страница сайта

Добавляем маршрут в файле routes/web.php, создаем контроллер IndexController и файл шаблона resouces/views/index.blade.php.

// маршрут для главной страницы без указания метода
Route::get('/', 'IndexController')->name('index');
> php artisan make:controller IndexController --invokable
namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;

class IndexController extends Controller {
    /**
     * Handle the incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function __invoke(Request $request) {
        return view('index');
    }
}
@extends('layout.site')

@section('content')
    <h1>Блог о веб-разработке</h1>
    <p>
        Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusantium deleniti incidunt
        minima, minus molestias necessitatibus neque placeat quae quaerat repellendus
        reprehenderit voluptatem. Aperiam excepturi nesciunt officia officiis omnis provident
        quidem repellat saepe, sed soluta? Animi atque cupiditate dicta doloribus id libero
        quibusdam. Adipisci alias consequatur consequuntur delectus, eligendi et facere fugiat id
        illum minus necessitatibus nemo nihil numquam perspiciatis quae quis quisquam sapiente
        sequi vitae voluptatum. At facilis soluta unde.
    </p>
@endsection

Поиск: Laravel • MySQL • PHP • Авторизация • База данных • Блог • Пользователь • Права доступа • Практика • Фреймворк

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.