Class: E3DB::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/e3db/client.rb,
lib/e3db/crypto.rb

Overview

A connection to the E3DB service used to perform database operations.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ Client

Create a connection to the E3DB service given a configuration.

Parameters:

  • config (Config)

    configuration and credentials to use



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/e3db/client.rb', line 118

def initialize(config)
  @config = config
  @public_key = RbNaCl::PublicKey.new(Crypto.base64decode(@config.public_key))
  @private_key = RbNaCl::PrivateKey.new(Crypto.base64decode(@config.private_key))

  @ak_cache = LruRedux::ThreadSafeCache.new(1024)
  @oauth_client = OAuth2::Client.new(
      config.api_key_id,
      config.api_secret,
      :site => config.api_url,
      :token_url => '/v1/auth/token',
      :auth_scheme => :basic_auth,
      :raise_errors => false)

  if config.logging
    @oauth_client.connection.response :logger, ::Logger.new($stdout)
  end

  @conn = Faraday.new(DEFAULT_API_URL) do |faraday|
    faraday.use TokenHelper, @oauth_client
    faraday.request :json
    faraday.response :raise_error
    if config.logging
      faraday.response :logger, nil, :bodies => true
    end
    faraday.adapter :net_http_persistent
  end
end

Instance Attribute Details

#configConfig (readonly)

Returns the client configuration object.

Returns:

  • (Config)

    the client configuration object



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/e3db/client.rb', line 111

class Client
  attr_reader :config

  # Create a connection to the E3DB service given a configuration.
  #
  # @param config [Config] configuration and credentials to use
  # @return [Client] a connection to the E3DB service
  def initialize(config)
    @config = config
    @public_key = RbNaCl::PublicKey.new(Crypto.base64decode(@config.public_key))
    @private_key = RbNaCl::PrivateKey.new(Crypto.base64decode(@config.private_key))

    @ak_cache = LruRedux::ThreadSafeCache.new(1024)
    @oauth_client = OAuth2::Client.new(
        config.api_key_id,
        config.api_secret,
        :site => config.api_url,
        :token_url => '/v1/auth/token',
        :auth_scheme => :basic_auth,
        :raise_errors => false)

    if config.logging
      @oauth_client.connection.response :logger, ::Logger.new($stdout)
    end

    @conn = Faraday.new(DEFAULT_API_URL) do |faraday|
      faraday.use TokenHelper, @oauth_client
      faraday.request :json
      faraday.response :raise_error
      if config.logging
        faraday.response :logger, nil, :bodies => true
      end
      faraday.adapter :net_http_persistent
    end
  end

  # Query the server for information about an E3DB client.
  #
  # @param client_id [String] client ID to look up
  # @return [ClientInfo] information about this client
  def client_info(client_id)
    resp = @conn.get(get_url('v1', 'storage', 'clients', client_id))
    ClientInfo.new(JSON.parse(resp.body, symbolize_names: true))
  end

  # Query the server for a client's public key.
  #
  # @param client_id [String] client ID to look up
  # @return [RbNaCl::PublicKey] decoded Curve25519 public key
  def client_key(client_id)
    if client_id == @config.client_id
      @public_key
    else
      Crypto.decode_public_key(client_info(client_id).public_key.curve25519)
    end
  end

  # Read a single record by ID from E3DB and return it without
  # decrypting the data fields.
  #
  # @param record_id [String] record ID to look up
  # @return [Record] encrypted record object
  def read_raw(record_id)
    resp = @conn.get(get_url('v1', 'storage', 'records', record_id))
    json = JSON.parse(resp.body, symbolize_names: true)
    Record.new(json)
  end

  # Read a single record by ID from E3DB and return it.
  #
  # @param record_id [String] record ID to look up
  # @return [Record] decrypted record object
  def read(record_id)
    decrypt_record(read_raw(record_id))
  end

  # Create a new, empty record that can be written to E3DB
  # by calling {Client#write}.
  #
  # @param type [String] free-form content type of this record
  # @return [Record] an empty record of `type`
  def new_record(type)
    id = @config.client_id
    meta = Meta.new(record_id: nil, writer_id: id, user_id: id,
                    type: type, plain: Hash.new, created: nil,
                    last_modified: nil)
    Record.new(meta: meta, data: Hash.new)
  end

  # Write a new record to the E3DB storage service.
  #
  # Create new records with {Client#new_record}.
  #
  # @param record [Record] record to write
  # @return [String] the unique ID of the written record
  def write(record)
    url = get_url('v1', 'storage', 'records')
    resp = @conn.post(url, encrypt_record(record).to_hash)
    json = JSON.parse(resp.body, symbolize_names: true)
    json[:meta][:record_id]
  end

  # Delete a record from the E3DB storage service.
  #
  # @param record_id [String] unique ID of record to delete
  def delete(record_id)
    resp = @conn.delete(get_url('v1', 'storage', 'records', record_id))
  end

  class Query < Dry::Struct
    attribute :count,         Types::Int
    attribute :include_data,  Types::Bool.optional
    attribute :writer_ids,    Types::Coercible::Array.member(Types::String).optional
    attribute :user_ids,      Types::Coercible::Array.member(Types::String).optional
    attribute :record_ids,    Types::Coercible::Array.member(Types::String).optional
    attribute :content_types, Types::Coercible::Array.member(Types::String).optional
    attribute :plain,         Types::Hash.optional
    attribute :after_index,   Types::Int.optional

    def after_index=(index)
      @after_index = index
    end

    def as_json
      JSON.generate(to_hash.reject { |k, v| v.nil? })
    end
  end

  private_constant :Query

  DEFAULT_QUERY_COUNT = 100
  private_constant :DEFAULT_QUERY_COUNT

  # Query E3DB records according to a set of selection criteria.
  #
  # Each record (optionally including data) is yielded to the block
  # argument.
  #
  # @param writer [String,Array<String>] select records written by these client IDs
  # @param record [String,Array<String>] select records with these record IDs
  # @param type [String,Array<string>] select records with these types
  # @param plain [Hash] plaintext query expression to select
  # @param data [Boolean] include data in records
  # @param raw [Boolean] when true don't decrypt record data
  def query(data: true, raw: false, writer: nil, record: nil, type: nil, plain: nil)
    q = Query.new(after_index: 0, include_data: data, writer_ids: writer,
                  record_ids: record, content_types: type, plain: plain,
                  user_ids: nil, count: DEFAULT_QUERY_COUNT)
    url = get_url('v1', 'storage', 'search')
    loop do
      resp = @conn.post(url, q.as_json)
      json = JSON.parse(resp.body, symbolize_names: true)
      results = json[:results]
      results.each do |r|
        record = Record.new(meta: r[:meta], data: r[:record_data] || Hash.new)
        if data && !raw
          record = decrypt_record(record)
        end
        yield record
      end

      if results.length < q.count
        break
      end

      q.after_index = json[:last_index]
    end
  end

  # Grant another E3DB client access to records of a particular type.
  #
  # @param type [String] type of records to share
  # @param reader_id [String] client ID of reader to grant access to
  def share(type, reader_id)
    if reader_id == @config.client_id
      return
    end

    id = @config.client_id
    ak = get_access_key(id, id, id, type)
    put_access_key(id, id, reader_id, type, ak)

    url = get_url('v1', 'storage', 'policy', id, id, reader_id, type)
    @conn.put(url, JSON.generate({:allow => [{:read => {}}]}))
  end

  # Revoke another E3DB client's access to records of a particular type.
  #
  # @param type [String] type of records to revoke access to
  # @param reader_id [String] client ID of reader to revoke access from
  def revoke(type, reader_id)
    if reader_id == @config.client_id
      return
    end

    id = @config.client_id
    url = get_url('v1', 'storage', 'policy', id, id, reader_id, type)
    @conn.put(url, JSON.generate({:deny => [{:read => {}}]}))
  end

  private
  def get_url(*paths)
    sprintf('%s/%s', @config.api_url.chomp('/'), paths.map { |x| URI.escape x }.join('/'))
  end
end

Instance Method Details

#client_info(client_id) ⇒ ClientInfo

Query the server for information about an E3DB client.

Parameters:

  • client_id (String)

    client ID to look up

Returns:



151
152
153
154
# File 'lib/e3db/client.rb', line 151

def client_info(client_id)
  resp = @conn.get(get_url('v1', 'storage', 'clients', client_id))
  ClientInfo.new(JSON.parse(resp.body, symbolize_names: true))
end

#client_key(client_id) ⇒ RbNaCl::PublicKey

Query the server for a client’s public key.

Parameters:

  • client_id (String)

    client ID to look up

Returns:

  • (RbNaCl::PublicKey)

    decoded Curve25519 public key



160
161
162
163
164
165
166
# File 'lib/e3db/client.rb', line 160

def client_key(client_id)
  if client_id == @config.client_id
    @public_key
  else
    Crypto.decode_public_key(client_info(client_id).public_key.curve25519)
  end
end

#delete(record_id) ⇒ Object

Delete a record from the E3DB storage service.

Parameters:

  • record_id (String)

    unique ID of record to delete



216
217
218
# File 'lib/e3db/client.rb', line 216

def delete(record_id)
  resp = @conn.delete(get_url('v1', 'storage', 'records', record_id))
end

#new_record(type) ⇒ Record

Create a new, empty record that can be written to E3DB by calling #write.

Parameters:

  • type (String)

    free-form content type of this record

Returns:

  • (Record)

    an empty record of type



192
193
194
195
196
197
198
# File 'lib/e3db/client.rb', line 192

def new_record(type)
  id = @config.client_id
  meta = Meta.new(record_id: nil, writer_id: id, user_id: id,
                  type: type, plain: Hash.new, created: nil,
                  last_modified: nil)
  Record.new(meta: meta, data: Hash.new)
end

#query(data: true, raw: false, writer: nil, record: nil, type: nil, plain: nil) ⇒ Object

Query E3DB records according to a set of selection criteria.

Each record (optionally including data) is yielded to the block argument.

Parameters:

  • writer (String, Array<String>) (defaults to: nil)

    select records written by these client IDs

  • record (String, Array<String>) (defaults to: nil)

    select records with these record IDs

  • type (String, Array<string>) (defaults to: nil)

    select records with these types

  • plain (Hash) (defaults to: nil)

    plaintext query expression to select

  • data (Boolean) (defaults to: true)

    include data in records

  • raw (Boolean) (defaults to: false)

    when true don’t decrypt record data



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/e3db/client.rb', line 255

def query(data: true, raw: false, writer: nil, record: nil, type: nil, plain: nil)
  q = Query.new(after_index: 0, include_data: data, writer_ids: writer,
                record_ids: record, content_types: type, plain: plain,
                user_ids: nil, count: DEFAULT_QUERY_COUNT)
  url = get_url('v1', 'storage', 'search')
  loop do
    resp = @conn.post(url, q.as_json)
    json = JSON.parse(resp.body, symbolize_names: true)
    results = json[:results]
    results.each do |r|
      record = Record.new(meta: r[:meta], data: r[:record_data] || Hash.new)
      if data && !raw
        record = decrypt_record(record)
      end
      yield record
    end

    if results.length < q.count
      break
    end

    q.after_index = json[:last_index]
  end
end

#read(record_id) ⇒ Record

Read a single record by ID from E3DB and return it.

Parameters:

  • record_id (String)

    record ID to look up

Returns:

  • (Record)

    decrypted record object



183
184
185
# File 'lib/e3db/client.rb', line 183

def read(record_id)
  decrypt_record(read_raw(record_id))
end

#read_raw(record_id) ⇒ Record

Read a single record by ID from E3DB and return it without decrypting the data fields.

Parameters:

  • record_id (String)

    record ID to look up

Returns:

  • (Record)

    encrypted record object



173
174
175
176
177
# File 'lib/e3db/client.rb', line 173

def read_raw(record_id)
  resp = @conn.get(get_url('v1', 'storage', 'records', record_id))
  json = JSON.parse(resp.body, symbolize_names: true)
  Record.new(json)
end

#revoke(type, reader_id) ⇒ Object

Revoke another E3DB client’s access to records of a particular type.

Parameters:

  • type (String)

    type of records to revoke access to

  • reader_id (String)

    client ID of reader to revoke access from



301
302
303
304
305
306
307
308
309
# File 'lib/e3db/client.rb', line 301

def revoke(type, reader_id)
  if reader_id == @config.client_id
    return
  end

  id = @config.client_id
  url = get_url('v1', 'storage', 'policy', id, id, reader_id, type)
  @conn.put(url, JSON.generate({:deny => [{:read => {}}]}))
end

#share(type, reader_id) ⇒ Object

Grant another E3DB client access to records of a particular type.

Parameters:

  • type (String)

    type of records to share

  • reader_id (String)

    client ID of reader to grant access to



284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/e3db/client.rb', line 284

def share(type, reader_id)
  if reader_id == @config.client_id
    return
  end

  id = @config.client_id
  ak = get_access_key(id, id, id, type)
  put_access_key(id, id, reader_id, type, ak)

  url = get_url('v1', 'storage', 'policy', id, id, reader_id, type)
  @conn.put(url, JSON.generate({:allow => [{:read => {}}]}))
end

#write(record) ⇒ String

Write a new record to the E3DB storage service.

Create new records with #new_record.

Parameters:

  • record (Record)

    record to write

Returns:

  • (String)

    the unique ID of the written record



206
207
208
209
210
211
# File 'lib/e3db/client.rb', line 206

def write(record)
  url = get_url('v1', 'storage', 'records')
  resp = @conn.post(url, encrypt_record(record).to_hash)
  json = JSON.parse(resp.body, symbolize_names: true)
  json[:meta][:record_id]
end