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

Senior: питання на співбесіді з теми «Авторизація»

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

3 питання

Починається все з простого $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» складно написати надійно, тому покладаються на тести ендпойнтів.

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