OtherTopics: other_topics.sql

File other_topics.sql, 13.6 KB (added by 183164, 12 days ago)
Line 
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
13SET search_path TO project;
14SET client_min_messages TO warning;
15
16
17-- =============================================================
18-- 1. ИНДЕКСИ
19-- =============================================================
20
21-- историјата на пријава и последниот статус (тригери, UC0004, погледи, извештаи)
22CREATE INDEX IF NOT EXISTS idx_status_logs_report_changed
23 ON status_logs (report_id, changed_at, log_id);
24
25-- активните пријави на работник (UC0005) и оптовареност (UC0007)
26CREATE INDEX IF NOT EXISTS idx_assignments_worker
27 ON assignments (worker_id, report_id);
28
29-- пријавите на граѓанин (UC0004)
30CREATE INDEX IF NOT EXISTS idx_reports_citizen
31 ON reports (citizen_id, created_at);
32
33-- извештаи за даден период
34CREATE INDEX IF NOT EXISTS idx_reports_created
35 ON reports (created_at);
36
37-- слични активни пријави во близина; делумен индекс само за отворените пријави
38CREATE 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)
43CREATE INDEX IF NOT EXISTS idx_comments_report ON comments (report_id);
44CREATE 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-- =============================================================
53CREATE 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)
58RETURNS TABLE (report_id INTEGER, description TEXT, status TEXT,
59 created_at TIMESTAMP, distance_m DOUBLE PRECISION)
60LANGUAGE 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-- =============================================================
81DROP MATERIALIZED VIEW IF EXISTS mv_report_times;
82CREATE MATERIALIZED VIEW mv_report_times AS
83SELECT 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
87FROM reports r
88JOIN status_logs l ON l.report_id = r.report_id
89GROUP BY r.report_id;
90
91CREATE UNIQUE INDEX idx_mv_report_times_report ON mv_report_times (report_id);
92CREATE INDEX idx_mv_report_times_quarter ON mv_report_times (quarter, category_id);
93
94-- Освежувањето бара сопственик на погледот, па функцијата е SECURITY DEFINER
95-- со фиксиран search_path (за да не може да се подметне објект со исто име).
96CREATE OR REPLACE FUNCTION refresh_report_stats()
97RETURNS VOID LANGUAGE plpgsql SECURITY DEFINER
98SET search_path = project, pg_temp AS $$
99BEGIN
100 REFRESH MATERIALIZED VIEW CONCURRENTLY project.mv_report_times;
101END $$;
102
103SET client_min_messages TO notice;
104DO $$
105BEGIN
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;
113END $$;
114SET client_min_messages TO warning;
115
116
117-- =============================================================
118-- 4. БЕЗБЕДНОСТ
119-- =============================================================
120
121-- 4.1 Фиксиран search_path за сите функции и процедури во шемата,
122-- за да не можат да се подметнат табели или функции со исто име
123-- во друга шема (напад преку search_path).
124DO $$
125DECLARE
126 f RECORD;
127BEGIN
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;
135END $$;
136
137-- 4.2 Јавниот поглед не смее да открие податоци преку функции во WHERE.
138ALTER VIEW v_public_reports SET (security_barrier = true);
139
140-- 4.3 Безбедно динамичко пребарување на пријави за администраторите.
141-- Колоната за подредување се проверува со листа на дозволени вредности
142-- и се вметнува со format('%I') како идентификатор; сите вредности
143-- од корисникот се предаваат како параметри (USING), никогаш како текст.
144CREATE 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)
150RETURNS TABLE (report_id INTEGER, category TEXT, description TEXT, status TEXT,
151 priority TEXT, created_at TIMESTAMP)
152LANGUAGE plpgsql STABLE
153SET search_path = project, pg_temp AS $$
154DECLARE
155 v_sql TEXT;
156BEGIN
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);
177END $$;
178
179-- 4.4 Никој освен сопственикот нема автоматски права врз објектите на шемата.
180REVOKE ALL ON SCHEMA project FROM PUBLIC;
181REVOKE ALL ON ALL TABLES IN SCHEMA project FROM PUBLIC;
182REVOKE ALL ON ALL SEQUENCES IN SCHEMA project FROM PUBLIC;
183REVOKE ALL ON ALL FUNCTIONS IN SCHEMA project FROM PUBLIC;
184REVOKE ALL ON ALL PROCEDURES IN SCHEMA project FROM PUBLIC;
185
186-- 4.5 Улоги со најмали потребни права и заштита на ниво на ред (RLS).
187-- Улогите се креираат само ако корисникот има право CREATEROLE.
188-- Корисникот со кој се најавува апликацијата треба да биде член на cityfix_app.
189SET client_min_messages TO notice;
190DO $$
191DECLARE
192 v_can_create BOOLEAN;
193BEGIN
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 се поставени.';
260END $$;