Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді: Авторизація

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

9 питань

Права описують у Gate або Policy, а перевіряють у трьох місцях: контролері, шаблоні й формі запиту.

У контролері - authorize(), який кидає 403 сам:

public function update(Request $request, Post $post)
{
    $this->authorize('update', $post);

    // сюди дійде лише той, кому можна
}

Або через фасад, коли потрібна саме перевірка, а не зупинка:

if (Gate::allows('update', $post)) {
    // ...
}

if ($request->user()->cannot('update', $post)) {
    abort(403);
}

У Blade - директиви, які ховають те, чого не можна:

@can('update', $post)
    <a href="{{ route('posts.edit', $post) }}">Редагувати</a>
@endcan

@cannot('update', $post)
    <span>Тільки перегляд</span>
@endcannot

У маршруті - middleware can:

Route::put('/posts/{post}', [PostController::class, 'update'])
    ->middleware('can:update,post');

Важливо: @can у шаблоні лише ховає кнопку. Це зручність для користувача, а не захист - без перевірки в контролері запит усе одно можна надіслати вручну. Перевірка на сервері обовʼязкова завжди, навіть коли кнопки не видно.

Докладніше в документації: Авторизація

За замовчуванням гейти й політики для неавтентифікованого відвідувача повертають false - навіть не викликаючи метод. Це безпечний дефолт: забута перевірка не відкриє дію гостям.

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

class PostPolicy
{
    public function view(?User $user, Post $post): bool
    {
        if ($post->is_published) {
            return true;              // опубліковане бачать усі
        }

        return $user?->id === $post->user_id;   // чернетку - лише автор
    }
}

Де це потрібно: публічний вміст із винятками - опубліковані статті для всіх, чернетки для автора; файли, частина яких доступна без входу.

Пастка: код усередині має пам'ятати про null - звернення $user->id без ?-> впаде на гостеві.

Для дій, які гостям не дозволені ніколи (редагування, видалення), параметр лишають обов'язковим - тоді Laravel відмовить ще до виклику методу.

Докладніше в документації: Гості

  • Gate - замикання для простих, не прив'язаних до моделі перевірок.
  • Policy - клас, що групує правила авторизації навколо конкретної моделі.
// Gate
Gate::define('view-admin', fn (User $u) => $u->is_admin);

// Policy
class PostPolicy
{
    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }
}

Застосування:

$this->authorize('update', $post); // у контролері
@can('update', $post) ... @endcan // у Blade
$user->can('update', $post); // будь-де

Політики автоматично відкривають 403 при відмові.

Докладніше в документації: Авторизація (Gates та Policies)

bool каже лише «можна» чи «ні». Response дає ще й пояснення та керує тим, що побачить користувач.

Повідомлення про причину:

public function update(User $user, Post $post): Response
{
    if ($post->is_locked) {
        return Response::deny('Статтю заблоковано модератором.');
    }

    return $user->id === $post->user_id
        ? Response::allow()
        : Response::deny('Ви не автор цієї статті.');
}

Коли Gate::authorize('update', $post) кидає AuthorizationException, це повідомлення потрапляє у відповідь 403. Без винятку його можна прочитати через Gate::inspect() - наприклад, щоб показати підказку біля неактивної кнопки.

404 замість 403:

return $user->id === $invoice->user_id
    ? Response::allow()
    : Response::denyAsNotFound();

403 підтверджує, що рахунок з таким номером існує. Для приватних ресурсів 404 нічого не розкриває - стороння людина не відрізнить чужий рахунок від неіснуючого. Є й denyWithStatus() для довільного коду.

Коли досить bool: коли причина очевидна й однакова - «не власник». Response окупається, де причин кілька або сам факт існування ресурсу приватний.

Докладніше в документації: Відповіді політик

Усі три способи ведуть до тієї самої політики - різниця в тому, коли перевірка спрацьовує і які дані їй доступні.

Middleware can - найраніше, ще до контролера:

Route::put('/posts/{post}', [PostController::class, 'update'])
    ->middleware('can:update,post');

Модель береться з прив'язки маршруту. Добре, коли рішення залежить лише від користувача й моделі з URL.

Form Request authorize() - перед валідацією:

public function authorize(): bool
{
    return $this->user()->can('update', $this->route('post'));
}

Зручно, коли запит і так має Form Request: права й правила даних в одному класі.

У контролері Gate::authorize() - коли рішення залежить від чогось, що з'являється лише в коді: від даних запиту чи від іншої моделі.

Gate::authorize('transfer', [$account, $request->integer('amount')]);

У Laravel 11+ базовий контролер порожній, тож $this->authorize() доступний лише з трейтом AuthorizesRequests.

Що важливо незалежно від способу: перевірка має бути на сервері для кожної дії. Схована кнопка в Blade чи Livewire не захищає - запит можна відправити напряму. І в Livewire публічний метод компонента - теж ендпойнт, який перевіряє права сам.

Докладніше в документації: Авторизація дій через політики

Через перевірку, що виконується перед усіма іншими.

Для всього застосунку - Gate::before():

// AppServiceProvider::boot()
Gate::before(function (User $user, string $ability) {
    return $user->isSuperAdmin() ? true : null;
});

Для однієї політики - метод before():

public function before(User $user, string $ability): ?bool
{
    return $user->isSuperAdmin() ? true : null;
}

Головне - повертати null, а не false, для всіх, хто не суперадмін. null означає «не вирішую, перевіряй далі», а false заборонив би дію всім іншим, не дійшовши до політики.

Gate::after() - навпаки, спрацьовує після перевірок і впливає на результат, лише якщо гейт чи політика повернули null. Зручно для правила за замовчуванням.

Обережно з «усім можна»:

  • суперадмін через before обійде навіть правила, які мали б діяти для всіх, - наприклад, «не можна видалити оплачене замовлення»; такі бізнес-обмеження краще перевіряти окремо від прав;
  • дії суперадміна варто журналювати;
  • для гнучкої моделі ролей і прав зазвичай беруть пакет на кшталт spatie/laravel-permission, а before лишають для однієї надролі.

Докладніше в документації: Фільтри політик

Починається все з простого $user->is_admin, далі зʼявляється редактор, потім модератор - і умови розповзаються по коду.

Крок перший - роль як enum:

enum Role: string
{
    case Admin = 'admin';
    case Editor = 'editor';
    case Author = 'author';
}

Це вже краще за рядки, але перевірки виду $user->role === Role::Editor розкидані по контролерах ламаються, щойно права ролі змінюються.

Крок другий - права, а не ролі. Код питає «чи можна публікувати», а не «чи ти редактор»:

Gate::define('publish', fn (User $user) => $user->hasPermission('publish'));

Роль стає лише набором прав, і зміна набору не потребує правок у коді.

Policy для дій над моделлю:

class PostPolicy
{
    public function update(User $user, Post $post): bool
    {
        return $user->hasPermission('posts.update')
            || $post->author_id === $user->id;
    }
}

before() для суперкористувача - щоб не дублювати перевірку в кожному методі:

public function before(User $user): ?bool
{
    return $user->isAdmin() ? true : null;
}

Повертати треба саме null, а не false: false заборонить дію остаточно й не дасть іншим методам відпрацювати.

Коли брати пакет. spatie/laravel-permission дає ролі, права й кеш перевірок з коробки. Він доречний, коли набір прав змінюють з адмінки; якщо ролей три й вони зашиті в код, enum із Policy простіший.

Що не забути: перевірки прав кешуються не самі - на кожен запит це кілька звернень до бази, тому права користувача варто завантажувати разом із ним.

Докладніше в документації: Авторизація

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

Три місця, де це трапляється:

1. Списки. Політика view не фільтрує Invoice::paginate(). Якщо запит не обмежено, сторінка покаже всі рахунки:

$invoices = $request->user()->invoices()->latest()->paginate();

Запит має починатися від користувача (чи від команди / тенанта), а не від моделі.

2. Вкладені маршрути. /users/{user}/posts/{post} без обмеження знайде пост 99, навіть якщо він належить іншому користувачу:

Route::get('/users/{user}/posts/{post}', ...)->scopeBindings();

scopeBindings() шукає пост через $user->posts(), а не по всій таблиці.

3. Пошук, експорт, API, черги. Будь-який код, що збирає дані поза звичайним контролером, - звіт, CSV, ендпойнт пошуку, - теж має обмежувати запит.

Як зробити це системно: глобальний скоп на модель для мультитенантності (з обережністю до withoutGlobalScopes()), репозиторій чи запит, що завжди починається від власника, і тест «користувач A не бачить даних користувача B» для кожного ендпойнта зі списком. UUID замість числових ID від цього не рятують - вони лише ускладнюють перебір.

Докладніше в документації: Обмеження прив'язки моделей

Права ламаються тихо: забута перевірка не дає помилки, вона просто дозволяє зайве. Тому їх тестують на двох рівнях.

1. Правило - напряму, через can():

it('lets only the author edit a draft', function () {
    $author = User::factory()->create();
    $post = Post::factory()->draft()->for($author)->create();

    expect($author->can('update', $post))->toBeTrue()
        ->and(User::factory()->create()->can('update', $post))->toBeFalse();
});

can() проходить увесь шлях: before, саму політику, after. Тест швидкий і точно вказує, яке правило зламалося.

2. Ендпойнт - що перевірку справді викликають:

it('forbids editing someone else\'s post', function () {
    $post = Post::factory()->create();

    $this->actingAs(User::factory()->create())
        ->put("/posts/{$post->id}", ['title' => 'Чуже'])
        ->assertForbidden();

    expect($post->fresh()->title)->not->toBe('Чуже');
});

Ідеальна політика нічого не варта, якщо контролер її не викликав - цей тест ловить саме таке.

Що варто покривати обов'язково:

  • «користувач A не бачить і не змінює даних користувача B» для кожного ресурсу;
  • гість, звичайний користувач, власник, адмін - матриця ролей для ключових дій;
  • списки й експорт, а не лише окремі сторінки;
  • denyAsNotFound() - що повертається саме 404.

Архітектурний тест на кшталт «кожен метод контролера, що змінює дані, викликає Gate::authorize» складно написати надійно, тому покладаються на тести ендпойнтів.

Докладніше в документації: Авторизація через модель користувача