couchbase-orm 2.0.5 → 2.0.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f52bb1912f8b6e060c4daffc1b9ef0bd0922b7dc4035c774743839ea54abcab4
4
- data.tar.gz: ba9cf86a6014a14a671c9a209f2c8ba58bd2acd8b1d54720af8c9237c4812cbc
3
+ metadata.gz: 8c4e1b3c58f8a68383f5e24c548e93d5a7d8ed5fd178ec932f856e64486dbe44
4
+ data.tar.gz: 2bc1a8a24e83a0267a114995587254e760ccc81f779382d40a7415cf832f4b6b
5
5
  SHA512:
6
- metadata.gz: 1c3dc0311028fb1edf5612632f9b8bbadd89bd540416f08989331075ee052d915f58481b2879b15f946810c0e4c735b8130d994c0f796c9cae4d50bf0a0e50ea
7
- data.tar.gz: '013937e07a05ec6386be0c65ef72dba41c67aa902680d5cb5f0bd936eaf592a1d5b024d7d4c6bcd06d5bc7195bb4bb44f8afc99beb010288f1be9807e117e207'
6
+ metadata.gz: 0164b5c46981c5303fcc7e5820918cafb073d788027aa7e5492d321073cdd1d17df8a6491ba6f1fb91ed6f2ba28ed1f03a3d7ac62da3e4905034c2b1950ce10b
7
+ data.tar.gz: bf5feabaaa6c4c8bec79f5ef1906519ee9da5861ecce359d25055ca806fcbcd781f6c291a98393a79ef4a17f0a85e0000536abaed6185dfcbefaab82d62d6c9d
data/README.md CHANGED
@@ -113,8 +113,11 @@ The following types have been tested :
113
113
  - :datetime (stored as iso8601, use precision: n to store more decimal precision)
114
114
  - :timestamp (stored as integer)
115
115
  - :encrypted
116
- - see <https://docs.couchbase.com/couchbase-lite/current/c/field-level-encryption.html>
117
- - You must store a string that can be encoded in json (not binary data), use base64 if needed
116
+ - Provides storage format compatible with Couchbase Lite field-level encryption
117
+ - See <https://docs.couchbase.com/couchbase-lite/current/c/field-level-encryption.html>
118
+ - **Important**: CouchbaseOrm does not perform encryption/decryption - your application must encrypt data before storing it
119
+ - Values must be Base64-encoded strings containing pre-encrypted ciphertext
120
+ - See the [encryption documentation](https://couchbase-ruby-orm.com/docs/tutorial-ruby-couchbase-orm/encryption) for details
118
121
  - :array (see below)
119
122
  - :nested (see below)
120
123
 
@@ -23,6 +23,7 @@ Gem::Specification.new do |gem|
23
23
  gem.add_runtime_dependency 'couchbase', '>= 3.4.2'
24
24
  gem.add_runtime_dependency 'radix', '~> 2.2' # converting numbers to and from any base
25
25
  gem.add_runtime_dependency 'json-schema', '>= 3' # validating JSON against a schema
26
+ gem.add_runtime_dependency 'logger', '~> 1.6' # Required from Ruby 3.5.0+
26
27
 
27
28
  gem.add_development_dependency 'rake', '~> 12.2'
28
29
  gem.add_development_dependency 'rspec', '~> 3.7'
@@ -1,13 +1,13 @@
1
1
  # Encryption
2
2
 
3
- CouchbaseOrm provides built-in support for encrypting sensitive data stored in your Couchbase documents. Encryption allows you to protect confidential information, such as personal data or financial details, by encrypting the values before storing them in the database and decrypting them when retrieving the data.
3
+ CouchbaseOrm provides built-in support for storing encrypted data in your Couchbase documents using a structured format. The `:encrypted` type provides a standardized storage format compatible with Couchbase Lite's field-level encryption, but **does not perform encryption/decryption itself**. Your application is responsible for encrypting data before storing it and decrypting it after retrieval.
4
4
 
5
5
  ## 11.1. Encrypted Attributes
6
6
 
7
7
  To mark an attribute as encrypted, you can use the `:encrypted` type when defining the attribute in your model.
8
8
 
9
9
  ```ruby
10
- # Define the Bank model with an encrypted attribute
10
+ # Define the Bank model with encrypted attributes
11
11
  class Bank < CouchbaseOrm::Base
12
12
  attribute :name, :string
13
13
  attribute :account_number, :encrypted
@@ -15,7 +15,7 @@ class Bank < CouchbaseOrm::Base
15
15
  end
16
16
  ```
17
17
 
18
- In this example, the `account_number` and `routing_number` attributes are marked as encrypted. By default, CouchbaseOrm uses the default `CB_MOBILE_CUSTOM` encryption algorithm for encrypting the values. You can specify a different encryption algorithm by providing the `alg` option.
18
+ In this example, the `account_number` and `routing_number` attributes are marked as encrypted. The `alg` option specifies the encryption algorithm identifier that will be stored in the document metadata (default is `"CB_MOBILE_CUSTOM"`). This identifier is for documentation purposes and Couchbase Lite compatibility - CouchbaseOrm does not use it for actual encryption.
19
19
 
20
20
  ```plaintext
21
21
  {
@@ -32,83 +32,171 @@ In this example, the `account_number` and `routing_number` attributes are marked
32
32
  }
33
33
  ```
34
34
 
35
- When a document is saved, CouchbaseOrm stores the encrypted values in the document with a prefix of `encrypted$`. The encrypted values are stored as JSON objects containing the encryption algorithm (`alg`) and the ciphertext (`ciphertext`) of the encrypted value.
35
+ When a document is saved, CouchbaseOrm stores encrypted attributes in the document with a prefix of `encrypted$`. The values are stored as JSON objects containing the encryption algorithm identifier (`alg`) and the ciphertext (`ciphertext`).
36
36
 
37
- You can assign values to encrypted attributes just like any other attribute.
37
+ **Important**: You must provide **pre-encrypted** values to encrypted attributes. CouchbaseOrm stores these values as-is in the `ciphertext` field without performing any encryption.
38
38
 
39
39
  ```ruby
40
- bank = Bank.new(name: 'My Bank', account_number: '123456789', routing_number: '987654321')
40
+ # You must encrypt the data BEFORE assigning it to the attribute
41
+ require 'base64'
42
+
43
+ # Assuming you have an encryption method (e.g., AES, Tanker, etc.)
44
+ encrypted_account = MyEncryptor.encrypt('123456789')
45
+ encrypted_routing = MyEncryptor.encrypt('987654321')
46
+
47
+ # Values must be Base64-encoded strings
48
+ bank = Bank.new(
49
+ name: 'My Bank',
50
+ account_number: Base64.strict_encode64(encrypted_account),
51
+ routing_number: Base64.strict_encode64(encrypted_routing)
52
+ )
41
53
  ```
42
54
 
43
- When the document is saved, CouchbaseOrm encrypts the value of `ssn` using the configured encryption key.
55
+ ## 11.2. Complete Example with Encryption
44
56
 
45
- ## 11.2. Encryption Process
57
+ Here's a complete example showing how to handle encryption in your application:
46
58
 
47
59
  ```ruby
48
60
  require 'base64'
49
- require 'logger'
61
+ require 'openssl'
62
+
63
+ # Example encryption helper (you should use a proper encryption library)
64
+ class SimpleEncryptor
65
+ def self.encrypt(plaintext)
66
+ # This is a simplified example - use a proper encryption library in production
67
+ cipher = OpenSSL::Cipher.new('AES-256-CBC')
68
+ cipher.encrypt
69
+ cipher.key = ENV['ENCRYPTION_KEY'] # Store securely, never commit to git
70
+ cipher.iv = iv = cipher.random_iv
71
+
72
+ encrypted = cipher.update(plaintext) + cipher.final
73
+ # Prepend IV for decryption (in real implementation, handle this properly)
74
+ iv + encrypted
75
+ end
50
76
 
51
- Bank.all.each(&:destroy)
77
+ def self.decrypt(ciphertext_with_iv)
78
+ cipher = OpenSSL::Cipher.new('AES-256-CBC')
79
+ cipher.decrypt
80
+ cipher.key = ENV['ENCRYPTION_KEY']
52
81
 
53
- # Method to print serialized attributes
54
- def expect_serialized_attributes(bank)
55
- serialized_attrs = bank.send(:serialized_attributes)
56
- serialized_attrs.each do |key, value|
57
- puts "#{key}: #{value}"
58
- end
59
- json_attrs = JSON.parse(bank.to_json)
60
- json_attrs.each do |key, value|
61
- puts "#{key}: #{value}"
62
- end
63
- bank.as_json.each do |key, value|
64
- puts "#{key}: #{value}"
82
+ # Extract IV and ciphertext
83
+ iv = ciphertext_with_iv[0..15]
84
+ ciphertext = ciphertext_with_iv[16..]
85
+
86
+ cipher.iv = iv
87
+ cipher.update(ciphertext) + cipher.final
65
88
  end
66
89
  end
67
90
 
68
- # Create a new bank record with encrypted attributes
91
+ # Create a bank record with encrypted attributes
92
+ plaintext_account = "123456789"
93
+ plaintext_routing = "987654321"
94
+
95
+ # 1. Encrypt the sensitive data
96
+ encrypted_account = SimpleEncryptor.encrypt(plaintext_account)
97
+ encrypted_routing = SimpleEncryptor.encrypt(plaintext_routing)
98
+
99
+ # 2. Encode as Base64 for storage
69
100
  bank = Bank.new(
70
101
  name: "Test Bank",
71
- account_number: Base64.strict_encode64("123456789"),
72
- routing_number: Base64.strict_encode64("987654321")
102
+ account_number: Base64.strict_encode64(encrypted_account),
103
+ routing_number: Base64.strict_encode64(encrypted_routing)
73
104
  )
74
105
 
75
- # Print serialized attributes before saving
76
- expect_serialized_attributes(bank)
77
-
78
- # Save the bank record to Couchbase
106
+ # 3. Save to Couchbase
79
107
  bank.save!
80
108
 
81
- # Reload the bank record from Couchbase
82
- bank.reload
109
+ # 4. Retrieve and decrypt
110
+ found_bank = Bank.find(bank.id)
83
111
 
84
- # Print serialized attributes after reloading
85
- expect_serialized_attributes(bank)
112
+ # 5. Decode Base64 and decrypt
113
+ account_encrypted = Base64.strict_decode64(found_bank.account_number)
114
+ routing_encrypted = Base64.strict_decode64(found_bank.routing_number)
86
115
 
87
- # Find the bank record by ID
88
- found_bank = Bank.find(bank.id)
116
+ decrypted_account = SimpleEncryptor.decrypt(account_encrypted)
117
+ decrypted_routing = SimpleEncryptor.decrypt(routing_encrypted)
89
118
 
90
- # Print serialized attributes after finding
91
- expect_serialized_attributes(found_bank)
119
+ puts "Decrypted account: #{decrypted_account}" # => "123456789"
120
+ puts "Decrypted routing: #{decrypted_routing}" # => "987654321"
92
121
  ```
93
122
 
94
- ## 11.3. Encryption and Decryption Process
123
+ ## 11.3. Storage Format
124
+
125
+ CouchbaseOrm handles the storage format for encrypted attributes but does not perform encryption/decryption. Here's what happens:
95
126
 
96
- When an encrypted attribute is assigned a value, CouchbaseOrm encrypts the value using the configured encryption key and algorithm. The encrypted value is then stored in the Couchbase document.
127
+ **When saving:**
128
+ 1. You assign a Base64-encoded ciphertext to the encrypted attribute
129
+ 2. CouchbaseOrm wraps it in the `encrypted$` format with `alg` and `ciphertext` fields
130
+ 3. The document is stored in Couchbase with this structure
97
131
 
98
- When retrieving a document with encrypted attributes, CouchbaseOrm automatically decrypts the encrypted values using the same encryption key and algorithm. The decrypted values are then accessible through the model's attributes.
132
+ **When loading:**
133
+ 1. CouchbaseOrm reads the document from Couchbase
134
+ 2. It unwraps the `encrypted$` format and extracts the `ciphertext` value
135
+ 3. The Base64-encoded ciphertext is assigned to the attribute
136
+ 4. Your application must decode and decrypt the value
99
137
 
100
- It's important to keep the encryption key secure and protect it from unauthorized access. If the encryption key is compromised, the encrypted data can be decrypted by anyone who obtains the key.
138
+ **Key Points:**
139
+ - CouchbaseOrm does **not** require or use any encryption key
140
+ - The `alg` field is purely informational (for compatibility with Couchbase Lite)
141
+ - All actual encryption/decryption is your application's responsibility
142
+ - Values must be valid Base64-encoded strings
101
143
 
102
144
  ## 11.4. Considerations and Best Practices
103
145
 
104
- When using encryption in CouchbaseOrm, consider the following best practices:
146
+ When using encrypted attributes in CouchbaseOrm, consider the following best practices:
147
+
148
+ ### Security
149
+ - **Encryption is your responsibility**: CouchbaseOrm only provides the storage format. Choose a robust encryption library (e.g., `rbnacl`, `openssl`, or a service like AWS KMS)
150
+ - **Key management**: Store encryption keys securely using environment variables, secret managers (AWS Secrets Manager, HashiCorp Vault), or key management services
151
+ - **Never commit keys**: Keep encryption keys out of version control systems
152
+ - **Key rotation**: Implement a key rotation strategy and maintain the ability to decrypt data encrypted with old keys
153
+ - **Use authenticated encryption**: Prefer AEAD modes (like AES-GCM) that provide both confidentiality and integrity
154
+
155
+ ### Performance and Querying
156
+ - **Cannot query encrypted fields**: Encrypted attributes cannot be used in WHERE clauses or indexed effectively
157
+ - **Consider searchable encryption**: If you need to search encrypted data, investigate specialized solutions like searchable encryption schemes or external encrypted search indexes
158
+ - **Selective encryption**: Only encrypt truly sensitive fields to minimize performance overhead
159
+
160
+ ### Implementation Patterns
161
+ - **Wrap in accessors**: Create getter/setter methods that automatically handle encryption/decryption:
162
+ ```ruby
163
+ class Bank < CouchbaseOrm::Base
164
+ attribute :account_number, :encrypted
165
+
166
+ def account_number=(plaintext)
167
+ encrypted = MyEncryptor.encrypt(plaintext)
168
+ super(Base64.strict_encode64(encrypted))
169
+ end
170
+
171
+ def account_number
172
+ encrypted = Base64.strict_decode64(super)
173
+ MyEncryptor.decrypt(encrypted)
174
+ end
175
+ end
176
+ ```
177
+
178
+ - **Separate concerns**: Consider using a concern or module to encapsulate encryption logic:
179
+ ```ruby
180
+ module EncryptedAttributes
181
+ def encrypted_attribute(name)
182
+ define_method("#{name}=") do |plaintext|
183
+ encrypted = MyEncryptor.encrypt(plaintext)
184
+ super(Base64.strict_encode64(encrypted))
185
+ end
186
+
187
+ define_method(name) do
188
+ encrypted = Base64.strict_decode64(super())
189
+ MyEncryptor.decrypt(encrypted)
190
+ end
191
+ end
192
+ end
193
+ ```
105
194
 
106
- - Keep the encryption key secure and protect it from unauthorized access. Store the key securely and avoid committing it to version control systems.
107
- - Use strong and unique encryption keys for each environment (development, staging, production) to prevent cross-environment access to encrypted data.
108
- - Be cautious when querying encrypted attributes as it may impact performance. Consider indexing encrypted attributes separately if frequent querying is required.
109
- - If you need to search or query encrypted data frequently, consider using a separate encrypted search index or a dedicated encryption service.
110
- - Ensure that the encryption key is properly rotated and managed. If the encryption key is compromised, you should generate a new key and re-encrypt the affected data.
195
+ ### Compatibility
196
+ - The `encrypted$` format is compatible with Couchbase Lite's field-level encryption
197
+ - The `alg` field helps document which encryption algorithm was used, aiding in key rotation and auditing
198
+ - Ensure your encryption implementation is compatible across all platforms that access the data (web, mobile, etc.)
111
199
 
112
- Encryption is a powerful tool for protecting sensitive data, but it should be used judiciously. Encrypting every attribute in your model may not be necessary or practical. Focus on encrypting the most sensitive and confidential data while balancing the trade-offs between security and performance.
200
+ Encryption is a powerful tool for protecting sensitive data, but it should be used judiciously. Focus on encrypting the most sensitive and confidential data while balancing the trade-offs between security, performance, and functionality.
113
201
 
114
202
  In the next section, we'll explore logging in CouchbaseOrm and how you can configure and customize logging to monitor and debug your application.
@@ -46,6 +46,7 @@ module CouchbaseOrm
46
46
  include Encrypt
47
47
 
48
48
  extend Enum
49
+ extend IgnoredProperties
49
50
 
50
51
  define_model_callbacks :initialize, :only => :after
51
52
 
@@ -134,7 +135,6 @@ module CouchbaseOrm
134
135
  extend EnsureUnique
135
136
  extend HasMany
136
137
  extend Index
137
- extend IgnoredProperties
138
138
  extend JsonSchema::Validation
139
139
  extend PropertiesAlwaysExistsInDocument
140
140
 
@@ -26,14 +26,29 @@ module CouchbaseOrm
26
26
  def cast(value)
27
27
  return nil if value.nil?
28
28
  return value if value.is_a?(@model_class)
29
- return @model_class.new(value) if value.is_a?(Hash)
29
+
30
+ if value.is_a?(Hash)
31
+ # Filter out ignored properties before creating the nested instance
32
+ # Optimization: only call .except if there are properties to ignore
33
+ ignored = @model_class.ignored_properties
34
+ filtered_value = ignored.empty? ? value : value.except(*ignored)
35
+ return @model_class.new(filtered_value)
36
+ end
30
37
 
31
38
  raise ArgumentError, "Nested: #{value.inspect} (#{value.class}) is not supported for cast"
32
39
  end
33
40
 
34
41
  def serialize(value)
35
42
  return nil if value.nil?
36
- value = @model_class.new(value) if value.is_a?(Hash)
43
+
44
+ if value.is_a?(Hash)
45
+ # Filter out ignored properties before creating the nested instance
46
+ # Optimization: only call .except if there are properties to ignore
47
+ ignored = @model_class.ignored_properties
48
+ filtered_value = ignored.empty? ? value : value.except(*ignored)
49
+ value = @model_class.new(filtered_value)
50
+ end
51
+
37
52
  return value.send(:serialized_attributes) if value.is_a?(@model_class)
38
53
 
39
54
  raise ArgumentError, "Nested: #{value.inspect} (#{value.class}) is not supported for serialization"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true, encoding: ASCII-8BIT
2
2
 
3
3
  module CouchbaseOrm
4
- VERSION = '2.0.5'
4
+ VERSION = '2.0.6'
5
5
  end
@@ -188,4 +188,64 @@ describe CouchbaseOrm::Types::Nested do
188
188
  expect(obj.child.child.errors[:name]).to eq ["can't be blank"]
189
189
  end
190
190
  end
191
+
192
+ describe "Ignored Properties" do
193
+ class SubTypeWithIgnoredProperties < CouchbaseOrm::NestedDocument
194
+ self.ignored_properties = [:deprecated_property]
195
+ attribute :name, :string
196
+ attribute :value, :string
197
+ end
198
+
199
+ class ParentWithNestedIgnoredProperties < CouchbaseOrm::Base
200
+ self.ignored_properties = [:deprecated_at_root]
201
+ attribute :title, :string
202
+ attribute :nested, :nested, type: SubTypeWithIgnoredProperties
203
+ end
204
+
205
+ it "should ignore deprecated properties in nested documents on reload" do
206
+ # Create and save a parent with nested document
207
+ parent = ParentWithNestedIgnoredProperties.new
208
+ parent.title = "Test Parent"
209
+ parent.nested = SubTypeWithIgnoredProperties.new(name: "Nested", value: "Valid")
210
+ parent.save!
211
+
212
+ # Manually add a deprecated property to the nested document in the database
213
+ doc_id = parent.id
214
+ raw_doc = ParentWithNestedIgnoredProperties.bucket.default_collection.get(doc_id).content
215
+ raw_doc["nested"]["deprecated_property"] = "This should be ignored"
216
+ ParentWithNestedIgnoredProperties.bucket.default_collection.replace(doc_id, raw_doc)
217
+
218
+ # Reload the parent
219
+ parent.reload
220
+
221
+ # The deprecated property should NOT be present in the nested document
222
+ expect(parent.nested.attributes.keys).not_to include("deprecated_property")
223
+ expect(parent.nested.name).to eq("Nested")
224
+ expect(parent.nested.value).to eq("Valid")
225
+ end
226
+
227
+ it "should ignore deprecated properties in deeply nested documents" do
228
+ # Create a parent with nested documents that have a child
229
+ parent = ParentWithNestedIgnoredProperties.new
230
+ parent.title = "Test Parent"
231
+ parent.nested = SubTypeWithIgnoredProperties.new(name: "Parent Nested", value: "Parent Value")
232
+ parent.save!
233
+
234
+ # Manually add deprecated properties at multiple levels
235
+ doc_id = parent.id
236
+ raw_doc = ParentWithNestedIgnoredProperties.bucket.default_collection.get(doc_id).content
237
+ raw_doc["deprecated_at_root"] = "Should be ignored at root level"
238
+ raw_doc["nested"]["deprecated_property"] = "Should be ignored in nested"
239
+ ParentWithNestedIgnoredProperties.bucket.default_collection.replace(doc_id, raw_doc)
240
+
241
+ # Reload the parent
242
+ parent.reload
243
+
244
+ # Deprecated properties should not be present at any level
245
+ expect(parent.attributes.keys).not_to include("deprecated_at_root")
246
+ expect(parent.nested.attributes.keys).not_to include("deprecated_property")
247
+ expect(parent.nested.name).to eq("Parent Nested")
248
+ expect(parent.nested.value).to eq("Parent Value")
249
+ end
250
+ end
191
251
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: couchbase-orm
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.5
4
+ version: 2.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen von Takach
@@ -68,6 +68,20 @@ dependencies:
68
68
  - - ">="
69
69
  - !ruby/object:Gem::Version
70
70
  version: '3'
71
+ - !ruby/object:Gem::Dependency
72
+ name: logger
73
+ requirement: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: '1.6'
78
+ type: :runtime
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - "~>"
83
+ - !ruby/object:Gem::Version
84
+ version: '1.6'
71
85
  - !ruby/object:Gem::Dependency
72
86
  name: rake
73
87
  requirement: !ruby/object:Gem::Requirement