Netpulse_SasS/server/migrations/0053_roles_editor.sql
byrsapty ed8fc831bf Дві сесії роботи: 0058–0068, розгортання однією командою, тести
Один коміт, а не десяток тематичних, свідомо: теми переплетені в
спільних файлах (store.go, docker-compose.yml, deploy/README.md), і
розділити їх можна було б лише індексуванням шматків. Коміти, які не
збираються, гірші за один великий — тим паче що це рівно той стан, який
перевірявся разом.

ЩО ПРАЦЮЄ НА СТЕНДІ Й ПЕРЕВІРЕНО ТАМ

  0058  подієві алерти: syslog, ncm, compliance спрацьовують у мить
        події; правило з нереалізованим джерелом більше не зберігається
        мовчки
  0059  snmp.walk і прототипи шаблонів — таблиці з динамічним індексом
        описуються шаблоном, а не Go
  0060  відкат конфігу: план як різниця, маскування паролів із підписом
        плану, обов'язковий контрольний збір, verifying при обриві
  0061  кнопки Telegram: довге опитування, авторизація не з callback_data
  0062  аудит і архів хостів; тест на AST, що падає на ключі без назви
  0063  RLS: три ролі, окремий пул для фонових тактів
  0064  строки зберігання даних і сторінка сховища
  0065  приймач SNMP-трапів; перевірено справжніми пакетами по дроту,
        переклад v1→v2 за RFC 3584 дає правильний OID
  0066  ескалації сповіщень
  0067  алерт про вичерпання диска
  0068  поля заливки конфігу переїхали в каталог профілів

Плюс: 137 тестів вебу з нуля (їх не було взагалі), одинадцять справжніх
вад, знайдених ними й виправлених, і виправлення двох інтеграційних
тестів grpcapi, які мовчки пропускались півтора року.

ЩО ЩЕ НЕ ЗАПУСКАЛОСЬ

  netpulse            установник: одна команда замість 18 змінних і
                      593 рядків інструкції
  RLS з першого запуску  нова інсталяція під політиками одразу;
                      RLS-EXISTING-INSTALL.md лишається тільки для
                      старих інсталяцій
  .forgejo + CI       раннер не зареєстрований

Ці три перевірені компіляцією й міркуванням, але не виконанням.

ГОЛОВНИЙ ВИСНОВОК ДВОХ СЕСІЙ

Зелена перевірка доводить рівно те, що вона перевіряє. Тест ізоляції RLS
був правильний і зелений — і пропустив зламаний вхід, бо перевіряв «чи
не видно чужого», коли зламалось «чи видно своє». Інтеграційні тести
grpcapi були зелені, бо не виконувались. Схема, довідник і протокол
описували те, чого в коді не існувало, і виглядало це як готове.

Тому в кожному завданні цих сесій стояла вимога назвати НЕПОКРИТЕ, а
чотири задачі закінчились не можливістю, а відмовою: правило з
нереалізованим джерелом не зберігається, профіль без команд заливки
каже про це замість мовчазної кнопки, міграція RLS валить сама себе на
таблиці без політики, тест словника аудиту падає на ключі без назви.

Подробиці — HISTORY.md, розділи за 26 і 27 серпня.
2026-08-27 17:32:49 +03:00

159 lines
12 KiB
PL/PgSQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- =====================================================================
-- NetPulse :: 0053_roles_editor.sql
-- Ролі, які редагують люди: людські назви прав і захист вбудованих
-- ролей на рівні бази.
--
-- Причина міграції — одне речення користувача: «Але це ніхто не
-- зрозуміє, чому не українською?!» — сказане про перелік
-- `agents:read / alerts:ack / ncm:exec`.
--
-- Опис українською в core.permissions був від самого початку (колонка
-- description оголошена NOT NULL ще в 0001, заповнена в 0010). Інтерфейс
-- просто показував не ту колонку. Тобто помилка була не в базі — але
-- частина описів однаково не годиться для екрана, на якому людина
-- вирішує, що комусь дозволити.
--
-- Різниця, заради якої переписано всі 24 рядки: опис має відповідати на
-- питання «що ця людина зможе робити», а не називати предметну область.
--
-- було: ncm:exec Масове виконання команд на обладнанні
-- стало: ncm:exec Виконувати команди на живому обладнанні — зокрема масово
--
-- Перше — назва функції продукту; друге — наслідок для того, кому ти
-- зараз ставиш галочку. Тому кожен рядок нижче починається з дієслова в
-- інфінітиві («Бачити», «Змінювати», «Видаляти») і доводить думку до
-- наслідку там, де наслідок незворотний.
--
-- Іменників «тенант», «mute», «diff», «білий лейбл» більше немає: це
-- слова розробника, а галочку ставить не він.
-- =====================================================================
-- ---------------------------------------------------------------------
-- Описи прав
-- ---------------------------------------------------------------------
-- UPDATE, а не INSERT ... ON CONFLICT DO UPDATE: усі 24 ключі вже є, а
-- сама вставка додала б у міграцію другу відповідальність — заводити
-- права. Заводять їх ті міграції, які додають функцію, що їх перевіряє
-- (0036 — ncm:exec, 0037 — ncm:delete, 0050 — audit:read), і так має
-- лишитись: право, яке з'явилось окремо від коду, нічого не означає.
UPDATE core.permissions AS p SET description = v.description
FROM (VALUES
('devices:read', 'Бачити хости, їхні перевірки та зібрані метрики'),
('devices:write', 'Додавати, змінювати й видаляти хости — поштучно й масово'),
('devices:control', 'Запускати перевірки вручну й керувати опитуванням хостів'),
('maps:read', 'Відкривати мапи топології'),
('maps:write', 'Малювати мапи: додавати вузли й зв''язки, змінювати їх розташування'),
('maps:publish', 'Відкривати мапу публічним посиланням — тим, хто не входить у систему'),
('alerts:read', 'Бачити активні та вже закриті алерти'),
('alerts:ack', 'Брати алерт у роботу й тимчасово глушити сповіщення про нього'),
('alerts:write', 'Змінювати тригери алертів, канали сповіщень та ескалації'),
('ncm:read', 'Читати збережені конфіги й порівнювати їхні версії між собою'),
('ncm:write', 'Налаштовувати збір конфігів: розклад, профілі, правила відповідності'),
('ncm:rollback', 'Заливати збережену версію конфігу назад на пристрій'),
('ncm:exec', 'Виконувати команди на живому обладнанні — зокрема масово'),
('ncm:delete', 'Видаляти збережені версії конфігів — назавжди, без кошика'),
('agents:read', 'Бачити зонди, їхній стан і черги збору'),
('agents:write', 'Реєструвати нові зонди й змінювати налаштування наявних'),
('users:read', 'Бачити перелік користувачів і те, які в них ролі'),
('users:write', 'Заводити користувачів, змінювати ролі та склад самих ролей'),
('billing:read', 'Бачити тариф, спожиті ліміти й рахунки'),
('billing:manage', 'Змінювати тариф і платіжні дані організації'),
('audit:read', 'Читати журнал аудиту — хто що робив у системі'),
('settings:write', 'Змінювати налаштування організації та її оформлення'),
('dashboards:read', 'Відкривати дашборди'),
('dashboards:write', 'Створювати дашборди, змінювати й видаляти їх')
) AS v(key, description)
WHERE p.key = v.key;
-- ---------------------------------------------------------------------
-- Вбудована роль лишається вбудованою
-- ---------------------------------------------------------------------
-- Вбудовані ролі спільні для ВСІХ організацій інсталяції: у них
-- tenant_id IS NULL, і політика roles_visible з 0011 показує їх кожному
-- тенанту. Тому «адмін одного кабінету зняв право з ролі Інженер» — це
-- не правка в його кабінеті, а зміна для всіх кабінетів сервера.
--
-- Друга причина, чому їх не можна правити, видима лише в історії
-- міграцій: 0036, 0037 і 0050 дописують права ролям a1/a2 за фіксованим
-- UUID. Наступна така міграція мовчки поверне те, що людина свідомо
-- зняла, — тобто «відредагована» вбудована роль не тримається навіть до
-- наступного оновлення. Роль, яка сама себе відновлює, гірша за
-- заборонену: заборону видно одразу, а повернення права — ні.
--
-- Тому в інтерфейсі вбудовану роль не редагують, а копіюють. Тригери
-- нижче — друга лінія: інтерфейс можна обійти, а їх ні.
ALTER TABLE core.roles
-- Системна роль без tenant_id — це визначення, а не збіг: рівно
-- цим вона й відрізняється від власної ролі кабінету. Без CHECK
-- «власна роль із is_system = true» була б одним UPDATE-ом від
-- ролі, яку не видалити й не змінити, і яку при цьому видно лише
-- одному тенанту.
ADD CONSTRAINT roles_system_is_global CHECK (NOT is_system OR tenant_id IS NULL);
CREATE FUNCTION core.roles_protect_system() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
RAISE EXCEPTION 'вбудована роль % не видаляється — вона спільна для всієї інсталяції',
OLD.key USING ERRCODE = 'raise_exception';
RETURN OLD;
END $$;
-- Лише DELETE. UPDATE навмисно лишається дозволеним: перейменувати
-- «Адмін» на «Адміністратор» — це робота майбутньої міграції, і
-- забороняти її означало б змусити наступного автора спершу зняти
-- тригер. А от видалення вбудованої ролі не має сенсу ні для міграції,
-- ні для застосунку: у ній сидять живі люди, і membership.role_id
-- оголошений ON DELETE RESTRICT саме тому.
CREATE TRIGGER roles_no_system_delete
BEFORE DELETE ON core.roles
FOR EACH ROW WHEN (OLD.is_system)
EXECUTE FUNCTION core.roles_protect_system();
CREATE FUNCTION core.role_permissions_protect_system() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE sys boolean;
BEGIN
SELECT is_system INTO sys FROM core.roles WHERE id = OLD.role_id;
IF sys THEN
RAISE EXCEPTION 'право % належить вбудованій ролі — зняти його не можна',
OLD.permission_key USING ERRCODE = 'raise_exception';
END IF;
RETURN OLD;
END $$;
-- Знову лише DELETE, і це головний рядок міграції: саме зняття права з
-- вбудованої ролі — та дія, яка тихо ламає систему. Права ncm:exec і
-- audit:read видані власнику й адміну не «бо вони головні», а окремим
-- рішенням, записаним у 0036 і 0050; знявши їх, кабінет втрачає
-- можливість дізнатись, хто виконував команди, — і сам факт втрати теж
-- нікуди не записується.
--
-- INSERT лишається вільним: 0036, 0037 і 0050 саме ним і додають права
-- вбудованим ролям, і наступна така міграція має працювати без правки
-- цього тригера.
CREATE TRIGGER role_permissions_no_system_revoke
BEFORE DELETE ON core.role_permissions
FOR EACH ROW EXECUTE FUNCTION core.role_permissions_protect_system();
COMMENT ON TABLE core.roles IS
'Ролі: вбудовані (tenant_id IS NULL, is_system) спільні для всієї '
'інсталяції й незмінні — їх копіюють, а не правлять; власні ролі '
'кабінету редагуються повністю. Запобіжник від самоблокування '
'(у кабінеті має лишитись хоч один учасник із users:write) живе в '
'застосунку: він вимагає знати, ХТО саме робить зміну.';
COMMENT ON COLUMN core.permissions.description IS
'Що людина зможе робити, у вигляді фрази з дієсловом. Саме це '
'показує інтерфейс; ключ стоїть поруч дрібним — за ним шукають і '
'його називають у документації.';