e3db 1.0.0 → 2.0.0.rc1

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 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