netpulse-migrate замість PowerShell-скрипта: у контейнері немає ані psql, ані PowerShell, а тягнути клієнт Postgres в образ заради одного запуску — це половина дистрибутива на порожньому місці. Міграції вшиті через embed і переїхали в server/migrations: embed не бачить нічого за межами кореня свого модуля, а міграції поруч із бінарником, який їх накочує, не можуть розійтися версіями. Накочування під advisory-блокуванням: два інстанси при rolling update інакше застосували б ту саму міграцію двічі. Кожен файл в одній транзакції разом із записом у schema_migrations; виняток — continuous aggregates, які TimescaleDB забороняє в транзакції. Змінена вже застосована міграція зупиняє запуск: у різних інсталяціях інакше опиниться різна схема під одним номером. Перевірено на чистій базі: 23 міграції, 101 таблиця, повторний запуск каже «схема актуальна». Веб віддає сам API через embed: на self-hosted це прибирає з інструкції встановлення цілий компонент. Три політики кешування — назавжди для assets із хешем у імені, ніколи для index.html, коротко для решти. Знайдено живим прогоном: невідомий шлях під /api/ віддавав 200 з index.html, і клієнт падав на розборі HTML як JSON замість чесного 404. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
93 lines
4.8 KiB
PL/PgSQL
93 lines
4.8 KiB
PL/PgSQL
-- =====================================================================
|
||
-- NetPulse :: 0021_map_access_icons.sql
|
||
-- Доступ до мап за групами користувачів і власні іконки вузлів.
|
||
-- =====================================================================
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- Хто бачить мапу
|
||
-- ---------------------------------------------------------------------
|
||
|
||
-- Та сама модель, що для хостів (core.group_permissions): роль каже, що
|
||
-- людина взагалі вміє робити з мапами, група — з якими саме.
|
||
--
|
||
-- Мапа без жодного запису доступна всім, хто має maps:read. Це свідомо
|
||
-- інакше, ніж у Zabbix: у типовій інсталяції мапа одна на всіх, і
|
||
-- вимагати налаштувати доступ до неї означало б зробити типовий випадок
|
||
-- найдовшим.
|
||
CREATE TABLE topo.map_permissions (
|
||
map_id uuid NOT NULL REFERENCES topo.maps(id) ON DELETE CASCADE,
|
||
group_id uuid NOT NULL REFERENCES core.user_groups(id) ON DELETE CASCADE,
|
||
tenant_id uuid NOT NULL REFERENCES core.tenants(id) ON DELETE CASCADE,
|
||
level core.access_level NOT NULL DEFAULT 'read',
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
PRIMARY KEY (map_id, group_id)
|
||
);
|
||
CREATE INDEX map_permissions_group_idx ON topo.map_permissions (group_id);
|
||
|
||
ALTER TABLE topo.map_permissions ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE topo.map_permissions FORCE ROW LEVEL SECURITY;
|
||
CREATE POLICY tenant_isolation ON topo.map_permissions
|
||
USING (tenant_id = core.current_tenant())
|
||
WITH CHECK (tenant_id = core.current_tenant());
|
||
|
||
GRANT SELECT, INSERT, UPDATE, DELETE ON topo.map_permissions
|
||
TO netpulse_app, netpulse_worker;
|
||
|
||
-- Рівень доступу людини до конкретної мапи.
|
||
--
|
||
-- Правила ті самі, що для хостів: deny перекриває все, потім write,
|
||
-- потім read. Відсутність записів для мапи означає «доступна всім» —
|
||
-- інакше кожна нова мапа була б невидимою до окремого налаштування.
|
||
CREATE OR REPLACE FUNCTION topo.map_access_level(p_user uuid, p_map uuid)
|
||
RETURNS core.access_level
|
||
LANGUAGE sql STABLE AS $$
|
||
SELECT CASE
|
||
WHEN NOT EXISTS (SELECT 1 FROM topo.map_permissions WHERE map_id = p_map)
|
||
THEN 'write'::core.access_level
|
||
WHEN EXISTS (
|
||
SELECT 1 FROM topo.map_permissions mp
|
||
JOIN core.user_group_members gm ON gm.group_id = mp.group_id
|
||
WHERE mp.map_id = p_map AND gm.user_id = p_user AND mp.level = 'deny'
|
||
) THEN 'deny'::core.access_level
|
||
WHEN EXISTS (
|
||
SELECT 1 FROM topo.map_permissions mp
|
||
JOIN core.user_group_members gm ON gm.group_id = mp.group_id
|
||
WHERE mp.map_id = p_map AND gm.user_id = p_user AND mp.level = 'write'
|
||
) THEN 'write'::core.access_level
|
||
WHEN EXISTS (
|
||
SELECT 1 FROM topo.map_permissions mp
|
||
JOIN core.user_group_members gm ON gm.group_id = mp.group_id
|
||
WHERE mp.map_id = p_map AND gm.user_id = p_user AND mp.level = 'read'
|
||
) THEN 'read'::core.access_level
|
||
ELSE 'deny'::core.access_level
|
||
END
|
||
$$;
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- Власні іконки
|
||
-- ---------------------------------------------------------------------
|
||
|
||
-- Картинка зберігається в БД, а не у файловій системі: інсталяція з
|
||
-- кількох процесів інакше вимагала б спільного тому, а бекап продукту
|
||
-- перестав би бути бекапом бази.
|
||
--
|
||
-- SVG і PNG до 256 КБ. Більше на мапі не потрібно: іконка вузла — це
|
||
-- 32–64 пікселі, і мегабайтний PNG там означає лише повільне полотно.
|
||
CREATE TABLE topo.icons (
|
||
id uuid PRIMARY KEY DEFAULT core.new_id(),
|
||
tenant_id uuid NOT NULL REFERENCES core.tenants(id) ON DELETE CASCADE,
|
||
name text NOT NULL,
|
||
mime text NOT NULL CHECK (mime IN ('image/svg+xml','image/png','image/jpeg','image/webp')),
|
||
bytes bytea NOT NULL CHECK (octet_length(bytes) <= 262144),
|
||
created_by uuid REFERENCES core.users(id) ON DELETE SET NULL,
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
UNIQUE (tenant_id, name)
|
||
);
|
||
|
||
ALTER TABLE topo.icons ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE topo.icons FORCE ROW LEVEL SECURITY;
|
||
CREATE POLICY tenant_isolation ON topo.icons
|
||
USING (tenant_id = core.current_tenant())
|
||
WITH CHECK (tenant_id = core.current_tenant());
|
||
|
||
GRANT SELECT, INSERT, UPDATE, DELETE ON topo.icons TO netpulse_app, netpulse_worker;
|