lhs 21.2.3 → 22.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +249 -116
  3. data/lhs.gemspec +1 -1
  4. data/lib/lhs.rb +8 -0
  5. data/lib/lhs/concerns/o_auth.rb +25 -0
  6. data/lib/lhs/concerns/record/chainable.rb +4 -4
  7. data/lib/lhs/concerns/record/configuration.rb +28 -11
  8. data/lib/lhs/concerns/record/request.rb +29 -8
  9. data/lib/lhs/config.rb +1 -1
  10. data/lib/lhs/interceptors/auto_oauth/interceptor.rb +33 -0
  11. data/lib/lhs/interceptors/auto_oauth/thread_registry.rb +18 -0
  12. data/lib/lhs/version.rb +1 -1
  13. data/spec/auto_oauth_spec.rb +169 -0
  14. data/spec/dummy/app/controllers/application_controller.rb +15 -0
  15. data/spec/dummy/app/controllers/automatic_authentication_controller.rb +29 -0
  16. data/spec/dummy/app/models/dummy_record_with_auto_oauth_provider.rb +6 -0
  17. data/spec/dummy/app/models/dummy_record_with_multiple_oauth_providers1.rb +7 -0
  18. data/spec/dummy/app/models/dummy_record_with_multiple_oauth_providers2.rb +7 -0
  19. data/spec/dummy/app/models/dummy_record_with_multiple_providers_per_endpoint.rb +6 -0
  20. data/spec/dummy/app/models/dummy_record_with_oauth.rb +7 -0
  21. data/spec/dummy/app/models/providers/internal_services.rb +7 -0
  22. data/spec/dummy/config/routes.rb +5 -0
  23. data/spec/item/destroy_spec.rb +1 -1
  24. data/spec/proxy/record_identification_spec.rb +1 -1
  25. data/spec/record/all_spec.rb +1 -1
  26. data/spec/record/endpoints_spec.rb +1 -1
  27. data/spec/record/error_handling_integration_spec.rb +1 -1
  28. data/spec/record/handle_includes_errors_spec.rb +1 -1
  29. data/spec/record/has_many_spec.rb +1 -1
  30. data/spec/record/has_one_spec.rb +1 -1
  31. data/spec/record/includes_first_page_spec.rb +727 -0
  32. data/spec/record/includes_spec.rb +546 -561
  33. data/spec/record/includes_warning_spec.rb +1 -1
  34. data/spec/record/mapping_spec.rb +2 -2
  35. data/spec/record/references_spec.rb +1 -1
  36. data/spec/record/relation_caching_spec.rb +3 -3
  37. data/spec/request_cycle_cache_spec.rb +3 -3
  38. metadata +27 -8
  39. data/spec/record/includes_all_spec.rb +0 -693
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+
5
+ module LHS
6
+ module OAuth
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ prepend_before_action :lhs_store_oauth_access_token
11
+ end
12
+
13
+ private
14
+
15
+ def lhs_store_oauth_access_token
16
+ lhs_check_auto_oauth_enabled!
17
+ LHS::Interceptors::AutoOauth::ThreadRegistry.access_token = instance_exec(&LHS.config.auto_oauth)
18
+ end
19
+
20
+ def lhs_check_auto_oauth_enabled!
21
+ return if LHS.config.auto_oauth.present? && LHS.config.auto_oauth.is_a?(Proc)
22
+ raise 'You have to enable LHS.config.auto_oauth by passing a proc returning an access token!'
23
+ end
24
+ end
25
+ end
@@ -64,11 +64,11 @@ class LHS::Record
64
64
  chain
65
65
  end
66
66
 
67
- def includes(*args)
67
+ def includes_first_page(*args)
68
68
  Chain.new(self, Include.new(Chain.unfold(args)))
69
69
  end
70
70
 
71
- def includes_all(*args)
71
+ def includes(*args)
72
72
  chain = Chain.new(self, Include.new(Chain.unfold(args)))
73
73
  chain.include_all!(args)
74
74
  chain
@@ -259,11 +259,11 @@ class LHS::Record
259
259
  push(ErrorHandling.new(error_class => handler))
260
260
  end
261
261
 
262
- def includes(*args)
262
+ def includes_first_page(*args)
263
263
  push(Include.new(Chain.unfold(args)))
264
264
  end
265
265
 
266
- def includes_all(*args)
266
+ def includes(*args)
267
267
  chain = push(Include.new(Chain.unfold(args)))
268
268
  chain.include_all!(args)
269
269
  chain
@@ -12,45 +12,62 @@ class LHS::Record
12
12
  mattr_accessor :configuration
13
13
 
14
14
  module ClassMethods
15
- def configuration(args)
16
- @configuration = args.freeze || {}
15
+ def configuration(args = nil)
16
+ if !args.nil?
17
+ @configuration = args
18
+ else
19
+ @configuration || {}
20
+ end
21
+ end
22
+
23
+ def auto_oauth?
24
+ LHS.config.auto_oauth && configuration && auto_oauth
25
+ end
26
+
27
+ def auto_oauth
28
+ configuration.fetch(:auto_oauth, false)
29
+ end
30
+
31
+ def oauth(provider = nil)
32
+ value = provider || true
33
+ configuration.present? ? configuration.merge!(auto_oauth: value) : configuration(auto_oauth: value)
17
34
  end
18
35
 
19
36
  def item_key
20
37
  symbolize_unless_complex(
21
- @configuration.try(:[], :item_key) || :item
38
+ configuration.dig(:item_key) || :item
22
39
  )
23
40
  end
24
41
 
25
42
  def items_key
26
43
  symbolize_unless_complex(
27
- @configuration.try(:[], :items_key) || :items
44
+ configuration.dig(:items_key) || :items
28
45
  )
29
46
  end
30
47
 
31
48
  def item_created_key
32
49
  symbolize_unless_complex(
33
- @configuration.try(:[], :item_created_key)
50
+ configuration.dig(:item_created_key)
34
51
  )
35
52
  end
36
53
 
37
54
  def limit_key(type = nil)
38
55
  symbolize_unless_complex(
39
- pagination_parameter(@configuration.try(:[], :limit_key), type) ||
56
+ pagination_parameter(configuration.dig(:limit_key), type) ||
40
57
  :limit
41
58
  )
42
59
  end
43
60
 
44
61
  def total_key
45
62
  symbolize_unless_complex(
46
- @configuration.try(:[], :total_key) || :total
63
+ configuration.dig(:total_key) || :total
47
64
  )
48
65
  end
49
66
 
50
67
  # Key used for determine current page
51
68
  def pagination_key(type = nil)
52
69
  symbolize_unless_complex(
53
- pagination_parameter(@configuration.try(:[], :pagination_key), type) ||
70
+ pagination_parameter(configuration.dig(:pagination_key), type) ||
54
71
  :offset
55
72
  )
56
73
  end
@@ -58,15 +75,15 @@ class LHS::Record
58
75
  # Strategy used for calculationg next pages and navigate pages
59
76
  def pagination_strategy
60
77
  symbolize_unless_complex(
61
- @configuration.try(:[], :pagination_strategy) || :offset
78
+ configuration.dig(:pagination_strategy) || :offset
62
79
  )
63
80
  end
64
81
 
65
82
  # Allows record to be configured as not paginated,
66
83
  # as by default it's considered paginated
67
84
  def paginated
68
- return true if @configuration.blank?
69
- @configuration.fetch(:paginated, true)
85
+ return true if configuration.blank?
86
+ configuration.fetch(:paginated, true)
70
87
  end
71
88
 
72
89
  private
@@ -246,7 +246,9 @@ class LHS::Record
246
246
  def skip_loading_includes?(data, included)
247
247
  if data.collection?
248
248
  data.to_a.none? { |item| item[included].present? }
249
- elsif data[included].present? && data[included].item? && data[included].href.blank?
249
+ elsif data.dig(included).blank?
250
+ true
251
+ elsif data[included].item? && data[included][:href].blank?
250
252
  true
251
253
  else
252
254
  !data._raw.key?(included)
@@ -515,20 +517,39 @@ class LHS::Record
515
517
  options[:url] = compute_url!(options[:params]) unless options.key?(:url)
516
518
  merge_explicit_params!(options[:params])
517
519
  options.delete(:params) if options[:params]&.empty?
518
- inject_request_cycle_cache!(options)
520
+ inject_interceptors!(options)
519
521
  options
520
522
  end
521
523
 
522
- # Injects options into request, that enable the request cycle cache interceptor
523
- def inject_request_cycle_cache!(options)
524
- return unless LHS.config.request_cycle_cache_enabled
524
+ def inject_interceptors!(options)
525
+ if LHS.config.request_cycle_cache_enabled
526
+ inject_interceptor!(
527
+ options,
528
+ LHS::Interceptors::RequestCycleCache::Interceptor,
529
+ LHC::Caching,
530
+ "[WARNING] Can't enable request cycle cache as LHC::Caching interceptor is not enabled/configured (see https://github.com/local-ch/lhc/blob/master/README.md#caching-interceptor)!"
531
+ )
532
+ end
533
+
534
+ endpoint = find_endpoint(options[:params], options.fetch(:url, nil))
535
+ if auto_oauth? || (endpoint.options&.dig(:oauth) && LHS.config.auto_oauth) || options[:oauth]
536
+ inject_interceptor!(
537
+ options.merge!(record: self),
538
+ LHS::Interceptors::AutoOauth::Interceptor,
539
+ LHC::Auth,
540
+ "[WARNING] Can't enable auto oauth as LHC::Auth interceptor is not enabled/configured (see https://github.com/local-ch/lhc/blob/master/README.md#authentication-interceptor)!"
541
+ )
542
+ end
543
+ end
544
+
545
+ def inject_interceptor!(options, interceptor, dependecy, warning)
525
546
  interceptors = options[:interceptors] || LHC.config.interceptors
526
- if interceptors.include?(LHC::Caching)
547
+ if interceptors.include?(dependecy)
527
548
  # Ensure interceptor is prepend
528
- interceptors = interceptors.unshift(LHS::Interceptors::RequestCycleCache::Interceptor)
549
+ interceptors = interceptors.unshift(interceptor)
529
550
  options[:interceptors] = interceptors
530
551
  else
531
- warn("[WARNING] Can't enable request cycle cache as LHC::Caching interceptor is not enabled/configured (see https://github.com/local-ch/lhc/blob/master/docs/interceptors/caching.md#caching-interceptor)!")
552
+ warn(warning)
532
553
  end
533
554
  end
534
555
 
@@ -5,7 +5,7 @@ require 'singleton'
5
5
  class LHS::Config
6
6
  include Singleton
7
7
 
8
- attr_accessor :request_cycle_cache_enabled, :request_cycle_cache, :trace
8
+ attr_accessor :request_cycle_cache_enabled, :request_cycle_cache, :trace, :auto_oauth
9
9
 
10
10
  def initialize
11
11
  self.request_cycle_cache_enabled ||= true
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+
5
+ module LHS
6
+ module Interceptors
7
+ module AutoOauth
8
+ extend ActiveSupport::Concern
9
+
10
+ class Interceptor < LHC::Interceptor
11
+
12
+ def before_request
13
+ request.options[:auth] = { bearer: token }
14
+ end
15
+
16
+ def tokens
17
+ @tokens ||= LHS::Interceptors::AutoOauth::ThreadRegistry.access_token
18
+ end
19
+
20
+ def token
21
+ if tokens.is_a?(Hash)
22
+ tokens.dig(
23
+ request.options[:oauth] ||
24
+ request.options[:record]&.auto_oauth
25
+ )
26
+ else
27
+ tokens
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/per_thread_registry'
5
+
6
+ module LHS
7
+ module Interceptors
8
+ module AutoOauth
9
+ extend ActiveSupport::Concern
10
+ class ThreadRegistry
11
+ # Using ActiveSupports PerThreadRegistry to be able to support Active Support v4.
12
+ # Will switch to thread_mattr_accessor (which comes with Activesupport) when we dropping support for Active Support v4.
13
+ extend ActiveSupport::PerThreadRegistry
14
+ attr_accessor :access_token
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LHS
4
- VERSION = '21.2.3'
4
+ VERSION = '22.0.0'
5
5
  end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ describe 'Auto OAuth Authentication', type: :request, dummy_models: true do
6
+
7
+ context 'without LHC::Auth interceptor enabled' do
8
+
9
+ before do
10
+ LHS.configure do |config|
11
+ config.auto_oauth = -> { access_token }
12
+ end
13
+ end
14
+
15
+ it 'shows a warning that it can not perform auto authentication' do
16
+ expect(lambda do
17
+ get '/automatic_authentication/oauth'
18
+ end).to output(
19
+ %r{\[WARNING\] Can't enable auto oauth as LHC::Auth interceptor is not enabled\/configured \(see https://github.com/local-ch/lhc/blob/master/README.md#authentication-interceptor\)!}
20
+ ).to_stderr
21
+ end
22
+ end
23
+
24
+ context 'with LHC::Auth interceptor enabled' do
25
+
26
+ context 'with only one auth provider' do
27
+
28
+ let(:token) { ApplicationController::ACCESS_TOKEN }
29
+
30
+ let(:record_request) do
31
+ stub_request(:get, "http://datastore/v2/records_with_oauth/1")
32
+ .with(
33
+ headers: { 'Authorization' => "Bearer #{token}" }
34
+ ).to_return(status: 200, body: { name: 'Record' }.to_json)
35
+ end
36
+
37
+ let(:records_request) do
38
+ stub_request(:get, "http://datastore/v2/records_with_oauth?color=blue")
39
+ .with(
40
+ headers: { 'Authorization' => "Bearer #{token}" }
41
+ ).to_return(status: 200, body: { items: [{ name: 'Record' }] }.to_json)
42
+ end
43
+
44
+ before do
45
+ LHS.configure do |config|
46
+ config.auto_oauth = -> { access_token }
47
+ end
48
+ LHC.configure do |config|
49
+ config.interceptors = [LHC::Auth]
50
+ end
51
+ record_request
52
+ records_request
53
+ end
54
+
55
+ after do
56
+ LHC.config.reset
57
+ end
58
+
59
+ it 'applies OAuth credentials for the individual request automatically' do
60
+ get '/automatic_authentication/oauth'
61
+ expect(record_request).to have_been_requested
62
+ expect(records_request).to have_been_requested
63
+ end
64
+ end
65
+
66
+ context 'with multiple auth providers' do
67
+
68
+ before do
69
+ LHS.configure do |config|
70
+ config.auto_oauth = proc do
71
+ {
72
+ provider1: access_token_provider_1,
73
+ provider2: access_token_provider_2
74
+ }
75
+ end
76
+ end
77
+ LHC.configure do |config|
78
+ config.interceptors = [LHC::Auth]
79
+ end
80
+ record_request_provider_1
81
+ records_request_provider_2
82
+ records_request_per_endpoint_provider_1
83
+ record_request_per_endpoint_provider_2
84
+ end
85
+
86
+ let(:token) { ApplicationController::ACCESS_TOKEN }
87
+
88
+ let(:record_request_provider_1) do
89
+ stub_request(:get, "http://datastore/v2/records_with_multiple_oauth_providers_1/1")
90
+ .with(
91
+ headers: { 'Authorization' => "Bearer #{token}_provider_1" }
92
+ ).to_return(status: 200, body: { name: 'Record' }.to_json)
93
+ end
94
+
95
+ let(:records_request_provider_2) do
96
+ stub_request(:get, "http://datastore/v2/records_with_multiple_oauth_providers_2?color=blue")
97
+ .with(
98
+ headers: { 'Authorization' => "Bearer #{token}_provider_2" }
99
+ ).to_return(status: 200, body: { items: [{ name: 'Record' }] }.to_json)
100
+ end
101
+
102
+ let(:records_request_per_endpoint_provider_1) do
103
+ stub_request(:get, "http://datastore/v2/records_with_multiple_oauth_providers_per_endpoint?color=blue")
104
+ .with(
105
+ headers: { 'Authorization' => "Bearer #{token}_provider_1" }
106
+ ).to_return(status: 200, body: { items: [{ name: 'Record' }] }.to_json)
107
+ end
108
+
109
+ let(:record_request_per_endpoint_provider_2) do
110
+ stub_request(:get, "http://datastore/v2/records_with_multiple_oauth_providers_per_endpoint/1")
111
+ .with(
112
+ headers: { 'Authorization' => "Bearer #{token}_provider_2" }
113
+ ).to_return(status: 200, body: { name: 'Record' }.to_json)
114
+ end
115
+
116
+ after do
117
+ LHC.config.reset
118
+ end
119
+
120
+ it 'applies OAuth credentials for the individual request automatically no matter how many auth providers are configured ' do
121
+ get '/automatic_authentication/oauth_with_multiple_providers'
122
+ expect(record_request_provider_1).to have_been_requested
123
+ expect(records_request_provider_2).to have_been_requested
124
+ expect(records_request_per_endpoint_provider_1).to have_been_requested
125
+ expect(record_request_per_endpoint_provider_2).to have_been_requested
126
+ end
127
+ end
128
+
129
+ context 'with provider enabled for auto oauth' do
130
+
131
+ let(:token) { ApplicationController::ACCESS_TOKEN }
132
+
133
+ let(:record_request) do
134
+ stub_request(:get, "http://internalservice/v2/records/1")
135
+ .with(
136
+ headers: { 'Authorization' => "Bearer #{token}" }
137
+ ).to_return(status: 200, body: { name: 'Record' }.to_json)
138
+ end
139
+
140
+ let(:records_request) do
141
+ stub_request(:get, "http://internalservice/v2/records?color=blue")
142
+ .with(
143
+ headers: { 'Authorization' => "Bearer #{token}" }
144
+ ).to_return(status: 200, body: { items: [{ name: 'Record' }] }.to_json)
145
+ end
146
+
147
+ before do
148
+ LHS.configure do |config|
149
+ config.auto_oauth = -> { access_token }
150
+ end
151
+ LHC.configure do |config|
152
+ config.interceptors = [LHC::Auth]
153
+ end
154
+ record_request
155
+ records_request
156
+ end
157
+
158
+ after do
159
+ LHC.config.reset
160
+ end
161
+
162
+ it 'applies OAuth credentials for the individual request automatically' do
163
+ get '/automatic_authentication/oauth_with_provider'
164
+ expect(record_request).to have_been_requested
165
+ expect(records_request).to have_been_requested
166
+ end
167
+ end
168
+ end
169
+ end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ApplicationController < ActionController::Base
4
+ include LHS::OAuth
5
+ ACCESS_TOKEN = 'token-12345'
6
+
4
7
  # Prevent CSRF attacks by raising an exception.
5
8
  # For APIs, you may want to use :null_session instead.
6
9
  protect_from_forgery with: :exception
@@ -8,4 +11,16 @@ class ApplicationController < ActionController::Base
8
11
  def root
9
12
  render nothing: true
10
13
  end
14
+
15
+ def access_token
16
+ ACCESS_TOKEN
17
+ end
18
+
19
+ def access_token_provider_1
20
+ "#{ACCESS_TOKEN}_provider_1"
21
+ end
22
+
23
+ def access_token_provider_2
24
+ "#{ACCESS_TOKEN}_provider_2"
25
+ end
11
26
  end