Skip to content

Commit

Permalink
Merge pull request #53 from cipherstash/CIP-926/constraint-errors
Browse files Browse the repository at this point in the history
Granular errors on encrypted column constraint
  • Loading branch information
tobyhede authored Nov 3, 2024
2 parents c71d9fe + d990761 commit 45aa246
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 44 deletions.
141 changes: 123 additions & 18 deletions sql/010-core.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;

Expand Down
77 changes: 64 additions & 13 deletions sql/020-config-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
Expand Down
6 changes: 4 additions & 2 deletions sql/021-config-functions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion sql/666-drop_types.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
8 changes: 6 additions & 2 deletions tests/config.sql
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,12 @@ INSERT INTO cs_configuration_v1 (state, data) VALUES (
"blah": {
"cast_as": "text",
"indexes": {
"match": {}
"match": {}
}
},
"vtha": {
"cast_as": "text",
"indexes": {}
}
}
}
Expand Down Expand Up @@ -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;
--
Expand Down
Loading

0 comments on commit 45aa246

Please sign in to comment.