event_store_client 0.1.9 → 0.1.10
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 +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
         |