lhs 21.2.2 → 21.3.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 +208 -107
- data/lhs.gemspec +1 -1
- data/lib/lhs.rb +8 -0
- data/lib/lhs/concerns/autoload_records.rb +20 -3
- data/lib/lhs/concerns/o_auth.rb +25 -0
- 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 +129 -0
- data/spec/autoloading_spec.rb +35 -8
- data/spec/dummy/app/controllers/application_controller.rb +15 -0
- data/spec/dummy/app/controllers/automatic_authentication_controller.rb +22 -0
- data/spec/dummy/app/controllers/error_handling_with_chains_controller.rb +2 -2
- data/spec/dummy/app/controllers/extended_rollbar_controller.rb +2 -2
- data/spec/dummy/app/controllers/option_blocks_controller.rb +2 -2
- data/spec/dummy/app/models/dummy_customer.rb +6 -0
- data/spec/dummy/app/models/{record.rb → dummy_record.rb} +1 -1
- 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/{user.rb → dummy_user.rb} +1 -1
- data/spec/dummy/app/models/providers/customer_system.rb +7 -0
- data/spec/dummy/config/routes.rb +4 -0
- data/spec/option_blocks/ensure_reset_between_requests_spec.rb +2 -1
- data/spec/record/error_handling_integration_spec.rb +1 -1
- data/spec/record/includes_spec.rb +41 -0
- data/spec/request_cycle_cache_spec.rb +3 -3
- data/spec/support/reset.rb +31 -9
- metadata +29 -10
@@ -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)
|
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,129 @@
|
|
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
|
+
end
|
129
|
+
end
|
data/spec/autoloading_spec.rb
CHANGED
@@ -4,18 +4,45 @@ require "rails_helper"
|
|
4
4
|
|
5
5
|
describe LHS, type: :request do
|
6
6
|
context 'autoloading' do
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
expect(
|
7
|
+
|
8
|
+
let(:endpoints) { LHS::Record::Endpoints.all }
|
9
|
+
|
10
|
+
it "pre/re-loads all LHS classes initialy, because it's necessary for endpoint-to-record-class-discovery", reset_before: false do
|
11
|
+
|
12
|
+
expect(endpoints['http://datastore/v2/users']).to be_present
|
13
|
+
expect(endpoints['http://datastore/v2/users/{id}']).to be_present
|
14
|
+
|
13
15
|
expect(
|
14
|
-
|
16
|
+
DummyUser.endpoints.detect { |endpoint| endpoint.url == 'http://datastore/v2/users' }
|
15
17
|
).to be_present
|
16
18
|
expect(
|
17
|
-
|
19
|
+
DummyUser.endpoints.detect { |endpoint| endpoint.url == 'http://datastore/v2/users/{id}' }
|
18
20
|
).to be_present
|
19
21
|
end
|
22
|
+
|
23
|
+
it "also pre/re-loads all LHS classes that inherited from an LHS provider, because it's necessary for endpoint-to-record-class-discovery", reset_before: false do
|
24
|
+
|
25
|
+
expect(endpoints['http://customers']).to be_present
|
26
|
+
expect(endpoints['http://customers/{id}']).to be_present
|
27
|
+
|
28
|
+
expect(
|
29
|
+
DummyCustomer.endpoints.detect { |endpoint| endpoint.url == 'http://customers' }
|
30
|
+
).to be_present
|
31
|
+
expect(
|
32
|
+
DummyCustomer.endpoints.detect { |endpoint| endpoint.url == 'http://customers/{id}' }
|
33
|
+
).to be_present
|
34
|
+
|
35
|
+
customer_request = stub_request(:get, "http://customers/1")
|
36
|
+
.with(
|
37
|
+
headers: {
|
38
|
+
'Authorization' => 'token123'
|
39
|
+
}
|
40
|
+
)
|
41
|
+
.to_return(body: { name: 'Steve' }.to_json)
|
42
|
+
|
43
|
+
DummyCustomer.find(1)
|
44
|
+
|
45
|
+
expect(customer_request).to have_been_requested
|
46
|
+
end
|
20
47
|
end
|
21
48
|
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
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class AutomaticAuthenticationController < ApplicationController
|
4
|
+
|
5
|
+
def o_auth
|
6
|
+
render json: {
|
7
|
+
record: DummyRecordWithOauth.find(1).as_json,
|
8
|
+
records: DummyRecordWithOauth.where(color: 'blue').as_json
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
def o_auth_with_multiple_providers
|
13
|
+
render json: {
|
14
|
+
record: DummyRecordWithMultipleOauthProviders1.find(1).as_json,
|
15
|
+
records: DummyRecordWithMultipleOauthProviders2.where(color: 'blue').as_json,
|
16
|
+
per_endpoint: {
|
17
|
+
record: DummyRecordWithMultipleOauthProvidersPerEndpoint.find(1).as_json,
|
18
|
+
records: DummyRecordWithMultipleOauthProvidersPerEndpoint.where(color: 'blue').as_json
|
19
|
+
}
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
@@ -5,7 +5,7 @@ class ErrorHandlingWithChainsController < ApplicationController
|
|
5
5
|
# Example where the query chain is resolved
|
6
6
|
# in the view (during render 'show')
|
7
7
|
def fetch_in_view
|
8
|
-
@records =
|
8
|
+
@records = DummyRecord
|
9
9
|
.handle(LHC::Error, ->(error) { handle_error(error) })
|
10
10
|
.where(color: 'blue')
|
11
11
|
render 'show'
|
@@ -15,7 +15,7 @@ class ErrorHandlingWithChainsController < ApplicationController
|
|
15
15
|
# Example where the query chain is resolved
|
16
16
|
# before the view is rendered
|
17
17
|
def fetch_in_controller
|
18
|
-
@records =
|
18
|
+
@records = DummyRecord
|
19
19
|
.handle(LHC::Error, ->(error) { handle_error(error) })
|
20
20
|
.where(color: 'blue').fetch
|
21
21
|
render 'show'
|
@@ -3,8 +3,8 @@
|
|
3
3
|
class ExtendedRollbarController < ApplicationController
|
4
4
|
|
5
5
|
def extended_rollbar
|
6
|
-
|
7
|
-
|
6
|
+
DummyRecord.where(color: 'blue').fetch
|
7
|
+
DummyRecord.where(color: 'red').fetch
|
8
8
|
raise "Let's see if rollbar logs information about what kind of requests where made around here!"
|
9
9
|
end
|
10
10
|
end
|
@@ -4,12 +4,12 @@ class OptionBlocksController < ApplicationController
|
|
4
4
|
|
5
5
|
def first
|
6
6
|
LHS::OptionBlocks::CurrentOptionBlock.options = { params: { request: 'first' } }
|
7
|
-
|
7
|
+
DummyRecord.where(request: 'second').fetch
|
8
8
|
render text: 'ok'
|
9
9
|
end
|
10
10
|
|
11
11
|
def second
|
12
|
-
|
12
|
+
DummyRecord.where(request: 'second').fetch
|
13
13
|
render text: 'ok'
|
14
14
|
end
|
15
15
|
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DummyRecordWithMultipleOauthProviders1 < LHS::Record
|
4
|
+
oauth(:provider1)
|
5
|
+
endpoint 'http://datastore/v2/records_with_multiple_oauth_providers_1'
|
6
|
+
endpoint 'http://datastore/v2/records_with_multiple_oauth_providers_1/{id}'
|
7
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DummyRecordWithMultipleOauthProviders2 < LHS::Record
|
4
|
+
oauth(:provider2)
|
5
|
+
endpoint 'http://datastore/v2/records_with_multiple_oauth_providers_2'
|
6
|
+
endpoint 'http://datastore/v2/records_with_multiple_oauth_providers_2/{id}'
|
7
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DummyRecordWithMultipleOauthProvidersPerEndpoint < LHS::Record
|
4
|
+
endpoint 'http://datastore/v2/records_with_multiple_oauth_providers_per_endpoint', oauth: :provider1
|
5
|
+
endpoint 'http://datastore/v2/records_with_multiple_oauth_providers_per_endpoint/{id}', oauth: :provider2
|
6
|
+
end
|