boxr 1.17.0 → 1.18.0

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
  SHA256:
3
- metadata.gz: e02b3471599db059d5a1e9f7ae8b06757c60fe66d576b3083fd1e3799fac49f1
4
- data.tar.gz: d0ce9eb18ef1464b14d48ce45872a6a3768e4be14ebe9d2c903cb3172e0e791c
3
+ metadata.gz: ecda6bc31119954e1441132e89cb3730c72832a9d27a4b50cef02631889ac16d
4
+ data.tar.gz: 78e5f729e9e50695560cc15b28286e865940e7222cf258794763ee8efa61c8ee
5
5
  SHA512:
6
- metadata.gz: 6d45931744e3c77a0a29ee071a5da104f91152fff14471421187435e186f62b1964361a81b97b697e38226da95af814d1d9cf3bc0a905aed1980090e47a55deb
7
- data.tar.gz: a3b3bad61ff4c59678e7d116e0f5250e6172b01d07572a96436e4b23af76dc72e6cca894233278e131610e91a62cb84ed63c1c520fa5ffb1e9a345b8f84da1be
6
+ metadata.gz: 4b35c744df6ef3a020d3d201ed8caae441b33595ebd4dbd22d510db478a39bd5c548dca20b01899ba38ded2d6a9d00e5df9659df62d535ba9442672a88395630
7
+ data.tar.gz: f83a2c3cdeec8795c57d22a3ae1e12850f62f68b984fe28fcc63b6885fb9b0ad4d009c7330821f8d449f15a7b430eb15262dc17ac74de896359e2616e5cd52e7
data/.env.example CHANGED
@@ -13,3 +13,5 @@ BOX_CLIENT_SECRET={client secret of your Box app}
13
13
  BOX_ENTERPRISE_ID={box enterprise id}
14
14
  JWT_PRIVATE_KEY_PATH={path to your JWT private key}
15
15
  JWT_PRIVATE_KEY_PASSWORD={JWT private key password}
16
+ BOX_PRIMARY_SIGNATURE_KEY={primary key for webhooks}
17
+ BOX_SECONDARY_SIGNATURE_KEY={secondary key for webhooks}
data/README.md CHANGED
@@ -476,6 +476,29 @@ apply_watermark_on_folder(folder)
476
476
 
477
477
  remove_watermark_on_folder(folder)
478
478
  ```
479
+
480
+ #### [Webhooks](https://developer.box.com/en/reference/resources/webhook/) & [Webhook Signatures](https://developer.box.com/guides/webhooks/handle/setup-signatures/)
481
+ ```ruby
482
+ create_webhook(target_id, target_type, triggers, address)
483
+
484
+ get_webhooks(marker: nil, limit: nil)
485
+
486
+ get_webhook(webhook_id)
487
+
488
+ update_webhook(webhook_id, attributes)
489
+
490
+ delete_webhook(webhook_id)
491
+
492
+ # When receiving a webhook, it is advised to verify that it was sent by Box
493
+ Boxr::WebhookValidator.new(
494
+ headers,
495
+ payload,
496
+ primary_signature_key: primary_signature_key,
497
+ secondary_signature_key: secondary_signature_key
498
+ ).valid_message?
499
+
500
+ ```
501
+
479
502
  ## Contributing
480
503
 
481
504
  1. Fork it ( https://github.com/cburnette/boxr/fork )
data/lib/boxr.rb CHANGED
@@ -24,6 +24,8 @@ require 'boxr/events'
24
24
  require 'boxr/auth'
25
25
  require 'boxr/web_links'
26
26
  require 'boxr/watermarking'
27
+ require 'boxr/webhooks'
28
+ require 'boxr/webhook_validator'
27
29
 
28
30
  class BoxrCollection < Array
29
31
  def files
data/lib/boxr/client.rb CHANGED
@@ -29,7 +29,7 @@ module Boxr
29
29
  METADATA_TEMPLATES_URI = "#{API_URI}/metadata_templates"
30
30
  EVENTS_URI = "#{API_URI}/events"
31
31
  WEB_LINKS_URI = "#{API_URI}/web_links"
32
-
32
+ WEBHOOKS_URI = "#{API_URI}/webhooks"
33
33
 
34
34
  DEFAULT_LIMIT = 100
35
35
  FOLDER_ITEMS_LIMIT = 1000
data/lib/boxr/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Boxr
2
- VERSION = "1.17.0".freeze
2
+ VERSION = "1.18.0".freeze
3
3
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Boxr
4
+ class WebhookValidator
5
+ attr_reader(
6
+ :payload,
7
+ :primary_signature,
8
+ :primary_signature_key,
9
+ :secondary_signature,
10
+ :secondary_signature_key,
11
+ :timestamp
12
+ )
13
+
14
+ MAXIMUM_MESSAGE_AGE = 600 # 10 minutes (in seconds)
15
+
16
+ def initialize(headers, payload, primary_signature_key: nil, secondary_signature_key: nil)
17
+ @payload = payload
18
+ @timestamp = headers['BOX-DELIVERY-TIMESTAMP'].to_s
19
+ @primary_signature_key = primary_signature_key.to_s
20
+ @secondary_signature_key = secondary_signature_key.to_s
21
+ @primary_signature = headers['BOX-SIGNATURE-PRIMARY']
22
+ @secondary_signature = headers['BOX-SIGNATURE-SECONDARY']
23
+ end
24
+
25
+ def valid_message?
26
+ verify_delivery_timestamp && verify_signature
27
+ end
28
+
29
+ def verify_delivery_timestamp
30
+ message_age < MAXIMUM_MESSAGE_AGE
31
+ end
32
+
33
+ def verify_signature
34
+ generate_signature(primary_signature_key) == primary_signature || generate_signature(secondary_signature_key) == secondary_signature
35
+ end
36
+
37
+ def generate_signature(key)
38
+ message_as_bytes = (payload.bytes + timestamp.bytes).pack('U')
39
+ digest = OpenSSL::HMAC.hexdigest('SHA256', key, message_as_bytes)
40
+ Base64.encode64(digest)
41
+ end
42
+
43
+ private
44
+
45
+ def current_time
46
+ Time.now.utc
47
+ end
48
+
49
+ def delivery_time
50
+ Time.parse(timestamp).utc
51
+ rescue ArgumentError
52
+ raise BoxrError.new(boxr_message: "Webhook authenticity not verified: invalid timestamp")
53
+ end
54
+
55
+ def message_age
56
+ current_time - delivery_time
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Boxr
4
+ class Client
5
+ def create_webhook(target_id, target_type, triggers, address)
6
+ attributes = { target: { id: target_id, type: target_type }, triggers: triggers, address: address }
7
+ new_webhook, response = post(WEBHOOKS_URI, attributes)
8
+ new_webhook
9
+ end
10
+
11
+ def get_webhooks(marker: nil, limit: nil)
12
+ query_params = { marker: marker, limit: limit }.compact
13
+ webhooks, response = get(WEBHOOKS_URI, query: query_params)
14
+ webhooks
15
+ end
16
+
17
+ def get_webhook(webhook)
18
+ webhook_id = ensure_id(webhook)
19
+ uri = "#{WEBHOOKS_URI}/#{webhook_id}"
20
+ webhook, response = get(uri)
21
+ webhook
22
+ end
23
+
24
+ def update_webhook(webhook, attributes = {})
25
+ webhook_id = ensure_id(webhook)
26
+ uri = "#{WEBHOOKS_URI}/#{webhook_id}"
27
+ updated_webhook, response = put(uri, attributes)
28
+ updated_webhook
29
+ end
30
+
31
+ def delete_webhook(webhook)
32
+ webhook_id = ensure_id(webhook)
33
+ uri = "#{WEBHOOKS_URI}/#{webhook_id}"
34
+ result, response = delete(uri)
35
+ result
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ def generate_signature(payload, timestamp, key)
4
+ message_as_bytes = (payload.bytes + timestamp.bytes).pack('U')
5
+ digest = OpenSSL::HMAC.hexdigest('SHA256', key, message_as_bytes)
6
+ Base64.encode64(digest)
7
+ end
8
+
9
+ # rake spec SPEC_OPTS="-e \"Boxr::WebhookValidator"\"
10
+ describe Boxr::WebhookValidator, :skip_reset do
11
+ describe '#verify_delivery_timestamp' do
12
+ let(:payload) { 'not relevant' }
13
+ subject { described_class.new(headers, payload).verify_delivery_timestamp }
14
+ context 'maximum age is under 10 minutes' do
15
+ let(:five_minutes_ago) { (Time.now.utc - 300).to_s } # 5 minutes (in seconds)
16
+ let(:headers) { { 'BOX-DELIVERY-TIMESTAMP' => five_minutes_ago} }
17
+ it 'returns true' do
18
+ expect(subject).to eq(true)
19
+ end
20
+ end
21
+
22
+ context 'maximum age is over 10 minute' do
23
+ let(:eleven_minutes_ago) { (Time.now.utc - 660).to_s } # 11 minutes (in seconds)
24
+ let(:headers) { { 'BOX-DELIVERY-TIMESTAMP' => eleven_minutes_ago } }
25
+ it 'returns false' do
26
+ expect(subject).to eq(false)
27
+ end
28
+ end
29
+
30
+ context 'no delivery timestamp is supplied' do
31
+ let(:headers) { { 'BOX-DELIVERY-TIMESTAMP' => nil } }
32
+ it 'raises an error' do
33
+ expect do
34
+ subject
35
+ end.to raise_error(RuntimeError, 'Webhook authenticity not verified: invalid timestamp')
36
+ end
37
+ end
38
+
39
+ context 'bogus timestamp is supplied' do
40
+ let(:headers) { { 'BOX-DELIVERY-TIMESTAMP' => 'foo' } }
41
+ it 'raises an error' do
42
+ expect do
43
+ subject
44
+ end.to raise_error(RuntimeError, 'Webhook authenticity not verified: invalid timestamp')
45
+ end
46
+ end
47
+ end
48
+
49
+ describe '#verify_signature' do
50
+ let(:payload) { 'some data' }
51
+ let(:timestamp) { (Time.now.utc - 300).to_s } # 5 minutes ago (in seconds)
52
+ let(:signature_primary) { generate_signature(payload, timestamp, ENV['BOX_PRIMARY_SIGNATURE_KEY'].to_s) }
53
+ let(:signature_secondary) { generate_signature(payload, timestamp, ENV['BOX_SECONDARY_SIGNATURE_KEY'].to_s) }
54
+ subject { described_class.new(headers,
55
+ payload,
56
+ primary_signature_key: ENV['BOX_PRIMARY_SIGNATURE_KEY'].to_s,
57
+ secondary_signature_key: ENV['BOX_SECONDARY_SIGNATURE_KEY'].to_s,
58
+ ).verify_signature }
59
+
60
+ context 'valid primary key' do
61
+ let(:headers) do
62
+ { 'BOX-DELIVERY-TIMESTAMP' => timestamp,
63
+ 'BOX-SIGNATURE-PRIMARY' => signature_primary }
64
+ end
65
+
66
+ it 'returns true' do
67
+ expect(subject).to eq(true)
68
+ end
69
+ end
70
+
71
+ context 'invalid primary key, valid secondary key' do
72
+ let(:headers) do
73
+ { 'BOX-DELIVERY-TIMESTAMP' => timestamp,
74
+ 'BOX-SIGNATURE-PRIMARY' => 'invalid',
75
+ 'BOX-SIGNATURE-SECONDARY' => signature_secondary }
76
+ end
77
+
78
+ it 'returns true' do
79
+ expect(subject).to eq(true)
80
+ end
81
+ end
82
+
83
+ context 'invalid primary key, invalid secondary key' do
84
+ let(:headers) do
85
+ { 'BOX-DELIVERY-TIMESTAMP' => timestamp,
86
+ 'BOX-SIGNATURE-PRIMARY' => 'invalid',
87
+ 'BOX-SIGNATURE-SECONDARY' => 'also invalid' }
88
+ end
89
+
90
+ it 'returns false' do
91
+ expect(subject).to eq(false)
92
+ end
93
+ end
94
+
95
+ context 'no signatures were supplied' do
96
+ let(:headers) do
97
+ { 'BOX-DELIVERY-TIMESTAMP' => timestamp,
98
+ 'BOX-SIGNATURE-PRIMARY' => nil,
99
+ 'BOX-SIGNATURE-SECONDARY' => nil }
100
+ end
101
+
102
+ it 'returns false' do
103
+ subject = described_class.new(headers, payload, primary_signature_key: nil, secondary_signature_key: nil).verify_signature
104
+ expect(subject).to eq(false)
105
+ end
106
+ end
107
+ end
108
+
109
+ describe '#valid_message?' do
110
+ let(:headers) { { 'doesnt' => 'matter' } }
111
+ let(:payload) { 'not relevant' }
112
+
113
+ it 'delegates to timestamp and signature verification' do
114
+ validator = described_class.new(headers, payload)
115
+ expect(validator).to receive(:verify_delivery_timestamp).and_return(true)
116
+ expect(validator).to receive(:verify_signature)
117
+ validator.valid_message?
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,30 @@
1
+ # rake spec SPEC_OPTS="-e \"invokes webhook operations"\"
2
+ describe "webhook operations" do
3
+ it 'invokes webhook operations' do
4
+ puts 'create webhook'
5
+ resource_id = @test_folder.id
6
+ type = 'folder'
7
+ triggers = ['FOLDER.RENAMED']
8
+ address = 'https://example.com'
9
+ new_webhook = BOX_CLIENT.create_webhook(resource_id, type, triggers, address)
10
+ new_webhook_id = new_webhook.id
11
+ expect(new_webhook.triggers.to_a).to eq(triggers)
12
+
13
+ puts 'get webhooks'
14
+ all_webhooks = BOX_CLIENT.get_webhooks
15
+ expect(all_webhooks.entries.first.id).to eq(new_webhook_id)
16
+
17
+ puts 'get webhook'
18
+ single_webhook = BOX_CLIENT.get_webhook(new_webhook_id)
19
+ expect(single_webhook.id).to eq(new_webhook_id)
20
+
21
+ puts 'update webhooks'
22
+ new_address = 'https://foo.com'
23
+ updated_webhook = BOX_CLIENT.update_webhook(new_webhook, { address: new_address })
24
+ expect(updated_webhook.address).to eq(new_address)
25
+
26
+ puts 'delete webhooks'
27
+ deleted_webhook = BOX_CLIENT.delete_webhook(updated_webhook)
28
+ expect(deleted_webhook).to be_empty
29
+ end
30
+ end
data/spec/spec_helper.rb CHANGED
@@ -5,6 +5,11 @@ require 'awesome_print'
5
5
 
6
6
  RSpec.configure do |config|
7
7
  config.before(:each) do
8
+ if test.metadata[:skip_reset]
9
+ puts "Skipping reset"
10
+ next
11
+ end
12
+
8
13
  puts "-----> Resetting Box Environment"
9
14
  sleep BOX_SERVER_SLEEP
10
15
  root_folders = BOX_CLIENT.root_folder_items.folders
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: boxr
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.17.0
4
+ version: 1.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chad Burnette
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-03-08 00:00:00.000000000 Z
12
+ date: 2021-03-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -227,6 +227,8 @@ files:
227
227
  - lib/boxr/version.rb
228
228
  - lib/boxr/watermarking.rb
229
229
  - lib/boxr/web_links.rb
230
+ - lib/boxr/webhook_validator.rb
231
+ - lib/boxr/webhooks.rb
230
232
  - spec/boxr/auth_spec.rb
231
233
  - spec/boxr/chunked_uploads_spec.rb
232
234
  - spec/boxr/collaborations_spec.rb
@@ -240,6 +242,8 @@ files:
240
242
  - spec/boxr/users_spec.rb
241
243
  - spec/boxr/watermarking_spec.rb
242
244
  - spec/boxr/web_links_spec.rb
245
+ - spec/boxr/webhook_validator_spec.rb
246
+ - spec/boxr/webhooks_spec.rb
243
247
  - spec/boxr_spec.rb
244
248
  - spec/spec_helper.rb
245
249
  - spec/test_files/large test file.txt
@@ -281,6 +285,8 @@ test_files:
281
285
  - spec/boxr/users_spec.rb
282
286
  - spec/boxr/watermarking_spec.rb
283
287
  - spec/boxr/web_links_spec.rb
288
+ - spec/boxr/webhook_validator_spec.rb
289
+ - spec/boxr/webhooks_spec.rb
284
290
  - spec/boxr_spec.rb
285
291
  - spec/spec_helper.rb
286
292
  - spec/test_files/large test file.txt