51b9c20d0b
Две находки обязательного проверяющего доступа (приёмка Этапа 2, журнал В-80). Обе — про молчаливые поломки: ошибок нет, тесты зелёные, защиты нет. 1. Индекс ключа заказа мог тихо не создаться. Миграция делает два шага — колонку и уникальный индекс, — а защита от повторного запуска стояла общим выходом в начале: «колонка есть, значит всё сделано». На бою SQL подаётся руками; прервалась подача между шагами — колонка легла, индекс нет, повторный накат прошёл мимо. Дальше два одновременных запроса с одним ключом создали бы две рассылки и списали деньги дважды. Проверка стала пошаговой. Доказано вырезанием: до правки новый тест краснеет («защита от двойного заказа потеряна молча»), после — зелёный. Живьём на локальной dev-базе: индекс уронен руками, повторный накат его вернул. 2. Сторож прав спрашивал базу только про рабочую роль. А гардов с именами служебных ролей в миграциях семь, и опечатка внутри такого гарда ошибки НЕ даёт — права просто молча не выдаются. Ровно тот блокер выката, ради которого сторож и заводился (В-36). Теперь спрашиваем и crm_admin_user, и crm_supplier_worker — по матрице, сверенной со всеми GRANT'ами миграций, — и счётчики для всех трёх ролей. Доказано вырезанием: опечатка в имени роли внутри гарда → сторож краснеет с именем роли, таблицы и права. Прогоны: СМС 171/171 (было 169, два новых теста), приём лидов 17/17, pint чисто. Структура таблиц не менялась — запись в журнале схемы v9.11.
148 lines
6.9 KiB
PHP
148 lines
6.9 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
/**
|
||
* Сторож прав рабочей роли на таблицы модуля.
|
||
*
|
||
* Зачем он появился (Task 8). В миграциях модуля права выдавались строкой
|
||
* `GRANT … TO crm_app_user` без проверки, есть ли такая роль: на чистой базе это
|
||
* валит `migrate` целиком. Проверку я добавил — но у неё есть своя опасность:
|
||
* опечатка в имени роли внутри проверки НЕ даёт ошибки, права просто молча не
|
||
* выдаются. А это ровно тот блокер выката, что стоил нам дня 27.07 (журнал В-36):
|
||
* на бою первая же запись падает с «нет доступа», а тесты и локальная база этого
|
||
* не видят — там суперпользователь.
|
||
*
|
||
* Поэтому здесь проверяется не текст миграции, а результат: реально ли у роли есть
|
||
* право писать в каждую таблицу модуля и пользоваться её счётчиком.
|
||
*
|
||
* ⚠️ Честное ограничение: тест доказывает это только там, где роли заведены
|
||
* (`db/00_create_roles.sql`). Где ролей нет — он пропускается и не доказывает ничего.
|
||
*/
|
||
uses(RefreshDatabase::class);
|
||
|
||
/** Таблица → права, которые модуль обещает рабочей роли. */
|
||
const CLIENT_SMS_TABLE_GRANTS = [
|
||
'client_sms_campaigns' => ['SELECT', 'INSERT', 'UPDATE'],
|
||
'client_sms_campaign_phones' => ['SELECT', 'INSERT', 'DELETE'],
|
||
'client_sms_messages' => ['SELECT', 'INSERT', 'UPDATE'],
|
||
'client_sms_optouts' => ['SELECT', 'INSERT', 'DELETE'],
|
||
'client_sms_contacts' => ['SELECT', 'INSERT', 'DELETE'],
|
||
'client_sms_templates' => ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
|
||
'client_sms_tariffs' => ['SELECT'],
|
||
'client_sms_settings' => ['SELECT'],
|
||
'client_sms_senders' => ['SELECT', 'INSERT', 'UPDATE'],
|
||
'client_sms_auto_rule' => ['SELECT', 'INSERT', 'UPDATE'],
|
||
'sms_global_optouts' => ['SELECT'],
|
||
];
|
||
|
||
/**
|
||
* Служебные роли: что модуль обещает АДМИН-зоне и служебному работнику.
|
||
*
|
||
* Появилось при приёмке Этапа 2 (журнал В-80, замечание проверяющего З-2): сторож
|
||
* выше спрашивал базу только про рабочую роль, а гардов с именами двух других ролей
|
||
* в миграциях семь. Опечатка внутри такого гарда ошибки НЕ даёт — права молча не
|
||
* выдаются, и это ровно тот блокер выката, ради которого сторож и заводился.
|
||
*
|
||
* Список сверен с миграциями (все `GRANT … TO …` семейства), а не переписан на глаз.
|
||
*/
|
||
const CLIENT_SMS_SERVICE_GRANTS = [
|
||
'crm_admin_user' => [
|
||
'client_sms_tariffs' => ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
|
||
'client_sms_settings' => ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
|
||
'client_sms_senders' => ['SELECT', 'UPDATE'],
|
||
'sms_global_optouts' => ['SELECT', 'INSERT', 'DELETE'],
|
||
],
|
||
'crm_supplier_worker' => [
|
||
'client_sms_senders' => ['SELECT'],
|
||
'client_sms_auto_rule' => ['SELECT'],
|
||
'sms_global_optouts' => ['SELECT'],
|
||
],
|
||
];
|
||
|
||
function clientSmsRoleExists(string $role): bool
|
||
{
|
||
return DB::selectOne('SELECT 1 AS ok FROM pg_roles WHERE rolname = ?', [$role]) !== null;
|
||
}
|
||
|
||
it('у рабочей роли есть обещанные права на таблицы модуля', function () {
|
||
if (! clientSmsRoleExists('crm_app_user')) {
|
||
$this->markTestSkipped('Роли crm_app_user на этой базе нет — проверять нечего.');
|
||
}
|
||
|
||
foreach (CLIENT_SMS_TABLE_GRANTS as $table => $privileges) {
|
||
foreach ($privileges as $privilege) {
|
||
$granted = DB::selectOne(
|
||
'SELECT has_table_privilege(?, ?, ?) AS ok',
|
||
['crm_app_user', $table, $privilege],
|
||
)->ok;
|
||
|
||
expect($granted)->toBeTrue("Роли crm_app_user не выдано право {$privilege} на {$table}");
|
||
}
|
||
}
|
||
});
|
||
|
||
it('у служебных ролей есть обещанные права на таблицы модуля', function () {
|
||
$checked = 0;
|
||
|
||
foreach (CLIENT_SMS_SERVICE_GRANTS as $role => $tables) {
|
||
if (! clientSmsRoleExists($role)) {
|
||
continue;
|
||
}
|
||
|
||
foreach ($tables as $table => $privileges) {
|
||
foreach ($privileges as $privilege) {
|
||
$granted = DB::selectOne(
|
||
'SELECT has_table_privilege(?, ?, ?) AS ok',
|
||
[$role, $table, $privilege],
|
||
)->ok;
|
||
|
||
expect($granted)->toBeTrue("Роли {$role} не выдано право {$privilege} на {$table}");
|
||
$checked++;
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($checked === 0) {
|
||
$this->markTestSkipped('Служебных ролей на этой базе нет — проверять нечего.');
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Счётчики (sequence) — отдельная история: право на таблицу их НЕ покрывает, и
|
||
* именно этого не хватало всем 14 миграциям модуля до фикса В-36.
|
||
*
|
||
* Спрашиваем про все три роли: миграция прав (`100600`) выдаёт счётчики каждой из них.
|
||
*/
|
||
it('у ролей есть право пользоваться счётчиками таблиц модуля', function () {
|
||
$roles = array_filter(
|
||
['crm_app_user', 'crm_admin_user', 'crm_supplier_worker'],
|
||
fn (string $role) => clientSmsRoleExists($role),
|
||
);
|
||
|
||
if ($roles === []) {
|
||
$this->markTestSkipped('Ролей модуля на этой базе нет — проверять нечего.');
|
||
}
|
||
|
||
$sequences = DB::select(<<<'SQL'
|
||
SELECT sequencename FROM pg_sequences
|
||
WHERE schemaname = 'public' AND sequencename LIKE 'client\_sms\_%'
|
||
SQL);
|
||
|
||
expect($sequences)->not->toBeEmpty();
|
||
|
||
foreach ($roles as $role) {
|
||
foreach ($sequences as $row) {
|
||
$granted = DB::selectOne(
|
||
'SELECT has_sequence_privilege(?, ?, ?) AS ok',
|
||
[$role, $row->sequencename, 'USAGE'],
|
||
)->ok;
|
||
|
||
expect($granted)->toBeTrue("Роли {$role} не выдано право на счётчик {$row->sequencename}");
|
||
}
|
||
}
|
||
});
|