database_security_postgresql.sql 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. -- Автопрокат: PostgreSQL 16, схема, роли, права и аудит операций
  2. CREATE TABLE IF NOT EXISTS brand (
  3. brand_id SERIAL PRIMARY KEY,
  4. name VARCHAR(50) NOT NULL CONSTRAINT uq_brand_name UNIQUE,
  5. country VARCHAR(50)
  6. );
  7. CREATE TABLE IF NOT EXISTS car (
  8. car_id SERIAL PRIMARY KEY,
  9. license_plate VARCHAR(15) NOT NULL CONSTRAINT uq_car_plate UNIQUE,
  10. brand_id INT NOT NULL CONSTRAINT fk_car_brand
  11. REFERENCES brand(brand_id) ON UPDATE CASCADE ON DELETE RESTRICT,
  12. year SMALLINT CONSTRAINT chk_car_year CHECK (year BETWEEN 1990 AND 2030),
  13. daily_rate NUMERIC(10,2) NOT NULL CONSTRAINT chk_car_rate CHECK (daily_rate > 0),
  14. status VARCHAR(20) DEFAULT 'Свободен'
  15. CONSTRAINT chk_car_status CHECK (status IN ('Свободен','В аренде','На ТО'))
  16. );
  17. CREATE TABLE IF NOT EXISTS client (
  18. client_id SERIAL PRIMARY KEY,
  19. full_name VARCHAR(100) NOT NULL,
  20. phone VARCHAR(20) CONSTRAINT uq_client_phone UNIQUE,
  21. address TEXT,
  22. license_no VARCHAR(20) CONSTRAINT uq_client_license UNIQUE,
  23. created_at TIMESTAMPTZ DEFAULT now()
  24. );
  25. CREATE TABLE IF NOT EXISTS rental (
  26. rental_id SERIAL PRIMARY KEY,
  27. car_id INT NOT NULL CONSTRAINT fk_rental_car
  28. REFERENCES car(car_id) ON UPDATE CASCADE ON DELETE RESTRICT,
  29. client_id INT NOT NULL CONSTRAINT fk_rental_client
  30. REFERENCES client(client_id) ON UPDATE CASCADE ON DELETE RESTRICT,
  31. start_date DATE NOT NULL,
  32. end_date DATE NOT NULL CONSTRAINT chk_rental_dates CHECK (end_date >= start_date),
  33. actual_return DATE,
  34. total_cost NUMERIC(10,2) CONSTRAINT chk_rental_cost CHECK (total_cost >= 0),
  35. status VARCHAR(20) DEFAULT 'Активна'
  36. CONSTRAINT chk_rental_status CHECK (status IN ('Активна','Завершена'))
  37. );
  38. CREATE INDEX IF NOT EXISTS idx_car_brand_id ON car(brand_id);
  39. CREATE INDEX IF NOT EXISTS idx_rental_car_id ON rental(car_id);
  40. CREATE INDEX IF NOT EXISTS idx_rental_client_id ON rental(client_id);
  41. CREATE INDEX IF NOT EXISTS idx_rental_dates ON rental(start_date, end_date);
  42. CREATE INDEX IF NOT EXISTS idx_rental_status ON rental(status);
  43. CREATE OR REPLACE VIEW v_rentals_full AS
  44. SELECT r.rental_id,
  45. c.full_name AS client_name,
  46. c.phone AS client_phone,
  47. ca.license_plate,
  48. b.name AS brand_name,
  49. r.start_date,
  50. r.end_date,
  51. r.actual_return,
  52. r.total_cost,
  53. r.status
  54. FROM rental r
  55. JOIN client c ON c.client_id = r.client_id
  56. JOIN car ca ON ca.car_id = r.car_id
  57. JOIN brand b ON b.brand_id = ca.brand_id;
  58. CREATE OR REPLACE VIEW v_available_cars AS
  59. SELECT ca.car_id, ca.license_plate, b.name AS brand, b.country, ca.year, ca.daily_rate
  60. FROM car ca
  61. JOIN brand b ON b.brand_id = ca.brand_id
  62. WHERE ca.status = 'Свободен';
  63. CREATE OR REPLACE VIEW v_client_stats AS
  64. SELECT c.client_id,
  65. c.full_name,
  66. COUNT(r.rental_id) AS total_rentals,
  67. COALESCE(SUM(r.total_cost), 0) AS total_spent
  68. FROM client c
  69. LEFT JOIN rental r ON r.client_id = c.client_id
  70. GROUP BY c.client_id, c.full_name;
  71. -- Пользователи предметной области
  72. CREATE ROLE rent_admin NOLOGIN;
  73. CREATE ROLE rent_manager NOLOGIN;
  74. CREATE ROLE rent_viewer NOLOGIN;
  75. -- Администратор: полный доступ
  76. GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO rent_admin;
  77. GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO rent_admin;
  78. -- Менеджер: рабочий CRUD без управления пользователями и системной структурой
  79. GRANT SELECT, INSERT, UPDATE ON client, car, rental TO rent_manager;
  80. GRANT SELECT ON brand, v_rentals_full, v_available_cars, v_client_stats TO rent_manager;
  81. GRANT USAGE ON SEQUENCE client_client_id_seq, car_car_id_seq, rental_rental_id_seq TO rent_manager;
  82. -- Аналитик: только чтение и отчеты
  83. GRANT SELECT ON brand, car, client, rental, v_rentals_full, v_available_cars, v_client_stats TO rent_viewer;
  84. -- Примеры учетных записей БД. В реальном проекте пароли задаются администратором отдельно.
  85. -- CREATE USER ivanov WITH PASSWORD 'Str0ng#Pass1' IN ROLE rent_manager;
  86. -- CREATE USER petrov WITH PASSWORD 'Str0ng#Pass2' IN ROLE rent_viewer;
  87. -- CREATE USER db_admin WITH PASSWORD 'Str0ng#Admin1' IN ROLE rent_admin;
  88. -- Row Level Security: явное разрешение по ролям.
  89. ALTER TABLE rental ENABLE ROW LEVEL SECURITY;
  90. DROP POLICY IF EXISTS rental_admin_all ON rental;
  91. CREATE POLICY rental_admin_all ON rental
  92. FOR ALL TO rent_admin
  93. USING (true)
  94. WITH CHECK (true);
  95. DROP POLICY IF EXISTS rental_manager_all ON rental;
  96. CREATE POLICY rental_manager_all ON rental
  97. FOR ALL TO rent_manager
  98. USING (true)
  99. WITH CHECK (true);
  100. DROP POLICY IF EXISTS rental_viewer_read ON rental;
  101. CREATE POLICY rental_viewer_read ON rental
  102. FOR SELECT TO rent_viewer
  103. USING (true);
  104. -- Аудит операций средствами СУБД
  105. CREATE TABLE IF NOT EXISTS audit_log (
  106. audit_id BIGSERIAL PRIMARY KEY,
  107. changed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  108. db_user TEXT NOT NULL DEFAULT current_user,
  109. action TEXT NOT NULL,
  110. table_name TEXT NOT NULL,
  111. row_id TEXT,
  112. old_data JSONB,
  113. new_data JSONB
  114. );
  115. CREATE OR REPLACE FUNCTION fn_audit_row()
  116. RETURNS TRIGGER
  117. LANGUAGE plpgsql
  118. AS $$
  119. DECLARE
  120. pk_name TEXT := TG_ARGV[0];
  121. BEGIN
  122. IF TG_OP = 'INSERT' THEN
  123. INSERT INTO audit_log(action, table_name, row_id, new_data)
  124. VALUES (TG_OP, TG_TABLE_NAME, to_jsonb(NEW)->>pk_name, to_jsonb(NEW));
  125. RETURN NEW;
  126. ELSIF TG_OP = 'UPDATE' THEN
  127. INSERT INTO audit_log(action, table_name, row_id, old_data, new_data)
  128. VALUES (TG_OP, TG_TABLE_NAME, to_jsonb(NEW)->>pk_name, to_jsonb(OLD), to_jsonb(NEW));
  129. RETURN NEW;
  130. ELSE
  131. INSERT INTO audit_log(action, table_name, row_id, old_data)
  132. VALUES (TG_OP, TG_TABLE_NAME, to_jsonb(OLD)->>pk_name, to_jsonb(OLD));
  133. RETURN OLD;
  134. END IF;
  135. END;
  136. $$;
  137. DROP TRIGGER IF EXISTS trg_audit_brand ON brand;
  138. CREATE TRIGGER trg_audit_brand
  139. AFTER INSERT OR UPDATE OR DELETE ON brand
  140. FOR EACH ROW EXECUTE FUNCTION fn_audit_row('brand_id');
  141. DROP TRIGGER IF EXISTS trg_audit_car ON car;
  142. CREATE TRIGGER trg_audit_car
  143. AFTER INSERT OR UPDATE OR DELETE ON car
  144. FOR EACH ROW EXECUTE FUNCTION fn_audit_row('car_id');
  145. DROP TRIGGER IF EXISTS trg_audit_client ON client;
  146. CREATE TRIGGER trg_audit_client
  147. AFTER INSERT OR UPDATE OR DELETE ON client
  148. FOR EACH ROW EXECUTE FUNCTION fn_audit_row('client_id');
  149. DROP TRIGGER IF EXISTS trg_audit_rental ON rental;
  150. CREATE TRIGGER trg_audit_rental
  151. AFTER INSERT OR UPDATE OR DELETE ON rental
  152. FOR EACH ROW EXECUTE FUNCTION fn_audit_row('rental_id');