rails-panda-sensitive-data 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +108 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +248 -0
- data/lib/rails/panda/sensitive/data.rb +3 -0
- data/lib/rails_panda_sensitive_data/config.rb +18 -0
- data/lib/rails_panda_sensitive_data/encryption/active_record__encrypted_attribute_type.rb +65 -0
- data/lib/rails_panda_sensitive_data/encryption/encryptor.rb +88 -0
- data/lib/rails_panda_sensitive_data/models/active_record.rb +134 -0
- data/lib/rails_panda_sensitive_data/models/mongoid.rb +217 -0
- data/lib/rails_panda_sensitive_data/sensitive_data.rb +10 -0
- data/lib/rails_panda_sensitive_data/version.rb +7 -0
- data/lib/rails_panda_sensitive_data.rb +12 -0
- data/rails_panda_sensitive_data.gemspec +44 -0
- metadata +252 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d553ba5052f74136215c8702a875806544e112cdb010d819d485ee0788d10ab5
|
|
4
|
+
data.tar.gz: bc7a5fdb96003b1db4b913a4d1aecff11c85f41f9216612e267d44ab498d364b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d37df001f8e66168694da9b8984f05794acd1fd1e25e051b62db95262463fe3b3e98d846ee40de22844cba199eba1e8991d8d1d3c7a7979fc29cf066f91887a9
|
|
7
|
+
data.tar.gz: 4228dec6ceea4da6fca90014248c1868ac1e2f895f8d9570a383d5321145915c51d40664d21cd6b98aa7d0e8a06368227316a4c3fbc4c86ee0248232e439cd18
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [3.0.0] - 2025-11-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Comprehensive RSpec test suite
|
|
14
|
+
- SimpleCov integration for test coverage tracking
|
|
15
|
+
- `nil_visible_in_db` option to control whether nil values are visible in the database or encrypted
|
|
16
|
+
- `whitespace_visible_in_db` option to control whether whitespace-only strings are visible in the database or encrypted
|
|
17
|
+
- `rails_default` option to `encrypts` to bypass custom logic and use Rails' standard behavior
|
|
18
|
+
- Full dirty tracking support for `has_sensitive_data` attributes:
|
|
19
|
+
- `#{field}_changed?` - Check if field has changed but not saved
|
|
20
|
+
- `#{field}_in_database` - Get value currently in database
|
|
21
|
+
- `#{field}_before_last_save` - Get value before last save
|
|
22
|
+
- `saved_change_to_#{field}?` - Check if field changed in last save
|
|
23
|
+
- `saved_change_to_#{field}` - Get `[before, after]` array of last save change
|
|
24
|
+
- `will_save_change_to_#{field}?` - Check if field will change on next save
|
|
25
|
+
- `#{field}_change_to_be_saved` - Get `[current_db_value, new_value]` array of pending changes
|
|
26
|
+
- Auto-inclusion of module in all ActiveRecord::Base models via `ActiveSupport.on_load`
|
|
27
|
+
- ArgumentError raised when `encrypts` or `has_sensitive_data` are called without attributes
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- Module is now automatically included in all ActiveRecord::Base models (no manual inclusion needed)
|
|
31
|
+
- Removed custom `KeyProvider` and `Key` classes - now uses Rails' default key management
|
|
32
|
+
- Renamed `store_nil_as_empty_string` to `nil_visible_in_db` for clarity
|
|
33
|
+
- `empty_string_visible_in_db` now defaults to `true` (was `false` in previous versions)
|
|
34
|
+
- `nil_visible_in_db` now defaults to `true` (nil values stored as nil, not encrypted)
|
|
35
|
+
- Improved code safety and type checking in Encryptor
|
|
36
|
+
- All dirty tracking methods match Rails' standard behavior exactly
|
|
37
|
+
- Improved `attribute_before_last_save` implementation to use Rails' built-in `attribute_before_last_save` method instead of manual method calls
|
|
38
|
+
- Improved `saved_change_to_attribute?` to correctly handle `nil` values (returns `false` when `before_last_save` is `nil` after reload, matching Rails' behavior)
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
- Fixed Encryptor to properly pass options to parent class methods
|
|
42
|
+
- Fixed type checking for empty string handling in Encryptor
|
|
43
|
+
|
|
44
|
+
## [2.0.6] - 2025-10-06
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
- Fixed loading issues - gem now loads correctly
|
|
48
|
+
- Added frozen string literal comments to all files
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
- Fixed module loading and initialization issues
|
|
52
|
+
|
|
53
|
+
## [2.0.5] - 2025-10-05
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
- Removed a useless module
|
|
57
|
+
|
|
58
|
+
## [2.0.4] - 2023-10-10
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
- Various improvements and bug fixes
|
|
62
|
+
|
|
63
|
+
## [2.0.2] - 2023-10-08
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
- Various improvements and bug fixes
|
|
67
|
+
|
|
68
|
+
## [2.0.1] - 2023-10-06
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
- Various improvements and bug fixes
|
|
72
|
+
|
|
73
|
+
## [2.0.0] - 2023-03-18
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
- Removed `attr_encrypted` dependency
|
|
77
|
+
- Migrated to use ActiveRecord::Encryption (Rails 7.0+)
|
|
78
|
+
- Major refactoring to use Rails' built-in encryption framework
|
|
79
|
+
|
|
80
|
+
## [1.0.2] - 2023-03-11
|
|
81
|
+
|
|
82
|
+
### Changed
|
|
83
|
+
- Various improvements and bug fixes
|
|
84
|
+
|
|
85
|
+
## [1.0.1] - 2022-03-23
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
- Various improvements and bug fixes
|
|
89
|
+
|
|
90
|
+
## [1.0.0] - 2018-02-20
|
|
91
|
+
|
|
92
|
+
### Added
|
|
93
|
+
- Initial release
|
|
94
|
+
- Module to handle encryption for GDPR compliance
|
|
95
|
+
- Support for encrypting sensitive data in ActiveRecord models
|
|
96
|
+
- Support for storing multiple sensitive fields in a single encrypted attribute
|
|
97
|
+
|
|
98
|
+
[Unreleased]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v3.0.0...HEAD
|
|
99
|
+
[3.0.0]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v2.0.6...v3.0.0
|
|
100
|
+
[2.0.6]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v2.0.5...v2.0.6
|
|
101
|
+
[2.0.5]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v2.0.4...v2.0.5
|
|
102
|
+
[2.0.4]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v2.0.2...v2.0.4
|
|
103
|
+
[2.0.2]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v2.0.1...v2.0.2
|
|
104
|
+
[2.0.1]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v2.0.0...v2.0.1
|
|
105
|
+
[2.0.0]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v1.0.2...v2.0.0
|
|
106
|
+
[1.0.2]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v1.0.1...v1.0.2
|
|
107
|
+
[1.0.1]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/compare/v1.0.0...v1.0.1
|
|
108
|
+
[1.0.0]: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data/releases/tag/v1.0.0
|
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2015 bigbadpanda.com
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# Rails Panda Sensitive Data
|
|
2
|
+
|
|
3
|
+
A Rails gem that provides enhanced encryption capabilities for handling (and encrypting) sensitive data in ActiveRecord models.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Enhanced ActiveRecord encryption** - Extends Rails' built-in `encrypts` method with additional options
|
|
8
|
+
- **Multiple sensitive fields in one attribute** - Store multiple sensitive data fields in a single encrypted attribute using `has_sensitive_data`
|
|
9
|
+
- **Customizable empty string handling** - Control whether empty strings are visible in the database or encrypted
|
|
10
|
+
- **Nil value handling** - Option to control whether nil values are visible in the database or encrypted
|
|
11
|
+
- **Whitespace-only string handling** - Option to store whitespace-only strings as-is (space-saving for non-PII data)
|
|
12
|
+
- **Full dirty tracking support** - Complete ActiveRecord dirty tracking methods for sensitive data fields
|
|
13
|
+
- **Auto-inclusion** - Automatically included in all ActiveRecord::Base models
|
|
14
|
+
- **Rails 7.0+ compatible** - Built on top of ActiveRecord::Encryption
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Add this line to your application's Gemfile:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem "rails-panda-sensitive-data"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
And then execute:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
$ bundle install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- Ruby >= 3.2
|
|
33
|
+
- Rails >= 7.0 (required for ActiveRecord::Encryption support)
|
|
34
|
+
- ActiveRecord::Encryption must be configured in your Rails application
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
### ActiveRecord::Encryption Setup
|
|
39
|
+
|
|
40
|
+
First, configure ActiveRecord::Encryption in your Rails application:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
# config/application.rb
|
|
44
|
+
config.active_record.encryption.primary_key = "your-primary-key-here"
|
|
45
|
+
config.active_record.encryption.deterministic_key = "your-deterministic-key-here"
|
|
46
|
+
config.active_record.encryption.key_derivation_salt = "your-key-derivation-salt-here"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
I don't mention including this in `config/initializers/active_record_encryption.rb` because I've often run into problems with this approach. Namely that ActiveRecord was already initialized (read: had fetched these encryption keys) by the time the initializers started running.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
### Basic Setup
|
|
54
|
+
|
|
55
|
+
The module is automatically included in all ActiveRecord::Base models. No manual inclusion needed!
|
|
56
|
+
|
|
57
|
+
### Using `encrypts`
|
|
58
|
+
|
|
59
|
+
The `encrypts` method works like Rails' built-in method but with additional options:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
class User < ApplicationRecord
|
|
63
|
+
encrypts :email
|
|
64
|
+
encrypts :phone_number
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### Options
|
|
69
|
+
|
|
70
|
+
- `nil_visible_in_db` (default: `true`) - If `true`, nil values are stored as nil in the database (saves space, but visible). If `false`, nil values are encrypted using a sentinel value (`\0`) to distinguish them from encrypted empty strings.
|
|
71
|
+
- `empty_string_visible_in_db` (default: `true`) - If `true`, empty strings are stored as empty strings in the database (saves space, but visible). If `false`, they are encrypted.
|
|
72
|
+
- `whitespace_visible_in_db` (default: `true`) - If `true`, whitespace-only strings (e.g., " ", "\t\n") are stored as-is in the database (saves space, no PII). If `false`, they are encrypted.
|
|
73
|
+
- `rails_default` (default: `false`) - If `true`, bypasses all custom logic and uses Rails' standard `encrypts` behavior.
|
|
74
|
+
- `encryptor` - Custom encryptor instance (if provided, custom encryptor won't be created automatically)
|
|
75
|
+
- All standard Rails `encrypts` options are also supported (e.g., `deterministic`, `key_provider`, etc.)
|
|
76
|
+
|
|
77
|
+
**Example:**
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
class User < ApplicationRecord
|
|
81
|
+
# Encrypt empty strings instead of storing them as-is
|
|
82
|
+
encrypts :email, empty_string_visible_in_db: false
|
|
83
|
+
|
|
84
|
+
# Encrypt nil values instead of storing them as nil
|
|
85
|
+
encrypts :phone_number, nil_visible_in_db: false
|
|
86
|
+
|
|
87
|
+
# Encrypt whitespace-only strings
|
|
88
|
+
encrypts :notes, whitespace_visible_in_db: false
|
|
89
|
+
|
|
90
|
+
# Use Rails' standard behavior (bypass custom logic)
|
|
91
|
+
encrypts :backup_email, rails_default: true
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Trade-offs:**
|
|
96
|
+
|
|
97
|
+
- **Visible in DB (`true`)**: Saves space, faster queries, but values are visible in the database
|
|
98
|
+
- **Encrypted (`false`)**: More secure, but takes more space. Note: Encrypted empty/nil values can still be inferred from their shorter encrypted blob size.
|
|
99
|
+
|
|
100
|
+
### Using `has_sensitive_data`
|
|
101
|
+
|
|
102
|
+
Store multiple sensitive data fields in a single encrypted attribute:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
class User < ApplicationRecord
|
|
106
|
+
has_sensitive_data :ssn, :credit_card, :bank_account
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
This will:
|
|
111
|
+
- Store all fields in a single `sensitive_data` encrypted attribute
|
|
112
|
+
- Provide accessor methods for each field (`ssn`, `credit_card`, `bank_account`)
|
|
113
|
+
- Automatically encrypt/decrypt the entire hash
|
|
114
|
+
|
|
115
|
+
**Example:**
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
user = User.new
|
|
119
|
+
user.ssn = "123-45-6789"
|
|
120
|
+
user.credit_card = "4111-1111-1111-1111"
|
|
121
|
+
user.bank_account = "1234567890"
|
|
122
|
+
user.save!
|
|
123
|
+
|
|
124
|
+
# Reading values
|
|
125
|
+
user.ssn # => "123-45-6789"
|
|
126
|
+
user.credit_card # => "4111-1111-1111-1111"
|
|
127
|
+
|
|
128
|
+
# The sensitive_data attribute is encrypted
|
|
129
|
+
user.read_attribute(:sensitive_data) # => encrypted value, not plain hash
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### Options
|
|
133
|
+
|
|
134
|
+
- `in` (default: `:sensitive_data`) - The attribute name to store the encrypted hash
|
|
135
|
+
- All options from `encrypts` are also supported
|
|
136
|
+
|
|
137
|
+
**Example with custom attribute name:**
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
class User < ApplicationRecord
|
|
141
|
+
has_sensitive_data :ssn, :credit_card, in: :encrypted_pii
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Change Tracking
|
|
146
|
+
|
|
147
|
+
The gem provides change tracking methods for sensitive data fields:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
user = User.new
|
|
151
|
+
user.ssn = "123-45-6789"
|
|
152
|
+
user.save!
|
|
153
|
+
|
|
154
|
+
user.ssn = "999-99-9999"
|
|
155
|
+
user.ssn_changed? # => true
|
|
156
|
+
user.ssn_in_database # => "123-45-6789"
|
|
157
|
+
|
|
158
|
+
user.save!
|
|
159
|
+
user.saved_change_to_ssn? # => true
|
|
160
|
+
user.ssn_before_last_save # => "123-45-6789"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Available methods (matching Rails' standard dirty tracking API):
|
|
164
|
+
- `#{field}_changed?` - Returns true if the field has been changed but not saved
|
|
165
|
+
- `#{field}_in_database` - Returns the value currently stored in the database
|
|
166
|
+
- `#{field}_before_last_save` - Returns the value before the last save (nil until after a save, and nil after reload)
|
|
167
|
+
- `saved_change_to_#{field}?` - Returns true if the field was changed in the last save
|
|
168
|
+
- `saved_change_to_#{field}` - Returns `[before, after]` array if changed, or `nil`
|
|
169
|
+
- `will_save_change_to_#{field}?` - Returns true if the field will change on the next save
|
|
170
|
+
- `#{field}_change_to_be_saved` - Returns `[current_db_value, new_value]` array if there are pending changes, or `nil`
|
|
171
|
+
|
|
172
|
+
### Combining `encrypts` and `has_sensitive_data`
|
|
173
|
+
|
|
174
|
+
You can use both methods in the same model:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
class User < ApplicationRecord
|
|
178
|
+
encrypts :email
|
|
179
|
+
has_sensitive_data :ssn, :credit_card
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Advanced Usage
|
|
184
|
+
|
|
185
|
+
### Custom Encryptor
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
custom_encryptor = RailsPanda::SensitiveData::Encryption::Encryptor.new(
|
|
189
|
+
empty_string_visible_in_db: false,
|
|
190
|
+
nil_visible_in_db: false,
|
|
191
|
+
whitespace_visible_in_db: false
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
class User < ApplicationRecord
|
|
195
|
+
encrypts :email, encryptor: custom_encryptor
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Note:** If you provide a custom `encryptor`, the gem will not automatically create a custom encryptor based on the visibility options. The provided encryptor will be used as-is.
|
|
200
|
+
|
|
201
|
+
### Key Management
|
|
202
|
+
|
|
203
|
+
The gem uses Rails' default key provider from your ActiveRecord::Encryption configuration. This supports deterministic encryption and follows Rails' standard key management practices. You can also provide a custom key provider if needed:
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
# Uses Rails' default key provider (recommended)
|
|
207
|
+
class User < ApplicationRecord
|
|
208
|
+
encrypts :email
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Or provide a custom key provider
|
|
212
|
+
custom_key_provider = ActiveRecord::Encryption::KeyProvider.new(primary_key: "your-key")
|
|
213
|
+
|
|
214
|
+
class User < ApplicationRecord
|
|
215
|
+
encrypts :email, key_provider: custom_key_provider
|
|
216
|
+
end
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## How It Works
|
|
220
|
+
|
|
221
|
+
### `encrypts`
|
|
222
|
+
|
|
223
|
+
This method extends Rails' built-in `encrypts` method with additional options for handling nil values, empty strings, and whitespace-only strings. It uses a custom `Encryptor` that wraps `ActiveRecord::Encryption::Encryptor` and provides space-saving options for non-sensitive values.
|
|
224
|
+
|
|
225
|
+
The custom encryptor is automatically created when visibility options are set to their default values (`true`). You can bypass custom logic entirely by passing `rails_default: true`.
|
|
226
|
+
|
|
227
|
+
### `has_sensitive_data`
|
|
228
|
+
|
|
229
|
+
This method:
|
|
230
|
+
1. Serializes multiple fields into a single Hash using YAML
|
|
231
|
+
2. Encrypts the entire Hash using `encrypts`
|
|
232
|
+
3. Provides accessor methods for each field
|
|
233
|
+
4. Handles nil values by removing them from the hash
|
|
234
|
+
5. Provides full dirty tracking support matching Rails' standard API
|
|
235
|
+
|
|
236
|
+
The encrypted hash is stored in a single database column, reducing the number of encrypted columns needed. All dirty tracking methods work exactly like Rails' built-in methods for regular attributes.
|
|
237
|
+
|
|
238
|
+
## Development
|
|
239
|
+
|
|
240
|
+
After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rspec` to run the tests.
|
|
241
|
+
|
|
242
|
+
## Contributing
|
|
243
|
+
|
|
244
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/bigbadpanda-tech/rails-panda-sensitive-data.
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsPanda
|
|
4
|
+
module SensitiveData
|
|
5
|
+
class Config # rubocop:disable Lint/EmptyClass
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def configure
|
|
10
|
+
yield config
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def config
|
|
14
|
+
@config ||= Config.new
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :nocov:
|
|
4
|
+
# We do not collect coverage for this file because the coverage tool cannot correctly analyze the coverage in this file, due to the way these methods are (re)implemented.
|
|
5
|
+
|
|
6
|
+
module ActiveRecord
|
|
7
|
+
module Encryption
|
|
8
|
+
class EncryptedAttributeType
|
|
9
|
+
alias_method :deserialize_original, :deserialize
|
|
10
|
+
alias_method :serialize_with_current_original, :serialize_with_current
|
|
11
|
+
|
|
12
|
+
# Override deserialize to ensure our custom decryptor converts the sentinel back to nil
|
|
13
|
+
def deserialize(value)
|
|
14
|
+
deserialize_original(value).then do |result|
|
|
15
|
+
# Check special values
|
|
16
|
+
if result == RailsPanda::SensitiveData::Encryption::Encryptor::NIL_SENTINEL
|
|
17
|
+
rails_panda_encryptor&.then do |enc|
|
|
18
|
+
return nil if !enc.instance_variable_get(:@nil_visible_in_db)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
result
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Override serialize_with_current to handle nil values when nil_visible_in_db is false
|
|
28
|
+
# Rails' EncryptedAttributeType checks for nil before calling encrypt, so we need to convert nil to our sentinel value before Rails processes it
|
|
29
|
+
def serialize_with_current(value)
|
|
30
|
+
if value.nil?
|
|
31
|
+
rails_panda_encryptor&.then do |enc|
|
|
32
|
+
return nil if enc.instance_variable_get(:@nil_visible_in_db)
|
|
33
|
+
|
|
34
|
+
# Convert nil to sentinel value and encrypt it directly
|
|
35
|
+
# We need to encrypt it ourselves because Rails' serialize_with_current might store single-byte values directly without encryption
|
|
36
|
+
# Get the key_provider from the scheme or use the default
|
|
37
|
+
key_provider = scheme&.key_provider
|
|
38
|
+
key_provider ||= scheme&.instance_variable_get(:@key_provider_param)
|
|
39
|
+
key_provider ||= ActiveRecord::Encryption.config.then do |config|
|
|
40
|
+
config.is_a?(ActiveRecord::Encryption::KeyProvider) ? config : nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
encrypted_sentinel = enc.encrypt(
|
|
44
|
+
RailsPanda::SensitiveData::Encryption::Encryptor::NIL_SENTINEL,
|
|
45
|
+
key_provider: key_provider
|
|
46
|
+
)
|
|
47
|
+
return encrypted_sentinel
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
serialize_with_current_original(value)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def rails_panda_encryptor
|
|
55
|
+
scheme&.instance_variable_get(:@context_properties)&.then do |props|
|
|
56
|
+
if props.is_a?(Hash)
|
|
57
|
+
# Get the encryptor from the scheme's context_properties
|
|
58
|
+
enc = props[:encryptor]
|
|
59
|
+
enc.is_a?(RailsPanda::SensitiveData::Encryption::Encryptor) ? enc : nil
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsPanda
|
|
4
|
+
module SensitiveData
|
|
5
|
+
module Encryption
|
|
6
|
+
class Encryptor < ::ActiveRecord::Encryption::Encryptor
|
|
7
|
+
# Sentinel value to distinguish encrypted nil from encrypted empty string.
|
|
8
|
+
# When nil_visible_in_db is false, we encrypt nil using this sentinel value (\0) instead of an empty string, allowing us to distinguish between encrypted nil and encrypted empty string during decryption.
|
|
9
|
+
NIL_SENTINEL = "\0"
|
|
10
|
+
|
|
11
|
+
# Custom encryptor that provides control over how nil, empty strings, and whitespace-only strings are stored.
|
|
12
|
+
#
|
|
13
|
+
# @param nil_visible_in_db [Boolean] When true (default), stores nil as nil in the database.
|
|
14
|
+
# This saves space and allows nil values to be queried directly, but makes them visible.
|
|
15
|
+
# When false, encrypts nil using a sentinel value. This hides nil values but takes more space.
|
|
16
|
+
# Note: Encrypted empty values can still be inferred from their shorter encrypted blob size.
|
|
17
|
+
#
|
|
18
|
+
# @param empty_string_visible_in_db [Boolean] When true (default), stores empty strings as ""
|
|
19
|
+
# in the database. This saves space and allows empty strings to be queried directly, but makes them visible. When false, encrypts empty strings normally. This hides empty strings but takes more space. Note: Encrypted empty values can still be inferred from their shorter encrypted blob size.
|
|
20
|
+
#
|
|
21
|
+
# @param whitespace_visible_in_db [Boolean] When true (default), stores whitespace-only strings (e.g., " ", "\t\n") as-is in the database. This saves space since whitespace-only strings contain no PII or sensitive data. When false, encrypts whitespace-only strings normally.
|
|
22
|
+
#
|
|
23
|
+
# Trade-offs:
|
|
24
|
+
# - true: Saves space, faster queries, but values are visible in the database
|
|
25
|
+
# - false: Encrypts values (more secure), but takes more space and encrypted blobs can be inferred as empty/nil from their size
|
|
26
|
+
def initialize(
|
|
27
|
+
nil_visible_in_db: true,
|
|
28
|
+
empty_string_visible_in_db: true,
|
|
29
|
+
whitespace_visible_in_db: true
|
|
30
|
+
)
|
|
31
|
+
super()
|
|
32
|
+
|
|
33
|
+
@nil_visible_in_db = nil_visible_in_db
|
|
34
|
+
@empty_string_visible_in_db = empty_string_visible_in_db
|
|
35
|
+
@whitespace_visible_in_db = whitespace_visible_in_db
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def encrypt(clear_text, **options)
|
|
39
|
+
if clear_text.nil?
|
|
40
|
+
if @nil_visible_in_db
|
|
41
|
+
nil
|
|
42
|
+
else
|
|
43
|
+
# When nil_visible_in_db is false, encrypt nil using a sentinel value
|
|
44
|
+
# This allows us to distinguish encrypted nil from encrypted empty string
|
|
45
|
+
# Rails' encryptor only accepts strings, so we use a null byte as sentinel
|
|
46
|
+
super(NIL_SENTINEL, **options)
|
|
47
|
+
end
|
|
48
|
+
elsif clear_text.is_a?(String)
|
|
49
|
+
if @empty_string_visible_in_db && clear_text == ""
|
|
50
|
+
""
|
|
51
|
+
elsif @whitespace_visible_in_db && whitespace_only?(clear_text)
|
|
52
|
+
clear_text
|
|
53
|
+
else
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
else
|
|
57
|
+
super
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def decrypt(encrypted_text, **options)
|
|
62
|
+
if @nil_visible_in_db && encrypted_text.nil?
|
|
63
|
+
nil
|
|
64
|
+
elsif @empty_string_visible_in_db && encrypted_text.is_a?(String) && encrypted_text == ""
|
|
65
|
+
""
|
|
66
|
+
elsif @whitespace_visible_in_db && encrypted_text.is_a?(String) && whitespace_only?(encrypted_text)
|
|
67
|
+
encrypted_text
|
|
68
|
+
else
|
|
69
|
+
decrypted = super
|
|
70
|
+
# If decrypted value is the nil sentinel, return nil instead
|
|
71
|
+
if decrypted == NIL_SENTINEL && !@nil_visible_in_db
|
|
72
|
+
nil
|
|
73
|
+
else
|
|
74
|
+
decrypted
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# Check if a string contains only whitespace characters (spaces, tabs, newlines, etc.) but is not empty (empty strings are handled separately)
|
|
82
|
+
def whitespace_only?(str)
|
|
83
|
+
!str.empty? && str.strip.empty?
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support"
|
|
4
|
+
require "active_support/concern"
|
|
5
|
+
|
|
6
|
+
module RailsPanda
|
|
7
|
+
module SensitiveData
|
|
8
|
+
module Models
|
|
9
|
+
module ActiveRecord
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
|
|
12
|
+
class_methods do
|
|
13
|
+
def encrypts(*attributes, **options)
|
|
14
|
+
raise ArgumentError, "encrypts must be called with at least one attribute" if attributes.blank?
|
|
15
|
+
|
|
16
|
+
super_options = options.dup
|
|
17
|
+
|
|
18
|
+
# If rails_default is true, skip our custom logic and use Rails' standard behavior
|
|
19
|
+
unless super_options.delete(:rails_default)
|
|
20
|
+
nil_visible_in_db = super_options.delete(:nil_visible_in_db) != false
|
|
21
|
+
empty_string_visible_in_db = super_options.delete(:empty_string_visible_in_db) != false
|
|
22
|
+
whitespace_visible_in_db = super_options.delete(:whitespace_visible_in_db) != false
|
|
23
|
+
needs_custom_encryptor =
|
|
24
|
+
nil_visible_in_db ||
|
|
25
|
+
empty_string_visible_in_db ||
|
|
26
|
+
whitespace_visible_in_db
|
|
27
|
+
|
|
28
|
+
# Pass encryptor as a keyword argument - Rails stores it in context_properties internally and retrieves it from there when needed
|
|
29
|
+
if needs_custom_encryptor && !super_options[:encryptor]
|
|
30
|
+
super_options[:encryptor] =
|
|
31
|
+
::RailsPanda::SensitiveData::Encryption::Encryptor.new(
|
|
32
|
+
empty_string_visible_in_db:,
|
|
33
|
+
nil_visible_in_db:,
|
|
34
|
+
whitespace_visible_in_db:
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
super(*attributes, **super_options)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def has_sensitive_data(*attributes, **options)
|
|
43
|
+
raise ArgumentError, "has_sensitive_data must be called with at least one attribute" if attributes.blank?
|
|
44
|
+
|
|
45
|
+
super_options = options.dup
|
|
46
|
+
|
|
47
|
+
in_attribute = super_options.delete(:in)&.to_sym || :sensitive_data
|
|
48
|
+
|
|
49
|
+
unless @__has_registered_sensitive_data_encrypted_attribute
|
|
50
|
+
serialize(in_attribute, type: Hash, coder: YAML)
|
|
51
|
+
encrypts(in_attribute, **super_options)
|
|
52
|
+
|
|
53
|
+
@__has_registered_sensitive_data_encrypted_attribute = true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
attributes.each do |attr|
|
|
57
|
+
attr = attr.to_sym
|
|
58
|
+
|
|
59
|
+
define_method attr do
|
|
60
|
+
the_hash = send(in_attribute)
|
|
61
|
+
return if the_hash.blank?
|
|
62
|
+
the_hash[attr]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
define_method "#{attr}=" do |the_value|
|
|
66
|
+
the_hash = send(in_attribute)
|
|
67
|
+
the_hash ||= {}
|
|
68
|
+
|
|
69
|
+
if the_value.nil?
|
|
70
|
+
the_hash.delete attr
|
|
71
|
+
else
|
|
72
|
+
the_hash[attr] = the_value
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
send("#{in_attribute}=", the_hash)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
attr_in_database = :"#{attr}_in_database"
|
|
79
|
+
attr_before_last_save = :"#{attr}_before_last_save"
|
|
80
|
+
saved_change_to_attr = :"saved_change_to_#{attr}"
|
|
81
|
+
|
|
82
|
+
define_method attr_in_database do
|
|
83
|
+
attribute_in_database(in_attribute)&.dig(attr)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Returns nil until after a save, and nil after reload (Rails clears mutations_before_last_save on reload)
|
|
87
|
+
define_method attr_before_last_save do
|
|
88
|
+
attribute_before_last_save(in_attribute)&.dig(attr)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# attribute_changed? - Has this attribute changed from the database value?
|
|
92
|
+
define_method "#{attr}_changed?" do
|
|
93
|
+
send(attr) != send(attr_in_database)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# saved_change_to_attribute - Returns the change to an attribute during the last save
|
|
97
|
+
define_method saved_change_to_attr do
|
|
98
|
+
before = send(attr_before_last_save)
|
|
99
|
+
current = send(attr)
|
|
100
|
+
return nil if before.nil? || before == current
|
|
101
|
+
[before, current]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# saved_change_to_attribute? - Did this attribute change when we last saved?
|
|
105
|
+
# Returns false if before_last_save is nil (e.g., after reload or before any save)
|
|
106
|
+
define_method "#{saved_change_to_attr}?" do
|
|
107
|
+
before = send(attr_before_last_save)
|
|
108
|
+
!before.nil? && send(attr) != before # No saved change if before_last_save is nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# will_save_change_to_attribute? - Will this attribute change the next time we save?
|
|
112
|
+
define_method "will_save_change_to_#{attr}?" do
|
|
113
|
+
send(attr) != send(attr_in_database)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# attribute_change_to_be_saved - Returns the change to an attribute that will be persisted during the next save
|
|
117
|
+
define_method "#{attr}_change_to_be_saved" do
|
|
118
|
+
in_db = send(attr_in_database)
|
|
119
|
+
current = send(attr)
|
|
120
|
+
return nil if in_db.nil? || in_db == current
|
|
121
|
+
[in_db, current]
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Auto-include the module in all ActiveRecord::Base models
|
|
132
|
+
ActiveSupport.on_load(:active_record) do
|
|
133
|
+
include RailsPanda::SensitiveData::Models::ActiveRecord
|
|
134
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support"
|
|
4
|
+
require "active_support/concern"
|
|
5
|
+
|
|
6
|
+
module RailsPanda
|
|
7
|
+
module SensitiveData
|
|
8
|
+
module Models
|
|
9
|
+
module Mongoid
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
|
|
12
|
+
included do
|
|
13
|
+
extend ::AttrEncrypted
|
|
14
|
+
|
|
15
|
+
before_save :set_encryption_key
|
|
16
|
+
|
|
17
|
+
field :encryption_key, type: String
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def set_encryption_key
|
|
21
|
+
encr_key = encryption_key
|
|
22
|
+
if encr_key.nil? || encr_key == ""
|
|
23
|
+
# 8 chars
|
|
24
|
+
self.encryption_key = SecureRandom.hex(4)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def sensitive_data_encryption_key
|
|
29
|
+
set_encryption_key
|
|
30
|
+
encryption_key + ::RailsPanda::SensitiveData.application_sensitive_data_encryption_key
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class_methods do
|
|
34
|
+
def add_encrypted_attribute(attribute_name, opts = {})
|
|
35
|
+
treat_nil_as_empty_value =
|
|
36
|
+
opts.delete(:treat_nil_as_empty_value) ? true : false
|
|
37
|
+
|
|
38
|
+
empty_value_visible_in_db =
|
|
39
|
+
opts.delete(:empty_value_visible_in_db) ? true : false
|
|
40
|
+
|
|
41
|
+
nil_value_visible_in_db =
|
|
42
|
+
(opts.delete(:nil_value_visible_in_db) ? true : false) ||
|
|
43
|
+
(treat_nil_as_empty_value && empty_value_visible_in_db)
|
|
44
|
+
|
|
45
|
+
prefix = opts[:prefix] || "encrypted_"
|
|
46
|
+
suffix = opts[:suffix] || ""
|
|
47
|
+
|
|
48
|
+
db_attribute_name = [prefix, attribute_name, suffix].join
|
|
49
|
+
|
|
50
|
+
attribute_name = opts.delete(:as) || attribute_name
|
|
51
|
+
|
|
52
|
+
field_name =
|
|
53
|
+
opts[:attribute] ||
|
|
54
|
+
[prefix, attribute_name, suffix].join
|
|
55
|
+
|
|
56
|
+
field db_attribute_name, as: field_name, type: String
|
|
57
|
+
field "#{db_attribute_name}_iv", as: "#{field_name}_iv", type: String
|
|
58
|
+
|
|
59
|
+
attr_encryptor(
|
|
60
|
+
attribute_name,
|
|
61
|
+
key: :sensitive_data_encryption_key,
|
|
62
|
+
marshal: true,
|
|
63
|
+
encode: true,
|
|
64
|
+
allow_empty_value: true,
|
|
65
|
+
**opts
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
encryptor_getter_method_name = "__encryptor_#{attribute_name}"
|
|
69
|
+
encryptor_setter_method_name = "__encryptor_#{attribute_name}="
|
|
70
|
+
mem_value_attr = "@#{attribute_name}"
|
|
71
|
+
|
|
72
|
+
attribute_name_before_last_save = :"#{attribute_name}_before_last_save"
|
|
73
|
+
|
|
74
|
+
alias_method encryptor_getter_method_name, attribute_name
|
|
75
|
+
alias_method encryptor_setter_method_name, "#{attribute_name}="
|
|
76
|
+
|
|
77
|
+
attr_reader attribute_name_before_last_save
|
|
78
|
+
after_initialize do
|
|
79
|
+
if respond_to? encrypted_attribute_name
|
|
80
|
+
instance_variable_set(
|
|
81
|
+
"@#{attribute_name_before_last_save}",
|
|
82
|
+
Marshal.load(Marshal.dump(send(attribute_name)))
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
define_method "saved_change_to_#{attribute_name}?" do
|
|
88
|
+
send(attribute_name) != send(attribute_name_before_last_save)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
define_method attribute_name do
|
|
92
|
+
unless (value = instance_variable_get(mem_value_attr))
|
|
93
|
+
|
|
94
|
+
db_value = send(encrypted_attribute_name)
|
|
95
|
+
|
|
96
|
+
if nil_value_visible_in_db && db_value.nil?
|
|
97
|
+
return "" if treat_nil_as_empty_value
|
|
98
|
+
return nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
if empty_value_visible_in_db && db_value == ""
|
|
102
|
+
return ""
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
value = send(encryptor_getter_method_name)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
return "" if treat_nil_as_empty_value && value.nil?
|
|
109
|
+
value
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
define_method "#{attribute_name}=" do |the_value|
|
|
113
|
+
encrypted_attribute_name__set = "#{field_name}="
|
|
114
|
+
encrypted_attribute_iv_name__set = "#{field_name}_iv="
|
|
115
|
+
|
|
116
|
+
if nil_value_visible_in_db && the_value.nil? && !treat_nil_as_empty_value
|
|
117
|
+
send(encrypted_attribute_name__set, nil)
|
|
118
|
+
send(encrypted_attribute_iv_name__set, nil)
|
|
119
|
+
instance_variable_set(mem_value_attr, nil)
|
|
120
|
+
return nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
if empty_value_visible_in_db &&
|
|
124
|
+
(
|
|
125
|
+
the_value == "" ||
|
|
126
|
+
(treat_nil_as_empty_value && the_value.nil?)
|
|
127
|
+
)
|
|
128
|
+
send(encrypted_attribute_name__set, "")
|
|
129
|
+
send(encrypted_attribute_iv_name__set, "")
|
|
130
|
+
instance_variable_set(mem_value_attr, "")
|
|
131
|
+
return ""
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
the_value = "" if treat_nil_as_empty_value && the_value.nil?
|
|
135
|
+
|
|
136
|
+
send(encryptor_setter_method_name, the_value)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def add_sensitive_attribute_accessors(attribute_name, opts = {})
|
|
141
|
+
in_attribute = opts.delete(:in) || :sensitive_data
|
|
142
|
+
in_attribute_before_last_save = :"#{in_attribute}_before_last_save"
|
|
143
|
+
in_field_name =
|
|
144
|
+
opts[:attribute] ||
|
|
145
|
+
opts.delete(:in_db) ||
|
|
146
|
+
[prefix, in_attribute, suffix].join
|
|
147
|
+
|
|
148
|
+
mem_value_attr = "@#{in_attribute}"
|
|
149
|
+
|
|
150
|
+
unless respond_to? in_attribute
|
|
151
|
+
field in_field_name, type: String
|
|
152
|
+
field "#{in_field_name}_iv", type: String
|
|
153
|
+
|
|
154
|
+
attr_encryptor(
|
|
155
|
+
in_attribute,
|
|
156
|
+
key: :sensitive_data_encryption_key,
|
|
157
|
+
marshal: true,
|
|
158
|
+
encode: true,
|
|
159
|
+
allow_empty_value: true,
|
|
160
|
+
attribute: in_field_name,
|
|
161
|
+
**opts
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
attr_reader in_attribute_before_last_save
|
|
165
|
+
after_initialize do
|
|
166
|
+
if respond_to?(in_field_name)
|
|
167
|
+
instance_variable_set(
|
|
168
|
+
"@#{in_attribute_before_last_save}",
|
|
169
|
+
Marshal.load(Marshal.dump(send(in_attribute)))
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
define_method "saved_change_to_#{attribute_name}?" do
|
|
176
|
+
send(attribute_name) != send(attribute_name_before_last_save)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
define_method "#{attribute_name}_before_last_save" do
|
|
180
|
+
send(in_attribute_before_last_save)&.dig(attribute_name)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
define_method attribute_name do
|
|
184
|
+
unless (the_hash = instance_variable_get(mem_value_attr))
|
|
185
|
+
db_value = send(in_field_name)
|
|
186
|
+
return nil if db_value.nil? || db_value == ""
|
|
187
|
+
|
|
188
|
+
the_hash = send(in_attribute)
|
|
189
|
+
end
|
|
190
|
+
return nil if the_hash.nil? || the_hash == ""
|
|
191
|
+
|
|
192
|
+
the_hash.dig(attribute_name)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
define_method "#{attribute_name}=" do |the_value|
|
|
196
|
+
unless (the_hash = instance_variable_get(mem_value_attr))
|
|
197
|
+
db_value = send(in_field_name)
|
|
198
|
+
unless db_value.nil? || db_value == ""
|
|
199
|
+
the_hash = send(in_attribute)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
the_hash = {} if the_hash.nil? || the_hash == ""
|
|
203
|
+
|
|
204
|
+
if the_value.nil?
|
|
205
|
+
the_hash.delete attribute_name
|
|
206
|
+
else
|
|
207
|
+
the_hash[attribute_name] = the_value
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
send("#{in_attribute}=", the_hash)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails_panda_sensitive_data/sensitive_data"
|
|
4
|
+
require "rails_panda_sensitive_data/encryption/encryptor"
|
|
5
|
+
require "rails_panda_sensitive_data/encryption/active_record__encrypted_attribute_type"
|
|
6
|
+
require "rails_panda_sensitive_data/models/active_record"
|
|
7
|
+
# require "rails_panda_sensitive_data/models/mongoid"
|
|
8
|
+
|
|
9
|
+
module RailsPanda
|
|
10
|
+
module SensitiveData
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
$LOAD_PATH.push File.expand_path("lib", __dir__)
|
|
2
|
+
|
|
3
|
+
require "rails_panda_sensitive_data/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.required_ruby_version = ">= 3.2"
|
|
7
|
+
|
|
8
|
+
spec.name = "rails-panda-sensitive-data"
|
|
9
|
+
spec.version = RailsPanda::SensitiveData::VERSION
|
|
10
|
+
spec.authors = ["João Saraiva"]
|
|
11
|
+
spec.email = ["panda@bigbadpanda.com"]
|
|
12
|
+
spec.homepage = "https://github.com/bigbadpanda-tech/rails-panda-sensitive-data"
|
|
13
|
+
spec.summary = "Code that Rails applications use for dealing with sensitive data (e.g., GDPR)."
|
|
14
|
+
spec.description = "Code that Rails applications use for dealing with sensitive data (e.g., GDPR)."
|
|
15
|
+
spec.license = "MIT"
|
|
16
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
17
|
+
|
|
18
|
+
spec.files = Dir[
|
|
19
|
+
"lib/**/*",
|
|
20
|
+
"rails_panda_sensitive_data.gemspec",
|
|
21
|
+
"Gemfile",
|
|
22
|
+
# "Rakefile",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"CHANGELOG.md",
|
|
25
|
+
"README.md"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
spec.add_dependency "rails", ">= 7.0.0"
|
|
29
|
+
|
|
30
|
+
# spec.add_development_dependency "combustion"
|
|
31
|
+
spec.add_development_dependency "rake"
|
|
32
|
+
spec.add_development_dependency "rspec"
|
|
33
|
+
spec.add_development_dependency "rspec-rails"
|
|
34
|
+
spec.add_development_dependency "simplecov"
|
|
35
|
+
spec.add_development_dependency "rubocop"
|
|
36
|
+
spec.add_development_dependency "rubocop-rails"
|
|
37
|
+
spec.add_development_dependency "rubocop-rspec"
|
|
38
|
+
spec.add_development_dependency "rubocop-rspec_rails"
|
|
39
|
+
spec.add_development_dependency "rubocop-rake"
|
|
40
|
+
spec.add_development_dependency "rubocop-performance"
|
|
41
|
+
spec.add_development_dependency "standard"
|
|
42
|
+
spec.add_development_dependency "standard-rails"
|
|
43
|
+
spec.add_development_dependency "sqlite3"
|
|
44
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rails-panda-sensitive-data
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 3.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- João Saraiva
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 7.0.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 7.0.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rspec-rails
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: simplecov
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rubocop
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: rubocop-rails
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: rubocop-rspec
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: rubocop-rspec_rails
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '0'
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: rubocop-rake
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - ">="
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '0'
|
|
145
|
+
type: :development
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - ">="
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '0'
|
|
152
|
+
- !ruby/object:Gem::Dependency
|
|
153
|
+
name: rubocop-performance
|
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
|
155
|
+
requirements:
|
|
156
|
+
- - ">="
|
|
157
|
+
- !ruby/object:Gem::Version
|
|
158
|
+
version: '0'
|
|
159
|
+
type: :development
|
|
160
|
+
prerelease: false
|
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - ">="
|
|
164
|
+
- !ruby/object:Gem::Version
|
|
165
|
+
version: '0'
|
|
166
|
+
- !ruby/object:Gem::Dependency
|
|
167
|
+
name: standard
|
|
168
|
+
requirement: !ruby/object:Gem::Requirement
|
|
169
|
+
requirements:
|
|
170
|
+
- - ">="
|
|
171
|
+
- !ruby/object:Gem::Version
|
|
172
|
+
version: '0'
|
|
173
|
+
type: :development
|
|
174
|
+
prerelease: false
|
|
175
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
176
|
+
requirements:
|
|
177
|
+
- - ">="
|
|
178
|
+
- !ruby/object:Gem::Version
|
|
179
|
+
version: '0'
|
|
180
|
+
- !ruby/object:Gem::Dependency
|
|
181
|
+
name: standard-rails
|
|
182
|
+
requirement: !ruby/object:Gem::Requirement
|
|
183
|
+
requirements:
|
|
184
|
+
- - ">="
|
|
185
|
+
- !ruby/object:Gem::Version
|
|
186
|
+
version: '0'
|
|
187
|
+
type: :development
|
|
188
|
+
prerelease: false
|
|
189
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
190
|
+
requirements:
|
|
191
|
+
- - ">="
|
|
192
|
+
- !ruby/object:Gem::Version
|
|
193
|
+
version: '0'
|
|
194
|
+
- !ruby/object:Gem::Dependency
|
|
195
|
+
name: sqlite3
|
|
196
|
+
requirement: !ruby/object:Gem::Requirement
|
|
197
|
+
requirements:
|
|
198
|
+
- - ">="
|
|
199
|
+
- !ruby/object:Gem::Version
|
|
200
|
+
version: '0'
|
|
201
|
+
type: :development
|
|
202
|
+
prerelease: false
|
|
203
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
204
|
+
requirements:
|
|
205
|
+
- - ">="
|
|
206
|
+
- !ruby/object:Gem::Version
|
|
207
|
+
version: '0'
|
|
208
|
+
description: Code that Rails applications use for dealing with sensitive data (e.g.,
|
|
209
|
+
GDPR).
|
|
210
|
+
email:
|
|
211
|
+
- panda@bigbadpanda.com
|
|
212
|
+
executables: []
|
|
213
|
+
extensions: []
|
|
214
|
+
extra_rdoc_files: []
|
|
215
|
+
files:
|
|
216
|
+
- CHANGELOG.md
|
|
217
|
+
- Gemfile
|
|
218
|
+
- LICENSE
|
|
219
|
+
- README.md
|
|
220
|
+
- lib/rails/panda/sensitive/data.rb
|
|
221
|
+
- lib/rails_panda_sensitive_data.rb
|
|
222
|
+
- lib/rails_panda_sensitive_data/config.rb
|
|
223
|
+
- lib/rails_panda_sensitive_data/encryption/active_record__encrypted_attribute_type.rb
|
|
224
|
+
- lib/rails_panda_sensitive_data/encryption/encryptor.rb
|
|
225
|
+
- lib/rails_panda_sensitive_data/models/active_record.rb
|
|
226
|
+
- lib/rails_panda_sensitive_data/models/mongoid.rb
|
|
227
|
+
- lib/rails_panda_sensitive_data/sensitive_data.rb
|
|
228
|
+
- lib/rails_panda_sensitive_data/version.rb
|
|
229
|
+
- rails_panda_sensitive_data.gemspec
|
|
230
|
+
homepage: https://github.com/bigbadpanda-tech/rails-panda-sensitive-data
|
|
231
|
+
licenses:
|
|
232
|
+
- MIT
|
|
233
|
+
metadata:
|
|
234
|
+
rubygems_mfa_required: 'true'
|
|
235
|
+
rdoc_options: []
|
|
236
|
+
require_paths:
|
|
237
|
+
- lib
|
|
238
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
239
|
+
requirements:
|
|
240
|
+
- - ">="
|
|
241
|
+
- !ruby/object:Gem::Version
|
|
242
|
+
version: '3.2'
|
|
243
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
244
|
+
requirements:
|
|
245
|
+
- - ">="
|
|
246
|
+
- !ruby/object:Gem::Version
|
|
247
|
+
version: '0'
|
|
248
|
+
requirements: []
|
|
249
|
+
rubygems_version: 3.7.2
|
|
250
|
+
specification_version: 4
|
|
251
|
+
summary: Code that Rails applications use for dealing with sensitive data (e.g., GDPR).
|
|
252
|
+
test_files: []
|