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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +208 -107
  3. data/lhs.gemspec +1 -1
  4. data/lib/lhs.rb +8 -0
  5. data/lib/lhs/concerns/autoload_records.rb +20 -3
  6. data/lib/lhs/concerns/o_auth.rb +25 -0
  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 +129 -0
  14. data/spec/autoloading_spec.rb +35 -8
  15. data/spec/dummy/app/controllers/application_controller.rb +15 -0
  16. data/spec/dummy/app/controllers/automatic_authentication_controller.rb +22 -0
  17. data/spec/dummy/app/controllers/error_handling_with_chains_controller.rb +2 -2
  18. data/spec/dummy/app/controllers/extended_rollbar_controller.rb +2 -2
  19. data/spec/dummy/app/controllers/option_blocks_controller.rb +2 -2
  20. data/spec/dummy/app/models/dummy_customer.rb +6 -0
  21. data/spec/dummy/app/models/{record.rb → dummy_record.rb} +1 -1
  22. data/spec/dummy/app/models/dummy_record_with_multiple_oauth_providers1.rb +7 -0
  23. data/spec/dummy/app/models/dummy_record_with_multiple_oauth_providers2.rb +7 -0
  24. data/spec/dummy/app/models/dummy_record_with_multiple_providers_per_endpoint.rb +6 -0
  25. data/spec/dummy/app/models/dummy_record_with_oauth.rb +7 -0
  26. data/spec/dummy/app/models/{user.rb → dummy_user.rb} +1 -1
  27. data/spec/dummy/app/models/providers/customer_system.rb +7 -0
  28. data/spec/dummy/config/routes.rb +4 -0
  29. data/spec/option_blocks/ensure_reset_between_requests_spec.rb +2 -1
  30. data/spec/record/error_handling_integration_spec.rb +1 -1
  31. data/spec/record/includes_spec.rb +41 -0
  32. data/spec/request_cycle_cache_spec.rb +3 -3
  33. data/spec/support/reset.rb +31 -9
  34. 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[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)
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.2'
4
+ VERSION = '21.3.0'
5
5
  end
@@ -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
@@ -4,18 +4,45 @@ require "rails_helper"
4
4
 
5
5
  describe LHS, type: :request do
6
6
  context 'autoloading' do
7
- it "pre/re-loads all LHS classes initialy,|
8
- because it's necessary for endpoint-to-record-class-discovery",
9
- reset_before: false do
10
- all_endpoints = LHS::Record::Endpoints.all
11
- expect(all_endpoints['http://datastore/v2/users']).to be_present
12
- expect(all_endpoints['http://datastore/v2/users/{id}']).to be_present
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
- User.endpoints.detect { |endpoint| endpoint.url == 'http://datastore/v2/users' }
16
+ DummyUser.endpoints.detect { |endpoint| endpoint.url == 'http://datastore/v2/users' }
15
17
  ).to be_present
16
18
  expect(
17
- User.endpoints.detect { |endpoint| endpoint.url == 'http://datastore/v2/users/{id}' }
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 = Record
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 = Record
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
- Record.where(color: 'blue').fetch
7
- Record.where(color: 'red').fetch
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
- Record.where(request: 'second').fetch
7
+ DummyRecord.where(request: 'second').fetch
8
8
  render text: 'ok'
9
9
  end
10
10
 
11
11
  def second
12
- Record.where(request: 'second').fetch
12
+ DummyRecord.where(request: 'second').fetch
13
13
  render text: 'ok'
14
14
  end
15
15
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DummyCustomer < Providers::CustomerSystem
4
+ endpoint 'http://customers'
5
+ endpoint 'http://customers/{id}'
6
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Record < LHS::Record
3
+ class DummyRecord < LHS::Record
4
4
  endpoint 'http://datastore/v2/records'
5
5
  endpoint 'http://datastore/v2/records/{id}'
6
6
  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