| 1 | -- =============================================================
|
|---|
| 2 | -- CityFix - other_topics.sql (фаза P9)
|
|---|
| 3 | -- Индекси, оптимизации и безбедносни мерки.
|
|---|
| 4 | --
|
|---|
| 5 | -- Редослед на извршување:
|
|---|
| 6 | -- 1. schema_creation.sql
|
|---|
| 7 | -- 2. advanced_db.sql
|
|---|
| 8 | -- 3. other_topics.sql (оваа скрипта)
|
|---|
| 9 | -- 4. data_load.sql
|
|---|
| 10 | -- Скриптата може да се извршува повеќепати.
|
|---|
| 11 | -- =============================================================
|
|---|
| 12 |
|
|---|
| 13 | SET search_path TO project;
|
|---|
| 14 | SET client_min_messages TO warning;
|
|---|
| 15 |
|
|---|
| 16 |
|
|---|
| 17 | -- =============================================================
|
|---|
| 18 | -- 1. ИНДЕКСИ
|
|---|
| 19 | -- =============================================================
|
|---|
| 20 |
|
|---|
| 21 | -- историјата на пријава и последниот статус (тригери, UC0004, погледи, извештаи)
|
|---|
| 22 | CREATE INDEX IF NOT EXISTS idx_status_logs_report_changed
|
|---|
| 23 | ON status_logs (report_id, changed_at, log_id);
|
|---|
| 24 |
|
|---|
| 25 | -- активните пријави на работник (UC0005) и оптовареност (UC0007)
|
|---|
| 26 | CREATE INDEX IF NOT EXISTS idx_assignments_worker
|
|---|
| 27 | ON assignments (worker_id, report_id);
|
|---|
| 28 |
|
|---|
| 29 | -- пријавите на граѓанин (UC0004)
|
|---|
| 30 | CREATE INDEX IF NOT EXISTS idx_reports_citizen
|
|---|
| 31 | ON reports (citizen_id, created_at);
|
|---|
| 32 |
|
|---|
| 33 | -- извештаи за даден период
|
|---|
| 34 | CREATE INDEX IF NOT EXISTS idx_reports_created
|
|---|
| 35 | ON reports (created_at);
|
|---|
| 36 |
|
|---|
| 37 | -- слични активни пријави во близина; делумен индекс само за отворените пријави
|
|---|
| 38 | CREATE INDEX IF NOT EXISTS idx_reports_active_location
|
|---|
| 39 | ON reports (category_id, latitude, longitude)
|
|---|
| 40 | WHERE status NOT IN ('resolved', 'rejected');
|
|---|
| 41 |
|
|---|
| 42 | -- коментари и фотографии на пријава (UC0004)
|
|---|
| 43 | CREATE INDEX IF NOT EXISTS idx_comments_report ON comments (report_id);
|
|---|
| 44 | CREATE INDEX IF NOT EXISTS idx_photos_report ON photos (report_id);
|
|---|
| 45 |
|
|---|
| 46 |
|
|---|
| 47 | -- =============================================================
|
|---|
| 48 | -- 2. ОПТИМИЗИРАНА ФУНКЦИЈА ЗА СЛИЧНИ ПРИЈАВИ
|
|---|
| 49 | -- Пред пресметката на точното растојание, пријавите се филтрираат
|
|---|
| 50 | -- со правоаголник околу точката (условите BETWEEN), кој може да го
|
|---|
| 51 | -- користи делумниот индекс idx_reports_active_location.
|
|---|
| 52 | -- =============================================================
|
|---|
| 53 | CREATE OR REPLACE FUNCTION find_similar_reports(p_category_id INTEGER,
|
|---|
| 54 | p_latitude NUMERIC,
|
|---|
| 55 | p_longitude NUMERIC,
|
|---|
| 56 | p_radius_m DOUBLE PRECISION DEFAULT 150,
|
|---|
| 57 | p_exclude_report_id INTEGER DEFAULT NULL)
|
|---|
| 58 | RETURNS TABLE (report_id INTEGER, description TEXT, status TEXT,
|
|---|
| 59 | created_at TIMESTAMP, distance_m DOUBLE PRECISION)
|
|---|
| 60 | LANGUAGE sql STABLE AS $$
|
|---|
| 61 | SELECT r.report_id, r.description, r.status::TEXT, r.created_at,
|
|---|
| 62 | ROUND(distance_m(p_latitude, p_longitude, r.latitude, r.longitude)::NUMERIC, 1)
|
|---|
| 63 | FROM project.reports r
|
|---|
| 64 | WHERE r.category_id = p_category_id
|
|---|
| 65 | AND r.status NOT IN ('resolved', 'rejected')
|
|---|
| 66 | AND r.latitude BETWEEN p_latitude - p_radius_m / 111320.0
|
|---|
| 67 | AND p_latitude + p_radius_m / 111320.0
|
|---|
| 68 | AND r.longitude BETWEEN p_longitude - p_radius_m / (111320.0 * cos(radians(p_latitude)))
|
|---|
| 69 | AND p_longitude + p_radius_m / (111320.0 * cos(radians(p_latitude)))
|
|---|
| 70 | AND r.report_id IS DISTINCT FROM p_exclude_report_id
|
|---|
| 71 | AND distance_m(p_latitude, p_longitude, r.latitude, r.longitude) <= p_radius_m
|
|---|
| 72 | ORDER BY 5;
|
|---|
| 73 | $$;
|
|---|
| 74 |
|
|---|
| 75 |
|
|---|
| 76 | -- =============================================================
|
|---|
| 77 | -- 3. МАТЕРИЈАЛИЗИРАН ПОГЛЕД ЗА КВАРТАЛНИТЕ ИЗВЕШТАИ (P6)
|
|---|
| 78 | -- Времињата за одговор и решавање по пријава се пресметуваат еднаш
|
|---|
| 79 | -- и се чуваат; се освежуваат секоја ноќ со позадинската задача.
|
|---|
| 80 | -- =============================================================
|
|---|
| 81 | DROP MATERIALIZED VIEW IF EXISTS mv_report_times;
|
|---|
| 82 | CREATE MATERIALIZED VIEW mv_report_times AS
|
|---|
| 83 | SELECT r.report_id, r.category_id, r.status::TEXT AS status, r.created_at,
|
|---|
| 84 | date_trunc('quarter', r.created_at) AS quarter,
|
|---|
| 85 | MIN(l.changed_at) FILTER (WHERE l.status = 'received') AS received_at,
|
|---|
| 86 | MIN(l.changed_at) FILTER (WHERE l.status = 'resolved') AS resolved_at
|
|---|
| 87 | FROM reports r
|
|---|
| 88 | JOIN status_logs l ON l.report_id = r.report_id
|
|---|
| 89 | GROUP BY r.report_id;
|
|---|
| 90 |
|
|---|
| 91 | CREATE UNIQUE INDEX idx_mv_report_times_report ON mv_report_times (report_id);
|
|---|
| 92 | CREATE INDEX idx_mv_report_times_quarter ON mv_report_times (quarter, category_id);
|
|---|
| 93 |
|
|---|
| 94 | -- Освежувањето бара сопственик на погледот, па функцијата е SECURITY DEFINER
|
|---|
| 95 | -- со фиксиран search_path (за да не може да се подметне објект со исто име).
|
|---|
| 96 | CREATE OR REPLACE FUNCTION refresh_report_stats()
|
|---|
| 97 | RETURNS VOID LANGUAGE plpgsql SECURITY DEFINER
|
|---|
| 98 | SET search_path = project, pg_temp AS $$
|
|---|
| 99 | BEGIN
|
|---|
| 100 | REFRESH MATERIALIZED VIEW CONCURRENTLY project.mv_report_times;
|
|---|
| 101 | END $$;
|
|---|
| 102 |
|
|---|
| 103 | SET client_min_messages TO notice;
|
|---|
| 104 | DO $$
|
|---|
| 105 | BEGIN
|
|---|
| 106 | IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_cron') THEN
|
|---|
| 107 | PERFORM cron.schedule('cityfix-refresh-report-stats', '30 2 * * *',
|
|---|
| 108 | 'SELECT project.refresh_report_stats()');
|
|---|
| 109 | RAISE NOTICE 'Освежувањето на mv_report_times е закажано со pg_cron.';
|
|---|
| 110 | ELSE
|
|---|
| 111 | RAISE NOTICE 'pg_cron не е достапен: refresh_report_stats() закажете ја однадвор.';
|
|---|
| 112 | END IF;
|
|---|
| 113 | END $$;
|
|---|
| 114 | SET client_min_messages TO warning;
|
|---|
| 115 |
|
|---|
| 116 |
|
|---|
| 117 | -- =============================================================
|
|---|
| 118 | -- 4. БЕЗБЕДНОСТ
|
|---|
| 119 | -- =============================================================
|
|---|
| 120 |
|
|---|
| 121 | -- 4.1 Фиксиран search_path за сите функции и процедури во шемата,
|
|---|
| 122 | -- за да не можат да се подметнат табели или функции со исто име
|
|---|
| 123 | -- во друга шема (напад преку search_path).
|
|---|
| 124 | DO $$
|
|---|
| 125 | DECLARE
|
|---|
| 126 | f RECORD;
|
|---|
| 127 | BEGIN
|
|---|
| 128 | FOR f IN SELECT p.oid::regprocedure AS sig, p.prokind
|
|---|
| 129 | FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
|
|---|
| 130 | WHERE n.nspname = 'project' AND p.prokind IN ('f', 'p')
|
|---|
| 131 | LOOP
|
|---|
| 132 | EXECUTE format('ALTER %s %s SET search_path = project, pg_temp',
|
|---|
| 133 | CASE f.prokind WHEN 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END, f.sig);
|
|---|
| 134 | END LOOP;
|
|---|
| 135 | END $$;
|
|---|
| 136 |
|
|---|
| 137 | -- 4.2 Јавниот поглед не смее да открие податоци преку функции во WHERE.
|
|---|
| 138 | ALTER VIEW v_public_reports SET (security_barrier = true);
|
|---|
| 139 |
|
|---|
| 140 | -- 4.3 Безбедно динамичко пребарување на пријави за администраторите.
|
|---|
| 141 | -- Колоната за подредување се проверува со листа на дозволени вредности
|
|---|
| 142 | -- и се вметнува со format('%I') како идентификатор; сите вредности
|
|---|
| 143 | -- од корисникот се предаваат како параметри (USING), никогаш како текст.
|
|---|
| 144 | CREATE OR REPLACE FUNCTION search_reports(p_text TEXT DEFAULT NULL,
|
|---|
| 145 | p_status TEXT DEFAULT NULL,
|
|---|
| 146 | p_category_id INTEGER DEFAULT NULL,
|
|---|
| 147 | p_sort_column TEXT DEFAULT 'created_at',
|
|---|
| 148 | p_descending BOOLEAN DEFAULT TRUE,
|
|---|
| 149 | p_limit INTEGER DEFAULT 50)
|
|---|
| 150 | RETURNS TABLE (report_id INTEGER, category TEXT, description TEXT, status TEXT,
|
|---|
| 151 | priority TEXT, created_at TIMESTAMP)
|
|---|
| 152 | LANGUAGE plpgsql STABLE
|
|---|
| 153 | SET search_path = project, pg_temp AS $$
|
|---|
| 154 | DECLARE
|
|---|
| 155 | v_sql TEXT;
|
|---|
| 156 | BEGIN
|
|---|
| 157 | IF p_sort_column NOT IN ('created_at', 'priority', 'status', 'report_id') THEN
|
|---|
| 158 | RAISE EXCEPTION 'Недозволена колона за подредување: %', p_sort_column;
|
|---|
| 159 | END IF;
|
|---|
| 160 |
|
|---|
| 161 | v_sql := format(
|
|---|
| 162 | 'SELECT r.report_id, c.name::TEXT, r.description, r.status::TEXT,
|
|---|
| 163 | r.priority::TEXT, r.created_at
|
|---|
| 164 | FROM reports r
|
|---|
| 165 | JOIN categories c ON c.category_id = r.category_id
|
|---|
| 166 | WHERE ($1 IS NULL OR r.description ILIKE ''%%'' || $1 || ''%%''
|
|---|
| 167 | OR r.location_text ILIKE ''%%'' || $1 || ''%%'')
|
|---|
| 168 | AND ($2 IS NULL OR r.status = $2)
|
|---|
| 169 | AND ($3 IS NULL OR r.category_id = $3)
|
|---|
| 170 | ORDER BY r.%I %s, r.report_id
|
|---|
| 171 | LIMIT $4',
|
|---|
| 172 | p_sort_column,
|
|---|
| 173 | CASE WHEN p_descending THEN 'DESC' ELSE 'ASC' END);
|
|---|
| 174 |
|
|---|
| 175 | RETURN QUERY EXECUTE v_sql
|
|---|
| 176 | USING p_text, p_status, p_category_id, LEAST(GREATEST(p_limit, 1), 500);
|
|---|
| 177 | END $$;
|
|---|
| 178 |
|
|---|
| 179 | -- 4.4 Никој освен сопственикот нема автоматски права врз објектите на шемата.
|
|---|
| 180 | REVOKE ALL ON SCHEMA project FROM PUBLIC;
|
|---|
| 181 | REVOKE ALL ON ALL TABLES IN SCHEMA project FROM PUBLIC;
|
|---|
| 182 | REVOKE ALL ON ALL SEQUENCES IN SCHEMA project FROM PUBLIC;
|
|---|
| 183 | REVOKE ALL ON ALL FUNCTIONS IN SCHEMA project FROM PUBLIC;
|
|---|
| 184 | REVOKE ALL ON ALL PROCEDURES IN SCHEMA project FROM PUBLIC;
|
|---|
| 185 |
|
|---|
| 186 | -- 4.5 Улоги со најмали потребни права и заштита на ниво на ред (RLS).
|
|---|
| 187 | -- Улогите се креираат само ако корисникот има право CREATEROLE.
|
|---|
| 188 | -- Корисникот со кој се најавува апликацијата треба да биде член на cityfix_app.
|
|---|
| 189 | SET client_min_messages TO notice;
|
|---|
| 190 | DO $$
|
|---|
| 191 | DECLARE
|
|---|
| 192 | v_can_create BOOLEAN;
|
|---|
| 193 | BEGIN
|
|---|
| 194 | SELECT rolcreaterole OR rolsuper INTO v_can_create
|
|---|
| 195 | FROM pg_roles WHERE rolname = current_user;
|
|---|
| 196 |
|
|---|
| 197 | IF v_can_create THEN
|
|---|
| 198 | IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'cityfix_app') THEN
|
|---|
| 199 | CREATE ROLE cityfix_app NOLOGIN;
|
|---|
| 200 | END IF;
|
|---|
| 201 | IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'cityfix_citizen') THEN
|
|---|
| 202 | CREATE ROLE cityfix_citizen NOLOGIN;
|
|---|
| 203 | END IF;
|
|---|
| 204 | IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'cityfix_analyst') THEN
|
|---|
| 205 | CREATE ROLE cityfix_analyst NOLOGIN;
|
|---|
| 206 | END IF;
|
|---|
| 207 | IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'cityfix_public') THEN
|
|---|
| 208 | CREATE ROLE cityfix_public NOLOGIN;
|
|---|
| 209 | END IF;
|
|---|
| 210 | END IF;
|
|---|
| 211 |
|
|---|
| 212 | IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'cityfix_app') THEN
|
|---|
| 213 | RAISE NOTICE 'Корисникот нема право CREATEROLE: улогите и RLS не се поставени.';
|
|---|
| 214 | RETURN;
|
|---|
| 215 | END IF;
|
|---|
| 216 |
|
|---|
| 217 | -- апликација: читање и внесување; без бришење, без DDL; историјата само се дополнува
|
|---|
| 218 | GRANT USAGE ON SCHEMA project TO cityfix_app, cityfix_citizen, cityfix_analyst, cityfix_public;
|
|---|
| 219 | GRANT SELECT, INSERT, UPDATE ON admins, workers, citizens, categories,
|
|---|
| 220 | reports, photos, comments, assignments TO cityfix_app;
|
|---|
| 221 | GRANT SELECT, INSERT ON status_logs TO cityfix_app;
|
|---|
| 222 | GRANT SELECT ON v_public_reports, v_report_overview, v_worker_workload,
|
|---|
| 223 | mv_report_times TO cityfix_app;
|
|---|
| 224 | GRANT USAGE ON ALL SEQUENCES IN SCHEMA project TO cityfix_app;
|
|---|
| 225 | GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA project TO cityfix_app;
|
|---|
| 226 | GRANT EXECUTE ON ALL PROCEDURES IN SCHEMA project TO cityfix_app;
|
|---|
| 227 |
|
|---|
| 228 | -- граѓанин (за идно директно поврзување, на пример мобилна апликација):
|
|---|
| 229 | -- ги гледа само своите пријави и јавниот преглед
|
|---|
| 230 | GRANT SELECT ON reports, categories, v_public_reports TO cityfix_citizen;
|
|---|
| 231 |
|
|---|
| 232 | -- аналитичар: извештаи без лични податоци (правата се по колони)
|
|---|
| 233 | GRANT SELECT ON reports, status_logs, assignments, categories,
|
|---|
| 234 | mv_report_times, v_worker_workload TO cityfix_analyst;
|
|---|
| 235 | GRANT SELECT (worker_id, full_name) ON workers TO cityfix_analyst;
|
|---|
| 236 | GRANT SELECT (citizen_id) ON citizens TO cityfix_analyst;
|
|---|
| 237 | GRANT EXECUTE ON FUNCTION distance_m(NUMERIC, NUMERIC, NUMERIC, NUMERIC),
|
|---|
| 238 | sla_days(TEXT), priority_rank(TEXT) TO cityfix_analyst;
|
|---|
| 239 |
|
|---|
| 240 | -- јавен пристап: само јавниот поглед и категориите
|
|---|
| 241 | GRANT SELECT ON v_public_reports, categories TO cityfix_public;
|
|---|
| 242 |
|
|---|
| 243 | -- RLS: граѓанинот ги гледа само редовите со неговиот citizen_id,
|
|---|
| 244 | -- кој апликацијата го поставува со SET cityfix.citizen_id = ...
|
|---|
| 245 | ALTER TABLE reports ENABLE ROW LEVEL SECURITY;
|
|---|
| 246 | IF EXISTS (SELECT 1 FROM pg_policies WHERE schemaname = 'project' AND tablename = 'reports') THEN
|
|---|
| 247 | DROP POLICY IF EXISTS reports_app_all ON reports;
|
|---|
| 248 | DROP POLICY IF EXISTS reports_analyst_read ON reports;
|
|---|
| 249 | DROP POLICY IF EXISTS reports_citizen_own ON reports;
|
|---|
| 250 | END IF;
|
|---|
| 251 | CREATE POLICY reports_app_all ON reports
|
|---|
| 252 | TO cityfix_app USING (true) WITH CHECK (true);
|
|---|
| 253 | CREATE POLICY reports_analyst_read ON reports
|
|---|
| 254 | FOR SELECT TO cityfix_analyst USING (true);
|
|---|
| 255 | CREATE POLICY reports_citizen_own ON reports
|
|---|
| 256 | FOR SELECT TO cityfix_citizen
|
|---|
| 257 | USING (citizen_id = NULLIF(current_setting('cityfix.citizen_id', true), '')::INTEGER);
|
|---|
| 258 |
|
|---|
| 259 | RAISE NOTICE 'Улогите cityfix_app, cityfix_citizen, cityfix_analyst и cityfix_public се поставени.';
|
|---|
| 260 | END $$;
|
|---|