event_store_client 0.1.9 → 0.1.10

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: b636decadc2eed63bfed5b4009ae853b491ddc7884d5e14f09d12f009c5abd08
4
- data.tar.gz: f6ce4fc1720012d02dba592e53f67d255bec169b13a5cf0a367f7e464c397b98
3
+ metadata.gz: d6afde0e97c048d663713a6ce14f603d2d1814ca28850b38a8f40c9542e88e0a
4
+ data.tar.gz: 01b0b79bcc76bd604338793f7370a174eadd4f8642c32573166be95d0ea544a6
5
5
  SHA512:
6
- metadata.gz: 8e075d3c6217f81cf54bbf532ccc67fe266b3678b7b443cdd9c56542820a8d219b12adb2fe87f7db1966cad06c6ce5672b7927314dc52e4ad1f627b45bf87417
7
- data.tar.gz: 3ccff060e33c5eb492e164b7d4b4d7c37d14d6b91dbc7d9e0d5c3dab10b68c980fbf41019024715e62f74d65391798a702c65fe0ba6e81550b4432f8068c0eff
6
+ metadata.gz: d78af12542b70c63b14876bd42fef3cf146b71ec38e4316b4039f27b560fd82c0b04b41c0780b33921fc4e0635218dc663cd50d81b37dcc945702ef60036d45e
7
+ data.tar.gz: f986873a36732ef1b05cc3c92afa1edf15fe00d72591b08e25a0e6f1e337b1b9fc49a931e64fce4c0217c5e36fbd697f0ce56e113f18ef989593e73beeac89c6
data/README.md CHANGED
@@ -137,6 +137,99 @@ client.link_to(stream: 'anotherstream', events: [exisiting_event1, ...])
137
137
  When you read from stream where links are placed. By default Event Store Client always resolve links for you returning the event that points to the link. You can use the ES-ResolveLinkTos: false HTTP header during readin stream to tell Event Store Client to return you the actual link and to not resolve it.
138
138
  More info: [ES-ResolveLinkTos](https://eventstore.org/docs/http-api/optional-http-headers/resolve-linkto/index.html?tabs=tabid-1%2Ctabid-3).
139
139
 
140
+ ## Event Mappers
141
+
142
+ We offer two types of mappers - default and encrypted.
143
+
144
+ ### Default Mapper
145
+
146
+ This is used out of the box. It just translates the EventClass defined in your application to
147
+ Event parsable by event_store and the other way around.
148
+
149
+ ### Encrypted Mapper
150
+
151
+ This is implemented to match GDPR requirements. It allows you to encrypt any event using your
152
+ encryption_key repository.
153
+
154
+ ```ruby
155
+ mapper = EventStoreClient::Mapper::Encrypted.new(key_repository)
156
+ EventStoreClient.configure do |config|
157
+ config.mapper = mapper
158
+ end
159
+ ```
160
+
161
+ The Encrypted mapper uses the encryption key repository to encrypt data in your events according to the event definition.
162
+
163
+ Here is the minimal repository interface for this to work.
164
+
165
+ ```ruby
166
+ class DummyRepository
167
+ class Key
168
+ attr_accessor :iv, :cipher, :id
169
+ def initialize(id:, **)
170
+ @id = id
171
+ end
172
+ end
173
+
174
+ def find(user_id)
175
+ Key.new(id: user_id)
176
+ end
177
+
178
+ def encrypt(*)
179
+ 'darthvader'
180
+ end
181
+
182
+ def decrypt(*)
183
+ { first_name: 'Anakin', last_name: 'Skylwalker'}
184
+ end
185
+ end
186
+ ```
187
+
188
+ Now, having that, you only need to define the event encryption schema:
189
+
190
+ ```ruby
191
+ class EncryptedEvent < EventStoreClient::DeserializedEvent
192
+ def schema
193
+ Dry::Schema.Params do
194
+ required(:user_id).value(:string)
195
+ required(:first_name).value(:string)
196
+ required(:last_name).value(:string)
197
+ required(:profession).value(:string)
198
+ end
199
+ end
200
+
201
+ def self.encryption_schema
202
+ {
203
+ key: ->(data) { data['user_id'] },
204
+ attributes: %i[first_name last_name email]
205
+ }
206
+ end
207
+ end
208
+
209
+ event = EncryptedEvent.new(
210
+ user_id: SecureRandom.uuid,
211
+ first_name: 'Anakin',
212
+ last_name: 'Skylwalker',
213
+ profession: 'Jedi'
214
+ )
215
+ ```
216
+
217
+ When you'll publish this event, in the store will be saved:
218
+
219
+ ```ruby
220
+ {
221
+ 'data' => {
222
+ 'user_id' => 'dab48d26-e4f8-41fc-a9a8-59657e590716',
223
+ 'first_name' => 'encrypted',
224
+ 'last_name' => 'encrypted',
225
+ 'profession' => 'Jedi',
226
+ 'encrypted' => '2345l423lj1#$!lkj24f1'
227
+ },
228
+ type: 'EncryptedEvent'
229
+ metadata: { ... }
230
+ }
231
+ ```
232
+
140
233
  ## Contributing
141
234
 
142
235
  Do you want to contribute? Welcome!
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ class DataDecryptor
5
+ KeyNotFoundError = Class.new(StandardError)
6
+
7
+ def call
8
+ return encrypted_data if encryption_metadata.empty?
9
+
10
+ decrypt_attributes(
11
+ key: find_key(encryption_metadata[:key]),
12
+ data: decrypted_data,
13
+ attributes: encryption_metadata[:attributes]
14
+ )
15
+ end
16
+
17
+ attr_reader :decrypted_data, :encryption_metadata
18
+
19
+ private
20
+
21
+ attr_reader :key_repository
22
+
23
+ def initialize(data:, schema:, repository:)
24
+ @decrypted_data = deep_dup(data).transform_keys!(&:to_sym)
25
+ @key_repository = repository
26
+ @encryption_metadata = schema.transform_keys(&:to_sym)
27
+ end
28
+
29
+ def decrypt_attributes(key:, data:, attributes:)
30
+ decrypted_text = key_repository.decrypt(
31
+ key_id: key.id, text: data[:es_encrypted], cipher: key.cipher, iv: key.iv
32
+ )
33
+ decrypted = JSON.parse(decrypted_text).transform_keys(&:to_sym)
34
+ decrypted.each { |key, value| data[key] = value if data.key?(key) }
35
+ data.delete(:es_encrypted)
36
+ data
37
+ end
38
+
39
+ def deep_dup(hash)
40
+ dupl = hash.dup
41
+ dupl.each { |k, v| dupl[k] = v.instance_of?(Hash) ? deep_dup(v) : v }
42
+ dupl
43
+ end
44
+
45
+ def find_key(identifier)
46
+ key =
47
+ begin
48
+ key_repository.find(identifier)
49
+ rescue StandardError => e
50
+ nil
51
+ end
52
+ raise KeyNotFoundError unless key
53
+
54
+ key
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStoreClient
4
+ class DataEncryptor
5
+ def call
6
+ return encrypted_data if encryption_metadata.empty?
7
+
8
+ key_id = encryption_metadata[:key]
9
+ key = key_repository.find(key_id) || key_repository.create(key_id)
10
+ encryption_metadata[:iv] = key.iv
11
+ encrypt_attributes(
12
+ key: key,
13
+ data: encrypted_data,
14
+ attributes: encryption_metadata[:attributes]
15
+ )
16
+ end
17
+
18
+ attr_reader :encrypted_data, :encryption_metadata
19
+
20
+ private
21
+
22
+ attr_reader :key_repository
23
+
24
+ def initialize(data:, schema:, repository:)
25
+ @encrypted_data = deep_dup(data).transform_keys!(&:to_sym)
26
+ @key_repository = repository
27
+ @encryption_metadata = EncryptionMetadata.new(data: data, schema: schema).call
28
+ end
29
+
30
+ def encrypt_attributes(key:, data:, attributes:)
31
+ text = JSON.generate(data.select { |hash_key, _value| attributes.include?(hash_key) })
32
+ encrypted = key_repository.encrypt(
33
+ key_id: key.id, text: text, cipher: key.cipher, iv: key.iv
34
+ )
35
+ attributes.each { |att| data[att] = 'es_encrypted' if data.key?(att) }
36
+ data[:es_encrypted] = encrypted
37
+ data
38
+ end
39
+
40
+ def deep_dup(hash)
41
+ dupl = hash.dup
42
+ dupl.each { |k, v| dupl[k] = v.instance_of?(Hash) ? deep_dup(v) : v }
43
+ dupl
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ module EventStoreClient
5
+ class EncryptionMetadata
6
+ def call
7
+ return {} unless schema
8
+
9
+ {
10
+ key: schema[:key].call(data),
11
+ attributes: schema[:attributes].map(&:to_sym)
12
+ }
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :data, :schema
18
+
19
+ def initialize(data:, schema:)
20
+ @data = data
21
+ @schema = schema
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'event_store_client/encryption_metadata'
4
+ require 'event_store_client/data_encryptor'
5
+ require 'event_store_client/data_decryptor'
6
+
7
+ module EventStoreClient
8
+ module Mapper
9
+ ##
10
+ # Transforms given event's data and encrypts/decrypts selected subset of data
11
+ # based on encryption schema stored in the event itself.
12
+
13
+ class Encrypted
14
+ MissingEncryptionKey = Class.new(StandardError)
15
+
16
+ ##
17
+ # Encrypts the given event's subset of data.
18
+ # Accepts specific event class instance with:
19
+ # * +#data+ - hash with non-encrypted values.
20
+ # * encryption_schema - hash with information which data to encrypt and
21
+ # which key should be used as an identifier.
22
+ # *Returns*: General +Event+ instance with encrypted data
23
+
24
+ def serialize(event)
25
+ encryption_schema = (
26
+ event.class.respond_to?(:encryption_schema) &&
27
+ event.class.encryption_schema
28
+ )
29
+ encryptor = DataEncryptor.new(
30
+ data: event.data,
31
+ schema: encryption_schema,
32
+ repository: key_repository
33
+ )
34
+ encryptor.call
35
+ Event.new(
36
+ data: serializer.serialize(encryptor.encrypted_data),
37
+ metadata: serializer.serialize(
38
+ event.metadata.merge(encryption: encryptor.encryption_metadata)
39
+ ),
40
+ type: event.class.to_s
41
+ )
42
+ end
43
+
44
+ ##
45
+ # Decrypts the given event's subset of data.
46
+ # General +Event+ instance with encrypted data
47
+ # * +#data+ - hash with encrypted values.
48
+ # * encryption_metadata - hash with information which data to encrypt and
49
+ # which key should be used as an identifier.
50
+ # *Returns*: Specific event class with all data decrypted
51
+
52
+ def deserialize(event)
53
+ metadata = serializer.deserialize(event.metadata)
54
+ encryption_schema = serializer.deserialize(event.metadata)['encryption']
55
+ decrypted_data = DataDecryptor.new(
56
+ data: serializer.deserialize(event.data),
57
+ schema: encryption_schema,
58
+ repository: key_repository
59
+ ).call
60
+
61
+ event_class =
62
+ begin
63
+ Object.const_get(event.type)
64
+ rescue NameError
65
+ EventStoreClient::DeserializedEvent
66
+ end
67
+
68
+ event_class.new(
69
+ id: event.id,
70
+ type: event.type,
71
+ title: event.title,
72
+ data: decrypted_data,
73
+ metadata: metadata
74
+ )
75
+ end
76
+
77
+ private
78
+
79
+ attr_reader :key_repository, :serializer
80
+
81
+ ##
82
+ # Initializes the mapper with required dependencies. Accepts:
83
+ # * +key_repoistory+ - repository stored encryption keys. Passed down to the +DataEncryptor+
84
+ # * +serializer+ - object used to serialize data. By default JSON serializer is used.
85
+ def initialize(key_repository, serializer: Serializer::Json)
86
+ @key_repository = key_repository
87
+ @serializer = serializer
88
+ end
89
+ end
90
+ end
91
+ end
@@ -6,3 +6,4 @@ module EventStoreClient
6
6
  end
7
7
 
8
8
  require 'event_store_client/mapper/default'
9
+ require 'event_store_client/mapper/encrypted'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EventStoreClient
4
- VERSION = '0.1.9'
4
+ VERSION = '0.1.10'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: event_store_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Wilgosz
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-09 00:00:00.000000000 Z
11
+ date: 2020-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-schema
@@ -109,11 +109,15 @@ files:
109
109
  - lib/event_store_client/client.rb
110
110
  - lib/event_store_client/configuration.rb
111
111
  - lib/event_store_client/connection.rb
112
+ - lib/event_store_client/data_decryptor.rb
113
+ - lib/event_store_client/data_encryptor.rb
112
114
  - lib/event_store_client/deserialized_event.rb
115
+ - lib/event_store_client/encryption_metadata.rb
113
116
  - lib/event_store_client/endpoint.rb
114
117
  - lib/event_store_client/event.rb
115
118
  - lib/event_store_client/mapper.rb
116
119
  - lib/event_store_client/mapper/default.rb
120
+ - lib/event_store_client/mapper/encrypted.rb
117
121
  - lib/event_store_client/serializer/json.rb
118
122
  - lib/event_store_client/store_adapter.rb
119
123
  - lib/event_store_client/store_adapter/api/client.rb