lhs 21.2.3 → 22.0.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/README.md +249 -116
- data/lhs.gemspec +1 -1
- data/lib/lhs.rb +8 -0
- data/lib/lhs/concerns/o_auth.rb +25 -0
- data/lib/lhs/concerns/record/chainable.rb +4 -4
- data/lib/lhs/concerns/record/configuration.rb +28 -11
- data/lib/lhs/concerns/record/request.rb +29 -8
- data/lib/lhs/config.rb +1 -1
- data/lib/lhs/interceptors/auto_oauth/interceptor.rb +33 -0
- data/lib/lhs/interceptors/auto_oauth/thread_registry.rb +18 -0
- data/lib/lhs/version.rb +1 -1
- data/spec/auto_oauth_spec.rb +169 -0
- data/spec/dummy/app/controllers/application_controller.rb +15 -0
- data/spec/dummy/app/controllers/automatic_authentication_controller.rb +29 -0
- data/spec/dummy/app/models/dummy_record_with_auto_oauth_provider.rb +6 -0
- data/spec/dummy/app/models/dummy_record_with_multiple_oauth_providers1.rb +7 -0
- data/spec/dummy/app/models/dummy_record_with_multiple_oauth_providers2.rb +7 -0
- data/spec/dummy/app/models/dummy_record_with_multiple_providers_per_endpoint.rb +6 -0
- data/spec/dummy/app/models/dummy_record_with_oauth.rb +7 -0
- data/spec/dummy/app/models/providers/internal_services.rb +7 -0
- data/spec/dummy/config/routes.rb +5 -0
- data/spec/item/destroy_spec.rb +1 -1
- data/spec/proxy/record_identification_spec.rb +1 -1
- data/spec/record/all_spec.rb +1 -1
- data/spec/record/endpoints_spec.rb +1 -1
- data/spec/record/error_handling_integration_spec.rb +1 -1
- data/spec/record/handle_includes_errors_spec.rb +1 -1
- data/spec/record/has_many_spec.rb +1 -1
- data/spec/record/has_one_spec.rb +1 -1
- data/spec/record/includes_first_page_spec.rb +727 -0
- data/spec/record/includes_spec.rb +546 -561
- data/spec/record/includes_warning_spec.rb +1 -1
- data/spec/record/mapping_spec.rb +2 -2
- data/spec/record/references_spec.rb +1 -1
- data/spec/record/relation_caching_spec.rb +3 -3
- data/spec/request_cycle_cache_spec.rb +3 -3
- metadata +27 -8
- 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
|
67
|
+
def includes_first_page(*args)
|
68
68
|
Chain.new(self, Include.new(Chain.unfold(args)))
|
69
69
|
end
|
70
70
|
|
71
|
-
def
|
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
|
262
|
+
def includes_first_page(*args)
|
263
263
|
push(Include.new(Chain.unfold(args)))
|
264
264
|
end
|
265
265
|
|
266
|
-
def
|
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
|
-
|
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
|
-
|
38
|
+
configuration.dig(:item_key) || :item
|
22
39
|
)
|
23
40
|
end
|
24
41
|
|
25
42
|
def items_key
|
26
43
|
symbolize_unless_complex(
|
27
|
-
|
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
|
-
|
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(
|
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
|
-
|
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(
|
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
|
-
|
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
|
69
|
-
|
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
|
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
|
-
|
520
|
+
inject_interceptors!(options)
|
519
521
|
options
|
520
522
|
end
|
521
523
|
|
522
|
-
|
523
|
-
|
524
|
-
|
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?(
|
547
|
+
if interceptors.include?(dependecy)
|
527
548
|
# Ensure interceptor is prepend
|
528
|
-
interceptors = interceptors.unshift(
|
549
|
+
interceptors = interceptors.unshift(interceptor)
|
529
550
|
options[:interceptors] = interceptors
|
530
551
|
else
|
531
|
-
warn(
|
552
|
+
warn(warning)
|
532
553
|
end
|
533
554
|
end
|
534
555
|
|
data/lib/lhs/config.rb
CHANGED
@@ -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
|
data/lib/lhs/version.rb
CHANGED
@@ -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
|