changeset 5025:4c658a8f34da

Fix row level security policies for waterway admin Since 'staging_done OR' was added to the conditions to improve performance for read access, it was also allowed to delete and partly update entries with staging_done set to true but otherwise being outside the country of the respective waterway admin. Using an extra policy for each command and using the 'staging_done OR' tweak only FOR SELECT should fix authorization while keeping performance.
author Tom Gottfried <tom@intevation.de>
date Wed, 18 Mar 2020 12:16:42 +0100
parents 36a3dce20232
children d02f0c5c60b3
files schema/auth.sql schema/auth_tests.sql schema/run_tests.sh schema/tap_tests_data.sql schema/updates/1427/01.fix_rls_policies.sql
diffstat 5 files changed, 216 insertions(+), 52 deletions(-) [+]
line wrap: on
line diff
--- a/schema/auth.sql	Mon Mar 16 17:11:53 2020 +0100
+++ b/schema/auth.sql	Wed Mar 18 12:16:42 2020 +0100
@@ -134,58 +134,87 @@
 
 -- Staging area
 
--- In many cases it is more efficient to check for "staging_done" to
--- prevent the more expensive checks for read only access (which is
--- allowed for all users, when staging is done).
-CREATE POLICY same_country ON waterway.gauge_measurements
-    FOR ALL TO waterway_admin
-    USING (staging_done
-           OR (location).country_code =
-               (SELECT country FROM users.list_users
-                WHERE username = current_user))
-    WITH CHECK ((location).country_code =
-                 (SELECT country FROM users.list_users
-                  WHERE username = current_user));
-
-CREATE POLICY same_country ON waterway.waterway_profiles
-    FOR ALL TO waterway_admin
-    USING (staging_done
-           OR (location).country_code =
-               (SELECT country FROM users.list_users
-                WHERE username = current_user))
-    WITH CHECK ((location).country_code =
-                (SELECT country FROM users.list_users
-                 WHERE username = current_user));
+DO LANGUAGE plpgsql
+$do$
+DECLARE
+    the_table varchar;
+    condition CONSTANT text = $$
+        (location).country_code =
+            (SELECT country FROM users.list_users
+                WHERE username = current_user)
+        $$;
+BEGIN
+    FOREACH the_table IN ARRAY ARRAY[
+        'gauge_measurements',
+        'waterway_profiles']
+    LOOP
+        EXECUTE format($$
+            CREATE POLICY same_country_insert ON waterway.%I
+                FOR INSERT TO waterway_admin
+                WITH CHECK (%s)
+            $$, the_table, condition);
+        -- In many cases it is more efficient to check for "staging_done" to
+        -- prevent the more expensive checks for read only access (which is
+        -- allowed for all users, when staging is done).
+        EXECUTE format($$
+            CREATE POLICY same_country_select ON waterway.%I
+                FOR SELECT TO waterway_admin
+                USING (staging_done OR %s)
+            $$, the_table, condition);
+        EXECUTE format($$
+            CREATE POLICY same_country_update ON waterway.%I
+                FOR UPDATE TO waterway_admin
+                USING (%s)
+            $$, the_table, condition);
+        EXECUTE format($$
+            CREATE POLICY same_country_delete ON waterway.%I
+                FOR DELETE TO waterway_admin
+                USING (%s)
+            $$, the_table, condition);
+    END LOOP;
+END;
+$do$;
 
-CREATE POLICY responsibility_area ON waterway.bottlenecks
-    FOR ALL TO waterway_admin
-    USING (staging_done
-        OR (SELECT ST_Covers(a,
-                ST_Transform(CAST(area AS geometry), ST_SRID(a)))
-            FROM users.current_user_area_utm() AS a (a)))
-    WITH CHECK ((SELECT ST_Covers(a,
-            ST_Transform(CAST(area AS geometry), ST_SRID(a)))
-        FROM users.current_user_area_utm() AS a (a)));
-
-CREATE POLICY responsibility_area ON waterway.sounding_results
-    FOR ALL TO waterway_admin
-    USING (staging_done
-        OR (SELECT ST_Covers(a,
-                ST_Transform(CAST(area AS geometry), ST_SRID(a)))
-            FROM users.current_user_area_utm() AS a (a)))
-    WITH CHECK ((SELECT ST_Covers(a,
-            ST_Transform(CAST(area AS geometry), ST_SRID(a)))
-        FROM users.current_user_area_utm() AS a (a)));
-
-CREATE POLICY responsibility_area ON waterway.fairway_dimensions
-    FOR ALL TO waterway_admin
-    USING (staging_done
-        OR (SELECT ST_Covers(a,
-                ST_Transform(CAST(area AS geometry), ST_SRID(a)))
-            FROM users.current_user_area_utm() AS a (a)))
-    WITH CHECK ((SELECT ST_Covers(a,
-            ST_Transform(CAST(area AS geometry), ST_SRID(a)))
-        FROM users.current_user_area_utm() AS a (a)));
+DO LANGUAGE plpgsql
+$do$
+DECLARE
+    the_table varchar;
+    condition CONSTANT text = $$
+        (SELECT ST_Covers(a, ST_Transform(CAST(area AS geometry), ST_SRID(a)))
+            FROM users.current_user_area_utm() AS a (a))
+        $$;
+BEGIN
+    FOREACH the_table IN ARRAY ARRAY[
+        'fairway_dimensions',
+        'bottlenecks',
+        'sounding_results']
+    LOOP
+        EXECUTE format($$
+            CREATE POLICY responsibility_area_insert ON waterway.%I
+                FOR INSERT TO waterway_admin
+                WITH CHECK (%s)
+            $$, the_table, condition);
+        -- In many cases it is more efficient to check for "staging_done" to
+        -- prevent the more expensive checks for read only access (which is
+        -- allowed for all users, when staging is done).
+        EXECUTE format($$
+            CREATE POLICY responsibility_area_select ON waterway.%I
+                FOR SELECT TO waterway_admin
+                USING (staging_done OR %s)
+            $$, the_table, condition);
+        EXECUTE format($$
+            CREATE POLICY responsibility_area_update ON waterway.%I
+                FOR UPDATE TO waterway_admin
+                USING (%s)
+            $$, the_table, condition);
+        EXECUTE format($$
+            CREATE POLICY responsibility_area_delete ON waterway.%I
+                FOR DELETE TO waterway_admin
+                USING (%s)
+            $$, the_table, condition);
+    END LOOP;
+END;
+$do$;
 
 -- In the case of sections differentiating between read and write
 -- access is not neccessary: the country code based access check is
--- a/schema/auth_tests.sql	Mon Mar 16 17:11:53 2020 +0100
+++ b/schema/auth_tests.sql	Wed Mar 18 12:16:42 2020 +0100
@@ -102,6 +102,41 @@
     42501, NULL,
     'Waterway admin cannot insert data outside his region');
 
+-- Ensure a USING clause prevents access in an UPDATE
+SELECT is_empty($$
+    WITH a AS (SELECT users.current_user_area_utm() AS a)
+    UPDATE waterway.bottlenecks
+        SET objnam = 'Now it''s mine',
+            area = ST_geomfromtext(
+                'MULTIPOLYGON(((0 0, 0 1, 1 1, 1 0, 0 0)))', 4326)
+        WHERE bottleneck_id = 'testbottleneck3'
+        RETURNING *
+    $$,
+    'Waterway admin cannot move data from outside his region inside');
+
+-- Ensure a WITH CHECK or USING clause prevents writing such rows
+SELECT throws_ok($$
+    WITH a AS (SELECT users.current_user_area_utm() AS a)
+    UPDATE waterway.bottlenecks
+        SET objnam = 'Give-away',
+            area = ST_geomfromtext(
+                'MULTIPOLYGON(((1 0, 1 1, 2 1, 2 0, 1 0)))', 4326)
+        WHERE bottleneck_id = 'testbottleneck2'
+        RETURNING *
+    $$,
+    42501, NULL,
+    'Waterway admin cannot move data from inside his region outside');
+
+SELECT is_empty($$
+    WITH a AS (SELECT users.current_user_area_utm() AS a)
+    DELETE FROM waterway.bottlenecks
+        WHERE NOT ST_Covers((SELECT a FROM a),
+            ST_Transform(
+                CAST(area AS geometry), ST_SRID((SELECT a FROM a))))
+        RETURNING *
+    $$,
+    'Waterway admin cannot delete data outside his region');
+
 -- template management
 SELECT lives_ok($$
     INSERT INTO users.templates (template_name, template_data, country)
--- a/schema/run_tests.sh	Mon Mar 16 17:11:53 2020 +0100
+++ b/schema/run_tests.sh	Wed Mar 18 12:16:42 2020 +0100
@@ -80,7 +80,7 @@
     -c 'SET client_min_messages TO WARNING' \
     -c "DROP ROLE IF EXISTS $TEST_ROLES" \
     -f "$BASEDIR"/tap_tests_data.sql \
-    -c "SELECT plan(82 + (
+    -c "SELECT plan(85 + (
             SELECT count(*)::int
                 FROM information_schema.tables
                 WHERE table_schema = 'waterway'))" \
--- a/schema/tap_tests_data.sql	Mon Mar 16 17:11:53 2020 +0100
+++ b/schema/tap_tests_data.sql	Wed Mar 18 12:16:42 2020 +0100
@@ -102,6 +102,20 @@
         ST_geomfromtext('MULTIPOLYGON(((0 0, 0 1, 1 1, 1 0, 0 0)))', 4326),
         'AT', 'AT', 'AT',
         1, 'depth', current_timestamp, 'testorganization', true
+    ), (
+        'testbottleneck3',
+        isrsrange(('RO', 'XXX', '00001', '00000', 0)::isrs,
+            ('RO', 'XXX', '00001', '00000', 2)::isrs),
+        ST_geomfromtext('MULTIPOLYGON(((1 0, 1 1, 2 1, 2 0, 1 0)))', 4326),
+        'RO', 'RO', 'RO',
+        1, 'depth', current_timestamp, 'testorganization', true
+    ), (
+        'testbottleneck4',
+        isrsrange(('RO', 'XXX', '00001', '00000', 0)::isrs,
+            ('RO', 'XXX', '00001', '00000', 2)::isrs),
+        ST_geomfromtext('MULTIPOLYGON(((1 0, 1 1, 2 1, 2 0, 1 0)))', 4326),
+        'RO', 'RO', 'RO',
+        1, 'depth', current_timestamp, 'testorganization', true
     ))
 INSERT INTO waterway.bottlenecks (
     gauge_location, validity,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/schema/updates/1427/01.fix_rls_policies.sql	Wed Mar 18 12:16:42 2020 +0100
@@ -0,0 +1,86 @@
+DROP POLICY same_country ON waterway.gauge_measurements;
+DROP POLICY same_country ON waterway.waterway_profiles;
+
+DO LANGUAGE plpgsql
+$do$
+DECLARE
+    the_table varchar;
+    condition CONSTANT text = $$
+        (location).country_code =
+            (SELECT country FROM users.list_users
+                WHERE username = current_user)
+        $$;
+BEGIN
+    FOREACH the_table IN ARRAY ARRAY[
+        'gauge_measurements',
+        'waterway_profiles']
+    LOOP
+        EXECUTE format($$
+            CREATE POLICY same_country_insert ON waterway.%I
+                FOR INSERT TO waterway_admin
+                WITH CHECK (%s)
+            $$, the_table, condition);
+        EXECUTE format($$
+            CREATE POLICY same_country_select ON waterway.%I
+                FOR SELECT TO waterway_admin
+                USING (staging_done OR %s)
+            $$, the_table, condition);
+        EXECUTE format($$
+            CREATE POLICY same_country_update ON waterway.%I
+                FOR UPDATE TO waterway_admin
+                USING (%s)
+            $$, the_table, condition);
+        EXECUTE format($$
+            CREATE POLICY same_country_delete ON waterway.%I
+                FOR DELETE TO waterway_admin
+                USING (%s)
+            $$, the_table, condition);
+    END LOOP;
+END;
+$do$;
+
+
+DROP POLICY responsibility_area ON waterway.bottlenecks;
+DROP POLICY responsibility_area ON waterway.sounding_results;
+DROP POLICY responsibility_area ON waterway.fairway_dimensions;
+
+DO LANGUAGE plpgsql
+$do$
+DECLARE
+    the_table varchar;
+    condition CONSTANT text = $$
+        (SELECT ST_Covers(a, ST_Transform(CAST(area AS geometry), ST_SRID(a)))
+            FROM users.current_user_area_utm() AS a (a))
+        $$;
+BEGIN
+    FOREACH the_table IN ARRAY ARRAY[
+        'fairway_dimensions',
+        'bottlenecks',
+        'sounding_results']
+    LOOP
+        EXECUTE format($$
+            CREATE POLICY responsibility_area_insert ON waterway.%I
+                FOR INSERT TO waterway_admin
+                WITH CHECK (%s)
+            $$, the_table, condition);
+        -- In many cases it is more efficient to check for "staging_done" to
+        -- prevent the more expensive checks for read only access (which is
+        -- allowed for all users, when staging is done).
+        EXECUTE format($$
+            CREATE POLICY responsibility_area_select ON waterway.%I
+                FOR SELECT TO waterway_admin
+                USING (staging_done OR %s)
+            $$, the_table, condition);
+        EXECUTE format($$
+            CREATE POLICY responsibility_area_update ON waterway.%I
+                FOR UPDATE TO waterway_admin
+                USING (%s)
+            $$, the_table, condition);
+        EXECUTE format($$
+            CREATE POLICY responsibility_area_delete ON waterway.%I
+                FOR DELETE TO waterway_admin
+                USING (%s)
+            $$, the_table, condition);
+    END LOOP;
+END;
+$do$;