diff --git a/sql/010-core.sql b/sql/010-core.sql index 9f3be0c..6ddc00d 100644 --- a/sql/010-core.sql +++ b/sql/010-core.sql @@ -14,17 +14,123 @@ BEGIN END $$; -DROP FUNCTION IF EXISTS _cs_encrypted_check_kind(jsonb); -CREATE FUNCTION _cs_encrypted_check_kind(val jsonb) - RETURNS BOOLEAN -LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -BEGIN ATOMIC - RETURN ( - (val->>'k' = 'ct' AND val ? 'c') OR - (val->>'k' = 'sv' AND val ? 'sv') - ) AND NOT val ? 'p'; -END; +-- Should include a kind field +DROP FUNCTION IF EXISTS _cs_encrypted_check_k(jsonb); +CREATE FUNCTION _cs_encrypted_check_k(val jsonb) + RETURNS boolean +AS $$ + BEGIN + IF (val->>'k' = ANY('{ct, sv}')) THEN + RETURN true; + END IF; + RAISE 'Invalid kind (%) in Encrypted column. Kind should be one of {ct, sv}', val; + END; +$$ LANGUAGE plpgsql; + + +-- +-- CT payload should include a c field +-- +DROP FUNCTION IF EXISTS _cs_encrypted_check_k_ct(jsonb); +CREATE FUNCTION _cs_encrypted_check_k_ct(val jsonb) + RETURNS boolean +AS $$ + BEGIN + IF (val->>'k' = 'ct') THEN + IF (val ? 'c') THEN + RETURN true; + END IF; + RAISE 'Encrypted column kind (k) of "ct" missing data field (c): %', val; + END IF; + RETURN true; + END; +$$ LANGUAGE plpgsql; + + +-- +-- SV payload should include an sv field +-- +DROP FUNCTION IF EXISTS _cs_encrypted_check_k_sv(jsonb); +CREATE FUNCTION _cs_encrypted_check_k_sv(val jsonb) + RETURNS boolean +AS $$ + BEGIN + IF (val->>'k' = 'sv') THEN + IF (val ? 'sv') THEN + RETURN true; + END IF; + RAISE 'Encrypted column kind (k) of "sv" missing data field (sv): %', val; + END IF; + RETURN true; + END; +$$ LANGUAGE plpgsql; + + +-- Plaintext field should never be present in an encrypted column +DROP FUNCTION IF EXISTS _cs_encrypted_check_p(jsonb); +CREATE FUNCTION _cs_encrypted_check_p(val jsonb) + RETURNS boolean +AS $$ + BEGIN + IF NOT val ? 'p' THEN + RETURN true; + END IF; + RAISE 'Encrypted column includes plaintext (p) field: %', val; + END; +$$ LANGUAGE plpgsql; + +-- Should include an ident field +DROP FUNCTION IF EXISTS _cs_encrypted_check_i(jsonb); +CREATE FUNCTION _cs_encrypted_check_i(val jsonb) + RETURNS boolean +AS $$ + BEGIN + IF val ? 'i' THEN + RETURN true; + END IF; + RAISE 'Encrypted column missing ident (i) field: %', val; + END; +$$ LANGUAGE plpgsql; + +-- Query field should never be present in an encrypted column +DROP FUNCTION IF EXISTS _cs_encrypted_check_q(jsonb); +CREATE FUNCTION _cs_encrypted_check_q(val jsonb) + RETURNS boolean +AS $$ + BEGIN + IF val ? 'q' + RAISE 'Encrypted column includes query (q) field: %', val; + END IF; + RETURN true; + END; +$$ LANGUAGE plpgsql; + +-- Ident field should include table and column +DROP FUNCTION IF EXISTS _cs_encrypted_check_i_ct(jsonb); +CREATE FUNCTION _cs_encrypted_check_i_ct(val jsonb) + RETURNS boolean +AS $$ + BEGIN + IF (val->'i' ?& array['t', 'c']) THEN + RETURN true; + END IF; + RAISE 'Encrypted column ident (i) missing table (t) or column (c) fields: %', val; + END; +$$ LANGUAGE plpgsql; + +-- Should include a version field +DROP FUNCTION IF EXISTS _cs_encrypted_check_v(jsonb); +CREATE FUNCTION _cs_encrypted_check_v(val jsonb) + RETURNS boolean +AS $$ + BEGIN + IF (val ? 'v') THEN + RETURN true; + END IF; + RAISE 'Encrypted column missing version (v) field: %', val; + END; +$$ LANGUAGE plpgsql; DROP FUNCTION IF EXISTS cs_check_encrypted_v1(val jsonb); @@ -34,14 +140,13 @@ CREATE FUNCTION cs_check_encrypted_v1(val jsonb) LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE BEGIN ATOMIC RETURN ( - -- version and source are required - val ?& array['v'] AND - - -- table and column - val->'i' ?& array['t', 'c'] AND - - -- plaintext or ciphertext for kind - _cs_encrypted_check_kind(val) + _cs_encrypted_check_v(val) AND + _cs_encrypted_check_i(val) AND + _cs_encrypted_check_k(val) AND + _cs_encrypted_check_k_ct(val) AND + _cs_encrypted_check_k_sv(val) AND + _cs_encrypted_check_q(val) AND + _cs_encrypted_check_p(val) ); END; diff --git a/sql/020-config-schema.sql b/sql/020-config-schema.sql index f4ff92c..a54b3dc 100644 --- a/sql/020-config-schema.sql +++ b/sql/020-config-schema.sql @@ -33,39 +33,90 @@ DO $$ END $$; + + +-- +-- Extracts index keys/names from configuration json +-- +-- Used by the _cs_config_check_indexes as part of the cs_configuration_data_v1_check constraint +-- +DROP FUNCTION IF EXISTS _cs_extract_indexes(jsonb); +CREATE FUNCTION _cs_extract_indexes(val jsonb) + RETURNS SETOF text + LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +BEGIN ATOMIC + SELECT jsonb_object_keys(jsonb_path_query(val,'$.tables.*.*.indexes')); +END; + -- -- _cs_check_config_indexes returns true if the table configuration only includes valid index types -- -- Used by the cs_configuration_data_v1_check constraint -- --- Function types cannot be changed after creation so we always DROP & CREATE for flexibility --- DROP FUNCTION IF EXISTS _cs_config_check_indexes(jsonb); - CREATE FUNCTION _cs_config_check_indexes(val jsonb) RETURNS BOOLEAN -LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -BEGIN ATOMIC - SELECT jsonb_object_keys(jsonb_path_query(val, '$.tables.*.*.indexes')) = ANY('{match, ore, unique, ste_vec}'); -END; +AS $$ + BEGIN + IF (SELECT EXISTS (SELECT _cs_extract_indexes(val))) THEN + IF (SELECT bool_and(index = ANY('{match, ore, unique, ste_vec}')) FROM _cs_extract_indexes(val) AS index) THEN + RETURN true; + END IF; + RAISE 'Configuration has an invalid index (%). Index should be one of {match, ore, unique, ste_vec}', val; + END IF; + RETURN true; + END; +$$ LANGUAGE plpgsql; DROP FUNCTION IF EXISTS _cs_config_check_cast(jsonb); CREATE FUNCTION _cs_config_check_cast(val jsonb) RETURNS BOOLEAN -LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE -BEGIN ATOMIC - SELECT jsonb_array_elements_text(jsonb_path_query_array(val, '$.tables.*.*.cast_as')) = ANY('{text, int, small_int, big_int, real, double, boolean, date, jsonb}'); -END; +AS $$ + BEGIN + IF EXISTS (SELECT jsonb_array_elements_text(jsonb_path_query_array(val, '$.tables.*.*.cast_as')) = ANY('{text, int, small_int, big_int, real, double, boolean, date, jsonb}')) THEN + RETURN true; + END IF; + RAISE 'Configuration has an invalid cast_as (%). Cast should be one of {text, int, small_int, big_int, real, double, boolean, date, jsonb}', val; + END; +$$ LANGUAGE plpgsql; + +-- +-- Should include a tables field +-- Tables should not be empty +DROP FUNCTION IF EXISTS _cs_config_check_tables(jsonb); +CREATE FUNCTION _cs_config_check_tables(val jsonb) + RETURNS boolean +AS $$ + BEGIN + IF (val ? 'tables') AND (val->'tables' <> '{}'::jsonb) THEN + RETURN true; + END IF; + RAISE 'Configuration missing tables (tables) field: %', val; + END; +$$ LANGUAGE plpgsql; + +-- Should include a version field +DROP FUNCTION IF EXISTS _cs_config_check_v(jsonb); +CREATE FUNCTION _cs_config_check_v(val jsonb) + RETURNS boolean +AS $$ + BEGIN + IF (val ? 'v') THEN + RETURN true; + END IF; + RAISE 'Configuration missing version (v) field: %', val; + END; +$$ LANGUAGE plpgsql; ALTER DOMAIN cs_configuration_data_v1 DROP CONSTRAINT IF EXISTS cs_configuration_data_v1_check; ALTER DOMAIN cs_configuration_data_v1 ADD CONSTRAINT cs_configuration_data_v1_check CHECK ( - VALUE ?& array['v', 'tables'] AND - VALUE->'tables' <> '{}'::jsonb AND + _cs_config_check_v(VALUE) AND + _cs_config_check_tables(VALUE) AND _cs_config_check_cast(VALUE) AND _cs_config_check_indexes(VALUE) ); diff --git a/sql/021-config-functions.sql b/sql/021-config-functions.sql index 511b53b..22093bc 100644 --- a/sql/021-config-functions.sql +++ b/sql/021-config-functions.sql @@ -299,9 +299,9 @@ AS $$ $$ LANGUAGE plpgsql; -DROP FUNCTION IF EXISTS cs_add_column_v1(table_name text, column_name text); +DROP FUNCTION IF EXISTS cs_add_column_v1(table_name text, column_name text, cast_as text); -CREATE FUNCTION cs_add_column_v1(table_name text, column_name text) +CREATE FUNCTION cs_add_column_v1(table_name text, column_name text, cast_as text DEFAULT 'text') RETURNS jsonb AS $$ DECLARE @@ -323,6 +323,8 @@ AS $$ SELECT _cs_config_add_column(table_name, column_name, _config) INTO _config; + SELECT _cs_config_add_cast(table_name, column_name, cast_as, _config) INTO _config; + -- create a new pending record if we don't have one INSERT INTO cs_configuration_v1 (state, data) VALUES ('pending', _config) ON CONFLICT (state) diff --git a/sql/666-drop_types.sql b/sql/666-drop_types.sql index b494f36..8de0cb6 100644 --- a/sql/666-drop_types.sql +++ b/sql/666-drop_types.sql @@ -2,7 +2,7 @@ DROP TYPE IF EXISTS ore_64_8_v1; DROP TYPE IF EXISTS ore_64_8_v1_term; DROP TYPE IF EXISTS cs_ste_vec_index_v1; -DROP TYPE IF EXISTS ste_vec_v1_entry; +DROP TYPE IF EXISTS cs_ste_vec_v1_entry; DROP TYPE IF EXISTS ore_cllw_8_v1; DROP TYPE IF EXISTS ore_cllw_8_variable_v1; DROP TYPE IF EXISTS cs_ste_vec_encrypted_term_v1; diff --git a/tests/config.sql b/tests/config.sql index d094d6a..8274935 100644 --- a/tests/config.sql +++ b/tests/config.sql @@ -148,8 +148,12 @@ INSERT INTO cs_configuration_v1 (state, data) VALUES ( "blah": { "cast_as": "text", "indexes": { - "match": {} + "match": {} } + }, + "vtha": { + "cast_as": "text", + "indexes": {} } } } @@ -207,7 +211,7 @@ TRUNCATE TABLE cs_configuration_v1; DO $$ BEGIN - RAISE NOTICE 'Configuration tests: 4 errors expected'; + RAISE NOTICE 'cs_configuration_v1 constraint tests: 4 errors expected here'; END; $$ LANGUAGE plpgsql; -- diff --git a/tests/core.sql b/tests/core.sql index 5480512..6349a4d 100644 --- a/tests/core.sql +++ b/tests/core.sql @@ -59,7 +59,7 @@ $$ LANGUAGE plpgsql; -- ----------------------------------------------- --- --- cs_enncrypted)v1 tyoe +-- cs_encrypted_v1 tyoe -- Validate configuration schema -- Try and insert many invalid configurations -- None should exist @@ -70,6 +70,12 @@ TRUNCATE TABLE users; \set ON_ERROR_STOP off \set ON_ERROR_ROLLBACK on +DO $$ + BEGIN + RAISE NOTICE 'cs_encrypted_v1 constraint tests: 10 errors expected here'; + END; +$$ LANGUAGE plpgsql; + -- no version INSERT INTO users (name_encrypted) VALUES ( @@ -84,7 +90,7 @@ INSERT INTO users (name_encrypted) VALUES ( }'::jsonb ); --- no source detauils +-- no ident details INSERT INTO users (name_encrypted) VALUES ( '{ "v": 1, @@ -93,10 +99,36 @@ INSERT INTO users (name_encrypted) VALUES ( }'::jsonb ); --- pt +-- no kind INSERT INTO users (name_encrypted) VALUES ( '{ "v": 1, + "c": "ciphertext", + "i": { + "t": "users", + "c": "name" + } + }'::jsonb +); + + + +-- bad kind +INSERT INTO users (name_encrypted) VALUES ( + '{ + "v": 1, + "k": "vtha", + "c": "ciphertext", + "i": { + "t": "users", + "c": "name" + } + }'::jsonb +); + +-- pt +INSERT INTO users (name_encrypted) VALUES ( + '{ "v": 1, "k": "pt", "i": { @@ -109,7 +141,6 @@ INSERT INTO users (name_encrypted) VALUES ( --pt with ciphertext INSERT INTO users (name_encrypted) VALUES ( '{ - "v": 1, "v": 1, "k": "pt", "c": "ciphertext", @@ -120,11 +151,9 @@ INSERT INTO users (name_encrypted) VALUES ( }'::jsonb ); - -- ct without ciphertext INSERT INTO users (name_encrypted) VALUES ( '{ - "v": 1, "v": 1, "k": "ct", "i": { @@ -138,7 +167,6 @@ INSERT INTO users (name_encrypted) VALUES ( -- ct with plaintext INSERT INTO users (name_encrypted) VALUES ( '{ - "v": 1, "v": 1, "k": "ct", "p": "plaintext", @@ -153,7 +181,6 @@ INSERT INTO users (name_encrypted) VALUES ( -- ciphertext without ct INSERT INTO users (name_encrypted) VALUES ( '{ - "v": 1, "v": 1, "c": "ciphertext", "i": { @@ -163,6 +190,19 @@ INSERT INTO users (name_encrypted) VALUES ( }'::jsonb ); +-- ciphertext with invalid q +INSERT INTO users (name_encrypted) VALUES ( + '{ + "v": 1, + "c": "ciphertext", + "i": { + "t": "users", + "c": "name" + }, + "q": "invalid" + }'::jsonb +); + -- Nothing should be in the DB DO $$ BEGIN