Skip to content

Commit 5d09904

Browse files
authored
Merge pull request #124 from cipherstash/fix-remvoe
fix: config functions remove process
2 parents 597db56 + b48efa0 commit 5d09904

File tree

7 files changed

+201
-63
lines changed

7 files changed

+201
-63
lines changed

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,37 @@ These frameworks use EQL to enable searchable encryption functionality in Postgr
8484
| Protect.php | [Protect.php](https://github.com/cipherstash/protectphp) |
8585
| CipherStash Proxy | [CipherStash Proxy](https://github.com/cipherstash/proxy) |
8686

87+
## Versioning
88+
89+
You can find the version of EQL installed in your database by running the following query:
90+
91+
```sql
92+
SELECT eql_v2.version();
93+
```
94+
95+
### Upgrading
96+
97+
To upgrade to the latest version of EQL, you can simply run the install script again.
98+
99+
1. Download the latest EQL install script:
100+
101+
```sh
102+
curl -sLo cipherstash-encrypt.sql https://github.com/cipherstash/encrypt-query-language/releases/latest/download/cipherstash-encrypt.sql
103+
```
104+
105+
2. Run this command to install the custom types and functions:
106+
107+
```sh
108+
psql -f cipherstash-encrypt.sql
109+
```
110+
111+
> [!NOTE]
112+
> The install script will not remove any existing configurations, so you can safely run it multiple times.
113+
114+
#### Using dbdev?
115+
116+
Follow the instructions in the [dbdev documentation](https://database.dev/cipherstash/eql) to upgrade the extension to your desired version.
117+
87118
## Developing
88119

89120
See the [development guide](./DEVELOPMENT.md).

dbdev/eql.control

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
default_version = 2.1.2
1+
default_version = 2.1.3
22
comment = 'Index and search encrypted data in PostgreSQL with SQL'
33
relocatable = true

docs/tutorials/proxy-configuration.md

Lines changed: 112 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,62 @@
11
# CipherStash Proxy Configuration with EQL functions
22

3-
Initialize the column using the `eql_v2.add_column` function to enable encryption and decryption via CipherStash Proxy.
3+
## Prerequisites
4+
5+
> [!IMPORTANT]
6+
> Before using any EQL configuration functions, you must first create the encrypted column in your database table:
47
58
```sql
6-
SELECT eql_v2.add_column('users', 'encrypted_email'); -- where users is the table name and encrypted_email is the column name of type eql_v2_encrypted
9+
-- First, add the encrypted column to your table
10+
ALTER TABLE users ADD COLUMN encrypted_email eql_v2_encrypted;
11+
```
12+
13+
The column **must** be of type `eql_v2_encrypted`.
14+
If you try to configure a column that doesn't exist in the database, you'll get the error:
15+
16+
```
17+
ERROR: Some pending columns do not have an encrypted target
18+
```
19+
20+
## Initializing column configuration
21+
22+
After creating the encrypted column, initialize it for use with CipherStash Proxy using the `eql_v2.add_column` function:
23+
24+
```sql
25+
SELECT eql_v2.add_column('users', 'encrypted_email', 'text'); -- Initialize the new encrypted column
26+
```
27+
28+
**Full signature:**
29+
```sql
30+
SELECT eql_v2.add_column(
31+
'table_name', -- Name of the table
32+
'column_name', -- Name of the encrypted column (must already exist as type eql_v2_encrypted)
33+
'cast_as', -- PostgreSQL type to cast decrypted data [optional, defaults to 'text']
34+
migrating -- If true, stages changes without immediate activation [optional, defaults to false]
35+
);
736
```
837

938
**Note:** This function allows you to encrypt and decrypt data but does not enable searchable encryption. See [Searching data with EQL](#searching-data-with-eql) for enabling searchable encryption.
1039

11-
## Refreshing CipherStash Proxy Configuration
40+
## Complete setup workflow
41+
42+
Here's the complete workflow to set up an encrypted column with search capabilities:
43+
44+
```sql
45+
-- Step 1: Create the encrypted column in your table
46+
ALTER TABLE users ADD COLUMN encrypted_email eql_v2_encrypted;
47+
48+
-- Step 2: Configure the column for encryption/decryption
49+
SELECT eql_v2.add_column('users', 'encrypted_email', 'text');
50+
51+
-- Step 3: Add search indexes as needed
52+
SELECT eql_v2.add_search_config('users', 'encrypted_email', 'unique', 'text');
53+
SELECT eql_v2.add_search_config('users', 'encrypted_email', 'match', 'text');
54+
55+
-- Step 4: Verify configuration
56+
SELECT * FROM eql_v2.config();
57+
```
58+
59+
## Refreshing CipherStash Proxy configuration
1260

1361
CipherStash Proxy refreshes the configuration every 60 seconds. To force an immediate refresh, run:
1462

@@ -25,7 +73,7 @@ Encrypted data is stored as `jsonb` values in the PostgreSQL database, regardles
2573

2674
You can read more about the data format [here](docs/reference/payload.md).
2775

28-
### Inserting Data
76+
### Inserting data
2977

3078
When inserting data into the encrypted column, wrap the plaintext in the appropriate EQL payload. These statements must be run through the CipherStash Proxy to **encrypt** the data.
3179

@@ -54,7 +102,7 @@ Data is stored in the PostgreSQL database as:
54102
}
55103
```
56104

57-
### Reading Data
105+
### Reading data
58106

59107
When querying data, select the encrypted column. CipherStash Proxy will **decrypt** the data automatically.
60108

@@ -90,15 +138,18 @@ In order to perform searchable operations on encrypted data, you must configure
90138
91139
### Adding an index
92140

141+
**Prerequisites:** The encrypted column must already exist in the database (see [Prerequisites](#prerequisites)) and be configured with `eql_v2.add_column`.
142+
93143
Add an index to an encrypted column using the `eql_v2.add_search_config` function:
94144

95145
```sql
96146
SELECT eql_v2.add_search_config(
97147
'table_name', -- Name of the table
98148
'column_name', -- Name of the column
99149
'index_name', -- Index kind ('unique', 'match', 'ore', 'ste_vec')
100-
'cast_as', -- PostgreSQL type to cast decrypted data ('text', 'int', etc.)
101-
'opts' -- Index options as JSONB (optional)
150+
'cast_as', -- PostgreSQL type to cast decrypted data ('text', 'int', etc.) [optional, defaults to 'text']
151+
'opts', -- Index options as JSONB [optional, defaults to '{}']
152+
migrating -- If true, stages changes without immediate activation [optional, defaults to false]
102153
);
103154
```
104155

@@ -115,7 +166,20 @@ SELECT eql_v2.add_search_config(
115166
);
116167
```
117168

118-
Configuration changes are automatically migrated and activated.
169+
**Example (With custom options and staging):**
170+
171+
```sql
172+
SELECT eql_v2.add_search_config(
173+
'users',
174+
'encrypted_name',
175+
'match',
176+
'text',
177+
'{"k": 6, "bf": 4096}',
178+
true -- Stage changes without immediate activation
179+
);
180+
```
181+
182+
Configuration changes are automatically migrated and activated unless the `migrating` parameter is set to `true`.
119183

120184
## Searching data with EQL
121185

@@ -322,9 +386,43 @@ EQL supports the following index types:
322386

323387
Use these functions to manage your EQL configurations:
324388

325-
- `eql_v2.add_column()` - Add a new encrypted column
326-
- `eql_v2.remove_column()` - Remove an encrypted column
327-
- `eql_v2.add_search_config()` - Add a search index
328-
- `eql_v2.remove_search_config()` - Remove a search index
329-
- `eql_v2.modify_search_config()` - Modify an existing search index
330-
- `eql_v2.config()` - View current configuration in tabular format
389+
**Column Management:**
390+
- `eql_v2.add_column(table_name, column_name, cast_as DEFAULT 'text', migrating DEFAULT false)` - Add a new encrypted column
391+
- `eql_v2.remove_column(table_name, column_name, migrating DEFAULT false)` - Remove an encrypted column completely
392+
393+
**Index Management:**
394+
- `eql_v2.add_search_config(table_name, column_name, index_name, cast_as DEFAULT 'text', opts DEFAULT '{}', migrating DEFAULT false)` - Add a search index to a column
395+
- `eql_v2.remove_search_config(table_name, column_name, index_name, migrating DEFAULT false)` - Remove a specific search index (preserves column configuration)
396+
- `eql_v2.modify_search_config(table_name, column_name, index_name, cast_as DEFAULT 'text', opts DEFAULT '{}', migrating DEFAULT false)` - Modify an existing search index
397+
398+
**Configuration Management:**
399+
- `eql_v2.migrate_config()` - Manually migrate pending configuration to encrypting state
400+
- `eql_v2.activate_config()` - Manually activate encrypting configuration
401+
- `eql_v2.discard()` - Discard pending configuration changes
402+
- `eql_v2.config()` - View current configuration in tabular format (returns a table with columns: state, relation, col_name, decrypts_as, indexes)
403+
404+
> [!NOTE]
405+
> All configuration functions automatically migrate and activate changes unless `migrating` is set to `true`.
406+
>
407+
> When `migrating` is `true`, changes are staged but not immediately applied, allowing for batch configuration updates.
408+
409+
**Important Behavior Differences:**
410+
- `remove_search_config()` removes only the specified index but preserves the column configuration (including `cast_as` setting)
411+
- `remove_column()` removes the entire column configuration including all its indexes
412+
- Empty configurations (no tables/columns) are automatically maintained as active to reflect the current state
413+
414+
## Troubleshooting
415+
416+
### Common errors
417+
418+
**Error: "Some pending columns do not have an encrypted target"**
419+
- **Cause**: You're trying to configure a column that doesn't exist as `eql_v2_encrypted` type in the database
420+
- **Solution**: First create the encrypted column with `ALTER TABLE table_name ADD COLUMN column_name eql_v2_encrypted;`
421+
422+
**Error: "Config exists for column: table_name column_name"**
423+
- **Cause**: You're trying to add a column that's already configured
424+
- **Solution**: Use `eql_v2.add_search_config()` to add indexes to existing columns, or `eql_v2.remove_column()` first if you want to reconfigure
425+
426+
**Error: "No configuration exists for column: table_name column_name"**
427+
- **Cause**: You're trying to add search config to a column that hasn't been configured with `add_column` yet
428+
- **Solution**: First run `eql_v2.add_column()` to configure the column, then add search indexes

src/config/config_test.sql

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,13 @@ DO $$
3737
c.data #> array['tables', 'users', 'name'] ? 'cast_as'));
3838

3939
-- Match index removed
40-
PERFORM eql_v2.remove_search_config('users', 'name', 'match');
40+
PERFORM eql_v2.remove_search_config('users', 'name', 'match', migrating => true);
4141
ASSERT NOT (SELECT _search_config_exists('users', 'name', 'match'));
4242

43-
-- All indexes removed, delete the emtpty pending config
44-
PERFORM eql_v2.remove_search_config('users', 'name', 'unique');
45-
ASSERT (SELECT NOT EXISTS (SELECT FROM eql_v2_configuration c WHERE c.state = 'pending'));
43+
-- All indexes removed, but column config preserved
44+
PERFORM eql_v2.remove_search_config('users', 'name', 'unique', migrating => true);
45+
ASSERT (SELECT EXISTS (SELECT FROM eql_v2_configuration c WHERE c.state = 'pending'));
46+
ASSERT (SELECT data #> array['tables', 'users', 'name', 'indexes'] = '{}' FROM eql_v2_configuration c WHERE c.state = 'pending');
4647

4748
END;
4849
$$ LANGUAGE plpgsql;
@@ -82,15 +83,16 @@ DO $$
8283

8384

8485
-- Match index removed
85-
PERFORM eql_v2.remove_search_config('users', 'name', 'match');
86+
PERFORM eql_v2.remove_search_config('users', 'name', 'match', migrating => true);
8687
ASSERT NOT (SELECT _search_config_exists('users', 'name', 'match'));
8788

8889
-- Match index removed
89-
PERFORM eql_v2.remove_search_config('blah', 'vtha', 'unique');
90+
PERFORM eql_v2.remove_search_config('blah', 'vtha', 'unique', migrating => true);
9091
ASSERT NOT (SELECT _search_config_exists('users', 'vtha', 'unique'));
9192

92-
-- All indexes removed, delete the emtpty pending config
93-
ASSERT (SELECT NOT EXISTS (SELECT FROM eql_v2_configuration c WHERE c.state = 'pending'));
93+
-- All indexes removed, but column config preserved
94+
ASSERT (SELECT EXISTS (SELECT FROM eql_v2_configuration c WHERE c.state = 'pending'));
95+
ASSERT (SELECT data #> array['tables', 'blah', 'vtha', 'indexes'] = '{}' FROM eql_v2_configuration c WHERE c.state = 'pending');
9496

9597
END;
9698
$$ LANGUAGE plpgsql;
@@ -122,9 +124,10 @@ DO $$
122124
WHERE c.state = 'pending' AND
123125
c.data #> array['tables', 'users', 'name'] ? 'cast_as'));
124126

125-
-- All indexes removed, delete the emtpty pending config
126-
PERFORM eql_v2.remove_search_config('users', 'name', 'match');
127-
ASSERT (SELECT NOT EXISTS (SELECT FROM eql_v2_configuration c WHERE c.state = 'pending'));
127+
-- All indexes removed, but column config preserved
128+
PERFORM eql_v2.remove_search_config('users', 'name', 'match', migrating => true);
129+
ASSERT (SELECT EXISTS (SELECT FROM eql_v2_configuration c WHERE c.state = 'pending'));
130+
ASSERT (SELECT data #> array['tables', 'users', 'name', 'indexes'] = '{}' FROM eql_v2_configuration c WHERE c.state = 'pending');
128131
END;
129132
$$ LANGUAGE plpgsql;
130133

@@ -215,9 +218,13 @@ DO $$
215218

216219
PERFORM eql_v2.remove_column('encrypted', 'e', migrating => true);
217220

218-
PERFORM assert_no_result(
219-
'Pending configuration was removed',
220-
'SELECT * FROM eql_v2_configuration c WHERE c.state = ''pending''');
221+
PERFORM assert_count(
222+
'Pending configuration exists but is empty',
223+
'SELECT * FROM eql_v2_configuration c WHERE c.state = ''pending''',
224+
1);
225+
226+
-- Verify the config is empty
227+
ASSERT (SELECT data #> array['tables'] = '{}' FROM eql_v2_configuration c WHERE c.state = 'pending');
221228

222229
END;
223230
$$ LANGUAGE plpgsql;

src/config/constraints.sql

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,16 @@ CREATE FUNCTION eql_v2.config_check_cast(val jsonb)
3939
RETURNS BOOLEAN
4040
AS $$
4141
BEGIN
42-
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
43-
RETURN true;
42+
-- If there are cast_as fields, validate them
43+
IF EXISTS (SELECT jsonb_array_elements_text(jsonb_path_query_array(val, '$.tables.*.*.cast_as'))) THEN
44+
IF (SELECT bool_and(cast_as = ANY('{text, int, small_int, big_int, real, double, boolean, date, jsonb}'))
45+
FROM (SELECT jsonb_array_elements_text(jsonb_path_query_array(val, '$.tables.*.*.cast_as')) AS cast_as) casts) THEN
46+
RETURN true;
47+
END IF;
48+
RAISE 'Configuration has an invalid cast_as (%). Cast should be one of {text, int, small_int, big_int, real, double, boolean, date, jsonb}', val;
4449
END IF;
45-
RAISE 'Configuration has an invalid cast_as (%). Cast should be one of {text, int, small_int, big_int, real, double, boolean, date, jsonb}', val;
50+
-- If no cast_as fields exist (empty config), that's valid
51+
RETURN true;
4652
END;
4753
$$ LANGUAGE plpgsql;
4854

@@ -53,7 +59,7 @@ CREATE FUNCTION eql_v2.config_check_tables(val jsonb)
5359
RETURNS boolean
5460
AS $$
5561
BEGIN
56-
IF (val ? 'tables') AND (val->'tables' <> '{}'::jsonb) THEN
62+
IF (val ? 'tables') THEN
5763
RETURN true;
5864
END IF;
5965
RAISE 'Configuration missing tables (tables) field: %', val;

src/config/functions.sql

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ $$ LANGUAGE plpgsql;
7070

7171

7272

73-
CREATE FUNCTION eql_v2.remove_search_config(table_name text, column_name text, index_name text)
73+
CREATE FUNCTION eql_v2.remove_search_config(table_name text, column_name text, index_name text, migrating boolean DEFAULT false)
7474
RETURNS jsonb
7575
AS $$
7676
DECLARE
@@ -105,22 +105,12 @@ AS $$
105105
-- remove the index
106106
SELECT _config #- array['tables', table_name, column_name, 'indexes', index_name] INTO _config;
107107

108-
-- if column is now empty, remove the column
109-
IF _config #> array['tables', table_name, column_name, 'indexes'] = '{}' THEN
110-
SELECT _config #- array['tables', table_name, column_name] INTO _config;
111-
END IF;
112-
113-
-- if table is now empty, remove the table
114-
IF _config #> array['tables', table_name] = '{}' THEN
115-
SELECT _config #- array['tables', table_name] INTO _config;
116-
END IF;
117-
118-
-- if config empty delete
119-
-- or update the config
120-
IF _config #> array['tables'] = '{}' THEN
121-
DELETE FROM public.eql_v2_configuration WHERE state = 'pending';
122-
ELSE
123-
UPDATE public.eql_v2_configuration SET data = _config WHERE state = 'pending';
108+
-- update the config and migrate (even if empty)
109+
UPDATE public.eql_v2_configuration SET data = _config WHERE state = 'pending';
110+
111+
IF NOT migrating THEN
112+
PERFORM eql_v2.migrate_config();
113+
PERFORM eql_v2.activate_config();
124114
END IF;
125115

126116
-- exeunt
@@ -134,7 +124,7 @@ CREATE FUNCTION eql_v2.modify_search_config(table_name text, column_name text, i
134124
RETURNS jsonb
135125
AS $$
136126
BEGIN
137-
PERFORM eql_v2.remove_search_config(table_name, column_name, index_name);
127+
PERFORM eql_v2.remove_search_config(table_name, column_name, index_name, migrating);
138128
RETURN eql_v2.add_search_config(table_name, column_name, index_name, cast_as, opts, migrating);
139129
END;
140130
$$ LANGUAGE plpgsql;
@@ -293,19 +283,20 @@ AS $$
293283
SELECT _config #- array['tables', table_name] INTO _config;
294284
END IF;
295285

296-
-- if config empty delete
297-
-- or update the config
298-
IF _config #> array['tables'] = '{}' THEN
299-
DELETE FROM public.eql_v2_configuration WHERE state = 'pending';
300-
ELSE
301-
UPDATE public.eql_v2_configuration SET data = _config WHERE state = 'pending';
302-
END IF;
303-
304286
PERFORM eql_v2.remove_encrypted_constraint(table_name, column_name);
305287

288+
-- update the config (even if empty) and activate
289+
UPDATE public.eql_v2_configuration SET data = _config WHERE state = 'pending';
290+
306291
IF NOT migrating THEN
307-
PERFORM eql_v2.migrate_config();
308-
PERFORM eql_v2.activate_config();
292+
-- For empty configs, skip migration validation and directly activate
293+
IF _config #> array['tables'] = '{}' THEN
294+
UPDATE public.eql_v2_configuration SET state = 'inactive' WHERE state = 'active';
295+
UPDATE public.eql_v2_configuration SET state = 'active' WHERE state = 'pending';
296+
ELSE
297+
PERFORM eql_v2.migrate_config();
298+
PERFORM eql_v2.activate_config();
299+
END IF;
309300
END IF;
310301

311302
-- exeunt

0 commit comments

Comments
 (0)