e3db 1.0.0 → 2.0.0.rc1

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
  SHA1:
3
- metadata.gz: 8130f8d97af89a3b0dadd56a137f215950df2ff6
4
- data.tar.gz: 1d8c7ea1c68e443417fe8426ae742e3546f3e378
3
+ metadata.gz: 4654bc1065622f2d3f1ad6fd263688780063f4c5
4
+ data.tar.gz: 0be6154d88d0cd709a7fca0e67013cc05ad7d1fd
5
5
  SHA512:
6
- metadata.gz: 40e554e311c5c25c8ea9eeffb2a33c0a27073b9d852ae23895b2577f0d279e909c6401eccc60b0725c0e6a395f558b6059a80801b3a4c65566f4f14795c6ede4
7
- data.tar.gz: ae4c58bacbeba5924d47a181b2d9e9f179bbdda47bc5987edc3a4baaa8e5b47dc19be87b206042d8832747bf813a9e2468c6414c3063715937a71629e2f591b6
6
+ metadata.gz: 1401984cc27238a0a168b7e71265bbdfe4b44d225b7e4edfede2519e4cd384a699f9fbd984ec61932ee7d0cc0347704bedcd08002a16081eb96d703b254324c3
7
+ data.tar.gz: 30ff287c9585276086c4fb9a7a936e3e20b9656dc5f410016b70c3833af4e29ade36207ad49e6c95db172d780b7c2abffaad261b53e3427a912c4c76750a6dfc
@@ -1,5 +1,19 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.0.0
4
+ - 2.3.4
5
+ - 2.4.1
6
+
7
+ cache:
8
+ bundler: true
9
+ directories:
10
+ - $HOME/libsodium
11
+
5
12
  before_install: gem install bundler -v 1.12.3
13
+
14
+ install:
15
+ - ./travis-install-libsodium.sh
16
+ - ./travis-install-configfile.sh
17
+ - export PKG_CONFIG_PATH=$HOME/libsodium/lib/pkgconfig:$PKG_CONFIG_PATH
18
+ - export LD_LIBRARY_PATH=$HOME/libsodium/lib:$LD_LIBRARY_PATH
19
+ - bundle install
@@ -0,0 +1 @@
1
+ --markup markdown
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in e3db.gemspec
4
- gemspec
4
+ gemspec
data/README.md CHANGED
@@ -1,3 +1,4 @@
1
+ [![Gem Version][gem-image]][gem-url] [![Build Status][travis-image]][travis-url] [![Coverage Status][coveralls-image]][coveralls-url]
1
2
 
2
3
  # Introduction
3
4
 
@@ -83,19 +84,18 @@ client = E3DB::Client.new(config)
83
84
 
84
85
  ## Writing a record
85
86
 
86
- To write new records to the database, first create a blank record
87
- of the correct type using `E3DB::Client#new_record`. Then fill in
88
- the fields of the record's `data` hash. Finally, write the record
89
- to the database with `E3DB::Client#write`, which returns the
90
- unique ID of the newly created record.
87
+ To write new records to the database, call the `E3DB::Client#write`
88
+ method with a string describing the type of data to be written,
89
+ along with a hash containing the fields of the record. `E3DB::Client#write`
90
+ returns the newly created record.
91
91
 
92
92
  ```ruby
93
- record = client.new_record('contact')
94
- record.data[:first_name] = 'Jon'
95
- record.data[:last_name] = 'Snow'
96
- record.data[:phone] = '555-555-1212'
97
- record_id = client.write(record)
98
- printf("Wrote record %s\n", record_id)
93
+ record = client.write('contact', {
94
+ :first_name => 'Jon',
95
+ :last_name => 'Snow',
96
+ :phone => '555-555-1212'
97
+ })
98
+ printf("Wrote record %s\n", record.meta.record_id)
99
99
  ```
100
100
 
101
101
  ## Querying Records
@@ -114,6 +114,29 @@ client.query(type: 'contact') do |record|
114
114
  end
115
115
  ```
116
116
 
117
+ In this example, the `E3DB::Client#query` method takes a block that will
118
+ execute for each record that matches the query. Records will be streamed
119
+ efficiently from the server in batches, allowing processing of large data
120
+ sets without consuming excessive memory.
121
+
122
+ In some cases, it is more convenient to load all results into memory
123
+ for processing. To achieve this, instead of passing a block to
124
+ `E3DB::Client#query`, you can call `Enumerable` methods on the query result,
125
+ including `Enumerable#to_a` to convert the results to an array.
126
+
127
+ For example:
128
+
129
+ ```ruby
130
+ results = client.query(type: 'contact').to_a
131
+ printf("There were %d results.\n", results.length)
132
+ results.each do |record|
133
+ puts record
134
+ end
135
+ ```
136
+
137
+ ## More examples
138
+ See the [simple example code](examples/simple.rb) for runnable detailed examples.
139
+
117
140
  ## Development
118
141
 
119
142
  Before running tests, register an `integration-test` profile using
@@ -133,6 +156,12 @@ then run `bundle exec rake release`, which will create a git tag for the
133
156
  version, push git commits and tags, and push the `.gem` file to
134
157
  [rubygems.org](https://rubygems.org).
135
158
 
159
+ ## Documentation
160
+
161
+ General E3DB documentation is [on our web site](https://tozny.com/documentation/e3db/).
162
+
163
+ Comprehensive documentation for the SDK can be found online [via RubyDoc.info](http://www.rubydoc.info/gems/e3db/1.0.0).
164
+
136
165
  ## Contributing
137
166
 
138
167
  Bug reports and pull requests are welcome on GitHub at https://github.com/tozny/e3db-ruby.
@@ -140,3 +169,10 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/tozny/
140
169
  ## License
141
170
 
142
171
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
172
+
173
+ [gem-image]: https://badge.fury.io/rb/e3db.svg
174
+ [gem-url]: https://rubygems.org/gems/e3db
175
+ [travis-image]: https://travis-ci.org/tozny/e3db-ruby.svg?branch=master
176
+ [travis-url]: https://travis-ci.org/tozny/e3db-ruby
177
+ [coveralls-image]: https://coveralls.io/repos/github/tozny/e3db-ruby/badge.svg?branch=master
178
+ [coveralls-url]: https://coveralls.io/github/tozny/e3db-ruby
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.add_development_dependency "rake", "~> 10.0"
23
23
  spec.add_development_dependency "rspec", "~> 3.0"
24
24
  spec.add_development_dependency 'simplecov', '~> 0.14.1'
25
+ spec.add_development_dependency 'coveralls', '~> 0.8.0'
25
26
 
26
27
  spec.add_dependency 'dry-struct', '~> 0.2.1'
27
28
  spec.add_dependency 'lru_redux', '~> 1.1'
@@ -0,0 +1,148 @@
1
+ # This program provides a few simple examples of reading, writing, and
2
+ # querying e3db records. For more detailed information, please see the
3
+ # documentation home page: https://tozny.com/documentation/e3db/
4
+ #
5
+ # Author:: Isaac Potoczny-Jones (mailto:ijones@tozny.com)
6
+ # Copyright:: Copyright (c) 2017 Tozny, LLC
7
+ # License:: Public Domain
8
+
9
+ # ---------------------------------------------------------
10
+ # Initialization
11
+ # ---------------------------------------------------------
12
+
13
+ require 'e3db'
14
+
15
+ # Configuration files live in ~/.tozny and you can have several
16
+ # different "profiles" like *dev* and *production*.
17
+ config = E3DB::Config.default
18
+
19
+ # Now create a client using that configuration.
20
+ client = E3DB::Client.new(config)
21
+
22
+ # ---------------------------------------------------------
23
+ # Writing a record
24
+ # ---------------------------------------------------------
25
+
26
+ # Create a record by first creating a local version as a map:
27
+ data = {
28
+ :name => 'Jon Snow',
29
+ :what_he_knows => 'Nothing'
30
+ }
31
+
32
+ # Now encrypt the *value* part of the record, write it to the server and
33
+ # the server returns the newly created record:
34
+ record = client.write('test-contact', data)
35
+ record_id = record.meta.record_id
36
+ puts("Wrote: " + record_id)
37
+
38
+ # ---------------------------------------------------------
39
+ # Simple reading and queries
40
+ # ---------------------------------------------------------
41
+
42
+ # Use the new record's unique ID to read the same record again from E3DB:
43
+ newRecord = client.read(record.meta.record_id)
44
+ puts 'Record: ' + newRecord.data[:name] + ' ' + record.data[:what_he_knows]
45
+
46
+ # Query for all records of type 'test-contact' and print out
47
+ # a little bit of data and metadata.
48
+ client.query(type: 'test-contact').each do |record|
49
+ puts 'Data: ' + record.data[:name] + ' ' + record.data[:what_he_knows]
50
+ puts 'Metadata: ' + record.meta.record_id + ' ' + record.meta.type
51
+ end
52
+
53
+ # ---------------------------------------------------------
54
+ # Simple sharing by record type
55
+ # ---------------------------------------------------------
56
+
57
+ # Share all of the records of type 'test-contact' with Isaac's client ID:
58
+ isaac_client_id = 'db1744b9-3fb6-4458-a291-0bc677dba08b'
59
+ client.share('test-contact', isaac_client_id)
60
+
61
+ # Share all of the records of type 'test-contact' with Isaac's email address.
62
+ # This only works if the client has opted into discovery of their client_id.
63
+ client.share('test-contact', 'ijones+feedback@tozny.com')
64
+
65
+ # ---------------------------------------------------------
66
+ # More complex queries
67
+ # ---------------------------------------------------------
68
+
69
+ # Create some new records of the same type (note that they are also shared
70
+ # automatically since they are a type that we have shared above. We
71
+ # will also add some "plain" fields that are not secret but can be used
72
+ # for efficient querying:
73
+
74
+ bran_data = { :name => 'Bran', :what_he_knows => 'Crow' }
75
+ bran_plain = { :house => 'Stark', :ageRange => 'child' }
76
+ client.write('test-contact', bran_data, bran_plain)
77
+
78
+ hodor_data = { :name => 'Hodor', :what_he_knows => 'Hodor' }
79
+ hodor_plain = { :house => 'Stark', :ageRange => 'adult' }
80
+ client.write('test-contact', hodor_data, hodor_plain)
81
+
82
+ doran_data = { :name => 'Doran', :what_he_knows => 'Oberyn' }
83
+ doran_plain = { :house => 'Martell', :ageRange => 'adult' }
84
+ client.write('test-contact', doran_data, doran_plain)
85
+
86
+ # Create a query that finds everyone from house Stark, but not others:
87
+ queryWesteros = Hash.new
88
+ queryWesteros = {:eq => {:name => 'house', :value => 'Stark'} }
89
+
90
+ # Execute that query:
91
+ client.query(plain: queryWesteros).each do |record|
92
+ puts record.data[:name]
93
+ end
94
+
95
+ # Now create a more complex query with only the adults from house Stark:
96
+ queryWesteros = {:and => [
97
+ {:eq => {:name => 'house', :value => 'Stark'} },
98
+ {:eq => {:name => 'ageRange', :value => 'adult'} }
99
+ ]}
100
+
101
+ # Execute that query:
102
+ client.query(plain: queryWesteros) do |record|
103
+ puts record.data[:name]
104
+ end
105
+
106
+ # ---------------------------------------------------------
107
+ # Learning about other clients
108
+ # ---------------------------------------------------------
109
+ isaac_client_info = client.client_info('ijones+feedback@tozny.com')
110
+ puts isaac_client_info.inspect
111
+
112
+ # Fetch the public key:
113
+ isaac_pub_key = client.client_key(isaac_client_id)
114
+ puts isaac_pub_key.inspect
115
+
116
+ # ---------------------------------------------------------
117
+ # More reading and inspection of records
118
+ # ---------------------------------------------------------
119
+
120
+ # read_raw gets a record without decrypting its data
121
+ rawRecord = client.read_raw(record_id)
122
+ newRecord = client.read(record_id)
123
+
124
+ # So let's compare them:
125
+
126
+ puts (rawRecord.meta == newRecord.meta).to_s # true
127
+ puts (rawRecord.data == newRecord.data).to_s # false
128
+
129
+ puts newRecord.data[:name] + ' encrypts to ' + rawRecord.data[:name]
130
+
131
+ # Records contain a few other fields that are fun to look at, and this gives
132
+ # you a good sense for what's encrypted and what's not:
133
+ puts rawRecord.inspect
134
+
135
+ # ---------------------------------------------------------
136
+ # Clean up - Comment these out if you want to experiment
137
+ # ---------------------------------------------------------
138
+
139
+ # Revoke the sharing created by the client.share
140
+ client.revoke('test-contact', 'ijones+feedback@tozny.com')
141
+
142
+ # Delete the record we created above
143
+ client.delete(record_id)
144
+
145
+ # Delete all of the records of type test-contact from previous runs:
146
+ client.query(type: 'test-contact') do |record|
147
+ client.delete(record.meta.record_id)
148
+ end
@@ -35,6 +35,23 @@ module E3DB
35
35
 
36
36
  private_constant :TokenHelper
37
37
 
38
+ # Exception thrown by {Client#update} when a concurrent modification
39
+ # is detected. Upon catching this exception, a client should re-fetch
40
+ # the affected record and retry the update operation.
41
+ class ConflictError < StandardError
42
+ def initialize(record)
43
+ super('Conflict updating record: ' + record.meta.record_id)
44
+ @record = record
45
+ end
46
+
47
+ # Return the record from the failing update attempt.
48
+ #
49
+ # @return [Record] the affected record
50
+ def record
51
+ @record
52
+ end
53
+ end
54
+
38
55
  # A client's public key information.
39
56
  #
40
57
  # @!attribute curve25519
@@ -72,6 +89,8 @@ module E3DB
72
89
  # @return [Time, nil] when this record was created, or nil if unavailable
73
90
  # @!attribute last_modified
74
91
  # @return [Time, nil] when this record was last modified, or nil if unavailable
92
+ # @!attribute version
93
+ # @return [String] opaque version identifier updated by server on changes
75
94
  class Meta < Dry::Struct
76
95
  attribute :record_id, Types::Strict::String.optional
77
96
  attribute :writer_id, Types::Strict::String
@@ -80,6 +99,7 @@ module E3DB
80
99
  attribute :plain, Types::Strict::Hash.default { Hash.new }
81
100
  attribute :created, Types::Json::DateTime.optional
82
101
  attribute :last_modified, Types::Json::DateTime.optional
102
+ attribute :version, Types::Strict::String.optional
83
103
  end
84
104
 
85
105
  # A E3DB record containing data and metadata. Records are
@@ -88,9 +108,8 @@ module E3DB
88
108
  # to the server for storage, and decrypted in the client after
89
109
  # they are read.
90
110
  #
91
- # The {Client#new_record} method should be used to create a
92
- # new record that can be written to the database with
93
- # {Client#write}.
111
+ # New records are written to the database by calling the
112
+ # {Client#write} method.
94
113
  #
95
114
  # To read a record by their unique ID, use {Client#read}, or to
96
115
  # query a set of records based on their attributes, use {Client#query}.
@@ -102,6 +121,49 @@ module E3DB
102
121
  class Record < Dry::Struct
103
122
  attribute :meta, Meta
104
123
  attribute :data, Types::Strict::Hash.default { Hash.new }
124
+
125
+ # Allow updating metadata, used on destructive update.
126
+ def meta=(meta)
127
+ @meta = meta
128
+ end
129
+ end
130
+
131
+ # Information about records shared with this client.
132
+ #
133
+ # The {Client#incoming_sharing} method returns a list of
134
+ # {IncomingSharingPolicy} instances, each of which describes
135
+ # a rule allowing this client to read records of a specific
136
+ # type, written by another client.
137
+ #
138
+ # @!attribute writer_id
139
+ # @return [String] unique ID of the writer that shared with this client
140
+ # @!attribute writer_name
141
+ # @return [String] display name of the writer, if available
142
+ # @!attribute record_type
143
+ # @return [String] type of record shared with this client
144
+ class IncomingSharingPolicy < Dry::Struct
145
+ attribute :writer_id, Types::Strict::String
146
+ attribute :writer_name, Types::Strict::String.optional
147
+ attribute :record_type, Types::Strict::String
148
+ end
149
+
150
+ # Information about records shared with another client.
151
+ #
152
+ # The {Client#outgoing_sharing} method returns a list of
153
+ # {OutgoingSharingPolicy} instances, each of which describes
154
+ # a rule allowing other E3DB clients to read records of a
155
+ # specific type.
156
+ #
157
+ # @!attribute reader_id
158
+ # @return [String] unique ID of the authorized reader
159
+ # @!attribute reader_name
160
+ # @return [String] display name of reader, if available
161
+ # @!attribute record_type
162
+ # @return [String] type of record shared with reader
163
+ class OutgoingSharingPolicy < Dry::Struct
164
+ attribute :reader_id, Types::Strict::String
165
+ attribute :reader_name, Types::Strict::String.optional
166
+ attribute :record_type, Types::Strict::String
105
167
  end
106
168
 
107
169
  # A connection to the E3DB service used to perform database operations.
@@ -146,10 +208,17 @@ module E3DB
146
208
 
147
209
  # Query the server for information about an E3DB client.
148
210
  #
149
- # @param client_id [String] client ID to look up
211
+ # @param client_id [String] client ID or e-mail address to look up
150
212
  # @return [ClientInfo] information about this client
151
213
  def client_info(client_id)
152
- resp = @conn.get(get_url('v1', 'storage', 'clients', client_id))
214
+ if client_id.include? "@"
215
+ base_url = get_url('v1', 'storage', 'clients', 'find')
216
+ url = base_url + sprintf('?email=%s', CGI.escape(client_id))
217
+ resp = @conn.post(url)
218
+ else
219
+ resp = @conn.get(get_url('v1', 'storage', 'clients', client_id))
220
+ end
221
+
153
222
  ClientInfo.new(JSON.parse(resp.body, symbolize_names: true))
154
223
  end
155
224
 
@@ -184,30 +253,48 @@ module E3DB
184
253
  decrypt_record(read_raw(record_id))
185
254
  end
186
255
 
187
- # Create a new, empty record that can be written to E3DB
188
- # by calling {Client#write}.
256
+ # Write a new record to the E3DB storage service.
189
257
  #
190
- # @param type [String] free-form content type of this record
191
- # @return [Record] an empty record of `type`
192
- def new_record(type)
258
+ # @param type [String] free-form content type name of this record
259
+ # @param data [Hash<String, String>] record data to be stored encrypted
260
+ # @param plain [Hash<String, String>] record data to be stored unencrypted for querying
261
+ # @return [Record] the newly created record object
262
+ def write(type, data, plain=Hash.new)
263
+ url = get_url('v1', 'storage', 'records')
193
264
  id = @config.client_id
194
265
  meta = Meta.new(record_id: nil, writer_id: id, user_id: id,
195
- type: type, plain: Hash.new, created: nil,
196
- last_modified: nil)
197
- Record.new(meta: meta, data: Hash.new)
266
+ type: type, plain: plain, created: nil,
267
+ last_modified: nil, version: nil)
268
+ record = Record.new(meta: meta, data: data)
269
+ resp = @conn.post(url, encrypt_record(record).to_hash)
270
+ decrypt_record(Record.new(JSON.parse(resp.body, symbolize_names: true)))
198
271
  end
199
272
 
200
- # Write a new record to the E3DB storage service.
273
+ # Update an existing record in the E3DB storage service.
201
274
  #
202
- # Create new records with {Client#new_record}.
275
+ # If the record has been modified by another client since it was
276
+ # read, this method raises {ConflictError}, which should be caught
277
+ # by the caller so that the record can be re-fetched and the update retried.
203
278
  #
204
- # @param record [Record] record to write
205
- # @return [String] the unique ID of the written record
206
- def write(record)
207
- url = get_url('v1', 'storage', 'records')
208
- resp = @conn.post(url, encrypt_record(record).to_hash)
279
+ # The metadata of the input record will be updated in-place to reflect
280
+ # the new version number and modification time returned by the server.
281
+ #
282
+ # @param record [Record] the record to update
283
+ def update(record)
284
+ record_id = record.meta.record_id
285
+ version = record.meta.version
286
+ url = get_url('v1', 'storage', 'records', 'safe', record_id, version)
287
+ begin
288
+ resp = @conn.put(url, encrypt_record(record).to_hash)
289
+ rescue Faraday::ClientError => e
290
+ if e.response[:status] == 409
291
+ raise E3DB::ConflictError, record
292
+ else
293
+ raise e # re-raise on other failures
294
+ end
295
+ end
209
296
  json = JSON.parse(resp.body, symbolize_names: true)
210
- json[:meta][:record_id]
297
+ record.meta = Meta.new(json[:meta])
211
298
  end
212
299
 
213
300
  # Delete a record from the E3DB storage service.
@@ -218,14 +305,15 @@ module E3DB
218
305
  end
219
306
 
220
307
  class Query < Dry::Struct
221
- attribute :count, Types::Int
222
- attribute :include_data, Types::Bool.optional
223
- attribute :writer_ids, Types::Coercible::Array.member(Types::String).optional
224
- attribute :user_ids, Types::Coercible::Array.member(Types::String).optional
225
- attribute :record_ids, Types::Coercible::Array.member(Types::String).optional
226
- attribute :content_types, Types::Coercible::Array.member(Types::String).optional
227
- attribute :plain, Types::Hash.optional
228
- attribute :after_index, Types::Int.optional
308
+ attribute :count, Types::Int
309
+ attribute :include_data, Types::Bool.optional
310
+ attribute :writer_ids, Types::Coercible::Array.member(Types::String).optional
311
+ attribute :user_ids, Types::Coercible::Array.member(Types::String).optional
312
+ attribute :record_ids, Types::Coercible::Array.member(Types::String).optional
313
+ attribute :content_types, Types::Coercible::Array.member(Types::String).optional
314
+ attribute :plain, Types::Hash.optional
315
+ attribute :after_index, Types::Int.optional
316
+ attribute :include_all_writers, Types::Bool.optional
229
317
 
230
318
  def after_index=(index)
231
319
  @after_index = index
@@ -241,49 +329,117 @@ module E3DB
241
329
  DEFAULT_QUERY_COUNT = 100
242
330
  private_constant :DEFAULT_QUERY_COUNT
243
331
 
332
+ # A set of records returned by {Client#query}. This implements the
333
+ # `Enumerable` interface which can be used to loop over the records
334
+ # in the result set (using eg: `Enumerable#each`).
335
+ #
336
+ # Every traversal of the result set will execute a query to the server,
337
+ # so if multiple in-memory traversals are needed, use `Enumerable#to_a` to
338
+ # fetch all records into an array first.
339
+ class Result
340
+ include Enumerable
341
+
342
+ def initialize(client, query, raw)
343
+ @client = client
344
+ @query = query
345
+ @raw = raw
346
+ end
347
+
348
+ # Invoke a block for each record matching a query.
349
+ def each
350
+ # Every invocation of 'each' gets its own copy of the query since
351
+ # it will be modified as we loop through the result pages. This
352
+ # allows multiple traversals of the same result set to start from
353
+ # the beginning each time.
354
+ q = Query.new(@query.to_hash)
355
+ loop do
356
+ json = @client.instance_eval { query1(q) }
357
+ results = json[:results]
358
+ results.each do |r|
359
+ record = Record.new(meta: r[:meta], data: r[:record_data] || Hash.new)
360
+ if q.include_data && !@raw
361
+ access_key = r[:access_key]
362
+ if access_key
363
+ record = @client.instance_eval {
364
+ ak = decrypt_eak(access_key)
365
+ decrypt_record_with_key(record, ak)
366
+ }
367
+ else
368
+ record = @client.instance_eval { decrypt_record(record) }
369
+ end
370
+ end
371
+ yield record
372
+ end
373
+
374
+ if results.length < q.count
375
+ break
376
+ end
377
+
378
+ q.after_index = json[:last_index]
379
+ end
380
+ end
381
+ end
382
+
244
383
  # Query E3DB records according to a set of selection criteria.
245
384
  #
246
- # Each record (optionally including data) is yielded to the block
247
- # argument.
385
+ # The default behavior is to return all records written by the
386
+ # current authenticated client.
387
+ #
388
+ # To restrict the results to a particular type, pass a type or
389
+ # list of types as the `type` argument.
390
+ #
391
+ # To restrict the results to a set of clients, pass a single or
392
+ # list of client IDs as the `writer` argument. To list records
393
+ # written by any client that has shared with the current client,
394
+ # pass the special token `:any` as the `writer` argument.
248
395
  #
249
- # @param writer [String,Array<String>] select records written by these client IDs
396
+ # If a block is supplied, each record matching the query parameters
397
+ # is fetched from the server and yielded to the block.
398
+ #
399
+ # If no block is supplied, a {Result} is returned that will
400
+ # iterate over the records matching the query parameters. This
401
+ # iterator is lazy and will query the server each time it is used,
402
+ # so calling `Enumerable#to_a` to convert to an array is recommended
403
+ # if multiple traversals are necessary.
404
+ #
405
+ # @param writer [String,Array<String>,:all] select records written by these client IDs or :all for all writers
250
406
  # @param record [String,Array<String>] select records with these record IDs
251
407
  # @param type [String,Array<string>] select records with these types
252
408
  # @param plain [Hash] plaintext query expression to select
253
409
  # @param data [Boolean] include data in records
254
410
  # @param raw [Boolean] when true don't decrypt record data
255
- def query(data: true, raw: false, writer: nil, record: nil, type: nil, plain: nil)
411
+ # @param page_size [Integer] number of records to fetch per request
412
+ # @return [Result] a result set object enumerating matched records
413
+ def query(data: true, raw: false, writer: nil, record: nil, type: nil, plain: nil, page_size: DEFAULT_QUERY_COUNT)
414
+ all_writers = false
415
+ if writer == :all
416
+ all_writers = true
417
+ writer = []
418
+ end
419
+
256
420
  q = Query.new(after_index: 0, include_data: data, writer_ids: writer,
257
421
  record_ids: record, content_types: type, plain: plain,
258
- user_ids: nil, count: DEFAULT_QUERY_COUNT)
259
- url = get_url('v1', 'storage', 'search')
260
- loop do
261
- resp = @conn.post(url, q.as_json)
262
- json = JSON.parse(resp.body, symbolize_names: true)
263
- results = json[:results]
264
- results.each do |r|
265
- record = Record.new(meta: r[:meta], data: r[:record_data] || Hash.new)
266
- if data && !raw
267
- record = decrypt_record(record)
268
- end
269
- yield record
270
- end
271
-
272
- if results.length < q.count
273
- break
422
+ user_ids: nil, count: page_size,
423
+ include_all_writers: all_writers)
424
+ result = Result.new(self, q, raw)
425
+ if block_given?
426
+ result.each do |rec|
427
+ yield rec
274
428
  end
275
-
276
- q.after_index = json[:last_index]
429
+ else
430
+ result
277
431
  end
278
432
  end
279
433
 
280
434
  # Grant another E3DB client access to records of a particular type.
281
435
  #
282
436
  # @param type [String] type of records to share
283
- # @param reader_id [String] client ID of reader to grant access to
437
+ # @param reader_id [String] client ID or e-mail address of reader to grant access to
284
438
  def share(type, reader_id)
285
439
  if reader_id == @config.client_id
286
440
  return
441
+ elsif reader_id.include? "@"
442
+ reader_id = client_info(reader_id).client_id
287
443
  end
288
444
 
289
445
  id = @config.client_id
@@ -301,6 +457,8 @@ module E3DB
301
457
  def revoke(type, reader_id)
302
458
  if reader_id == @config.client_id
303
459
  return
460
+ elsif reader_id.include? "@"
461
+ reader_id = client_info(reader_id).client_id
304
462
  end
305
463
 
306
464
  id = @config.client_id
@@ -308,9 +466,31 @@ module E3DB
308
466
  @conn.put(url, JSON.generate({:deny => [{:read => {}}]}))
309
467
  end
310
468
 
469
+ def outgoing_sharing
470
+ url = get_url('v1', 'storage', 'policy', 'outgoing')
471
+ resp = @conn.get(url)
472
+ json = JSON.parse(resp.body, symbolize_names: true)
473
+ return json.map {|x| OutgoingSharingPolicy.new(x)}
474
+ end
475
+
476
+ def incoming_sharing
477
+ url = get_url('v1', 'storage', 'policy', 'incoming')
478
+ resp = @conn.get(url)
479
+ json = JSON.parse(resp.body, symbolize_names: true)
480
+ return json.map {|x| IncomingSharingPolicy.new(x)}
481
+ end
482
+
311
483
  private
484
+
485
+ # Fetch a single page of query results. Used internally by {Client#query}.
486
+ def query1(query)
487
+ url = get_url('v1', 'storage', 'search')
488
+ resp = @conn.post(url, query.as_json)
489
+ return JSON.parse(resp.body, symbolize_names: true)
490
+ end
491
+
312
492
  def get_url(*paths)
313
- sprintf('%s/%s', @config.api_url.chomp('/'), paths.map { |x| URI.escape x }.join('/'))
493
+ sprintf('%s/%s', @config.api_url.chomp('/'), paths.map { |x| CGI.escape x }.join('/'))
314
494
  end
315
495
  end
316
496
  end
@@ -7,14 +7,14 @@
7
7
 
8
8
 
9
9
  module E3DB
10
- DEFAULT_API_URL = 'https://dev.e3db.com/'
10
+ DEFAULT_API_URL = 'https://api.e3db.com/'
11
11
 
12
12
  # Configuration and credentials for E3DB.
13
13
  #
14
14
  # Typically a configuration is loaded from a JSON file generated
15
15
  # during registration via the E3DB administration console
16
16
  # or command-line tool. To load a configuration from a JSON file,
17
- # use {Config.load}.
17
+ # use {E3DB::Config.load}.
18
18
  #
19
19
  # @!attribute version
20
20
  # @return [Int] the version number of the configuration format (currently 1)
@@ -47,7 +47,7 @@ module E3DB
47
47
  attribute :logging, Types::Bool
48
48
 
49
49
  # Load configuration from a JSON file created during registration
50
- # or with {Config.save}.
50
+ # or with {E3DB::Config.save}.
51
51
  #
52
52
  # The configuration file should contain a single JSON object
53
53
  # with the following structure:
@@ -9,50 +9,16 @@
9
9
  module E3DB
10
10
  class Client
11
11
  private
12
- def get_access_key(writer_id, user_id, reader_id, type)
13
- ak_cache_key = [writer_id, user_id, type]
14
- if @ak_cache.key? ak_cache_key
15
- return @ak_cache[ak_cache_key]
16
- end
17
-
18
- url = get_url('v1', 'storage', 'access_keys', writer_id, user_id, reader_id, type)
19
- resp = @conn.get(url)
20
- json = JSON.parse(resp.body, symbolize_names: true)
21
-
22
- k = json[:authorizer_public_key][:curve25519]
23
- authorizer_pubkey = Crypto.decode_public_key(k)
24
-
25
- fields = json[:eak].split('.', 2)
26
- ciphertext = Crypto.base64decode(fields[0])
27
- nonce = Crypto.base64decode(fields[1])
28
- box = RbNaCl::Box.new(authorizer_pubkey, @private_key)
29
-
30
- ak = box.decrypt(nonce, ciphertext)
31
- @ak_cache[ak_cache_key] = ak
32
- ak
33
- end
34
-
35
- def put_access_key(writer_id, user_id, reader_id, type, ak)
36
- ak_cache_key = [writer_id, user_id, type]
37
- @ak_cache[ak_cache_key] = ak
38
-
39
- reader_key = client_key(reader_id)
40
- nonce = RbNaCl::Random.random_bytes(RbNaCl::Box.nonce_bytes)
41
- eak = RbNaCl::Box.new(reader_key, @private_key).encrypt(nonce, ak)
42
-
43
- encoded_eak = sprintf('%s.%s', Crypto.base64encode(eak), Crypto.base64encode(nonce))
44
-
45
- url = get_url('v1', 'storage', 'access_keys', writer_id, user_id, reader_id, type)
46
- @conn.put(url, { :eak => encoded_eak })
47
- end
48
-
49
- def decrypt_record(encrypted_record)
50
- record = Record.new(meta: encrypted_record.meta.clone, data: Hash.new)
51
-
12
+ def decrypt_record(record)
52
13
  writer_id = record.meta.writer_id
53
14
  user_id = record.meta.user_id
54
15
  type = record.meta.type
55
16
  ak = get_access_key(writer_id, user_id, @config.client_id, type)
17
+ decrypt_record_with_key(record, ak)
18
+ end
19
+
20
+ def decrypt_record_with_key(encrypted_record, ak)
21
+ record = Record.new(meta: encrypted_record.meta.clone, data: Hash.new)
56
22
 
57
23
  encrypted_record.data.each do |k, v|
58
24
  fields = v.split('.', 4)
@@ -99,6 +65,48 @@ module E3DB
99
65
 
100
66
  record
101
67
  end
68
+
69
+ def decrypt_eak(json)
70
+ k = json[:authorizer_public_key][:curve25519]
71
+ authorizer_pubkey = Crypto.decode_public_key(k)
72
+
73
+ fields = json[:eak].split('.', 2)
74
+ ciphertext = Crypto.base64decode(fields[0])
75
+ nonce = Crypto.base64decode(fields[1])
76
+ box = RbNaCl::Box.new(authorizer_pubkey, @private_key)
77
+
78
+ box.decrypt(nonce, ciphertext)
79
+ end
80
+
81
+ def get_access_key(writer_id, user_id, reader_id, type)
82
+ ak_cache_key = [writer_id, user_id, type]
83
+ if @ak_cache.key? ak_cache_key
84
+ return @ak_cache[ak_cache_key]
85
+ end
86
+
87
+ url = get_url('v1', 'storage', 'access_keys', writer_id, user_id, reader_id, type)
88
+ resp = @conn.get(url)
89
+ json = JSON.parse(resp.body, symbolize_names: true)
90
+
91
+ ak = decrypt_eak(json)
92
+ @ak_cache[ak_cache_key] = ak
93
+ ak
94
+ end
95
+
96
+ def put_access_key(writer_id, user_id, reader_id, type, ak)
97
+ ak_cache_key = [writer_id, user_id, type]
98
+ @ak_cache[ak_cache_key] = ak
99
+
100
+ reader_key = client_key(reader_id)
101
+ nonce = RbNaCl::Random.random_bytes(RbNaCl::Box.nonce_bytes)
102
+ eak = RbNaCl::Box.new(reader_key, @private_key).encrypt(nonce, ak)
103
+
104
+ encoded_eak = sprintf('%s.%s', Crypto.base64encode(eak), Crypto.base64encode(nonce))
105
+
106
+ url = get_url('v1', 'storage', 'access_keys', writer_id, user_id, reader_id, type)
107
+ @conn.put(url, { :eak => encoded_eak })
108
+ end
109
+
102
110
  end
103
111
 
104
112
  class Crypto
@@ -1,3 +1,3 @@
1
1
  module E3DB
2
- VERSION = "1.0.0"
2
+ VERSION = "2.0.0.rc1"
3
3
  end
@@ -0,0 +1,24 @@
1
+ #!/bin/sh
2
+ # The purpose of this file is to install a default
3
+ # e3db profile configuration so tests can execute
4
+ # against a live server.
5
+
6
+ set -e
7
+
8
+ # Check if the config is already set
9
+ if [ ! -d "$HOME/.tozny/integration-test" ]; then
10
+ mkdir -p "$HOME/.tozny/integration-test"
11
+ fi
12
+
13
+ cat > "$HOME/.tozny/integration-test/e3db.json" <<EOT
14
+ {
15
+ "version":1,
16
+ "api_url":"${API_URL}",
17
+ "api_key_id":"${API_KEY_ID}",
18
+ "api_secret":"${API_SECRET}",
19
+ "client_id":"${CLIENT_ID}",
20
+ "client_email":"${CLIENT_EMAIL}",
21
+ "public_key":"${PUBLIC_KEY}",
22
+ "private_key":"${PRIVATE_KEY}"
23
+ }
24
+ EOT
@@ -0,0 +1,18 @@
1
+ #!/bin/sh
2
+ # The purpose of this file is to install libsodium in
3
+ # the Travis CI environment. Outside this environment,
4
+ # you would probably not want to install it like this.
5
+
6
+ set -e
7
+
8
+ # check if libsodium is already installed
9
+ if [ ! -d "$HOME/libsodium/lib" ]; then
10
+ wget https://github.com/jedisct1/libsodium/releases/download/1.0.12/libsodium-1.0.12.tar.gz
11
+ tar xvfz libsodium-1.0.12.tar.gz
12
+ cd libsodium-1.0.12
13
+ ./configure --prefix=$HOME/libsodium
14
+ make
15
+ make install
16
+ else
17
+ echo 'Using cached directory.'
18
+ fi
metadata CHANGED
@@ -1,165 +1,179 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: e3db
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tozny, LLC
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-05-04 00:00:00.000000000 Z
11
+ date: 2017-06-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ~>
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.12'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ~>
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.12'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ~>
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
33
  version: '10.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ~>
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '10.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ~>
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
47
  version: '3.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ~>
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: simplecov
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ~>
59
+ - - "~>"
60
60
  - !ruby/object:Gem::Version
61
61
  version: 0.14.1
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ~>
66
+ - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: 0.14.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: coveralls
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.8.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.8.0
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: dry-struct
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
- - - ~>
87
+ - - "~>"
74
88
  - !ruby/object:Gem::Version
75
89
  version: 0.2.1
76
90
  type: :runtime
77
91
  prerelease: false
78
92
  version_requirements: !ruby/object:Gem::Requirement
79
93
  requirements:
80
- - - ~>
94
+ - - "~>"
81
95
  - !ruby/object:Gem::Version
82
96
  version: 0.2.1
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: lru_redux
85
99
  requirement: !ruby/object:Gem::Requirement
86
100
  requirements:
87
- - - ~>
101
+ - - "~>"
88
102
  - !ruby/object:Gem::Version
89
103
  version: '1.1'
90
104
  type: :runtime
91
105
  prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
- - - ~>
108
+ - - "~>"
95
109
  - !ruby/object:Gem::Version
96
110
  version: '1.1'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: rbnacl
99
113
  requirement: !ruby/object:Gem::Requirement
100
114
  requirements:
101
- - - ~>
115
+ - - "~>"
102
116
  - !ruby/object:Gem::Version
103
117
  version: '4.0'
104
- - - '>='
118
+ - - ">="
105
119
  - !ruby/object:Gem::Version
106
120
  version: 4.0.2
107
121
  type: :runtime
108
122
  prerelease: false
109
123
  version_requirements: !ruby/object:Gem::Requirement
110
124
  requirements:
111
- - - ~>
125
+ - - "~>"
112
126
  - !ruby/object:Gem::Version
113
127
  version: '4.0'
114
- - - '>='
128
+ - - ">="
115
129
  - !ruby/object:Gem::Version
116
130
  version: 4.0.2
117
131
  - !ruby/object:Gem::Dependency
118
132
  name: net-http-persistent
119
133
  requirement: !ruby/object:Gem::Requirement
120
134
  requirements:
121
- - - ~>
135
+ - - "~>"
122
136
  - !ruby/object:Gem::Version
123
137
  version: 2.9.4
124
138
  type: :runtime
125
139
  prerelease: false
126
140
  version_requirements: !ruby/object:Gem::Requirement
127
141
  requirements:
128
- - - ~>
142
+ - - "~>"
129
143
  - !ruby/object:Gem::Version
130
144
  version: 2.9.4
131
145
  - !ruby/object:Gem::Dependency
132
146
  name: faraday_middleware
133
147
  requirement: !ruby/object:Gem::Requirement
134
148
  requirements:
135
- - - ~>
149
+ - - "~>"
136
150
  - !ruby/object:Gem::Version
137
151
  version: 0.11.0.1
138
152
  type: :runtime
139
153
  prerelease: false
140
154
  version_requirements: !ruby/object:Gem::Requirement
141
155
  requirements:
142
- - - ~>
156
+ - - "~>"
143
157
  - !ruby/object:Gem::Version
144
158
  version: 0.11.0.1
145
159
  - !ruby/object:Gem::Dependency
146
160
  name: oauth2
147
161
  requirement: !ruby/object:Gem::Requirement
148
162
  requirements:
149
- - - ~>
163
+ - - "~>"
150
164
  - !ruby/object:Gem::Version
151
165
  version: '1.3'
152
- - - '>='
166
+ - - ">="
153
167
  - !ruby/object:Gem::Version
154
168
  version: 1.3.1
155
169
  type: :runtime
156
170
  prerelease: false
157
171
  version_requirements: !ruby/object:Gem::Requirement
158
172
  requirements:
159
- - - ~>
173
+ - - "~>"
160
174
  - !ruby/object:Gem::Version
161
175
  version: '1.3'
162
- - - '>='
176
+ - - ">="
163
177
  - !ruby/object:Gem::Version
164
178
  version: 1.3.1
165
179
  description:
@@ -169,9 +183,10 @@ executables: []
169
183
  extensions: []
170
184
  extra_rdoc_files: []
171
185
  files:
172
- - .gitignore
173
- - .rspec
174
- - .travis.yml
186
+ - ".gitignore"
187
+ - ".rspec"
188
+ - ".travis.yml"
189
+ - ".yardopts"
175
190
  - Gemfile
176
191
  - LICENSE.txt
177
192
  - README.md
@@ -179,12 +194,15 @@ files:
179
194
  - bin/console
180
195
  - bin/setup
181
196
  - e3db.gemspec
197
+ - examples/simple.rb
182
198
  - lib/e3db.rb
183
199
  - lib/e3db/client.rb
184
200
  - lib/e3db/config.rb
185
201
  - lib/e3db/crypto.rb
186
202
  - lib/e3db/types.rb
187
203
  - lib/e3db/version.rb
204
+ - travis-install-configfile.sh
205
+ - travis-install-libsodium.sh
188
206
  homepage: https://tozny.com/e3db
189
207
  licenses:
190
208
  - MIT
@@ -195,17 +213,17 @@ require_paths:
195
213
  - lib
196
214
  required_ruby_version: !ruby/object:Gem::Requirement
197
215
  requirements:
198
- - - '>='
216
+ - - ">="
199
217
  - !ruby/object:Gem::Version
200
218
  version: '0'
201
219
  required_rubygems_version: !ruby/object:Gem::Requirement
202
220
  requirements:
203
- - - '>='
221
+ - - ">"
204
222
  - !ruby/object:Gem::Version
205
- version: '0'
223
+ version: 1.3.1
206
224
  requirements: []
207
225
  rubyforge_project:
208
- rubygems_version: 2.0.14.1
226
+ rubygems_version: 2.6.8
209
227
  signing_key:
210
228
  specification_version: 4
211
229
  summary: e3db client SDK