diff --git a/nix/plpgsql-check.nix b/nix/plpgsql-check.nix new file mode 100644 index 0000000..3031ce4 --- /dev/null +++ b/nix/plpgsql-check.nix @@ -0,0 +1,40 @@ +{ + lib, + stdenv, + fetchFromGitHub, + postgresql, +}: + +stdenv.mkDerivation rec { + pname = "plpgsql-check"; + version = "2.10.15"; + + src = fetchFromGitHub { + owner = "okbob"; + repo = "plpgsql_check"; + rev = "v${version}"; + hash = "sha256-war+GVHMDegfDXjxdIsUkrPPDntKeyb/bGxibHVbpDo="; + }; + + buildInputs = [ postgresql ]; + + installPhase = '' + install -D -t $out *${postgresql.dlSuffix} + install -D -t $out *.sql + install -D -t $out *.control + + # Needed for postgres 18+ native implementation of extension_search_path + mkdir $out/extension + install -D -t $out/extension *.sql + install -D -t $out/extension *.control + ''; + + meta = with lib; { + description = "Linter tool for language PL/pgSQL"; + homepage = "https://github.com/okbob/plpgsql_check"; + changelog = "https://github.com/okbob/plpgsql_check/releases/tag/v${version}"; + platforms = postgresql.meta.platforms; + license = licenses.mit; + maintainers = [ maintainers.marsam ]; + }; +} diff --git a/shell.nix b/shell.nix index c28d90a..9f65472 100644 --- a/shell.nix +++ b/shell.nix @@ -19,6 +19,25 @@ in cassert ? true, }: let + plpgsqlcheck14 = pkgs.callPackage ./nix/plpgsql-check.nix { + postgresql = xpgPkgs.postgresql_14; + }; + plpgsqlcheck15 = pkgs.callPackage ./nix/plpgsql-check.nix { + postgresql = xpgPkgs.postgresql_15; + }; + plpgsqlcheck16 = pkgs.callPackage ./nix/plpgsql-check.nix { + postgresql = xpgPkgs.postgresql_16; + }; + plpgsqlcheck17 = pkgs.callPackage ./nix/plpgsql-check.nix { + postgresql = xpgPkgs.postgresql_17; + }; + plpgsqlcheck18 = pkgs.callPackage ./nix/plpgsql-check.nix { + postgresql = xpgPkgs.postgresql_18; + }; + plpgsqlcheck19 = pkgs.callPackage ./nix/plpgsql-check.nix { + postgresql = xpgPkgs.postgresql_19; + }; + style = pkgs.writeShellScriptBin "vault-style" '' ${pkgs.clang-tools}/bin/clang-format -i src/* ${pkgs.findutils}/bin/find . -name '*.nix' -exec ${pkgs.nixfmt}/bin/nixfmt {} \+ @@ -34,16 +53,22 @@ pkgs.mkShell { (xpgPkgs.xpg.withExtensions { extensions = { "14" = [ + plpgsqlcheck14 ]; "15" = [ + plpgsqlcheck15 ]; "16" = [ + plpgsqlcheck16 ]; "17" = [ + plpgsqlcheck17 ]; "18" = [ + plpgsqlcheck18 ]; "19" = [ + plpgsqlcheck19 ]; }; }) diff --git a/sql/supabase_vault--0.3.1--0.3.2.sql b/sql/supabase_vault--0.3.1--0.3.2.sql index 9d6d736..05d7b05 100644 --- a/sql/supabase_vault--0.3.1--0.3.2.sql +++ b/sql/supabase_vault--0.3.1--0.3.2.sql @@ -1,3 +1,33 @@ -- Search pg_temp last so callers can't shadow types (e.g. text, bytea) with temp objects that run as the function owner ALTER FUNCTION vault.create_secret(text, text, text, uuid) SET search_path = pg_catalog, pg_temp; -ALTER FUNCTION vault.update_secret(uuid, text, text, text, uuid) SET search_path = pg_catalog, pg_temp; + +CREATE OR REPLACE FUNCTION vault.update_secret( + secret_id uuid, + new_secret text = NULL, + new_name text = NULL, + new_description text = NULL, + -- unused + new_key_id uuid = NULL +) +RETURNS void +SECURITY DEFINER +LANGUAGE plpgsql +SET search_path = pg_catalog, pg_temp +AS $$ +BEGIN + UPDATE vault.secrets s + SET + secret = CASE WHEN new_secret IS NULL THEN s.secret + ELSE encode(vault._crypto_aead_det_encrypt( + message := convert_to(new_secret, 'utf8'), + additional := convert_to(s.id::text, 'utf8'), + key_id := 0, + context := 'pgsodium'::bytea, + nonce := s.nonce + ), 'base64') END, + name = coalesce(new_name, s.name), + description = coalesce(new_description, s.description), + updated_at = now() + WHERE s.id = secret_id; +END +$$; diff --git a/supabase_vault.sql b/supabase_vault.sql index b327262..de0b3be 100644 --- a/supabase_vault.sql +++ b/supabase_vault.sql @@ -71,8 +71,6 @@ CREATE FUNCTION vault.update_secret(secret_id uuid RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'pg_catalog', 'pg_temp' AS $$ -DECLARE - decrypted_secret text := (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE id = secret_id); BEGIN UPDATE vault.secrets s SET diff --git a/test/expected/test.out b/test/expected/test.out index ca6e401..1517987 100644 --- a/test/expected/test.out +++ b/test/expected/test.out @@ -119,3 +119,34 @@ WITH security_definer_fn_settings AS ( -------------+---------------+----------------- (0 rows) +-- below query is sourced from from pgsql_check's git repo, link below +-- https://github.com/okbob/plpgsql_check/blob/0ac96adc13b34d071c4be8ba0ec4f73e6e7520f5/README.md?plain=1#L310 +-- check all plpgsql functions (functions or trigger functions with defined triggers) +SELECT + (pcf).functionid::regprocedure, (pcf).lineno, (pcf).statement, + (pcf).sqlstate, (pcf).message, (pcf).detail, (pcf).hint, (pcf).level, + (pcf)."position", (pcf).query, (pcf).context +FROM +( + SELECT + plpgsql_check_function_tb(pg_proc.oid, COALESCE(pg_trigger.tgrelid, 0), + oldtable=>pg_trigger.tgoldtable, + newtable=>pg_trigger.tgnewtable) AS pcf + FROM pg_proc + LEFT JOIN pg_trigger + ON (pg_trigger.tgfoid = pg_proc.oid) + WHERE + prolang = (SELECT lang.oid FROM pg_language lang WHERE lang.lanname = 'plpgsql') AND + pronamespace <> (SELECT nsp.oid FROM pg_namespace nsp WHERE nsp.nspname = 'pg_catalog') AND + -- ignore unused triggers + (pg_proc.prorettype <> (SELECT typ.oid FROM pg_type typ WHERE typ.typname = 'trigger') OR + pg_trigger.tgfoid IS NOT NULL) + OFFSET 0 +) ss +ORDER BY (pcf).functionid::regprocedure::text, (pcf).lineno; + functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context +-----------------------------------------------+--------+-----------+----------+-------------------------------+--------+------+---------------+----------+-------+--------- + vault.create_secret(text,text,text,uuid) | | | 00000 | unused parameter "new_key_id" | | | warning extra | | | + vault.update_secret(uuid,text,text,text,uuid) | | | 00000 | unused parameter "new_key_id" | | | warning extra | | | +(2 rows) + diff --git a/test/init.sql b/test/init.sql index 4bd8803..2943d18 100644 --- a/test/init.sql +++ b/test/init.sql @@ -1,5 +1,6 @@ CREATE ROLE bob login password 'bob'; +CREATE EXTENSION plpgsql_check; CREATE EXTENSION supabase_vault CASCADE; GRANT USAGE ON SCHEMA vault TO bob WITH GRANT OPTION; diff --git a/test/sql/test.sql b/test/sql/test.sql index 778bbb8..f9aa172 100644 --- a/test/sql/test.sql +++ b/test/sql/test.sql @@ -97,3 +97,29 @@ WITH security_definer_fn_settings AS ( WHERE array_position(s.search_path_ary, 'pg_temp') < array_position(s.search_path_ary, 'pg_catalog') AND array_position(s.search_path_ary, 'pg_temp') IS NOT NULL AND array_position(s.search_path_ary, 'pg_catalog') IS NOT NULL; + +-- below query is sourced from from pgsql_check's git repo, link below +-- https://github.com/okbob/plpgsql_check/blob/0ac96adc13b34d071c4be8ba0ec4f73e6e7520f5/README.md?plain=1#L310 +-- check all plpgsql functions (functions or trigger functions with defined triggers) +SELECT + (pcf).functionid::regprocedure, (pcf).lineno, (pcf).statement, + (pcf).sqlstate, (pcf).message, (pcf).detail, (pcf).hint, (pcf).level, + (pcf)."position", (pcf).query, (pcf).context +FROM +( + SELECT + plpgsql_check_function_tb(pg_proc.oid, COALESCE(pg_trigger.tgrelid, 0), + oldtable=>pg_trigger.tgoldtable, + newtable=>pg_trigger.tgnewtable) AS pcf + FROM pg_proc + LEFT JOIN pg_trigger + ON (pg_trigger.tgfoid = pg_proc.oid) + WHERE + prolang = (SELECT lang.oid FROM pg_language lang WHERE lang.lanname = 'plpgsql') AND + pronamespace <> (SELECT nsp.oid FROM pg_namespace nsp WHERE nsp.nspname = 'pg_catalog') AND + -- ignore unused triggers + (pg_proc.prorettype <> (SELECT typ.oid FROM pg_type typ WHERE typ.typname = 'trigger') OR + pg_trigger.tgfoid IS NOT NULL) + OFFSET 0 +) ss +ORDER BY (pcf).functionid::regprocedure::text, (pcf).lineno;