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 +4 -4
- data/README.md +93 -0
- data/lib/event_store_client/data_decryptor.rb +57 -0
- data/lib/event_store_client/data_encryptor.rb +46 -0
- data/lib/event_store_client/encryption_metadata.rb +24 -0
- data/lib/event_store_client/mapper/encrypted.rb +91 -0
- data/lib/event_store_client/mapper.rb +1 -0
- data/lib/event_store_client/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d6afde0e97c048d663713a6ce14f603d2d1814ca28850b38a8f40c9542e88e0a
|
4
|
+
data.tar.gz: 01b0b79bcc76bd604338793f7370a174eadd4f8642c32573166be95d0ea544a6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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.
|
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-
|
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
|