contentful-scheduler 0.3.0 → 0.4.0

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