contentful-scheduler 0.3.0 → 0.4.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
  SHA1:
3
- metadata.gz: bd007e6c6d4a25ead5ebc991d556bcf0c54999ff
4
- data.tar.gz: d1c46c66389f3e0e3156a74452c481677471df8e
3
+ metadata.gz: f3171ffa6b6fee5eb141d60da86e954868dbfd5e
4
+ data.tar.gz: 2afa33f5a560b6f330b337b10a9cd4edb2082fd1
5
5
  SHA512:
6
- metadata.gz: a93a782354a79b12c9c939ff29574a6fd647544865fad71447c5bac64af3f08e30ef666bb3a34581a835a4e95fb96918b1530e96558d11169e7cc6765b854ee2
7
- data.tar.gz: f7a8ed7913141c24f9fc040d17232d451842a6ce4d4f4f80781b7158ea3990ce651466bcdef68eb299d85c9d2a32846ff593e12a6755ce276f9b62ee61f24f6a
6
+ metadata.gz: 91dd5e9622d174eefaccb065e09a27d40d683b978567f795ebba156679bb624aa7eb0500ae93245186eea6756519c6178861c14b8d0d1b52fa13879db62ad818
7
+ data.tar.gz: ba0f65329a151445be938e9301e5710c895e0a61569afc20a83c5d2ece65864230c49bdd21de8a195a093068895db3c4af587ed59c47c3853597db625db83791
data/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.4.0
6
+ ### Fixed
7
+ * Fixed User Agent Header to comply with specification.
8
+
9
+ ### Added
10
+ * Added authentication mechanisms. [#9](https://github.com/contentful/contentful-scheduler.rb/issues/9)
11
+
5
12
  ## 0.3.0
6
13
  ### Added
7
14
  * Added possibility to republish already published content. [#5](https://github.com/contentful/contentful-scheduler.rb/issues/5)
@@ -9,7 +16,7 @@
9
16
  ## 0.2.1
10
17
 
11
18
  ### Fixed
12
- * Fix time parsing.
19
+ * Fixed time parsing.
13
20
 
14
21
  ## 0.2.0
15
22
 
data/README.md CHANGED
@@ -13,8 +13,7 @@ entries for scheduled publishing.
13
13
  `contentful-scheduler` provides a web endpoint to receive webhook calls from Contentful.
14
14
 
15
15
  Every time the endpoint recieves a call it looks for the value of the field defined in the configuration.
16
- If the value is a time in the future -- and if the entry has not already been published -- it will schedule
17
- the entry for publishing at the specified time.
16
+ If the value is a time in the future it will schedule the entry for publishing at the specified time.
18
17
 
19
18
  A background worker based on the popular `resque` gem will then proceed to actually make the publish call
20
19
  against the Content Management API at the due time. For this the Entries you wish to publish require a
@@ -92,7 +91,10 @@ config = {
92
91
  spaces: {
93
92
  'YOUR_SPACE_ID' => {
94
93
  publish_field: 'publishDate', # It specifies the field ID for your Publish Date in your Content Type
95
- management_token: 'YOUR_TOKEN'
94
+ management_token: 'YOUR_TOKEN',
95
+ auth: { # This is optional
96
+ # ... content in this section will be explained in a separate section ...
97
+ }
96
98
  }
97
99
  },
98
100
  }
@@ -153,6 +155,96 @@ Under the space settings menu choose webhook and add a new webhook pointing to `
153
155
 
154
156
  Keep in mind that if you modify the defaults, the URL should be changed to the values specified in the configuration.
155
157
 
158
+ ## Authentication
159
+
160
+ You may want to provide an additional layer of security to your scheduler server, therefore an additional option to add space based authentication is provided.
161
+
162
+ There are two available authentication methods. Static string matching and lambda validations, which will be explained in the next section.
163
+
164
+ Any of both mechanisms require you to add additional headers to your webhook set up, which can be done through the [Contentful Web App](https://app.contentful.com),
165
+ or through the [CMA](https://www.contentful.com/developers/docs/references/content-management-api/#/reference/webhooks/webhook/create-update-a-webhook/console/ruby).
166
+
167
+ ### Authentication via static token matching
168
+
169
+ The simplest authentication mechanism, is to provide a static set of valid strings that are considered valid when found in a determined header.
170
+
171
+ For example:
172
+
173
+ ```ruby
174
+ config = {
175
+ # ... the rest of the config ...
176
+ spaces: {
177
+ 'my_space' => {
178
+ # ... the rest of the space specific configuration ...
179
+ auth: {
180
+ key: 'X-Webhook-Server-Auth-Header',
181
+ valid_tokens: ['some_valid_static_token']
182
+ }
183
+ }
184
+ }
185
+ }
186
+ ```
187
+
188
+ The above example, whenever your webhook sends the `X-Webhook-Server-Auth-Header` with a value of `some_valid_static_token`,
189
+ it will accept the request and queue your webhook for processing.
190
+
191
+ You can provide multiple or a single token. If a single token is provided, it's not necessary to include it in an array.
192
+
193
+ ### Authentication via lambda
194
+
195
+ A more complicated solution, but far more secure, is the ability to execute a lambda as the validator function.
196
+ This allows you define a function for authentication. This function can call an external authentication service,
197
+ make checks against a database or do internal processing.
198
+
199
+ The function must return a truthy/falsey value in order for the authentication to be successful/unsuccessful.
200
+
201
+ For example, we validate that the token provided is either `foo` or `bar`:
202
+
203
+ ```ruby
204
+ config = {
205
+ # ... the rest of the config ...
206
+ spaces: {
207
+ 'my_space' => {
208
+ # ... the rest of the space specific configuration ...
209
+ auth: {
210
+ key: 'X-Webhook-Server-Auth-Header',
211
+ validation: -> (value) { /^(foo|bar)$/ =~ value }
212
+ }
213
+ }
214
+ }
215
+ }
216
+ ```
217
+
218
+ Or a more complicated example, checking if the header is a valid OAuth token, and then making a request to our OAuth database.
219
+ For this example we'll consider you have a table called `tokens` and are using [DataMapper](https://datamapper.org) as a ORM,
220
+ and have a `valid?` method checking if the token is not expired.
221
+
222
+ ```ruby
223
+ config = {
224
+ # ... the rest of the config ...
225
+ spaces: {
226
+ 'my_space' => {
227
+ # ... the rest of the space specific configuration ...
228
+ auth: {
229
+ key: 'X-Webhook-Server-Auth-Header',
230
+ validation: proc do |value|
231
+ return false unless /^Bearer \w+/ =~ value
232
+
233
+ token = Token.first(token: value.gsub('Bearer ', ''))
234
+
235
+ return false if token.nil?
236
+
237
+ token.valid?
238
+ end
239
+ }
240
+ }
241
+ }
242
+ }
243
+ ```
244
+
245
+ If you have multiple spaces and all share the same auth strategy, you can extract the authentication method to a variable,
246
+ and assign it to all the applicable spaces in order to reduce the code duplication.
247
+
156
248
  ## Running in Heroku
157
249
 
158
250
  Heroku offers various Redis plugins, select the one of your liking, add the credentials into your configuration, and proceed to
@@ -0,0 +1,64 @@
1
+ module Contentful
2
+ module Scheduler
3
+ class Auth
4
+ attr_reader :webhook
5
+
6
+ def initialize(webhook)
7
+ @webhook = webhook
8
+ end
9
+
10
+ def auth
11
+ return true if auth_config.nil?
12
+
13
+ return verify_key_value_config if key_value_config?
14
+ return verify_lambda_config if lambda_config?
15
+
16
+ false
17
+ end
18
+
19
+ private
20
+
21
+ def key_value_config?
22
+ auth_config.key?(:key) && auth_config.key?(:valid_tokens)
23
+ end
24
+
25
+ def verify_key_value_config
26
+ value = webhook.raw_headers[auth_config[:key]]
27
+
28
+ return false if value.nil?
29
+
30
+ valid_tokens = auth_config[:valid_tokens]
31
+
32
+ return valid_tokens.include?(value) if valid_tokens.is_a?(::Array)
33
+ valid_tokens == value
34
+ end
35
+
36
+ def lambda_config?
37
+ auth_config.key?(:key) && auth_config.key?(:validation)
38
+ end
39
+
40
+ def verify_lambda_config
41
+ value = webhook.raw_headers[auth_config[:key]]
42
+
43
+ return false if value.nil?
44
+
45
+ validation = auth_config[:validation]
46
+
47
+ return false unless validation.is_a?(::Proc)
48
+
49
+ validation[value]
50
+ end
51
+
52
+ def auth_config
53
+ ::Contentful::Scheduler.config
54
+ .fetch(:spaces, {})
55
+ .fetch(space_id, {})
56
+ .fetch(:auth, nil)
57
+ end
58
+
59
+ def space_id
60
+ webhook.space_id
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,4 +1,5 @@
1
1
  require 'contentful/webhook/listener'
2
+ require_relative 'auth'
2
3
  require_relative 'queue'
3
4
 
4
5
  module Contentful
@@ -7,6 +8,11 @@ module Contentful
7
8
  def create
8
9
  return unless webhook.entry?
9
10
 
11
+ if !Auth.new(webhook).auth
12
+ logger.warn "Skipping - Authentication failed for Space: #{webhook.space_id} - Entry: #{webhook.id}"
13
+ return
14
+ end
15
+
10
16
  logger.info "Queueing - Space: #{webhook.space_id} - Entry: #{webhook.id}"
11
17
 
12
18
  Queue.instance(logger).update_or_create(webhook)
@@ -18,6 +24,11 @@ module Contentful
18
24
  def delete
19
25
  return unless webhook.entry?
20
26
 
27
+ if !Auth.new(webhook).auth
28
+ logger.warn "Skipping - Authentication failed for Space: #{webhook.space_id} - Entry: #{webhook.id}"
29
+ return
30
+ end
31
+
21
32
  logger.info "Unqueueing - Space: #{webhook.space_id} - Entry: #{webhook.id}"
22
33
 
23
34
  Queue.instance(logger).remove(webhook)
@@ -10,7 +10,7 @@ module Contentful
10
10
  client = ::Contentful::Management::Client.new(
11
11
  token,
12
12
  raise_errors: true,
13
- application_name: 'contentful-scheduler',
13
+ application_name: 'contentful.scheduler',
14
14
  application_version: Contentful::Scheduler::VERSION
15
15
  )
16
16
  client.entries.find(space_id, entry_id).publish
@@ -1,5 +1,5 @@
1
1
  module Contentful
2
2
  module Scheduler
3
- VERSION = "0.3.0"
3
+ VERSION = "0.4.0"
4
4
  end
5
5
  end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+
3
+ describe Contentful::Scheduler::Auth do
4
+ before :each do
5
+ Contentful::Scheduler.config = base_config
6
+ end
7
+
8
+ describe 'auth' do
9
+ context 'when no auth is provided' do
10
+ it 'always returns true' do
11
+ webhook = WebhookDouble.new('id', 'no_auth')
12
+ expect(described_class.new(webhook).auth).to be_truthy
13
+ end
14
+ end
15
+
16
+ context 'when providing token array auth' do
17
+ it 'false when key not found' do
18
+ webhook = WebhookDouble.new('id', 'valid_token_array')
19
+ expect(described_class.new(webhook).auth).to be_falsey
20
+ end
21
+
22
+ it 'false when key found but value not matched' do
23
+ webhook = WebhookDouble.new('id', 'valid_token_array', {}, {}, {'auth' => 'not_valid'})
24
+ expect(described_class.new(webhook).auth).to be_falsey
25
+ end
26
+
27
+ it 'true when key found and value matched' do
28
+ webhook = WebhookDouble.new('id', 'valid_token_array', {}, {}, {'auth' => 'test_1'})
29
+ expect(described_class.new(webhook).auth).to be_truthy
30
+ end
31
+ end
32
+
33
+ context 'when providing token string auth' do
34
+ it 'false when key not found' do
35
+ webhook = WebhookDouble.new('id', 'valid_token_string')
36
+ expect(described_class.new(webhook).auth).to be_falsey
37
+ end
38
+
39
+ it 'false when key found but value not matched' do
40
+ webhook = WebhookDouble.new('id', 'valid_token_string', {}, {}, {'auth' => 'not_valid'})
41
+ expect(described_class.new(webhook).auth).to be_falsey
42
+ end
43
+
44
+ it 'true when key found and value matched' do
45
+ webhook = WebhookDouble.new('id', 'valid_token_string', {}, {}, {'auth' => 'test_2'})
46
+ expect(described_class.new(webhook).auth).to be_truthy
47
+ end
48
+ end
49
+
50
+ context 'when providing lambda auth' do
51
+ it 'false when key not found' do
52
+ webhook = WebhookDouble.new('id', 'lambda_auth')
53
+ expect(described_class.new(webhook).auth).to be_falsey
54
+ end
55
+
56
+ it 'false when key found but value not matched' do
57
+ webhook = WebhookDouble.new('id', 'lambda_auth', {}, {}, {'auth' => 'not_valid'})
58
+ expect(described_class.new(webhook).auth).to be_falsey
59
+ end
60
+
61
+ it 'true when key found and value matched' do
62
+ webhook = WebhookDouble.new('id', 'lambda_auth', {}, {}, {'auth' => 'test'})
63
+ expect(described_class.new(webhook).auth).to be_truthy
64
+ end
65
+ end
66
+ end
67
+ end
@@ -9,6 +9,10 @@ describe Contentful::Scheduler::Controller do
9
9
  let(:queue) { ::Contentful::Scheduler::Queue.instance }
10
10
  subject { described_class.new server, logger, timeout }
11
11
 
12
+ before :each do
13
+ Contentful::Scheduler.config = base_config
14
+ end
15
+
12
16
  describe 'events' do
13
17
  [:create, :save, :auto_save, :unarchive].each do |event|
14
18
  it "creates or updates webhook metadata in publish queue on #{event}" do
@@ -42,5 +46,19 @@ describe Contentful::Scheduler::Controller do
42
46
  end
43
47
  end
44
48
  end
49
+
50
+ describe 'auth' do
51
+ context 'on auth failure' do
52
+ let(:body) { {sys: { id: 'invalid_auth', space: { sys: { id: 'valid_token_string' } } }, fields: {} } }
53
+
54
+ it 'will stop the queueing process' do
55
+ expect(queue).not_to receive(:update_or_create)
56
+
57
+ headers['X-Contentful-Topic'] = "ContentfulManagement.Entry.save"
58
+ request = RequestDummy.new(headers, body)
59
+ subject.respond(request, MockResponse.new).join
60
+ end
61
+ end
62
+ end
45
63
  end
46
64
  end
@@ -1,67 +1,27 @@
1
1
  require 'spec_helper'
2
2
 
3
- class WebhookDouble
4
- attr_reader :id, :space_id, :sys, :fields
5
- def initialize(id, space_id, sys = {}, fields = {})
6
- @id = id
7
- @space_id = space_id
8
- @sys = sys
9
- @fields = fields
10
- end
11
- end
12
-
13
3
  describe Contentful::Scheduler::Queue do
14
- let(:config) {
15
- {
16
- logger: ::Contentful::Scheduler::DEFAULT_LOGGER,
17
- endpoint: ::Contentful::Scheduler::DEFAULT_ENDPOINT,
18
- port: ::Contentful::Scheduler::DEFAULT_PORT,
19
- redis: {
20
- host: 'localhost',
21
- port: 12341,
22
- password: 'foobar'
23
- },
24
- spaces: {
25
- 'foo' => {
26
- publish_field: 'my_field',
27
- management_token: 'foo'
28
- }
29
- }
30
- }
31
- }
32
-
4
+ let(:config) { base_config }
33
5
  subject { described_class.instance }
34
6
 
35
7
  before :each do
36
8
  allow(Resque).to receive(:redis=)
37
9
  described_class.class_variable_set(:@@instance, nil)
38
- ::Contentful::Scheduler.config = config
10
+
11
+ ::Contentful::Scheduler.class_variable_set(:@@config, base_config)
39
12
  end
40
13
 
41
14
  describe 'singleton' do
42
15
  it 'creates an instance if not initialized' do
43
- queue = described_class.instance
44
- expect(queue).to be_a described_class
16
+ expect(subject).to be_a described_class
45
17
  end
46
18
 
47
19
  it 'reuses same instance' do
48
- queue = described_class.instance
49
-
50
- expect(queue).to eq described_class.instance
51
- end
52
- end
53
-
54
- describe 'attributes' do
55
- it '.config' do
56
- expect(subject.config).to eq config
20
+ expect(subject).to eq described_class.instance
57
21
  end
58
22
  end
59
23
 
60
24
  describe 'instance methods' do
61
- it '#spaces' do
62
- expect(subject.spaces).to eq config[:spaces]
63
- end
64
-
65
25
  it '#webhook_publish_field?' do
66
26
  expect(subject.webhook_publish_field?(
67
27
  WebhookDouble.new('bar', 'foo', {}, {'my_field' => 'something'})
@@ -21,22 +21,22 @@ describe Contentful::Scheduler::Tasks::Publish do
21
21
  let(:mock_entry) { MockEntry.new }
22
22
 
23
23
  before :each do
24
- ::Contentful::Scheduler.class_variable_set(:@@config, {management_token: 'foobar'})
24
+ ::Contentful::Scheduler.config = base_config
25
25
  end
26
26
 
27
27
  describe 'class methods' do
28
28
  it '::perform' do
29
29
  expect(::Contentful::Management::Client).to receive(:new).with(
30
- 'foobar',
30
+ 'foo',
31
31
  raise_errors: true,
32
- application_name: 'contentful-scheduler',
32
+ application_name: 'contentful.scheduler',
33
33
  application_version: Contentful::Scheduler::VERSION
34
34
  ) { mock_client }
35
35
  expect(mock_client).to receive(:entries) { mock_entries }
36
36
  expect(mock_entries).to receive(:find).with('foo', 'bar') { mock_entry }
37
37
  expect(mock_entry).to receive(:publish)
38
38
 
39
- described_class.perform('foo', 'bar', ::Contentful::Scheduler.config[:management_token])
39
+ described_class.perform('foo', 'bar', ::Contentful::Scheduler.config[:spaces]['foo'][:management_token])
40
40
  end
41
41
  end
42
42
  end
data/spec/spec_helper.rb CHANGED
@@ -40,6 +40,17 @@ class RequestDummy
40
40
  end
41
41
  end
42
42
 
43
+ class WebhookDouble
44
+ attr_reader :id, :space_id, :sys, :fields, :raw_headers
45
+ def initialize(id, space_id, sys = {}, fields = {}, headers = {})
46
+ @id = id
47
+ @space_id = space_id
48
+ @sys = sys
49
+ @fields = fields
50
+ @raw_headers = headers
51
+ end
52
+ end
53
+
43
54
  class Contentful::Webhook::Listener::Controllers::Wait
44
55
  @@sleeping = false
45
56
 
@@ -54,6 +65,53 @@ class Contentful::Webhook::Listener::Controllers::Wait
54
65
  end
55
66
  end
56
67
 
68
+ def base_config
69
+ {
70
+ logger: ::Contentful::Scheduler::DEFAULT_LOGGER,
71
+ endpoint: ::Contentful::Scheduler::DEFAULT_ENDPOINT,
72
+ port: ::Contentful::Scheduler::DEFAULT_PORT,
73
+ redis: {
74
+ host: 'localhost',
75
+ port: 12341,
76
+ password: 'foobar'
77
+ },
78
+ spaces: {
79
+ 'foo' => {
80
+ publish_field: 'my_field',
81
+ management_token: 'foo'
82
+ },
83
+ 'no_auth' => {
84
+ publish_field: 'my_field',
85
+ management_token: 'foo'
86
+ },
87
+ 'valid_token_array' => {
88
+ publish_field: 'my_field',
89
+ management_token: 'foo',
90
+ auth: {
91
+ key: 'auth',
92
+ valid_tokens: ['test_1']
93
+ }
94
+ },
95
+ 'valid_token_string' => {
96
+ publish_field: 'my_field',
97
+ management_token: 'foo',
98
+ auth: {
99
+ key: 'auth',
100
+ valid_tokens: 'test_2'
101
+ }
102
+ },
103
+ 'lambda_auth' => {
104
+ publish_field: 'my_field',
105
+ management_token: 'foo',
106
+ auth: {
107
+ key: 'auth',
108
+ validation: -> (value) { value.size == 4 }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ end
114
+
57
115
  RSpec.configure do |config|
58
116
  config.filter_run :focus => true
59
117
  config.run_all_when_everything_filtered = true
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: contentful-scheduler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Contentful GmbH (David Litvak Bruno0
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-02-06 00:00:00.000000000 Z
11
+ date: 2018-02-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: contentful-webhook-listener
@@ -201,11 +201,13 @@ files:
201
201
  - example/Rakefile
202
202
  - example/config.ru
203
203
  - lib/contentful/scheduler.rb
204
+ - lib/contentful/scheduler/auth.rb
204
205
  - lib/contentful/scheduler/controller.rb
205
206
  - lib/contentful/scheduler/queue.rb
206
207
  - lib/contentful/scheduler/tasks.rb
207
208
  - lib/contentful/scheduler/tasks/publish.rb
208
209
  - lib/contentful/scheduler/version.rb
210
+ - spec/contentful/scheduler/auth_spec.rb
209
211
  - spec/contentful/scheduler/controller_spec.rb
210
212
  - spec/contentful/scheduler/queue_spec.rb
211
213
  - spec/contentful/scheduler/tasks/publish_spec.rb
@@ -236,6 +238,7 @@ signing_key:
236
238
  specification_version: 4
237
239
  summary: Customizable Scheduler for Contentful Entries.
238
240
  test_files:
241
+ - spec/contentful/scheduler/auth_spec.rb
239
242
  - spec/contentful/scheduler/controller_spec.rb
240
243
  - spec/contentful/scheduler/queue_spec.rb
241
244
  - spec/contentful/scheduler/tasks/publish_spec.rb