active_enquo 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c0103ba2927e84bdc1c6fda73e7103ffe090316fac200d7d429a95e4f4632f0
4
- data.tar.gz: 11d5f5d66a753fe18f79f198c7b243d9fa3b194929c6e2e63c6f4a93ce25ed06
3
+ metadata.gz: 9d538e223b9e7485c4b08cf8ac38f2cc6a08f5d3c3b72426167606fdc7a5fdf5
4
+ data.tar.gz: 33e80addf4beb8bbe8f5328cbfd093cb70264ec2804d148026cbdabbd36cd1b8
5
5
  SHA512:
6
- metadata.gz: 693f14e986cca46c810505664833cd4e211c170cd287d43fbbb429ba7a5ef0e830404d71597a314e97cdde4fe911edf14066f9af446510ce82c05b3e73507c6d
7
- data.tar.gz: afba7f08679b1b45f6484f9a3bf1b870fc2a824edec9b8b33483f40b7661c940f55d8135992885011d25a4c57d6c02ecb5f069ebde0bed0d59daec85d6ad301d
6
+ metadata.gz: af6a5e20a215234ce3bce1656a7e02e94f536bf1a3c8d674732d7d49cd8ab5265fe4c306ea730cfe60bba707df5436cf9048d9ce8b85f021fc92639835a0424d
7
+ data.tar.gz: e9f6b9d0f5f5f5fb5b6fdecd8ba7fdcadf5b7be0c363787d78a2a989bb753b506d9d914122041614aad4cb3032803d6f5075ec02019a81457104471decd4fec7
data/README.md CHANGED
@@ -3,12 +3,12 @@ This allows you to keep the data you store safe, by encrypting it, without compr
3
3
 
4
4
  Sounds like magic?
5
5
  Well, maybe a little bit.
6
- Read our [how it works](https://enquo.org/how-it-works) if you're interested in the details, or read on for how to use it.
6
+ Read our [how it works](https://enquo.org/how-it-works) if you're interested in the gory cryptographic details, or read on for how to use it.
7
7
 
8
8
 
9
9
  # Pre-requisites
10
10
 
11
- In order to make use of this extension, you must be running Postgres 10 or higher, with the [`pg_enquo`](https://github.com/enquo/pg_enquo) extension enabled in the database you're working in.
11
+ In order to make use of ActiveRecord extension, you must be running Postgres 11 or higher, with the [`pg_enquo`](https://github.com/enquo/pg_enquo) extension enabled in the database you're working in.
12
12
  See [the `pg_enquo` installation guide](https://github.com/enquo/pg_enquo/tree/main/doc/installation.md) for instructions on how to install `pg_enquo`.
13
13
 
14
14
  Also, if you're installing this gem from source, you'll need a reasonably recent [Rust](https://rust-lang.org) toolchain installed.
@@ -16,33 +16,41 @@ Also, if you're installing this gem from source, you'll need a reasonably recent
16
16
 
17
17
  # Installation
18
18
 
19
- It's a gem:
19
+ It's a gem, so the usual methods should work Just Fine:
20
20
 
21
- gem install active_enquo
22
-
23
- There's also the wonders of [the Gemfile](http://bundler.io):
21
+ ```sh
22
+ gem install active_enquo
23
+ # OR
24
+ echo "gem 'active_enquo'" >> Gemfile
25
+ ```
24
26
 
25
- gem 'active_enquo'
27
+ On macOS, and Linux `x86-64`/`aarch64`, you'll get a pre-built binary gem that contains everything you need.
28
+ For other platforms, you'll need to have Rust 1.59.0 or later installed in order to build the native code portion of the gem.
26
29
 
27
- If you're the sturdy type that likes to run from git:
28
30
 
29
- rake install
31
+ # Configuration
30
32
 
31
- Or, if you've eschewed the convenience of Rubygems entirely, then you
32
- presumably know what to do already.
33
+ The only setting that ActiveEnquo needs is to be given a "root" key, which is used to derive the keys which are used to actually encrypt data.
33
34
 
34
35
 
35
- # Configuration
36
+ ## Step 1: Generate a Root Key
36
37
 
37
- The only setting that ActiveEnquo needs is to be given a "root" key, which is used to derive the keys which are used to actually encrypt data.
38
- This key ***MUST*** be generated by a cryptographically-secure random number generator, and must also be 64 hex digits in length.
38
+ The ActiveEnquo root key ***MUST*** be generated by a cryptographically-secure random number generator, and must also be 64 hex digits in length.
39
39
  A good way to generate this key is with the `SecureRandom` module:
40
40
 
41
41
  ```sh
42
42
  ruby -r securerandom -e 'puts SecureRandom.hex(32)'
43
43
  ```
44
44
 
45
- With this key in hand, you can store it in the Rails credential store, like this:
45
+
46
+ ## Step 2: Configure Your Application
47
+
48
+ With this key in hand, you need to store it somewhere.
49
+
50
+
51
+ ### Using Rails Credential Store (Recommended)
52
+
53
+ The recommended way to store your root key, at present, is in the [Rails credentials store](https://guides.rubyonrails.org/security.html#custom-credentials).
46
54
 
47
55
  1. Open up the Rails credentials editor:
48
56
 
@@ -59,7 +67,11 @@ With this key in hand, you can store it in the Rails credential store, like this
59
67
 
60
68
  3. Save and exit the editor. Commit the changes to your revision control system.
61
69
 
62
- This only works if you are using Rails, of course; if you're using ActiveRecord by itself, you must set the root key yourself during application initialization.
70
+
71
+ ### Direct Assignment (Only If You Must)
72
+
73
+ Using the Rails credential store only works if you are using Rails, of course.
74
+ If you're using ActiveRecord by itself, you must set the root key yourself during application initialization.
63
75
  You do this by assigning a `RootKey` to `ActiveEnquo.root_key`, like this:
64
76
 
65
77
  ```ruby
@@ -67,9 +79,12 @@ You do this by assigning a `RootKey` to `ActiveEnquo.root_key`, like this:
67
79
  ActiveEnquo.root_key = ActiveEnquo::RootKey::Static.new("0000000000000000000000000000000000000000000000000000000000000000")
68
80
  ```
69
81
 
70
- However, this leaves the key exposed to anyone who comes along and takes a glance at your code.
71
- Losing control of this root key is catastrophic for the security of your system.
72
- Instead, you should use a secrets vault of some sort to store the key, and pass it into your initialization code somehow.
82
+ Preferably, you would pass the key into your application via, say, an environment variable, and then immediately clear the environment variable:
83
+
84
+ ```ruby
85
+ ActiveEnquo.root_key = ActiveEnquo::RootKey::Static.new(ENV.fetch("ENQUO_ROOT_KEY"))
86
+ ENV.delete("ENQUO_ROOT_KEY")
87
+ ```
73
88
 
74
89
  Support for cloud keystores, such as AWS KMS, GCP KMS, Azure KeyVault, and HashiCorp Vault, will be implemented sooner or later.
75
90
  If you have a burning desire to see that more on the "sooner" end than "later", PRs are welcome.
@@ -77,67 +92,78 @@ If you have a burning desire to see that more on the "sooner" end than "later",
77
92
 
78
93
  # Usage
79
94
 
80
- Start by creating a column in your database that uses one of the [available enquo types](https://github.com/enquo/pg_enquo/doc/data_types), with a Rails migration:
95
+ We try to make using ActiveEnquo as simple as possible.
96
+
97
+
98
+ ## Create Your Encrypted Column
99
+
100
+ Start by creating a column in your database that uses one of the [available `enquo_*` types](https://github.com/enquo/pg_enquo/tree/main/doc/data_types), with a Rails migration:
81
101
 
82
102
  ```ruby
83
103
  class AddEncryptedBigintColumn < ActiveRecord::Migration[6.0]
84
104
  def change
85
- add_column :users, :age, :enquo_bigint
105
+ add_column :users, :date_of_birth, :enquo_date
86
106
  end
87
107
  end
88
108
  ```
89
109
 
110
+ Apply this migration in the usual fashion (`rails db:migrate`).
111
+
90
112
 
91
113
  ## Reading and Writing
92
114
 
93
- You can now use that attribute in your models as you would normally.
94
- For example, insert a new record:
115
+ You can now, without any further ado, use that attribute in your models as you would normally.
116
+ For example, you can insert a new record:
95
117
 
96
118
  ```ruby
97
- User.create!([{name: "Clara Bloggs", username: "cbloggs", age: 42}])
119
+ User.create!([{name: "Clara Bloggs", username: "cbloggs", date_of_birth: Date.new(1970, 1, 1)}])
98
120
  ```
99
121
 
100
122
  When you retrieve a record, the value is there for you to read:
101
123
 
102
124
  ```ruby
103
- User.where(username: "cbloggs").first.age # => 42
125
+ User.where(username: "cbloggs").first.date_of_birth.to_s # => "1970-01-01"
104
126
  ```
105
127
 
106
- So far, nothing more spectacular than what AR Encryption will get you.
107
- The fun begins now...
108
-
109
128
 
110
129
  ## Querying
111
130
 
112
- You can query for records with an exact age:
131
+ This is where things get *neat*.
132
+
133
+ Performing a query on Enquo-encrypted data is done the same way as on unencrypted data.
134
+
135
+ You can query for records that have the exact value you're looking for:
113
136
 
114
137
  ```ruby
115
- User.where(age: User.enquo(:age, 42))
138
+ User.where(date_of_birth: Date(1970, 1, 1))
116
139
  ```
117
140
 
118
- Or you can query for records with an age of less than 50:
141
+ Or you can query for users born less than 50 years ago:
119
142
 
120
143
  ```ruby
121
- # This is AR's idiomatic way of saying "less-than"
122
- User.where(age: User.enquo(:age, ...50))
144
+ User.where(date_of_birth: (Date.today - 50.years))..)
123
145
  ```
124
146
 
125
- *However*, if you examine the record in the database, it's encrypted:
147
+ This doesn't seem so magical, until you take a peek in the database, and realise that *all the data is still encrypted*:
126
148
 
127
149
  ```sh
128
- psql> SELECT age FROM users WHERE username='cbloggs';
150
+ psql> SELECT date_of_birth FROM users WHERE username='cbloggs';
129
151
  age
130
152
  -------
131
- {"ct":[<lots of numbers>],"ore":[<lots and LOTS of numbers>]}
153
+ {"v1":{"a":[<lots of numbers>],"y":[<lots and LOTS of numbers>],<etc etc>}}
132
154
  ```
133
155
 
134
- And that, as they say, is that.
156
+
157
+ ## Migrating Existing Data to Encrypted Form
158
+
159
+ This is a topic on which a lot of words can be written.
160
+ For the sake of tidiness, these words are in [a guide of their own](docs/MIGRATION.md).
135
161
 
136
162
 
137
163
  ## Indexing and Ordering
138
164
 
139
- To maintain [security](https://enquo.org/about/threat-models#snapshot-security), ActiveEnquo isn't able to `ORDER BY` or index columns.
140
- This is fine for many situations -- many columns don't need indexes.
165
+ To maintain [security by default](https://enquo.org/about/threat-models#snapshot-security), ActiveEnquo doesn't provide the ability to `ORDER BY` or index columns by default.
166
+ This is fine for many situations -- many columns don't need indexes or to be ordered in a query.
141
167
 
142
168
  For those columns that *do* need indexes or `ORDER BY` support, you can enable support for them by setting the `enable_reduced_security_operations` flag on the attribute, like this:
143
169
 
@@ -148,10 +174,12 @@ class User < ApplicationRecord
148
174
  end
149
175
  ```
150
176
 
151
- ### SECURITY ALERT
177
+
178
+ ### Security Considerations
152
179
 
153
180
  As the name implies, "reduced security operations" require that the security of the data in the column be lower than [Enquo's default security properties](https://enquo.org/about/threat-models#snapshot-security).
154
- In particular, extra data is stored in the value which can be used by an attacker to:
181
+ Specifically, extra data needs to be stored in the value to enable indexing and ordering.
182
+ This extra data can be used by an attacker to:
155
183
 
156
184
  * Identify all rows which have the same value for the column (although not what that value actually *is*); and
157
185
 
@@ -174,22 +202,14 @@ class User < ApplicationRecord
174
202
  end
175
203
  ```
176
204
 
205
+ More accurate indications of the disk space requirements for the supported data types can be found in [the description of each data type](https://github.com/enquo/pg_enquo/tree/main/doc/data_types).
177
206
 
178
- # Future Developments
179
207
 
180
- These are some of the things that are definitely planned to be added to ActiveEnquo in the nearish future.
181
-
182
- * **Support for key rotation**: this isn't tricky, just a bit fiddly.
183
- Encrypted values have a "key ID" associated with them, so we can find which values are out-of-date, but multiple keys need to useable for decryption.
184
- Closely related to this is **support for renaming columns**, which is problematic because keys are derived based on the column name.
185
- Again, key IDs (to find values encrypted with the previous column name) and the ability to attempt decryption with a key based on the previous column name sorts this out.
186
-
187
- * **Strings**: a great deal of the sensitive data that needs protecting is in the form of strings.
188
- Querying strings is somewhat more involved than numeric data, and so the means of encrypting strings such that they're queryable *and* secure are more complex.
189
- Enquo cannot be a really useful library for supporting encrypted querying until at least some common string query operations are supported, though, so it is important that this be implemented.
208
+ # Future Developments
190
209
 
191
- * **Other data types**: while you can go a long way with strings and bigints, part of the power of SQL is the sheer variety of interesting data types it has, and the variety of things you can do with them.
192
- So more integer types, floats, decimals, dates, times, timestamps, and more, are all on the cards for future development.
210
+ ActiveEnquo is far from finished.
211
+ Many more features are coming in the future.
212
+ See [the Enquo project roadmap](https://enquo.org/roadmap) for details of what we're still intending to implement.
193
213
 
194
214
 
195
215
  # Contributing
data/active_enquo.gemspec CHANGED
@@ -27,7 +27,7 @@ Gem::Specification.new do |s|
27
27
 
28
28
  s.required_ruby_version = ">= 2.7.0"
29
29
 
30
- s.add_runtime_dependency "enquo-core", "~> 0.6"
30
+ s.add_runtime_dependency "enquo-core", "~> 0.7"
31
31
  s.add_runtime_dependency "activerecord", ">= 6"
32
32
 
33
33
  s.add_development_dependency "bundler"
data/docs/MIGRATION.md ADDED
@@ -0,0 +1,107 @@
1
+ If you have data already in your database that you wish to protect using ActiveEnquo, you can migrate the data into an encrypted form.
2
+
3
+ There are two approaches that can be used:
4
+
5
+ * [Direct Migration](#data-migration-with-downtime), which is straightforward but involves application downtime; or
6
+
7
+ * [Live Migration](#live-data-migration), which is more complicated, but can be done while keeping the application available at all times.
8
+
9
+
10
+ # Data Migration (with Downtime)
11
+
12
+ If your application can withstand being down for a period of time, you can use a simple migration process that encrypts the data and modifies the application appropriate in a single pass.
13
+ It relies on their being a period with nothing accessing the column(s) being migrated, which usually means that the entire application (including background workers and periodic tasks) being shut down, before being restarted.
14
+ The total downtime will depend on how long it takes to encrypt and write all the data being migrated, which is mostly a function of the amount of data being stored.
15
+
16
+
17
+ ## Step 1: Configure the Encrypted Column(s)
18
+
19
+ Create an ActiveRecord migration which renames the existing column and creates a new `enquo_*` column with the old name.
20
+ For example, if you already had a `date_of_birth` column, your migration would look like this:
21
+
22
+ ```ruby
23
+ class EncryptUsersDateOfBirth < ActiveRecord::Migration[7.0]
24
+ def change
25
+ rename_column :users, :date_of_birth, :date_of_birth_plaintext
26
+ add_column :users, :date_of_birth, :enquo_date
27
+ User.enquo_encrypt_columns(date_of_birth_plaintext: :date_of_birth)
28
+ remove_column :users, :date_of_birth_plaintext
29
+ end
30
+ end
31
+ ```
32
+
33
+ The `Model.encrypt_columns` method loads all the records in the table, and encrypts the value in the plaintext column and writes it to the corresponding encrypted column.
34
+
35
+ If you want to encrypt several columns in a single model, you can do so in a single migration, by renaming all the columns and adding `enquo_*` type columns, and then providing the mapping of all columns together in a single `Model.encrypt_columns` call.
36
+ This is the recommended approach, as it improves efficiency because the records only have to be loaded and saved once.
37
+
38
+ If you want to encrypt columns in multiple models in one downtime, just repeat the above steps for each table and model involved.
39
+
40
+
41
+ ## Step 2: Modify Queries
42
+
43
+ When providing data to a query on an encrypted column, you need to make a call to `Model.enquo` in order to encrypt the value for querying.
44
+
45
+ To continue our `date_of_birth` example above, you need to find any queries that reference the `date_of_birth` column, and modify the code to pass the value for the `date_of_birth` column through a call to `User.enquo(:date_of_birth, <value>)`.
46
+
47
+ For a query that found all users with a date of birth equal to a query parameter, that originally looked like this:
48
+
49
+ ```ruby
50
+ User.where(date_of_birth: params[:dob])
51
+ ```
52
+
53
+ You'd modify it to look like this, instead:
54
+
55
+ ```ruby
56
+ User.where(date_of_birth: User.enquo(:date_of_birth, params[:dob]))
57
+ ```
58
+
59
+ If the value for the query was passed in as a positional parameter, you just encrypt the value the same way, so that a query might look like this:
60
+
61
+ ```ruby
62
+ User.where("date_of_birth > ? OR date_of_birth IS NULL", User.enquo(:date_of_birth, params[:dob]))
63
+ ```
64
+
65
+
66
+ ## Step 3: Deploy
67
+
68
+ Once the above changes are all made and committed to revision control, it's time to commence the downtime.
69
+ Shutdown all the application servers, background job workers, and anything else that accesses the database, then perform a normal deployment -- running the database migration process before starting the application again.
70
+
71
+ The migration may take some time to run, if the table is large.
72
+
73
+
74
+ ## Step 4: Enjoy Fully Encrypted Data
75
+
76
+ The column(s) you migrated are now fully protected by Enquo's queryable encryption.
77
+ Relax and enjoy your preferred beverage in celebration!
78
+
79
+
80
+ # Live Data Migration
81
+
82
+ Converting the data in a column to be fully encrypted, while avoiding any application downtime, requires making several changes to the application and database schema in alternating sequence.
83
+ This is necessary to ensure that parts of the application stack running both older and newer code versions can work with the database schema in place at all times.
84
+
85
+ > # WORK IN PROGRESS
86
+ >
87
+ > This section has not been written out in detail.
88
+ > The short version is:
89
+ >
90
+ > 1. Rename the unencrypted column:
91
+ > 1. `create_column :users, :date_of_birth_plaintext, :date`
92
+ > 2. Modify the model to write changes to `date_of_birth` to `date_of_birth_plaintext` as well
93
+ > 3. Deploy
94
+ > 4. Migration to copy all `date_of_birth` values to `date_of_birth_plaintext`
95
+ > 5. Deploy
96
+ > 6. Modify app to read/query from `date_of_birth_plaintext`, add `date_of_birth` to `User.ignored_columns`
97
+ > 7. Deploy
98
+ > 8. `drop_column :users, :date_of_birth`
99
+ > 2. Create the encrypted column:
100
+ > 1. `create_column :users, :date_of_birth, :enquo_date`
101
+ > 2. Modify the model to write changes to `date_of_birth_plaintext` to `date_of_birth` as well, remove `date_of_birth` from `User.ignored_columns`
102
+ > 3. Deploy
103
+ > 4. Migration to encrypt all `date_of_birth_plaintext` values into `date_of_birth`
104
+ > 5. Deploy
105
+ > 6. Modify app to read/encrypted query from `date_of_birth`, add `date_of_birth_plaintext` to `User.ignored_columns`
106
+ > 7. Deploy
107
+ > 8. `drop_column :users, :date_of_birth_plaintext`
@@ -0,0 +1 @@
1
+ /.pgdbenv
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "active_record"
4
+ require "active_enquo"
5
+ require "pg"
6
+
7
+ require_relative "../init"
8
+
9
+ def assert_eq(expected, actual)
10
+ unless expected == actual
11
+ $stderr.puts "Expected #{expected.inspect} to equal #{actual.inspect}"
12
+ exit 1
13
+ end
14
+ end
15
+
16
+ def value(f, v)
17
+ if ENV.key?("USING_ENQUO")
18
+ People.enquo(f, v)
19
+ else
20
+ v
21
+ end
22
+ end
23
+
24
+ ActiveRecord::Base.establish_connection(adapter: ENV.fetch("DBTYPE"))
25
+
26
+ assert_eq(["Meyers"], People.where(first_name: value(:first_name, "Seth")).all.map { |p| p.last_name })
27
+ assert_eq(8, People.where(date_of_birth: value(:date_of_birth, "1980-01-01")..).count)
@@ -0,0 +1,11 @@
1
+ class CreatePeopleTable < ActiveRecord::Migration[ENV.fetch("AR_VERSION", "7.0").to_f]
2
+ def change
3
+ create_table :people do |t|
4
+ t.string :first_name
5
+ t.string :last_name
6
+ t.date :date_of_birth
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ class EncryptPeopleData < ActiveRecord::Migration[ENV.fetch("AR_VERSION", "7.0").to_f]
2
+ def up
3
+ rename_column :people, :first_name, :first_name_plaintext
4
+ rename_column :people, :last_name, :last_name_plaintext
5
+ rename_column :people, :date_of_birth, :date_of_birth_plaintext
6
+
7
+ add_column :people, :first_name, :enquo_text
8
+ add_column :people, :last_name, :enquo_text
9
+ add_column :people, :date_of_birth, :enquo_date
10
+
11
+ People.enquo_encrypt_columns(
12
+ {
13
+ first_name_plaintext: :first_name,
14
+ last_name_plaintext: :last_name,
15
+ date_of_birth_plaintext: :date_of_birth,
16
+ },
17
+ # Smol batch size exercises the batching functionality
18
+ batch_size: 5
19
+ )
20
+
21
+ remove_column :people, :first_name_plaintext
22
+ remove_column :people, :last_name_plaintext
23
+ remove_column :people, :date_of_birth_plaintext
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ . ../helper.sh
6
+
7
+ export DBTYPE="postgresql"
8
+
9
+ check_enquo_pg_db
10
+ clear_pg_db
11
+
12
+ ar_db_migrate "001"
13
+ load_people
14
+
15
+ ./exercise_model
16
+
17
+ ar_db_migrate "002"
18
+ USING_ENQUO="y" ./exercise_model
@@ -0,0 +1,34 @@
1
+ if [ -f "../.pgdbenv" ]; then
2
+ . ../.pgdbenv
3
+ fi
4
+
5
+ check_enquo_pg_db() {
6
+ if [ "$(psql -tAc "select count(extname) FROM pg_catalog.pg_extension WHERE extname='pg_enquo'")" != "1" ]; then
7
+ echo "Specified PostgreSQL database does not have the pg_enquo extension." >&2
8
+ echo "Check your PG* env vars for correctness, and install the extension if needed." >&2
9
+ exit 1
10
+ fi
11
+ }
12
+
13
+ clear_pg_db() {
14
+ for tbl in $(psql -tAc "select relname from pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n on n.oid = c.relnamespace where c.relkind='r' and n.nspname = 'public'"); do
15
+ psql -c "DROP TABLE $tbl" >/dev/null
16
+ done
17
+ for seq in $(psql -tAc "select relname from pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n on n.oid = c.relnamespace where c.relkind='S' and n.nspname = 'public'"); do
18
+ psql -c "DROP SEQUENCE $seq" >/dev/null
19
+ done
20
+ }
21
+
22
+ run_ruby() {
23
+ ruby -r ../init "$@"
24
+ }
25
+
26
+ ar_db_migrate() {
27
+ local target_version="$1"
28
+
29
+ run_ruby -e "ActiveRecord::MigrationContext.new(['migrations']).up($target_version)" >/dev/null
30
+ }
31
+
32
+ load_people() {
33
+ run_ruby -e "People.create!(JSON.parse(File.read('../people.json')))"
34
+ }
data/e2e_tests/init.rb ADDED
@@ -0,0 +1,18 @@
1
+ require "active_record"
2
+ require "active_enquo"
3
+
4
+ ActiveEnquo.root_key = Enquo::RootKey::Static.new("f91c5017a2d946403cc90a688266ff32d186aa2a00efd34dcaa86be802e179d0")
5
+
6
+ DBTYPE = ENV.fetch("DBTYPE")
7
+
8
+ case DBTYPE
9
+ when "postgresql"
10
+ require "pg"
11
+ else
12
+ raise "Unsupported DBTYPE: #{DBTYPE.inspect}"
13
+ end
14
+
15
+ class People < ActiveRecord::Base
16
+ end
17
+
18
+ ActiveRecord::Base.establish_connection(adapter: DBTYPE)
@@ -0,0 +1 @@
1
+ [{"first_name":"Sergio","last_name":"Leone","date_of_birth":"1929-01-03"},{"first_name":"Bradley","last_name":"Cooper","date_of_birth":"1975-01-05"},{"first_name":"Robert","last_name":"Duvall","date_of_birth":"1931-01-05"},{"first_name":"Kate","last_name":"Moss","date_of_birth":"1974-01-16"},{"first_name":"Laura","last_name":"Schlessinger","date_of_birth":"1947-01-16"},{"first_name":"Jason","last_name":"Segel","date_of_birth":"1980-01-18"},{"first_name":"Pete","last_name":"Buttigieg","date_of_birth":"1982-01-19"},{"first_name":"Mischa","last_name":"Barton","date_of_birth":"1986-01-24"},{"first_name":"Shirley","last_name":"Mason","date_of_birth":"1923-01-25"},{"first_name":"Boris","last_name":"Yeltsin","date_of_birth":"1931-02-01"},{"first_name":"Tom","last_name":"Wilkinson","date_of_birth":"1948-02-05"},{"first_name":"Tony","last_name":"Iommi","date_of_birth":"1948-02-19"},{"first_name":"Sri","last_name":"Srinivasan","date_of_birth":"1967-02-23"},{"first_name":"Tony","last_name":"Randall","date_of_birth":"1920-02-26"},{"first_name":"David","last_name":"Blaine","date_of_birth":"1973-04-04"},{"first_name":"Muddy","last_name":"Waters","date_of_birth":"1915-04-04"},{"first_name":"Danny","last_name":"Almonte","date_of_birth":"1987-04-07"},{"first_name":"Meghann","last_name":"Shaughnessy","date_of_birth":"1979-04-13"},{"first_name":"Maisie","last_name":"Williams","date_of_birth":"1997-04-15"},{"first_name":"Immanuel","last_name":"Kant","date_of_birth":"1724-04-22"},{"first_name":"John","last_name":"Oliver","date_of_birth":"1977-04-23"},{"first_name":"Penelope","last_name":"Cruz","date_of_birth":"1974-04-28"},{"first_name":"Anne","last_name":"Parillaud","date_of_birth":"1961-05-06"},{"first_name":"Cate","last_name":"Blanchett","date_of_birth":"1969-05-14"},{"first_name":"Tucker","last_name":"Carlson","date_of_birth":"1969-05-16"},{"first_name":"Nancy","last_name":"Kwan","date_of_birth":"1939-05-19"},{"first_name":"Melissa","last_name":"Etheridge","date_of_birth":"1961-05-29"},{"first_name":"Marissa","last_name":"Mayer","date_of_birth":"1975-05-30"},{"first_name":"Marvin","last_name":"Hamlisch","date_of_birth":"1944-06-02"},{"first_name":"Gwendolyn","last_name":"Brooks","date_of_birth":"1917-06-07"},{"first_name":"Paul","last_name":"Lynde","date_of_birth":"1927-06-13"},{"first_name":"Paul","last_name":"McCartney","date_of_birth":"1942-06-18"},{"first_name":"Susan","last_name":"Hayward","date_of_birth":"1917-06-30"},{"first_name":"Léa","last_name":"Seydoux","date_of_birth":"1985-07-01"},{"first_name":"Amanda","last_name":"Knox","date_of_birth":"1987-07-09"},{"first_name":"Cindy","last_name":"Sheehan","date_of_birth":"1957-07-10"},{"first_name":"Bill","last_name":"Cosby","date_of_birth":"1937-07-12"},{"first_name":"Raymond","last_name":"Chandler","date_of_birth":"1888-07-23"},{"first_name":"Christopher","last_name":"Nolan","date_of_birth":"1970-07-30"},{"first_name":"Melanie","last_name":"Griffith","date_of_birth":"1957-08-09"},{"first_name":"Freddie","last_name":"Gray","date_of_birth":"1989-08-16"},{"first_name":"Steve","last_name":"Case","date_of_birth":"1958-08-21"},{"first_name":"Cal","last_name":"Ripken","date_of_birth":"1960-08-24"},{"first_name":"Regis","last_name":"Philbin","date_of_birth":"1931-08-25"},{"first_name":"Shania","last_name":"Twain","date_of_birth":"1965-08-28"},{"first_name":"Adam","last_name":"Curry","date_of_birth":"1964-09-03"},{"first_name":"Rachel","last_name":"Hunter","date_of_birth":"1969-09-09"},{"first_name":"Bashar","last_name":"al-Assad","date_of_birth":"1965-09-11"},{"first_name":"Amy","last_name":"Poehler","date_of_birth":"1971-09-16"},{"first_name":"Jim","last_name":"Thompson","date_of_birth":"1906-09-27"},{"first_name":"Matt","last_name":"Damon","date_of_birth":"1970-10-08"},{"first_name":"Leopold","last_name":"Senghor","date_of_birth":"1906-10-09"},{"first_name":"William","last_name":"Penn","date_of_birth":"1644-10-14"},{"first_name":"Rebecca","last_name":"Romijn","date_of_birth":"1971-11-06"},{"first_name":"Auguste","last_name":"Rodin","date_of_birth":"1840-11-12"},{"first_name":"Nate","last_name":"Parker","date_of_birth":"1979-11-18"},{"first_name":"Larry","last_name":"Bird","date_of_birth":"1956-12-07"},{"first_name":"Bobby","last_name":"Flay","date_of_birth":"1964-12-10"},{"first_name":"Chris","last_name":"Evert","date_of_birth":"1954-12-21"},{"first_name":"Frank","last_name":"Zappa","date_of_birth":"1940-12-21"},{"first_name":"Estella","last_name":"Warren","date_of_birth":"1978-12-23"},{"first_name":"Carlos","last_name":"Castaneda","date_of_birth":"1925-12-25"},{"first_name":"Seth","last_name":"Meyers","date_of_birth":"1973-12-28"}]
data/e2e_tests/run ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ cd "$(dirname "${BASH_SOURCE[0]}")"
6
+
7
+ for t in [0-9]*; do
8
+ rv="0"
9
+ cd "$t"
10
+
11
+ ./run || rv="$?"
12
+
13
+ if [ "$rv" != "0" ]; then
14
+ echo "Test $i failed with exit code $rv" >&2
15
+ exit 1
16
+ fi
17
+ done
18
+
19
+ echo "All tests passed."
data/lib/active_enquo.rb CHANGED
@@ -21,7 +21,57 @@ module ActiveEnquo
21
21
  RootKey = Enquo::RootKey
22
22
 
23
23
  module ActiveRecord
24
- module ModelExtension
24
+ module QueryFilterMangler
25
+ class Ciphertext < String
26
+ end
27
+ private_constant :Ciphertext
28
+
29
+ private
30
+
31
+ def mangle_query_filter(a)
32
+ args = a.first
33
+ if args.is_a?(Hash)
34
+ args.keys.each do |attr|
35
+ next unless enquo_attr?(attr)
36
+
37
+ if args[attr].is_a?(Array)
38
+ args[attr] = args[attr].map { |v| maybe_enquo(attr, v) }
39
+ elsif args[attr].is_a?(Range)
40
+ r = args[attr]
41
+ args[attr] = if r.exclude_end?
42
+ if r.begin.nil?
43
+ ...maybe_enquo(attr, r.end)
44
+ elsif r.end.nil?
45
+ (maybe_enquo(attr.r.begin)...)
46
+ else
47
+ maybe_enquo(attr.r.begin)...maybe_enquo(attr, r.end)
48
+ end
49
+ else
50
+ if r.begin.nil?
51
+ ..maybe_enquo(attr, r.end)
52
+ elsif r.end.nil?
53
+ maybe_enquo(attr.r.begin)..
54
+ else
55
+ maybe_enquo(attr.r.begin)..maybe_enquo(attr, r.end)
56
+ end
57
+ end
58
+ else
59
+ args[attr] = maybe_enquo(attr, args[attr])
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def maybe_enquo(attr, v)
66
+ if v.nil? || v.is_a?(Ciphertext) || v.is_a?(::ActiveRecord::StatementCache::Substitute)
67
+ v
68
+ else
69
+ Ciphertext.new(enquo(attr, v))
70
+ end
71
+ end
72
+ end
73
+
74
+ module BaseExtension
25
75
  extend ActiveSupport::Concern
26
76
 
27
77
  def _read_attribute(attr_name, &block)
@@ -29,6 +79,7 @@ module ActiveEnquo
29
79
  if t.is_a?(::ActiveEnquo::Type)
30
80
  relation = self.class.arel_table.name
31
81
  value = @attributes.fetch_value(attr_name, &block)
82
+ return nil if value.nil?
32
83
  field = ::ActiveEnquo.root.field(relation, attr_name)
33
84
  begin
34
85
  t.decrypt(value, @attributes.fetch_value(@primary_key).to_s, field)
@@ -50,10 +101,7 @@ module ActiveEnquo
50
101
  relation = self.class.arel_table.name
51
102
  field = ::ActiveEnquo.root.field(relation, attr_name)
52
103
  attr_opts = self.class.enquo_attribute_options.fetch(attr_name.to_sym, {})
53
- safety = if attr_opts[:enable_reduced_security_operations]
54
- :unsafe
55
- end
56
- db_value = t.encrypt(value, @attributes.fetch_value(@primary_key).to_s, field, safety: safety, no_query: attr_opts[:no_query])
104
+ db_value = t.encrypt(value, @attributes.fetch_value(@primary_key).to_s, field, **attr_opts)
57
105
  @attributes.write_from_user(attr_name, db_value)
58
106
  else
59
107
  super
@@ -61,31 +109,148 @@ module ActiveEnquo
61
109
  end
62
110
 
63
111
  module ClassMethods
64
- def enquo(attr_name, value)
112
+ include QueryFilterMangler
113
+
114
+ def find_by(*a)
115
+ mangle_query_filter(a)
116
+ super
117
+ end
118
+
119
+ def enquo(attr_name, value_or_meta_id, maybe_value = nil)
120
+ meta_id, value = if value_or_meta_id.is_a?(Symbol)
121
+ [value_or_meta_id, maybe_value]
122
+ else
123
+ [nil, value_or_meta_id]
124
+ end
125
+
65
126
  t = self.attribute_types[attr_name.to_s]
66
127
  if t.is_a?(::ActiveEnquo::Type)
67
128
  relation = self.arel_table.name
68
129
  field = ::ActiveEnquo.root.field(relation, attr_name)
69
- t.encrypt(value, "", field, safety: :unsafe)
130
+ if meta_id.nil?
131
+ t.encrypt(value, "", field, enable_reduced_security_operations: true)
132
+ else
133
+ t.encrypt_metadata_value(meta_id, value, field)
134
+ end
70
135
  else
71
136
  raise ArgumentError, "Cannot produce encrypted value on a non-enquo attribute '#{attr_name}'"
72
137
  end
73
138
  end
74
139
 
140
+ def unenquo(attr_name, value, ctx)
141
+ t = self.attribute_types[attr_name.to_s]
142
+ if t.is_a?(::ActiveEnquo::Type)
143
+ relation = self.arel_table.name
144
+ field = ::ActiveEnquo.root.field(relation, attr_name)
145
+ begin
146
+ t.decrypt(value, ctx, field)
147
+ rescue Enquo::Error
148
+ t.decrypt(value, "", field)
149
+ end
150
+ else
151
+ raise ArgumentError, "Cannot decrypt value on a non-enquo attribute '#{attr_name}'"
152
+ end
153
+ end
154
+
155
+ def enquo_attr?(attr_name)
156
+ self.attribute_types[attr_name.to_s].is_a?(::ActiveEnquo::Type)
157
+ end
158
+
75
159
  def enquo_attr(attr_name, opts)
160
+ if opts.key?(:default)
161
+ default_value = opts.delete(:default)
162
+ after_initialize do
163
+ next if persisted?
164
+ next unless self.send(attr_name).nil?
165
+ self.send(:"#{attr_name}=", default_value.duplicable? ? default_value.dup : default_value)
166
+ end
167
+ end
168
+
76
169
  enquo_attribute_options[attr_name] = @enquo_attribute_options[attr_name].merge(opts)
77
170
  end
78
171
 
79
172
  def enquo_attribute_options
80
173
  @enquo_attribute_options ||= Hash.new({})
81
174
  end
175
+
176
+ def enquo_encrypt_columns(column_map, batch_size: 10_000)
177
+ plaintext_columns = column_map.keys
178
+ relation = self.arel_table.name
179
+ in_progress = true
180
+ self.reset_column_information
181
+
182
+ while in_progress
183
+ self.transaction do
184
+ # The .where("0=1") here is a dummy condition so that the q.or in the .each will work properly
185
+ q = self.select(self.primary_key).select(plaintext_columns).where("0=1")
186
+ column_map.each do |pt_col, ct_col|
187
+ q = q.or(self.where(ct_col => nil).where.not(pt_col => nil))
188
+ end
189
+
190
+ q = q.limit(batch_size).lock
191
+
192
+ rows = ::ActiveRecord::Base.connection.exec_query(q.to_sql)
193
+ if rows.length == 0
194
+ in_progress = false
195
+ else
196
+ rows.each do |row|
197
+ values = Hash[column_map.map do |pt_col, ct_col|
198
+ field = ::ActiveEnquo.root.field(relation, ct_col)
199
+ attr_opts = self.enquo_attribute_options.fetch(ct_col.to_sym, {})
200
+ t = self.attribute_types[ct_col.to_s]
201
+ db_value = t.encrypt(row[pt_col.to_s], row[self.primary_key].to_s, field, **attr_opts)
202
+
203
+ [ct_col, db_value]
204
+ end]
205
+
206
+ self.where(self.primary_key => row[self.primary_key]).update_all(values)
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ module TableDefinitionExtension
216
+ def enquo_boolean(name, **options)
217
+ column(name, :enquo_boolean, **options)
218
+ end
219
+
220
+ def enquo_bigint(name, **options)
221
+ column(name, :enquo_bigint, **options)
222
+ end
223
+
224
+ def enquo_date(name, **options)
225
+ column(name, :enquo_date, **options)
226
+ end
227
+
228
+ def enquo_text(name, **options)
229
+ column(name, :enquo_text, **options)
82
230
  end
83
231
  end
232
+
233
+ module RelationExtension
234
+ include QueryFilterMangler
235
+ extend ActiveSupport::Concern
236
+
237
+ def where(*a)
238
+ mangle_query_filter(a)
239
+ super
240
+ end
241
+
242
+ def exists?(*a)
243
+ mangle_query_filter(a)
244
+ super
245
+ end
246
+
247
+ end
84
248
  end
85
249
 
86
250
  module Postgres
87
251
  module ConnectionAdapter
88
252
  def initialize_type_map(m = type_map)
253
+ m.register_type "enquo_boolean", ActiveEnquo::Type::Boolean.new
89
254
  m.register_type "enquo_bigint", ActiveEnquo::Type::Bigint.new
90
255
  m.register_type "enquo_date", ActiveEnquo::Type::Date.new
91
256
  m.register_type "enquo_text", ActiveEnquo::Type::Text.new
@@ -96,13 +261,35 @@ module ActiveEnquo
96
261
  end
97
262
 
98
263
  class Type < ::ActiveRecord::Type::Value
264
+ class Boolean < Type
265
+ def type
266
+ :enquo_boolean
267
+ end
268
+
269
+ def encrypt(value, context, field, enable_reduced_security_operations: false, no_query: false)
270
+ if value.nil? || value.is_a?(::ActiveRecord::StatementCache::Substitute)
271
+ value
272
+ else
273
+ field.encrypt_boolean(value, context, safety: enable_reduced_security_operations ? :unsafe : true, no_query: no_query)
274
+ end
275
+ end
276
+
277
+ def decrypt(value, context, field)
278
+ field.decrypt_boolean(value, context)
279
+ end
280
+ end
281
+
99
282
  class Bigint < Type
100
283
  def type
101
284
  :enquo_bigint
102
285
  end
103
286
 
104
- def encrypt(value, context, field, safety: true, no_query: false)
105
- field.encrypt_i64(value, context, safety: safety, no_query: no_query)
287
+ def encrypt(value, context, field, enable_reduced_security_operations: false, no_query: false)
288
+ if value.nil? || value.is_a?(::ActiveRecord::StatementCache::Substitute)
289
+ value
290
+ else
291
+ field.encrypt_i64(value, context, safety: enable_reduced_security_operations ? :unsafe : true, no_query: no_query)
292
+ end
106
293
  end
107
294
 
108
295
  def decrypt(value, context, field)
@@ -115,9 +302,13 @@ module ActiveEnquo
115
302
  :enquo_date
116
303
  end
117
304
 
118
- def encrypt(value, context, field, safety: true, no_query: false)
305
+ def encrypt(value, context, field, enable_reduced_security_operations: false, no_query: false)
119
306
  value = cast_to_date(value)
120
- field.encrypt_date(value, context, safety: safety, no_query: no_query)
307
+ if value.nil?
308
+ value
309
+ else
310
+ field.encrypt_date(value, context, safety: enable_reduced_security_operations ? :unsafe : true, no_query: no_query)
311
+ end
121
312
  end
122
313
 
123
314
  def decrypt(value, context, field)
@@ -131,6 +322,8 @@ module ActiveEnquo
131
322
  value
132
323
  elsif value.respond_to?(:to_date)
133
324
  value.to_date
325
+ elsif value.nil? || (value.respond_to?(:empty?) && value.empty?) || value.is_a?(::ActiveRecord::StatementCache::Substitute)
326
+ nil
134
327
  else
135
328
  Time.parse(value.to_s).to_date
136
329
  end
@@ -142,26 +335,73 @@ module ActiveEnquo
142
335
  :enquo_text
143
336
  end
144
337
 
145
- def encrypt(value, context, field, safety: true, no_query: false)
146
- field.encrypt_text(value, context, safety: safety, no_query: no_query)
338
+ def encrypt(value, context, field, enable_reduced_security_operations: false, no_query: false, enable_ordering: false)
339
+ if enable_ordering && !enable_reduced_security_operations
340
+ raise ArgumentError, "Cannot enable ordering on an Enquo attribute unless Reduced Security Operations are enabled"
341
+ end
342
+
343
+ if value.nil? || value.is_a?(::ActiveRecord::StatementCache::Substitute)
344
+ value
345
+ else
346
+ field.encrypt_text(value.respond_to?(:encode) ? value.encode("UTF-8") : value, context, safety: enable_reduced_security_operations ? :unsafe : true, no_query: no_query, order_prefix_length: enable_ordering ? 8 : nil)
347
+ end
348
+ end
349
+
350
+ def encrypt_metadata_value(name, value, field)
351
+ case name
352
+ when :length
353
+ field.encrypt_text_length_query(value)
354
+ else
355
+ raise ArgumentError, "Unknown metadata name for Text field: #{name.inspect}"
356
+ end
147
357
  end
148
358
 
149
359
  def decrypt(value, context, field)
150
- field.decrypt_text(value, context)
360
+ if value.nil?
361
+ nil
362
+ else
363
+ field.decrypt_text(value, context)
364
+ end
365
+ end
366
+ end
367
+ end
368
+
369
+ if defined?(Rails::Railtie)
370
+ class Initializer < Rails::Railtie
371
+ initializer "active_enquo.root_key" do |app|
372
+ if app
373
+ if app.credentials
374
+ if app.credentials.active_enquo
375
+ if root_key = app.credentials.active_enquo.root_key
376
+ ActiveEnquo.root_key = Enquo::RootKey::Static.new(root_key)
377
+ else
378
+ Rails.logger.warn "Could not initialize ActiveEnquo, as no active_enquo.root_key credential was found for this environment"
379
+ end
380
+ else
381
+ Rails.logger.warn "Could not initialize ActiveEnquo, as no active_enquo credentials were found for this environment"
382
+ end
383
+ else
384
+ Rails.logger.warn "Could not initialize ActiveEnquo, as no credentials were found for this environment"
385
+ end
386
+ else
387
+ Rails.logger.warn "Could not initialize ActiveEnquo, as no app was found for this environment"
388
+ end
151
389
  end
152
390
  end
153
391
  end
154
392
  end
155
393
 
156
394
  ActiveSupport.on_load(:active_record) do
157
- ::ActiveRecord::Base.send :include, ActiveEnquo::ActiveRecord::ModelExtension
395
+ ::ActiveRecord::Relation.prepend ActiveEnquo::ActiveRecord::RelationExtension
396
+ ::ActiveRecord::Base.include ActiveEnquo::ActiveRecord::BaseExtension
397
+
398
+ ::ActiveRecord::ConnectionAdapters::Table.include ActiveEnquo::ActiveRecord::TableDefinitionExtension
399
+ ::ActiveRecord::ConnectionAdapters::TableDefinition.include ActiveEnquo::ActiveRecord::TableDefinitionExtension
158
400
 
159
401
  ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend ActiveEnquo::Postgres::ConnectionAdapter
160
- # ::ActiveRecord::Type.register(:enquo_bigint, ActiveEnquo::Type::Bigint, adapter: :postgresql)
161
402
 
162
- unless ActiveRecord::VERSION::MAJOR == 7
163
- ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:enquo_bigint] = {}
164
- ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:enquo_date] = {}
165
- ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:enquo_text] = {}
166
- end
403
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:enquo_boolean] = { name: "enquo_boolean" }
404
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:enquo_bigint] = { name: "enquo_bigint" }
405
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:enquo_date] = { name: "enquo_date" }
406
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:enquo_text] = { name: "enquo_text" }
167
407
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_enquo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Palmer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-07 00:00:00.000000000 Z
11
+ date: 2023-04-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: enquo-core
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.6'
19
+ version: '0.7'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0.6'
26
+ version: '0.7'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activerecord
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -195,6 +195,16 @@ files:
195
195
  - README.md
196
196
  - active_enquo.gemspec
197
197
  - docs/DEVELOPMENT.md
198
+ - docs/MIGRATION.md
199
+ - e2e_tests/.gitignore
200
+ - e2e_tests/001_direct_migration/exercise_model
201
+ - e2e_tests/001_direct_migration/migrations/001_create_people_table.rb
202
+ - e2e_tests/001_direct_migration/migrations/002_encrypt_people_data.rb
203
+ - e2e_tests/001_direct_migration/run
204
+ - e2e_tests/helper.sh
205
+ - e2e_tests/init.rb
206
+ - e2e_tests/people.json
207
+ - e2e_tests/run
198
208
  - lib/active_enquo.rb
199
209
  homepage: https://enquo.org
200
210
  licenses: []