diff --git a/client/.gitignore b/client/.gitignore index dff332e..4e31250 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -2,9 +2,10 @@ results/ regression.diffs regression.out 20_public_functions.sql +25_exception_safe_private_functions.sql 30_plproxy_functions.sql 90_grant_execute.sql cdb_geocoder_client--0.0.1.sql cdb_geocoder_client--0.1.0.sql cdb_geocoder_client--0.2.0.sql -cdb_geocoder_client--0.3.0.sql \ No newline at end of file +cdb_geocoder_client--0.3.0.sql diff --git a/client/Makefile b/client/Makefile index fe6939d..3930d5d 100644 --- a/client/Makefile +++ b/client/Makefile @@ -57,9 +57,11 @@ all: $(DATA) .PHONY: release release: $(EXTENSION).control $(SOURCES_DATA) test -n "$(NEW_VERSION)" # $$NEW_VERSION VARIABLE MISSING. Eg. make release NEW_VERSION=0.x.0 - mv *.sql old_versions + git mv *.sql old_versions $(SED) $(REPLACEMENTS) $(EXTENSION).control + git add $(EXTENSION).control cat $(SOURCES_DATA_DIR)/*.sql > $(EXTENSION)--$(NEW_VERSION).sql + git add $(EXTENSION)--$(NEW_VERSION).sql $(ERB) version=$(NEW_VERSION) upgrade_downgrade_template.erb > $(EXTENSION)--$(EXTVERSION)--$(NEW_VERSION).sql $(ERB) version=$(EXTVERSION) upgrade_downgrade_template.erb > $(EXTENSION)--$(NEW_VERSION)--$(EXTVERSION).sql diff --git a/client/renderer/templates/25_exception_safe_private_functions.erb b/client/renderer/templates/25_exception_safe_private_functions.erb new file mode 100644 index 0000000..2dde980 --- /dev/null +++ b/client/renderer/templates/25_exception_safe_private_functions.erb @@ -0,0 +1,61 @@ +-- +-- Exception-safe private DataServices API function +-- + +CREATE OR REPLACE FUNCTION <%= DATASERVICES_CLIENT_SCHEMA %>._<%= name %>_exception_safe (<%= params_with_type_and_default.join(' ,') %>) +RETURNS <%= return_type %> AS $$ +DECLARE + <% if not multi_row %>ret <%= return_type %>;<% end %> + username text; + orgname text; + _returned_sqlstate TEXT; + _message_text TEXT; + _pg_exception_context TEXT; +BEGIN + IF session_user = 'publicuser' OR session_user ~ 'cartodb_publicuser_*' THEN + RAISE EXCEPTION 'The api_key must be provided'; + END IF; + SELECT u, o INTO username, orgname FROM <%= DATASERVICES_CLIENT_SCHEMA %>._cdb_entity_config() AS (u text, o text); + -- JSON value stored "" is taken as literal + IF username IS NULL OR username = '' OR username = '""' THEN + RAISE EXCEPTION 'Username is a mandatory argument, check it out'; + END IF; + + <% if multi_row %> + BEGIN + RETURN QUERY + SELECT * FROM <%= DATASERVICES_CLIENT_SCHEMA %>._<%= name %>(<%= ['username', 'orgname'].concat(params).join(', ') %>); + EXCEPTION + WHEN OTHERS THEN + GET STACKED DIAGNOSTICS _returned_sqlstate = RETURNED_SQLSTATE, + _message_text = MESSAGE_TEXT, + _pg_exception_context = PG_EXCEPTION_CONTEXT; + RAISE WARNING USING ERRCODE = _returned_sqlstate, MESSAGE = _message_text, DETAIL = _pg_exception_context; + END; + <% elsif multi_field %> + BEGIN + SELECT * FROM <%= DATASERVICES_CLIENT_SCHEMA %>._<%= name %>(<%= ['username', 'orgname'].concat(params).join(', ') %>) INTO ret; + RETURN ret; + EXCEPTION + WHEN OTHERS THEN + GET STACKED DIAGNOSTICS _returned_sqlstate = RETURNED_SQLSTATE, + _message_text = MESSAGE_TEXT, + _pg_exception_context = PG_EXCEPTION_CONTEXT; + RAISE WARNING USING ERRCODE = _returned_sqlstate, MESSAGE = _message_text, DETAIL = _pg_exception_context; + RETURN ret; + END; + <% else %> + BEGIN + SELECT <%= DATASERVICES_CLIENT_SCHEMA %>._<%= name %>(<%= ['username', 'orgname'].concat(params).join(', ') %>) INTO ret; + RETURN ret; + EXCEPTION + WHEN OTHERS THEN + GET STACKED DIAGNOSTICS _returned_sqlstate = RETURNED_SQLSTATE, + _message_text = MESSAGE_TEXT, + _pg_exception_context = PG_EXCEPTION_CONTEXT; + RAISE WARNING USING ERRCODE = _returned_sqlstate, MESSAGE = _message_text, DETAIL = _pg_exception_context; + RETURN ret; + END; + <% end %> +END; +$$ LANGUAGE 'plpgsql' SECURITY DEFINER; diff --git a/client/renderer/templates/90_grant_execute.erb b/client/renderer/templates/90_grant_execute.erb index bde5944..b6c797e 100644 --- a/client/renderer/templates/90_grant_execute.erb +++ b/client/renderer/templates/90_grant_execute.erb @@ -1 +1,2 @@ GRANT EXECUTE ON FUNCTION <%= DATASERVICES_CLIENT_SCHEMA %>.<%= name %>(<%= params_with_type.join(', ') %>) TO publicuser; +GRANT EXECUTE ON FUNCTION <%= DATASERVICES_CLIENT_SCHEMA %>._<%= name %>_exception_safe(<%= params_with_type.join(', ') %>) TO publicuser; diff --git a/client/test/expected/25_exception_safe_private_functions_test.out b/client/test/expected/25_exception_safe_private_functions_test.out new file mode 100644 index 0000000..6d18ab6 --- /dev/null +++ b/client/test/expected/25_exception_safe_private_functions_test.out @@ -0,0 +1,56 @@ +SET client_min_messages TO warning; +SET search_path TO public,cartodb,cdb_dataservices_client; +-- Mock the server functions to raise exceptions +CREATE OR REPLACE FUNCTION cdb_dataservices_server.cdb_geocode_street_point (username text, orgname text, searchtext text, city text DEFAULT NULL, state_province text DEFAULT NULL, country text DEFAULT NULL) +RETURNS Geometry AS $$ +BEGIN + RAISE EXCEPTION 'Not enough quota or any other exception whatsoever.'; + RETURN NULL; +END; +$$ LANGUAGE 'plpgsql'; +CREATE OR REPLACE FUNCTION cdb_dataservices_server.cdb_isodistance(username text, orgname text, source geometry, mode text, range integer[], options text[] DEFAULT ARRAY[]::text[]) +RETURNS SETOF isoline AS $$ +BEGIN + RAISE EXCEPTION 'Not enough quota or any other exception whatsoever.'; +END; +$$ LANGUAGE 'plpgsql'; +CREATE OR REPLACE FUNCTION cdb_dataservices_server.cdb_route_point_to_point (username text, orgname text, origin geometry(Point, 4326), destination geometry(Point, 4326), mode TEXT, options text[] DEFAULT ARRAY[]::text[], units text DEFAULT 'kilometers') +RETURNS cdb_dataservices_client.simple_route AS $$ +DECLARE + ret cdb_dataservices_client.simple_route; +BEGIN + RAISE EXCEPTION 'Not enough quota or any other exception whatsoever.'; + + -- This code shall never be reached + SELECT NULL, 5.33, 100 INTO ret; + RETURN ret; +END; +$$ LANGUAGE 'plpgsql'; +-- Use regular user role +SET ROLE test_regular_user; +-- Exercise the exception safe and the proxied functions +SELECT _cdb_geocode_street_point_exception_safe('One street, 1'); +WARNING: cdb_dataservices_client._cdb_geocode_street_point(6): [contrib_regression] REMOTE ERROR: Not enough quota or any other exception whatsoever. +DETAIL: SQL statement "SELECT cdb_dataservices_client._cdb_geocode_street_point(username, orgname, searchtext, city, state_province, country)" +PL/pgSQL function _cdb_geocode_street_point_exception_safe(text,text,text,text) line 21 at SQL statement + _cdb_geocode_street_point_exception_safe +------------------------------------------ + +(1 row) + +SELECT * FROM _cdb_isodistance_exception_safe('POINT(-3.70568 40.42028)'::geometry, 'walk', ARRAY[300]::integer[]); +WARNING: cdb_dataservices_client._cdb_isodistance(6): [contrib_regression] REMOTE ERROR: Not enough quota or any other exception whatsoever. +DETAIL: PL/pgSQL function _cdb_isodistance_exception_safe(geometry,text,integer[],text[]) line 21 at RETURN QUERY + center | data_range | the_geom +--------+------------+---------- +(0 rows) + +SELECT * FROM _cdb_route_point_to_point_exception_safe('POINT(-3.70237112 40.41706163)'::geometry,'POINT(-3.69909883 40.41236875)'::geometry, 'car', ARRAY['mode_type=shortest']::text[]); +WARNING: cdb_dataservices_client._cdb_route_point_to_point(7): [contrib_regression] REMOTE ERROR: Not enough quota or any other exception whatsoever. +DETAIL: SQL statement "SELECT * FROM cdb_dataservices_client._cdb_route_point_to_point(username, orgname, origin, destination, mode, options, units)" +PL/pgSQL function _cdb_route_point_to_point_exception_safe(geometry,geometry,text,text[],text) line 21 at SQL statement + shape | length | duration +-------+--------+---------- + | | +(1 row) + diff --git a/client/test/sql/25_exception_safe_private_functions_test.sql b/client/test/sql/25_exception_safe_private_functions_test.sql new file mode 100644 index 0000000..ba45130 --- /dev/null +++ b/client/test/sql/25_exception_safe_private_functions_test.sql @@ -0,0 +1,41 @@ +SET client_min_messages TO warning; +SET search_path TO public,cartodb,cdb_dataservices_client; + +-- Mock the server functions to raise exceptions +CREATE OR REPLACE FUNCTION cdb_dataservices_server.cdb_geocode_street_point (username text, orgname text, searchtext text, city text DEFAULT NULL, state_province text DEFAULT NULL, country text DEFAULT NULL) +RETURNS Geometry AS $$ +BEGIN + RAISE EXCEPTION 'Not enough quota or any other exception whatsoever.'; + RETURN NULL; +END; +$$ LANGUAGE 'plpgsql'; + +CREATE OR REPLACE FUNCTION cdb_dataservices_server.cdb_isodistance(username text, orgname text, source geometry, mode text, range integer[], options text[] DEFAULT ARRAY[]::text[]) +RETURNS SETOF isoline AS $$ +BEGIN + RAISE EXCEPTION 'Not enough quota or any other exception whatsoever.'; +END; +$$ LANGUAGE 'plpgsql'; + +CREATE OR REPLACE FUNCTION cdb_dataservices_server.cdb_route_point_to_point (username text, orgname text, origin geometry(Point, 4326), destination geometry(Point, 4326), mode TEXT, options text[] DEFAULT ARRAY[]::text[], units text DEFAULT 'kilometers') +RETURNS cdb_dataservices_client.simple_route AS $$ +DECLARE + ret cdb_dataservices_client.simple_route; +BEGIN + RAISE EXCEPTION 'Not enough quota or any other exception whatsoever.'; + + -- This code shall never be reached + SELECT NULL, 5.33, 100 INTO ret; + RETURN ret; +END; +$$ LANGUAGE 'plpgsql'; + + + +-- Use regular user role +SET ROLE test_regular_user; + +-- Exercise the exception safe and the proxied functions +SELECT _cdb_geocode_street_point_exception_safe('One street, 1'); +SELECT * FROM _cdb_isodistance_exception_safe('POINT(-3.70568 40.42028)'::geometry, 'walk', ARRAY[300]::integer[]); +SELECT * FROM _cdb_route_point_to_point_exception_safe('POINT(-3.70237112 40.41706163)'::geometry,'POINT(-3.69909883 40.41236875)'::geometry, 'car', ARRAY['mode_type=shortest']::text[]);