# HG changeset patch # User Tom Gottfried # Date 1584530202 -3600 # Node ID 4c658a8f34da0e296e51a989ae5080a24ad21bd1 # Parent 36a3dce202321eb28e556cd05c10d5993d7bc763 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. diff -r 36a3dce20232 -r 4c658a8f34da schema/auth.sql --- 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 diff -r 36a3dce20232 -r 4c658a8f34da schema/auth_tests.sql --- 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) diff -r 36a3dce20232 -r 4c658a8f34da schema/run_tests.sh --- 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'))" \ diff -r 36a3dce20232 -r 4c658a8f34da schema/tap_tests_data.sql --- 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, diff -r 36a3dce20232 -r 4c658a8f34da schema/updates/1427/01.fix_rls_policies.sql --- /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$;