Skip to content

Commit 45aa246

Browse files
authored
Merge pull request #53 from cipherstash/CIP-926/constraint-errors
Granular errors on encrypted column constraint
2 parents c71d9fe + d990761 commit 45aa246

File tree

6 files changed

+246
-44
lines changed

6 files changed

+246
-44
lines changed

sql/010-core.sql

Lines changed: 123 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,123 @@ BEGIN
1414
END
1515
$$;
1616

17-
DROP FUNCTION IF EXISTS _cs_encrypted_check_kind(jsonb);
1817

19-
CREATE FUNCTION _cs_encrypted_check_kind(val jsonb)
20-
RETURNS BOOLEAN
21-
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
22-
BEGIN ATOMIC
23-
RETURN (
24-
(val->>'k' = 'ct' AND val ? 'c') OR
25-
(val->>'k' = 'sv' AND val ? 'sv')
26-
) AND NOT val ? 'p';
27-
END;
18+
-- Should include a kind field
19+
DROP FUNCTION IF EXISTS _cs_encrypted_check_k(jsonb);
20+
CREATE FUNCTION _cs_encrypted_check_k(val jsonb)
21+
RETURNS boolean
22+
AS $$
23+
BEGIN
24+
IF (val->>'k' = ANY('{ct, sv}')) THEN
25+
RETURN true;
26+
END IF;
27+
RAISE 'Invalid kind (%) in Encrypted column. Kind should be one of {ct, sv}', val;
28+
END;
29+
$$ LANGUAGE plpgsql;
30+
31+
32+
--
33+
-- CT payload should include a c field
34+
--
35+
DROP FUNCTION IF EXISTS _cs_encrypted_check_k_ct(jsonb);
36+
CREATE FUNCTION _cs_encrypted_check_k_ct(val jsonb)
37+
RETURNS boolean
38+
AS $$
39+
BEGIN
40+
IF (val->>'k' = 'ct') THEN
41+
IF (val ? 'c') THEN
42+
RETURN true;
43+
END IF;
44+
RAISE 'Encrypted column kind (k) of "ct" missing data field (c): %', val;
45+
END IF;
46+
RETURN true;
47+
END;
48+
$$ LANGUAGE plpgsql;
49+
50+
51+
--
52+
-- SV payload should include an sv field
53+
--
54+
DROP FUNCTION IF EXISTS _cs_encrypted_check_k_sv(jsonb);
55+
CREATE FUNCTION _cs_encrypted_check_k_sv(val jsonb)
56+
RETURNS boolean
57+
AS $$
58+
BEGIN
59+
IF (val->>'k' = 'sv') THEN
60+
IF (val ? 'sv') THEN
61+
RETURN true;
62+
END IF;
63+
RAISE 'Encrypted column kind (k) of "sv" missing data field (sv): %', val;
64+
END IF;
65+
RETURN true;
66+
END;
67+
$$ LANGUAGE plpgsql;
68+
69+
70+
-- Plaintext field should never be present in an encrypted column
71+
DROP FUNCTION IF EXISTS _cs_encrypted_check_p(jsonb);
72+
CREATE FUNCTION _cs_encrypted_check_p(val jsonb)
73+
RETURNS boolean
74+
AS $$
75+
BEGIN
76+
IF NOT val ? 'p' THEN
77+
RETURN true;
78+
END IF;
79+
RAISE 'Encrypted column includes plaintext (p) field: %', val;
80+
END;
81+
$$ LANGUAGE plpgsql;
82+
83+
-- Should include an ident field
84+
DROP FUNCTION IF EXISTS _cs_encrypted_check_i(jsonb);
85+
CREATE FUNCTION _cs_encrypted_check_i(val jsonb)
86+
RETURNS boolean
87+
AS $$
88+
BEGIN
89+
IF val ? 'i' THEN
90+
RETURN true;
91+
END IF;
92+
RAISE 'Encrypted column missing ident (i) field: %', val;
93+
END;
94+
$$ LANGUAGE plpgsql;
95+
96+
-- Query field should never be present in an encrypted column
97+
DROP FUNCTION IF EXISTS _cs_encrypted_check_q(jsonb);
98+
CREATE FUNCTION _cs_encrypted_check_q(val jsonb)
99+
RETURNS boolean
100+
AS $$
101+
BEGIN
102+
IF val ? 'q'
103+
RAISE 'Encrypted column includes query (q) field: %', val;
104+
END IF;
105+
RETURN true;
106+
END;
107+
$$ LANGUAGE plpgsql;
108+
109+
-- Ident field should include table and column
110+
DROP FUNCTION IF EXISTS _cs_encrypted_check_i_ct(jsonb);
111+
CREATE FUNCTION _cs_encrypted_check_i_ct(val jsonb)
112+
RETURNS boolean
113+
AS $$
114+
BEGIN
115+
IF (val->'i' ?& array['t', 'c']) THEN
116+
RETURN true;
117+
END IF;
118+
RAISE 'Encrypted column ident (i) missing table (t) or column (c) fields: %', val;
119+
END;
120+
$$ LANGUAGE plpgsql;
121+
122+
-- Should include a version field
123+
DROP FUNCTION IF EXISTS _cs_encrypted_check_v(jsonb);
124+
CREATE FUNCTION _cs_encrypted_check_v(val jsonb)
125+
RETURNS boolean
126+
AS $$
127+
BEGIN
128+
IF (val ? 'v') THEN
129+
RETURN true;
130+
END IF;
131+
RAISE 'Encrypted column missing version (v) field: %', val;
132+
END;
133+
$$ LANGUAGE plpgsql;
28134

29135

30136
DROP FUNCTION IF EXISTS cs_check_encrypted_v1(val jsonb);
@@ -34,14 +140,13 @@ CREATE FUNCTION cs_check_encrypted_v1(val jsonb)
34140
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
35141
BEGIN ATOMIC
36142
RETURN (
37-
-- version and source are required
38-
val ?& array['v'] AND
39-
40-
-- table and column
41-
val->'i' ?& array['t', 'c'] AND
42-
43-
-- plaintext or ciphertext for kind
44-
_cs_encrypted_check_kind(val)
143+
_cs_encrypted_check_v(val) AND
144+
_cs_encrypted_check_i(val) AND
145+
_cs_encrypted_check_k(val) AND
146+
_cs_encrypted_check_k_ct(val) AND
147+
_cs_encrypted_check_k_sv(val) AND
148+
_cs_encrypted_check_q(val) AND
149+
_cs_encrypted_check_p(val)
45150
);
46151
END;
47152

sql/020-config-schema.sql

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,39 +33,90 @@ DO $$
3333
END
3434
$$;
3535

36+
37+
38+
--
39+
-- Extracts index keys/names from configuration json
40+
--
41+
-- Used by the _cs_config_check_indexes as part of the cs_configuration_data_v1_check constraint
42+
--
43+
DROP FUNCTION IF EXISTS _cs_extract_indexes(jsonb);
44+
CREATE FUNCTION _cs_extract_indexes(val jsonb)
45+
RETURNS SETOF text
46+
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
47+
BEGIN ATOMIC
48+
SELECT jsonb_object_keys(jsonb_path_query(val,'$.tables.*.*.indexes'));
49+
END;
50+
3651
--
3752
-- _cs_check_config_indexes returns true if the table configuration only includes valid index types
3853
--
3954
-- Used by the cs_configuration_data_v1_check constraint
4055
--
41-
-- Function types cannot be changed after creation so we always DROP & CREATE for flexibility
42-
--
4356
DROP FUNCTION IF EXISTS _cs_config_check_indexes(jsonb);
44-
4557
CREATE FUNCTION _cs_config_check_indexes(val jsonb)
4658
RETURNS BOOLEAN
47-
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
48-
BEGIN ATOMIC
49-
SELECT jsonb_object_keys(jsonb_path_query(val, '$.tables.*.*.indexes')) = ANY('{match, ore, unique, ste_vec}');
50-
END;
59+
AS $$
60+
BEGIN
61+
IF (SELECT EXISTS (SELECT _cs_extract_indexes(val))) THEN
62+
IF (SELECT bool_and(index = ANY('{match, ore, unique, ste_vec}')) FROM _cs_extract_indexes(val) AS index) THEN
63+
RETURN true;
64+
END IF;
65+
RAISE 'Configuration has an invalid index (%). Index should be one of {match, ore, unique, ste_vec}', val;
66+
END IF;
67+
RETURN true;
68+
END;
69+
$$ LANGUAGE plpgsql;
5170

5271

5372
DROP FUNCTION IF EXISTS _cs_config_check_cast(jsonb);
5473

5574
CREATE FUNCTION _cs_config_check_cast(val jsonb)
5675
RETURNS BOOLEAN
57-
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
58-
BEGIN ATOMIC
59-
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}');
60-
END;
76+
AS $$
77+
BEGIN
78+
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
79+
RETURN true;
80+
END IF;
81+
RAISE 'Configuration has an invalid cast_as (%). Cast should be one of {text, int, small_int, big_int, real, double, boolean, date, jsonb}', val;
82+
END;
83+
$$ LANGUAGE plpgsql;
84+
85+
--
86+
-- Should include a tables field
87+
-- Tables should not be empty
88+
DROP FUNCTION IF EXISTS _cs_config_check_tables(jsonb);
89+
CREATE FUNCTION _cs_config_check_tables(val jsonb)
90+
RETURNS boolean
91+
AS $$
92+
BEGIN
93+
IF (val ? 'tables') AND (val->'tables' <> '{}'::jsonb) THEN
94+
RETURN true;
95+
END IF;
96+
RAISE 'Configuration missing tables (tables) field: %', val;
97+
END;
98+
$$ LANGUAGE plpgsql;
99+
100+
-- Should include a version field
101+
DROP FUNCTION IF EXISTS _cs_config_check_v(jsonb);
102+
CREATE FUNCTION _cs_config_check_v(val jsonb)
103+
RETURNS boolean
104+
AS $$
105+
BEGIN
106+
IF (val ? 'v') THEN
107+
RETURN true;
108+
END IF;
109+
RAISE 'Configuration missing version (v) field: %', val;
110+
END;
111+
$$ LANGUAGE plpgsql;
61112

62113

63114
ALTER DOMAIN cs_configuration_data_v1 DROP CONSTRAINT IF EXISTS cs_configuration_data_v1_check;
64115

65116
ALTER DOMAIN cs_configuration_data_v1
66117
ADD CONSTRAINT cs_configuration_data_v1_check CHECK (
67-
VALUE ?& array['v', 'tables'] AND
68-
VALUE->'tables' <> '{}'::jsonb AND
118+
_cs_config_check_v(VALUE) AND
119+
_cs_config_check_tables(VALUE) AND
69120
_cs_config_check_cast(VALUE) AND
70121
_cs_config_check_indexes(VALUE)
71122
);

sql/021-config-functions.sql

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,9 +299,9 @@ AS $$
299299
$$ LANGUAGE plpgsql;
300300

301301

302-
DROP FUNCTION IF EXISTS cs_add_column_v1(table_name text, column_name text);
302+
DROP FUNCTION IF EXISTS cs_add_column_v1(table_name text, column_name text, cast_as text);
303303

304-
CREATE FUNCTION cs_add_column_v1(table_name text, column_name text)
304+
CREATE FUNCTION cs_add_column_v1(table_name text, column_name text, cast_as text DEFAULT 'text')
305305
RETURNS jsonb
306306
AS $$
307307
DECLARE
@@ -323,6 +323,8 @@ AS $$
323323

324324
SELECT _cs_config_add_column(table_name, column_name, _config) INTO _config;
325325

326+
SELECT _cs_config_add_cast(table_name, column_name, cast_as, _config) INTO _config;
327+
326328
-- create a new pending record if we don't have one
327329
INSERT INTO cs_configuration_v1 (state, data) VALUES ('pending', _config)
328330
ON CONFLICT (state)

sql/666-drop_types.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
DROP TYPE IF EXISTS ore_64_8_v1;
33
DROP TYPE IF EXISTS ore_64_8_v1_term;
44
DROP TYPE IF EXISTS cs_ste_vec_index_v1;
5-
DROP TYPE IF EXISTS ste_vec_v1_entry;
5+
DROP TYPE IF EXISTS cs_ste_vec_v1_entry;
66
DROP TYPE IF EXISTS ore_cllw_8_v1;
77
DROP TYPE IF EXISTS ore_cllw_8_variable_v1;
88
DROP TYPE IF EXISTS cs_ste_vec_encrypted_term_v1;

tests/config.sql

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,12 @@ INSERT INTO cs_configuration_v1 (state, data) VALUES (
148148
"blah": {
149149
"cast_as": "text",
150150
"indexes": {
151-
"match": {}
151+
"match": {}
152152
}
153+
},
154+
"vtha": {
155+
"cast_as": "text",
156+
"indexes": {}
153157
}
154158
}
155159
}
@@ -207,7 +211,7 @@ TRUNCATE TABLE cs_configuration_v1;
207211

208212
DO $$
209213
BEGIN
210-
RAISE NOTICE 'Configuration tests: 4 errors expected';
214+
RAISE NOTICE 'cs_configuration_v1 constraint tests: 4 errors expected here';
211215
END;
212216
$$ LANGUAGE plpgsql;
213217
--

0 commit comments

Comments
 (0)