diff --git a/src/config/config_test.sql b/src/config/config_test.sql index 227e9c93..26b02f51 100644 --- a/src/config/config_test.sql +++ b/src/config/config_test.sql @@ -4,7 +4,7 @@ -- -- Helper function for assertions -- --- DROP FUNCTION IF EXISTS _index_exists(text, text, text, text); +DROP FUNCTION IF EXISTS _index_exists(text, text, text, text); CREATE FUNCTION _index_exists(table_name text, column_name text, index_name text, state text DEFAULT 'pending') RETURNS boolean LANGUAGE sql STRICT PARALLEL SAFE @@ -21,7 +21,6 @@ END; -- ----------------------------------------------- TRUNCATE TABLE eql_v1_configuration; - DO $$ BEGIN @@ -29,21 +28,21 @@ DO $$ PERFORM eql_v1.add_index('users', 'name', 'match'); ASSERT (SELECT _index_exists('users', 'name', 'match')); - -- -- Add index with cast - -- PERFORM eql_v1.add_index('users', 'name', 'unique', 'int'); - -- ASSERT (SELECT _index_exists('users', 'name', 'unique')); + -- Add index with cast + PERFORM eql_v1.add_index('users', 'name', 'unique', 'int'); + ASSERT (SELECT _index_exists('users', 'name', 'unique')); - -- ASSERT (SELECT EXISTS (SELECT id FROM eql_v1_configuration c - -- WHERE c.state = 'pending' AND - -- c.data #> array['tables', 'users', 'name'] ? 'cast_as')); + ASSERT (SELECT EXISTS (SELECT id FROM eql_v1_configuration c + WHERE c.state = 'pending' AND + c.data #> array['tables', 'users', 'name'] ? 'cast_as')); - -- -- Match index removed - -- PERFORM eql_v1.remove_index('users', 'name', 'match'); - -- ASSERT NOT (SELECT _index_exists('users', 'name', 'match')); + -- Match index removed + PERFORM eql_v1.remove_index('users', 'name', 'match'); + ASSERT NOT (SELECT _index_exists('users', 'name', 'match')); - -- -- All indexes removed, delete the emtpty pending config - -- PERFORM eql_v1.remove_index('users', 'name', 'unique'); - -- ASSERT (SELECT NOT EXISTS (SELECT FROM eql_v1_configuration c WHERE c.state = 'pending')); + -- All indexes removed, delete the emtpty pending config + PERFORM eql_v1.remove_index('users', 'name', 'unique'); + ASSERT (SELECT NOT EXISTS (SELECT FROM eql_v1_configuration c WHERE c.state = 'pending')); END; $$ LANGUAGE plpgsql; @@ -96,7 +95,7 @@ DO $$ END; $$ LANGUAGE plpgsql; -SELECT FROM eql_v1_configuration c WHERE c.state = 'pending'; +-- SELECT FROM eql_v1_configuration c WHERE c.state = 'pending'; -- ----------------------------------------------- @@ -183,17 +182,47 @@ $$ LANGUAGE plpgsql; TRUNCATE TABLE eql_v1_configuration; DO $$ BEGIN - -- Create pending configuration - PERFORM eql_v1.add_column('user', 'name'); - ASSERT (SELECT EXISTS (SELECT FROM eql_v1_configuration c WHERE c.state = 'pending')); - PERFORM eql_v1.remove_column('user', 'name'); + PERFORM assert_exception( + 'Cannot add index to column that does not exist', + 'SELECT eql_v1.add_column(''user'', ''name'')'); + + PERFORM assert_no_result( + 'No configuration was created', + 'SELECT * FROM eql_v1_configuration'); + END; +$$ LANGUAGE plpgsql; + + + +-- -- ----------------------------------------------- +-- -- Add and remove column +-- -- +-- -- ----------------------------------------------- +TRUNCATE TABLE eql_v1_configuration; +DO $$ + BEGIN + -- reset the table + PERFORM create_table_with_encrypted(); + + PERFORM eql_v1.add_column('encrypted', 'e'); + + PERFORM assert_count( + 'Pending configuration was created', + 'SELECT * FROM eql_v1_configuration c WHERE c.state = ''pending''', + 1); + + + PERFORM eql_v1.remove_column('encrypted', 'e'); + + PERFORM assert_no_result( + 'Pending configuration was removed', + 'SELECT * FROM eql_v1_configuration c WHERE c.state = ''pending'''); - -- Config now empty and removed - ASSERT (SELECT NOT EXISTS (SELECT FROM eql_v1_configuration c WHERE c.state = 'pending')); END; $$ LANGUAGE plpgsql; + -- ----------------------------------------------- --- -- eql_v1_configuration tyoe diff --git a/src/config/functions.sql b/src/config/functions.sql index 100d00a3..257aa704 100644 --- a/src/config/functions.sql +++ b/src/config/functions.sql @@ -233,7 +233,7 @@ $$ LANGUAGE plpgsql; -- -- Marks the currently `pending` configuration as `encrypting`. -- --- Validates the database schema and raises an exception if the configured columns are not of `jsonb` or `cs_encrypted_v1` type. +-- Validates the database schema and raises an exception if the configured columns are not `cs_encrypted_v1` type. -- -- Accepts an optional `force` parameter. -- If `force` is `true`, the schema validation is skipped. @@ -242,7 +242,7 @@ $$ LANGUAGE plpgsql; -- -- DROP FUNCTION IF EXISTS eql_v1.encrypt(); -CREATE FUNCTION eql_v1.encrypt(force boolean DEFAULT false) +CREATE FUNCTION eql_v1.encrypt() RETURNS boolean AS $$ BEGIN @@ -255,10 +255,8 @@ AS $$ RAISE EXCEPTION 'No pending configuration exists to encrypt'; END IF; - IF NOT force THEN - IF NOT eql_v1.ready_for_encryption() THEN - RAISE EXCEPTION 'Some pending columns do not have an encrypted target'; - END IF; + IF NOT eql_v1.ready_for_encryption() THEN + RAISE EXCEPTION 'Some pending columns do not have an encrypted target'; END IF; UPDATE public.eql_v1_configuration SET state = 'encrypting' WHERE state = 'pending'; @@ -334,6 +332,8 @@ AS $$ DO UPDATE SET data = _config; + PERFORM eql_v1.add_encrypted_constraint(table_name, column_name); + -- exeunt RETURN _config; END; @@ -389,6 +389,8 @@ AS $$ UPDATE public.eql_v1_configuration SET data = _config WHERE state = 'pending'; END IF; + PERFORM eql_v1.remove_encrypted_constraint(table_name, column_name); + -- exeunt RETURN _config; diff --git a/src/encrypted/casts.sql b/src/encrypted/casts.sql index 2b6470be..adf555e8 100644 --- a/src/encrypted/casts.sql +++ b/src/encrypted/casts.sql @@ -20,7 +20,7 @@ $$ LANGUAGE plpgsql; -- DROP CAST IF EXISTS (jsonb AS public.eql_v1_encrypted); CREATE CAST (jsonb AS public.eql_v1_encrypted) - WITH FUNCTION eql_v1.to_encrypted(jsonb) AS IMPLICIT; + WITH FUNCTION eql_v1.to_encrypted(jsonb) AS ASSIGNMENT; -- @@ -41,7 +41,7 @@ $$ LANGUAGE plpgsql; -- DROP CAST IF EXISTS (text AS public.eql_v1_encrypted); CREATE CAST (text AS public.eql_v1_encrypted) - WITH FUNCTION eql_v1.to_encrypted(text) AS IMPLICIT; + WITH FUNCTION eql_v1.to_encrypted(text) AS ASSIGNMENT; diff --git a/src/encrypted/constraints.sql b/src/encrypted/constraints.sql index 3296b25a..e7edf3f9 100644 --- a/src/encrypted/constraints.sql +++ b/src/encrypted/constraints.sql @@ -1,82 +1,9 @@ +-- REQUIRE: src/schema.sql -- REQUIRE: src/encrypted/types.sql -- REQUIRE: src/encrypted/functions.sql - --- --- DEPRECATED --- --- -- Should include a kind field --- DROP FUNCTION IF EXISTS eql_v1._encrypted_check_k(jsonb); --- CREATE FUNCTION eql_v1._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; - --- --- DEPRECATED --- --- --- CT payload should include a c field --- --- DROP FUNCTION IF EXISTS eql_v1._encrypted_check_k_ct(jsonb); --- CREATE FUNCTION eql_v1._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 eql_v1._encrypted_check_k_sv(jsonb); --- CREATE FUNCTION eql_v1._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; - --- --- DEPRECATED --- --- Plaintext field should never be present in an encrypted column --- DROP FUNCTION IF EXISTS eql_v1._encrypted_check_p(jsonb); --- CREATE FUNCTION eql_v1._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 eql_v1._encrypted_check_i(jsonb); CREATE FUNCTION eql_v1._encrypted_check_i(val jsonb) RETURNS boolean AS $$ @@ -89,24 +16,7 @@ AS $$ $$ LANGUAGE plpgsql; --- --- DEPRECATED --- --- Query field should never be present in an encrypted column --- DROP FUNCTION IF EXISTS eql_v1._encrypted_check_q(jsonb); --- CREATE FUNCTION eql_v1._encrypted_check_q(val jsonb) --- RETURNS boolean --- AS $$ --- BEGIN --- IF val ? 'q' THEN --- 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 eql_v1._encrypted_check_i_ct(jsonb); CREATE FUNCTION eql_v1._encrypted_check_i_ct(val jsonb) RETURNS boolean AS $$ @@ -119,48 +29,48 @@ AS $$ $$ LANGUAGE plpgsql; -- -- Should include a version field --- DROP FUNCTION IF EXISTS eql_v1._encrypted_check_v(jsonb); --- CREATE FUNCTION eql_v1._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; +CREATE FUNCTION eql_v1._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 eql_v1.check_encrypted(val jsonb); +-- -- Should include a ciphertext field +CREATE FUNCTION eql_v1._encrypted_check_c(val jsonb) + RETURNS boolean +AS $$ + BEGIN + IF (val ? 'c') THEN + RETURN true; + END IF; + RAISE 'Encrypted column missing ciphertext (c) field: %', val; + END; +$$ LANGUAGE plpgsql; + CREATE FUNCTION eql_v1.check_encrypted(val jsonb) RETURNS BOOLEAN LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE BEGIN ATOMIC RETURN ( - -- eql_v1._encrypted_check_v(val) AND + eql_v1._encrypted_check_v(val) AND + eql_v1._encrypted_check_c(val) AND eql_v1._encrypted_check_i(val) AND eql_v1._encrypted_check_i_ct(val) - -- eql_v1._encrypted_check_k(val) AND - -- eql_v1._encrypted_check_k_ct(val) AND - -- eql_v1._encrypted_check_k_sv(val) AND - -- eql_v1._encrypted_check_q(val) AND - -- eql_v1._encrypted_check_p(val) ); END; --- ALTER DOMAIN eql_v1_encrypted DROP CONSTRAINT IF EXISTS eql_v1_encrypted_check; - --- ALTER DOMAIN eql_v1_encrypted --- ADD CONSTRAINT eql_v1_encrypted_check CHECK ( --- eql_v1.check_encrypted(VALUE) --- ); --- ALTER DOMAIN eql_v1_encrypted DROP CONSTRAINT IF EXISTS eql_v1_encrypted_check; - --- ALTER DOMAIN eql_v1_encrypted --- ADD CONSTRAINT eql_v1_encrypted_check CHECK ( --- eql_v1.check_encrypted(VALUE) --- ); +CREATE FUNCTION eql_v1.check_encrypted(val eql_v1_encrypted) + RETURNS BOOLEAN +LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE +BEGIN ATOMIC + RETURN eql_v1.check_encrypted(val.data); +END; diff --git a/src/encrypted/constraints_test.sql b/src/encrypted/constraints_test.sql new file mode 100644 index 00000000..75a825f0 --- /dev/null +++ b/src/encrypted/constraints_test.sql @@ -0,0 +1,48 @@ +\set ON_ERROR_STOP on + +SELECT create_table_with_encrypted(); + +DO $$ + BEGIN + -- insert without constraint works + INSERT INTO encrypted(e) VALUES ('{}'::jsonb::eql_v1_encrypted); + + -- delete the data + PERFORM create_table_with_encrypted(); + + -- add constraint + PERFORM eql_v1.add_encrypted_constraint('encrypted', 'e'); + + PERFORM assert_exception( + 'Constraint catches invalid eql_v1_encrypted', + 'INSERT INTO encrypted (e) VALUES (''{}''::jsonb::eql_v1_encrypted)'); + + END; +$$ LANGUAGE plpgsql; + + +DO $$ + BEGIN + -- reset the table + PERFORM create_table_with_encrypted(); + + -- add constraint + PERFORM eql_v1.add_encrypted_constraint('encrypted', 'e'); + + PERFORM assert_exception( + 'Constraint catches invalid eql_v1_encrypted', + 'INSERT INTO encrypted (e) VALUES (''{}''::jsonb::eql_v1_encrypted)'); + + PERFORM eql_v1.remove_encrypted_constraint('encrypted', 'e'); + + PERFORM assert_result( + 'Insert invalid data without constraint', + 'INSERT INTO encrypted (e) VALUES (''{}''::jsonb::eql_v1_encrypted) RETURNING id'); + + END; +$$ LANGUAGE plpgsql; + + + + + diff --git a/src/encrypted/functions.sql b/src/encrypted/functions.sql index f2009431..198d4ac1 100644 --- a/src/encrypted/functions.sql +++ b/src/encrypted/functions.sql @@ -44,3 +44,36 @@ CREATE AGGREGATE eql_v1.cs_grouped_value(jsonb) ( SFUNC = eql_v1._first_grouped_value, STYPE = jsonb ); + + +-- +-- Adds eql_v1.check_encrypted constraint to the column_name in table_name +-- +-- Executes the ALTER TABLE statement +-- `ALTER TABLE {table_name} ADD CONSTRAINT eql_v1_encrypted_check_{column_name} CHECK (eql_v1.check_encrypted({column_name}))` +-- +-- +CREATE FUNCTION eql_v1.add_encrypted_constraint(table_name TEXT, column_name TEXT) + RETURNS void +AS $$ + BEGIN + EXECUTE format('ALTER TABLE %I ADD CONSTRAINT eql_v1_encrypted_check_%I CHECK (eql_v1.check_encrypted(%I))', table_name, column_name, column_name); + END; +$$ LANGUAGE plpgsql; + + +-- +-- Removes the eql_v1.check_encrypted constraint from the column_name in table_name +-- +-- Executes the ALTER TABLE statement +-- `ALTER TABLE {table_name} DROP CONSTRAINT eql_v1_encrypted_check_{column_name}` +-- +CREATE FUNCTION eql_v1.remove_encrypted_constraint(table_name TEXT, column_name TEXT) + RETURNS void +AS $$ + BEGIN + EXECUTE format('ALTER TABLE %I DROP CONSTRAINT IF EXISTS eql_v1_encrypted_check_%I', table_name, column_name); + END; +$$ LANGUAGE plpgsql; + + diff --git a/src/encrypted/functions_test.sql b/src/encrypted/functions_test.sql deleted file mode 100644 index 15d7c286..00000000 --- a/src/encrypted/functions_test.sql +++ /dev/null @@ -1,22 +0,0 @@ -\set ON_ERROR_STOP on - - -SELECT create_table_with_encrypted(); - - --- DO $$ --- BEGIN --- PERFORM assert_result( --- 'Fetch ciphertext from encrypted column', --- 'SELECT e->>''selector.1'' FROM encrypted;'); --- END; --- $$ LANGUAGE plpgsql; - - --- DO $$ --- BEGIN --- PERFORM assert_result( --- 'Fetch ciphertext from encrypted column', --- 'SELECT e->>''selector.1'' FROM encrypted;'); --- END; --- $$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/src/encryptindex/functions_test.sql b/src/encryptindex/functions_test.sql index 5e89d196..53929b19 100644 --- a/src/encryptindex/functions_test.sql +++ b/src/encryptindex/functions_test.sql @@ -113,59 +113,6 @@ DO $$ $$ LANGUAGE plpgsql; --- ----------------------------------------------- --- Start encryptindexing with no target table --- --- The schema should be validated first. --- Users table does not exist, so should fail. --- ----------------------------------------------- -DROP TABLE IF EXISTS users; -TRUNCATE TABLE eql_v1_configuration; - - -DO $$ - BEGIN - PERFORM eql_v1.add_index('users', 'name', 'match'); - - BEGIN - PERFORM eql_v1.encrypt(); - RAISE NOTICE 'Missing users table. Encrypt should have failed.'; - ASSERT false; -- skipped by exception - EXCEPTION - WHEN OTHERS THEN - ASSERT true; - END; - -- configuration state should not be changed - ASSERT (SELECT EXISTS (SELECT FROM eql_v1_configuration c WHERE c.state = 'pending')); - ASSERT (SELECT NOT EXISTS (SELECT FROM eql_v1_configuration c WHERE c.state = 'encrypting')); - - END; -$$ LANGUAGE plpgsql; - - --- ----------------------------------------------- --- FORCE start encryptindexing with no target table --- --- Schema validation is skipped --- ----------------------------------------------- -DROP TABLE IF EXISTS users; -TRUNCATE TABLE eql_v1_configuration; - -DO $$ - BEGIN - PERFORM eql_v1.add_index('users', 'name', 'match'); - - PERFORM eql_v1.encrypt(true); - RAISE NOTICE 'Missing users table. Encrypt should have failed.'; - - -- configuration state should be changed - ASSERT (SELECT NOT EXISTS (SELECT FROM eql_v1_configuration c WHERE c.state = 'pending')); - ASSERT (SELECT EXISTS (SELECT FROM eql_v1_configuration c WHERE c.state = 'encrypting')); - - END; -$$ LANGUAGE plpgsql; - - -- ----------------------------------------------- -- With existing active config -- and an updated schema