Skip to content

Commit 0e24389

Browse files
authored
Merge pull request #104 from cipherstash/encrypted-column-constraint
feat: encrypted column constraint
2 parents bb0ceb9 + b3ea42f commit 0e24389

File tree

8 files changed

+172
-225
lines changed

8 files changed

+172
-225
lines changed

src/config/config_test.sql

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
--
55
-- Helper function for assertions
66
--
7-
-- DROP FUNCTION IF EXISTS _index_exists(text, text, text, text);
7+
DROP FUNCTION IF EXISTS _index_exists(text, text, text, text);
88
CREATE FUNCTION _index_exists(table_name text, column_name text, index_name text, state text DEFAULT 'pending')
99
RETURNS boolean
1010
LANGUAGE sql STRICT PARALLEL SAFE
@@ -21,29 +21,28 @@ END;
2121
-- -----------------------------------------------
2222
TRUNCATE TABLE eql_v1_configuration;
2323

24-
2524
DO $$
2625
BEGIN
2726

2827
-- Add indexes
2928
PERFORM eql_v1.add_index('users', 'name', 'match');
3029
ASSERT (SELECT _index_exists('users', 'name', 'match'));
3130

32-
-- -- Add index with cast
33-
-- PERFORM eql_v1.add_index('users', 'name', 'unique', 'int');
34-
-- ASSERT (SELECT _index_exists('users', 'name', 'unique'));
31+
-- Add index with cast
32+
PERFORM eql_v1.add_index('users', 'name', 'unique', 'int');
33+
ASSERT (SELECT _index_exists('users', 'name', 'unique'));
3534

36-
-- ASSERT (SELECT EXISTS (SELECT id FROM eql_v1_configuration c
37-
-- WHERE c.state = 'pending' AND
38-
-- c.data #> array['tables', 'users', 'name'] ? 'cast_as'));
35+
ASSERT (SELECT EXISTS (SELECT id FROM eql_v1_configuration c
36+
WHERE c.state = 'pending' AND
37+
c.data #> array['tables', 'users', 'name'] ? 'cast_as'));
3938

40-
-- -- Match index removed
41-
-- PERFORM eql_v1.remove_index('users', 'name', 'match');
42-
-- ASSERT NOT (SELECT _index_exists('users', 'name', 'match'));
39+
-- Match index removed
40+
PERFORM eql_v1.remove_index('users', 'name', 'match');
41+
ASSERT NOT (SELECT _index_exists('users', 'name', 'match'));
4342

44-
-- -- All indexes removed, delete the emtpty pending config
45-
-- PERFORM eql_v1.remove_index('users', 'name', 'unique');
46-
-- ASSERT (SELECT NOT EXISTS (SELECT FROM eql_v1_configuration c WHERE c.state = 'pending'));
43+
-- All indexes removed, delete the emtpty pending config
44+
PERFORM eql_v1.remove_index('users', 'name', 'unique');
45+
ASSERT (SELECT NOT EXISTS (SELECT FROM eql_v1_configuration c WHERE c.state = 'pending'));
4746

4847
END;
4948
$$ LANGUAGE plpgsql;
@@ -96,7 +95,7 @@ DO $$
9695
END;
9796
$$ LANGUAGE plpgsql;
9897

99-
SELECT FROM eql_v1_configuration c WHERE c.state = 'pending';
98+
-- SELECT FROM eql_v1_configuration c WHERE c.state = 'pending';
10099

101100

102101
-- -----------------------------------------------
@@ -183,17 +182,47 @@ $$ LANGUAGE plpgsql;
183182
TRUNCATE TABLE eql_v1_configuration;
184183
DO $$
185184
BEGIN
186-
-- Create pending configuration
187-
PERFORM eql_v1.add_column('user', 'name');
188-
ASSERT (SELECT EXISTS (SELECT FROM eql_v1_configuration c WHERE c.state = 'pending'));
189185

190-
PERFORM eql_v1.remove_column('user', 'name');
186+
PERFORM assert_exception(
187+
'Cannot add index to column that does not exist',
188+
'SELECT eql_v1.add_column(''user'', ''name'')');
189+
190+
PERFORM assert_no_result(
191+
'No configuration was created',
192+
'SELECT * FROM eql_v1_configuration');
193+
END;
194+
$$ LANGUAGE plpgsql;
195+
196+
197+
198+
-- -- -----------------------------------------------
199+
-- -- Add and remove column
200+
-- --
201+
-- -- -----------------------------------------------
202+
TRUNCATE TABLE eql_v1_configuration;
203+
DO $$
204+
BEGIN
205+
-- reset the table
206+
PERFORM create_table_with_encrypted();
207+
208+
PERFORM eql_v1.add_column('encrypted', 'e');
209+
210+
PERFORM assert_count(
211+
'Pending configuration was created',
212+
'SELECT * FROM eql_v1_configuration c WHERE c.state = ''pending''',
213+
1);
214+
215+
216+
PERFORM eql_v1.remove_column('encrypted', 'e');
217+
218+
PERFORM assert_no_result(
219+
'Pending configuration was removed',
220+
'SELECT * FROM eql_v1_configuration c WHERE c.state = ''pending''');
191221

192-
-- Config now empty and removed
193-
ASSERT (SELECT NOT EXISTS (SELECT FROM eql_v1_configuration c WHERE c.state = 'pending'));
194222
END;
195223
$$ LANGUAGE plpgsql;
196224

225+
197226
-- -----------------------------------------------
198227
---
199228
-- eql_v1_configuration tyoe

src/config/functions.sql

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ $$ LANGUAGE plpgsql;
233233
--
234234
-- Marks the currently `pending` configuration as `encrypting`.
235235
--
236-
-- Validates the database schema and raises an exception if the configured columns are not of `jsonb` or `cs_encrypted_v1` type.
236+
-- Validates the database schema and raises an exception if the configured columns are not `cs_encrypted_v1` type.
237237
--
238238
-- Accepts an optional `force` parameter.
239239
-- If `force` is `true`, the schema validation is skipped.
@@ -242,7 +242,7 @@ $$ LANGUAGE plpgsql;
242242
--
243243
-- DROP FUNCTION IF EXISTS eql_v1.encrypt();
244244

245-
CREATE FUNCTION eql_v1.encrypt(force boolean DEFAULT false)
245+
CREATE FUNCTION eql_v1.encrypt()
246246
RETURNS boolean
247247
AS $$
248248
BEGIN
@@ -255,10 +255,8 @@ AS $$
255255
RAISE EXCEPTION 'No pending configuration exists to encrypt';
256256
END IF;
257257

258-
IF NOT force THEN
259-
IF NOT eql_v1.ready_for_encryption() THEN
260-
RAISE EXCEPTION 'Some pending columns do not have an encrypted target';
261-
END IF;
258+
IF NOT eql_v1.ready_for_encryption() THEN
259+
RAISE EXCEPTION 'Some pending columns do not have an encrypted target';
262260
END IF;
263261

264262
UPDATE public.eql_v1_configuration SET state = 'encrypting' WHERE state = 'pending';
@@ -334,6 +332,8 @@ AS $$
334332
DO UPDATE
335333
SET data = _config;
336334

335+
PERFORM eql_v1.add_encrypted_constraint(table_name, column_name);
336+
337337
-- exeunt
338338
RETURN _config;
339339
END;
@@ -389,6 +389,8 @@ AS $$
389389
UPDATE public.eql_v1_configuration SET data = _config WHERE state = 'pending';
390390
END IF;
391391

392+
PERFORM eql_v1.remove_encrypted_constraint(table_name, column_name);
393+
392394
-- exeunt
393395
RETURN _config;
394396

src/encrypted/casts.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ $$ LANGUAGE plpgsql;
2020
-- DROP CAST IF EXISTS (jsonb AS public.eql_v1_encrypted);
2121

2222
CREATE CAST (jsonb AS public.eql_v1_encrypted)
23-
WITH FUNCTION eql_v1.to_encrypted(jsonb) AS IMPLICIT;
23+
WITH FUNCTION eql_v1.to_encrypted(jsonb) AS ASSIGNMENT;
2424

2525

2626
--
@@ -41,7 +41,7 @@ $$ LANGUAGE plpgsql;
4141
-- DROP CAST IF EXISTS (text AS public.eql_v1_encrypted);
4242

4343
CREATE CAST (text AS public.eql_v1_encrypted)
44-
WITH FUNCTION eql_v1.to_encrypted(text) AS IMPLICIT;
44+
WITH FUNCTION eql_v1.to_encrypted(text) AS ASSIGNMENT;
4545

4646

4747

src/encrypted/constraints.sql

Lines changed: 31 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,9 @@
1+
-- REQUIRE: src/schema.sql
12
-- REQUIRE: src/encrypted/types.sql
23
-- REQUIRE: src/encrypted/functions.sql
34

45

5-
6-
--
7-
-- DEPRECATED
8-
--
9-
-- -- Should include a kind field
10-
-- DROP FUNCTION IF EXISTS eql_v1._encrypted_check_k(jsonb);
11-
-- CREATE FUNCTION eql_v1._encrypted_check_k(val jsonb)
12-
-- RETURNS boolean
13-
-- AS $$
14-
-- BEGIN
15-
-- IF (val->>'k' = ANY('{ct, sv}')) THEN
16-
-- RETURN true;
17-
-- END IF;
18-
-- RAISE 'Invalid kind (%) in Encrypted column. Kind should be one of {ct, sv}', val;
19-
-- END;
20-
-- $$ LANGUAGE plpgsql;
21-
22-
--
23-
-- DEPRECATED
24-
--
25-
--
26-
-- CT payload should include a c field
27-
--
28-
-- DROP FUNCTION IF EXISTS eql_v1._encrypted_check_k_ct(jsonb);
29-
-- CREATE FUNCTION eql_v1._encrypted_check_k_ct(val jsonb)
30-
-- RETURNS boolean
31-
-- AS $$
32-
-- BEGIN
33-
-- IF (val->>'k' = 'ct') THEN
34-
-- IF (val ? 'c') THEN
35-
-- RETURN true;
36-
-- END IF;
37-
-- RAISE 'Encrypted column kind (k) of "ct" missing data field (c): %', val;
38-
-- END IF;
39-
-- RETURN true;
40-
-- END;
41-
-- $$ LANGUAGE plpgsql;
42-
43-
44-
--
45-
-- SV payload should include an sv field
46-
--
47-
-- DROP FUNCTION IF EXISTS eql_v1._encrypted_check_k_sv(jsonb);
48-
-- CREATE FUNCTION eql_v1._encrypted_check_k_sv(val jsonb)
49-
-- RETURNS boolean
50-
-- AS $$
51-
-- BEGIN
52-
-- IF (val->>'k' = 'sv') THEN
53-
-- IF (val ? 'sv') THEN
54-
-- RETURN true;
55-
-- END IF;
56-
-- RAISE 'Encrypted column kind (k) of "sv" missing data field (sv): %', val;
57-
-- END IF;
58-
-- RETURN true;
59-
-- END;
60-
-- $$ LANGUAGE plpgsql;
61-
62-
--
63-
-- DEPRECATED
64-
--
65-
-- Plaintext field should never be present in an encrypted column
66-
-- DROP FUNCTION IF EXISTS eql_v1._encrypted_check_p(jsonb);
67-
-- CREATE FUNCTION eql_v1._encrypted_check_p(val jsonb)
68-
-- RETURNS boolean
69-
-- AS $$
70-
-- BEGIN
71-
-- IF NOT val ? 'p' THEN
72-
-- RETURN true;
73-
-- END IF;
74-
-- RAISE 'Encrypted column includes plaintext (p) field: %', val;
75-
-- END;
76-
-- $$ LANGUAGE plpgsql;
77-
786
-- Should include an ident field
79-
-- DROP FUNCTION IF EXISTS eql_v1._encrypted_check_i(jsonb);
807
CREATE FUNCTION eql_v1._encrypted_check_i(val jsonb)
818
RETURNS boolean
829
AS $$
@@ -89,24 +16,7 @@ AS $$
8916
$$ LANGUAGE plpgsql;
9017

9118

92-
--
93-
-- DEPRECATED
94-
--
95-
-- Query field should never be present in an encrypted column
96-
-- DROP FUNCTION IF EXISTS eql_v1._encrypted_check_q(jsonb);
97-
-- CREATE FUNCTION eql_v1._encrypted_check_q(val jsonb)
98-
-- RETURNS boolean
99-
-- AS $$
100-
-- BEGIN
101-
-- IF val ? 'q' THEN
102-
-- RAISE 'Encrypted column includes query (q) field: %', val;
103-
-- END IF;
104-
-- RETURN true;
105-
-- END;
106-
-- $$ LANGUAGE plpgsql;
107-
10819
-- Ident field should include table and column
109-
-- DROP FUNCTION IF EXISTS eql_v1._encrypted_check_i_ct(jsonb);
11020
CREATE FUNCTION eql_v1._encrypted_check_i_ct(val jsonb)
11121
RETURNS boolean
11222
AS $$
@@ -119,48 +29,48 @@ AS $$
11929
$$ LANGUAGE plpgsql;
12030

12131
-- -- Should include a version field
122-
-- DROP FUNCTION IF EXISTS eql_v1._encrypted_check_v(jsonb);
123-
-- CREATE FUNCTION eql_v1._encrypted_check_v(val jsonb)
124-
-- RETURNS boolean
125-
-- AS $$
126-
-- BEGIN
127-
-- IF (val ? 'v') THEN
128-
-- RETURN true;
129-
-- END IF;
130-
-- RAISE 'Encrypted column missing version (v) field: %', val;
131-
-- END;
132-
-- $$ LANGUAGE plpgsql;
32+
CREATE FUNCTION eql_v1._encrypted_check_v(val jsonb)
33+
RETURNS boolean
34+
AS $$
35+
BEGIN
36+
IF (val ? 'v') THEN
37+
RETURN true;
38+
END IF;
39+
RAISE 'Encrypted column missing version (v) field: %', val;
40+
END;
41+
$$ LANGUAGE plpgsql;
13342

13443

135-
-- DROP FUNCTION IF EXISTS eql_v1.check_encrypted(val jsonb);
44+
-- -- Should include a ciphertext field
45+
CREATE FUNCTION eql_v1._encrypted_check_c(val jsonb)
46+
RETURNS boolean
47+
AS $$
48+
BEGIN
49+
IF (val ? 'c') THEN
50+
RETURN true;
51+
END IF;
52+
RAISE 'Encrypted column missing ciphertext (c) field: %', val;
53+
END;
54+
$$ LANGUAGE plpgsql;
55+
13656

13757
CREATE FUNCTION eql_v1.check_encrypted(val jsonb)
13858
RETURNS BOOLEAN
13959
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
14060
BEGIN ATOMIC
14161
RETURN (
142-
-- eql_v1._encrypted_check_v(val) AND
62+
eql_v1._encrypted_check_v(val) AND
63+
eql_v1._encrypted_check_c(val) AND
14364
eql_v1._encrypted_check_i(val) AND
14465
eql_v1._encrypted_check_i_ct(val)
145-
-- eql_v1._encrypted_check_k(val) AND
146-
-- eql_v1._encrypted_check_k_ct(val) AND
147-
-- eql_v1._encrypted_check_k_sv(val) AND
148-
-- eql_v1._encrypted_check_q(val) AND
149-
-- eql_v1._encrypted_check_p(val)
15066
);
15167
END;
15268

153-
-- ALTER DOMAIN eql_v1_encrypted DROP CONSTRAINT IF EXISTS eql_v1_encrypted_check;
154-
155-
-- ALTER DOMAIN eql_v1_encrypted
156-
-- ADD CONSTRAINT eql_v1_encrypted_check CHECK (
157-
-- eql_v1.check_encrypted(VALUE)
158-
-- );
15969

160-
-- ALTER DOMAIN eql_v1_encrypted DROP CONSTRAINT IF EXISTS eql_v1_encrypted_check;
161-
162-
-- ALTER DOMAIN eql_v1_encrypted
163-
-- ADD CONSTRAINT eql_v1_encrypted_check CHECK (
164-
-- eql_v1.check_encrypted(VALUE)
165-
-- );
70+
CREATE FUNCTION eql_v1.check_encrypted(val eql_v1_encrypted)
71+
RETURNS BOOLEAN
72+
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
73+
BEGIN ATOMIC
74+
RETURN eql_v1.check_encrypted(val.data);
75+
END;
16676

0 commit comments

Comments
 (0)