Skip to content

feat: encrypted column constraint #104

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 50 additions & 21 deletions src/config/config_test.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,29 +21,28 @@ END;
-- -----------------------------------------------
TRUNCATE TABLE eql_v1_configuration;


DO $$
BEGIN

-- Add indexes
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;
Expand Down Expand Up @@ -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';


-- -----------------------------------------------
Expand Down Expand Up @@ -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
Expand Down
14 changes: 8 additions & 6 deletions src/config/functions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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';
Expand Down Expand Up @@ -334,6 +332,8 @@ AS $$
DO UPDATE
SET data = _config;

PERFORM eql_v1.add_encrypted_constraint(table_name, column_name);

-- exeunt
RETURN _config;
END;
Expand Down Expand Up @@ -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;

Expand Down
4 changes: 2 additions & 2 deletions src/encrypted/casts.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;


--
Expand All @@ -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;



Expand Down
152 changes: 31 additions & 121 deletions src/encrypted/constraints.sql
Original file line number Diff line number Diff line change
@@ -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 $$
Expand All @@ -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 $$
Expand All @@ -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;

Loading