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 +4 -4
- data/.env.example +2 -0
- data/README.md +23 -0
- data/lib/boxr.rb +2 -0
- data/lib/boxr/client.rb +1 -1
- data/lib/boxr/version.rb +1 -1
- data/lib/boxr/webhook_validator.rb +59 -0
- data/lib/boxr/webhooks.rb +38 -0
- data/spec/boxr/webhook_validator_spec.rb +120 -0
- data/spec/boxr/webhooks_spec.rb +30 -0
- data/spec/spec_helper.rb +5 -0
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ecda6bc31119954e1441132e89cb3730c72832a9d27a4b50cef02631889ac16d
|
4
|
+
data.tar.gz: 78e5f729e9e50695560cc15b28286e865940e7222cf258794763ee8efa61c8ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/lib/boxr/client.rb
CHANGED
data/lib/boxr/version.rb
CHANGED
@@ -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.
|
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-
|
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
|