hoodoo 1.0.4 → 1.0.5

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 CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- ZTQ3MTIxMDliMDRhYjA0ZTQyNTkxZTQ5NDFjMzBkNGZmMzA0MGU1MA==
4
+ YzEwZWNjMTA5OTJhODlhN2VkZGQxMDQ0NzNhOGVjZjUxMTM2ZmM1Nw==
5
5
  data.tar.gz: !binary |-
6
- MzM3MTY2NzNjNjg5YWVmOGY0NTZkYTUzMDMxOGRmZDc0ODg3YmZkNA==
6
+ OTA1MzJjZTZlYzFhY2FiNWUxZWM3MDBiN2QwYzEwNDYyNDM2Y2ZjOA==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- MGFhZmZkNTQ4OTc1OGZlMDMyZGNkZWM1Mzk1OTk4NTg2M2U1NTMzZmFmZjY5
10
- YjkyNWQxNGNlNTg1MGI2MTk1MDBjZGI1MTJiMzViMGU2NjgzODVlMDdlMTkx
11
- ZDUxYzRmNTkxZjQ3OTc3MThkMjE0MWFkZmMxNGI3NzZiOTU5NTA=
9
+ ZDQ4YmVkYmQ3YWEwMWY1NGU1ZDZlYTA2NjAxNzM2NTFiYzk5NjI1NTNmZWE0
10
+ NGYwYjhlMmIxZmY0YTViZjk4Zjg4ODgzODRjNGQ5MDhmNjEwZjVmZDJhM2Yx
11
+ ODQzZjliM2NhYTE1NTQ2ZjdjNzk1YTZmNmE3NTM2MGQ2NWFhNjE=
12
12
  data.tar.gz: !binary |-
13
- N2RhYzdlNDVmYjVhMWIwMjMzNWNmZTI5YzE1NTc0MWI5ZjQ4NjU3YTBjMDg0
14
- MDQ1MzdiMzYxMDgyMjFiNDc2ZWMyMjUwY2NhNWE2NmNlMTU0MDE1NTY4M2Vk
15
- ZDVkZjMwMzE4ZmEyMzY4NmM2Yzk0ZDA0ODQ1ZGRmOTUwNzRhZTE=
13
+ ODhiYjgzMTFmYTZhOGFhZGM4NDM2OWU3N2MwNjE3OGVhMTk0MGFkNGFhZTg5
14
+ Y2E5Mzg0YjI3OTBkNzcyNjJhZGI5MjYyN2M5YTYxOWYyYjVjMTgxMGRiM2Ix
15
+ MGQ4YTBiNWY4MzMzNTI3NWJkODI3ZTE0YWJmYjlkYjI3YWQ4MTg=
@@ -35,6 +35,29 @@ module Hoodoo
35
35
  #
36
36
  UUID_HEADER_PROC = -> ( value ) { value }
37
37
 
38
+ # Used by HEADER_TO_PROPERTY; this Proc when called with some non-nil
39
+ # value from an HTTP header containing URL-encoded simple key/value
40
+ # pair data returns a decoded Hash of key/value pairs. Use URL encoding
41
+ # in the HTTP header value as per:
42
+ #
43
+ # http://www.w3.org/TR/html5/forms.html#url-encoded-form-data
44
+ #
45
+ # Invalid input will produce unusual results, e.g. an empty Hash or a
46
+ # Hash where certain keys may have empty string values.
47
+ #
48
+ KVP_PROPERTY_PROC = -> ( value ) {
49
+ Hash[ URI.decode_www_form( value ) ]
50
+ }
51
+
52
+ # Used by HEADER_TO_PROPERTY; this Proc when called with some non-nested
53
+ # Hash evaluates to a URL-encoded form data String as per:
54
+ #
55
+ # http://www.w3.org/TR/html5/forms.html#url-encoded-form-data
56
+ #
57
+ KVP_HEADER_PROC = -> ( value ) {
58
+ URI.encode_www_form( value )
59
+ }
60
+
38
61
  # Used by HEADER_TO_PROPERTY; this Proc when called with some non-nil
39
62
  # value from an HTTP header representing a Date/Time in a supported
40
63
  # format, evaluates to either a parsed DateTime instance or +nil+ if the
@@ -152,6 +175,16 @@ module Hoodoo
152
175
  :secured => true,
153
176
  },
154
177
 
178
+ 'HTTP_X_ASSUME_IDENTITY_OF' => {
179
+ :property => :assume_identity_of,
180
+ :property_proc => KVP_PROPERTY_PROC,
181
+ :header => 'X-Assume-Identity-Of',
182
+ :header_proc => KVP_HEADER_PROC,
183
+
184
+ :secured => true,
185
+ :auto_transfer => true,
186
+ },
187
+
155
188
  'HTTP_X_DATED_AT' => {
156
189
  :property => :dated_at,
157
190
  :property_proc => DATETIME_IN_PAST_ONLY_PROPERTY_PROC,
@@ -758,6 +758,10 @@ module Hoodoo; module Services
758
758
 
759
759
  return add_local_errors.call() if local_response.halt_processing?
760
760
 
761
+ deal_with_x_assume_identity_of( local_interaction )
762
+
763
+ return add_local_errors.call() if local_response.halt_processing?
764
+
761
765
  # Construct the local request details.
762
766
 
763
767
  local_request.uri_path_components = upc
@@ -1186,7 +1190,7 @@ module Hoodoo; module Services
1186
1190
  end
1187
1191
 
1188
1192
  if secure == false || level == :error
1189
- body = ''
1193
+ body = String.new
1190
1194
  rack_data[ 2 ].each { | thing | body << thing.to_s }
1191
1195
 
1192
1196
  if interaction.context.response.halt_processing?
@@ -1547,10 +1551,13 @@ module Hoodoo; module Services
1547
1551
 
1548
1552
  # Load the session and then, in the context of a loaded session, process
1549
1553
  # any remaining extension ("X-...") HTTP headers, checking up on secured
1550
- # headers in passing.
1554
+ # headers in passing. There's special handling for X-Assume-Identity-Of,
1555
+ # which may update the session data loaded into 'interaction' with new
1556
+ # identity information.
1551
1557
 
1552
1558
  load_session_into( interaction )
1553
1559
  deal_with_x_headers( interaction )
1560
+ deal_with_x_assume_identity_of( interaction )
1554
1561
 
1555
1562
  return nil
1556
1563
  end
@@ -1934,6 +1941,167 @@ module Hoodoo; module Services
1934
1941
  end
1935
1942
  end
1936
1943
 
1944
+ # The X-Assume-Identity-Of secured HTTP header allows a caller to specify
1945
+ # values for parts of their session's "identity" section, based upon
1946
+ # permitted values described in their session's "scoping" section. This
1947
+ # method assumes that the permission to use the header in the first place
1948
+ # has already been established by #deal_with_x_headers and, as a result,
1949
+ # relevant property information has been written into the request object.
1950
+ #
1951
+ # The header's value is parsed and checked against the session scoping
1952
+ # data. If everything looks good, the loaded session's identity is
1953
+ # updated accordingly. If there are any problems, one or more errors will
1954
+ # be added to the interaction's context's response object.
1955
+ #
1956
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
1957
+ # describing the current interaction. Updated on exit.
1958
+ #
1959
+ def deal_with_x_assume_identity_of( interaction )
1960
+
1961
+ # Header not in use? Exit now.
1962
+ #
1963
+ return if interaction.context.request.assume_identity_of.nil?
1964
+
1965
+ input_hash = interaction.context.request.assume_identity_of
1966
+ rules_hash = interaction.context.session.scoping.authorised_identities rescue {}
1967
+
1968
+ if ( input_hash.empty? )
1969
+ interaction.context.response.errors.add_error(
1970
+ 'generic.malformed',
1971
+ {
1972
+ :message => "X-Assume-Identity-Of header value is malformed",
1973
+ :reference => { :header_value => ( interaction.context.request.assume_identity_of rescue 'unknown' ) }
1974
+ }
1975
+ )
1976
+ end
1977
+
1978
+ return if interaction.context.response.halt_processing?
1979
+
1980
+ identity_overrides = validate_x_assume_identity_of( interaction, input_hash, rules_hash )
1981
+
1982
+ return if interaction.context.response.halt_processing?
1983
+
1984
+ identity_overrides.each do | key, value |
1985
+ interaction.context.session.identity.send( "#{ key }=", value )
1986
+ end
1987
+ end
1988
+
1989
+ # Back-end to #deal_with_x_assume_identity_of which recursively processes
1990
+ # a rule set against a value from the X-Assume-Identity-Of HTTP header and
1991
+ # either updates the interaction's context's response object with error
1992
+ # details if anything is wrong, or returns a flat Hash of keys and values
1993
+ # to (over-)write in the session's identity section.
1994
+ #
1995
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
1996
+ # describing the current interaction. Will be updated on
1997
+ # exit if errors occur.
1998
+ #
1999
+ # +input_hash+:: Header value for X-Assume-Identity-Of processed into a
2000
+ # flat Hash of String keys and String values.
2001
+ #
2002
+ # +rules_hash+:: Rules Hash from the session scoping data - usually its
2003
+ # "authorised_identities" key - or a sub-hash from nested
2004
+ # data during recursive calls.
2005
+ #
2006
+ # +recursive+:: Top-level callers MUST omit this parameter. Internal
2007
+ # recursive callers MUST set this to +true+.
2008
+ #
2009
+ def validate_x_assume_identity_of( interaction, input_hash, rules_hash, recursive = false )
2010
+ identity_overrides = {}
2011
+
2012
+ unless rules_hash.is_a?( Hash )
2013
+ interaction.context.response.errors.add_error(
2014
+ 'generic.malformed',
2015
+ :message => "X-Assume-Identity-Of header cannot be processed because of malformed scoping rules in Session's associated Caller",
2016
+ )
2017
+
2018
+ return nil
2019
+ end
2020
+
2021
+ rules_hash.each do | rules_key, rules_value |
2022
+
2023
+ next unless input_hash.has_key?( rules_key )
2024
+ input_value = input_hash[ rules_key ]
2025
+
2026
+ unless input_value.is_a?( String )
2027
+ raise "Internal error - internal validation input value for X-Assume-Identity-Of is not a String"
2028
+ end
2029
+
2030
+ if rules_value.is_a?( Array )
2031
+ if rules_value.include?( input_value )
2032
+ identity_overrides[ rules_key ] = input_value
2033
+ else
2034
+ interaction.context.response.errors.add_error(
2035
+ 'platform.forbidden',
2036
+ {
2037
+ :message => "X-Assume-Identity-Of header value requests a prohibited identity quantity",
2038
+ :reference =>
2039
+ {
2040
+ :name => rules_key,
2041
+ :value => input_value
2042
+ }
2043
+ }
2044
+ )
2045
+ return nil
2046
+ end
2047
+
2048
+ elsif rules_value.is_a?( Hash )
2049
+ if rules_value.has_key?( input_value )
2050
+ identity_overrides[ rules_key ] = input_value
2051
+
2052
+ nested_identity_overrides = validate_x_assume_identity_of(
2053
+ interaction,
2054
+ input_hash,
2055
+ rules_value[ input_value ],
2056
+ true
2057
+ )
2058
+
2059
+ return if nested_identity_overrides.nil?
2060
+ identity_overrides.merge!( nested_identity_overrides )
2061
+
2062
+ else
2063
+ interaction.context.response.errors.add_error(
2064
+ 'platform.forbidden',
2065
+ {
2066
+ :message => "X-Assume-Identity-Of header value requests a prohibited identity quantity",
2067
+ :reference =>
2068
+ {
2069
+ :name => rules_key,
2070
+ :value => input_value
2071
+ }
2072
+ }
2073
+ )
2074
+ return nil
2075
+
2076
+ end
2077
+
2078
+ else
2079
+ interaction.context.response.errors.add_error(
2080
+ 'generic.malformed',
2081
+ :message => "X-Assume-Identity-Of header cannot be processed because of malformed scoping rules in Session's associated Caller",
2082
+ )
2083
+ return nil
2084
+
2085
+ end
2086
+ end
2087
+
2088
+ unless recursive || ( input_hash.keys - identity_overrides.keys ).empty?
2089
+ interaction.context.response.errors.add_error(
2090
+ 'platform.forbidden',
2091
+ {
2092
+ :message => "X-Assume-Identity-Of header value requests prohibited identity name(s)",
2093
+ :reference =>
2094
+ {
2095
+ :names => ( input_hash.keys - identity_overrides.keys ).sort().join( ',' )
2096
+ }
2097
+ }
2098
+ )
2099
+ return nil
2100
+ end
2101
+
2102
+ return identity_overrides
2103
+ end
2104
+
1937
2105
  # Preprocessing stage that sets up common headers required in any response.
1938
2106
  # May vary according to inbound content type requested. If processing was
1939
2107
  # aborted early (e.g. missing inbound Content-Type) we may fall to defaults.
@@ -1941,7 +2109,8 @@ module Hoodoo; module Services
1941
2109
  # (At the time of writing, platform documentations say we're JSON only - but
1942
2110
  # there's an strong chance of e.g. XML representation being demanded later).
1943
2111
  #
1944
- # +response+:: Hoodoo::Services::Response instance to update.
2112
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
2113
+ # describing the current interaction. Updated on exit.
1945
2114
  #
1946
2115
  def set_common_response_headers( interaction )
1947
2116
  interaction.context.response.add_header( 'X-Interaction-ID', interaction.interaction_id )
@@ -12,6 +12,6 @@ module Hoodoo
12
12
  # The Hoodoo gem version. If this changes, ensure that the date in
13
13
  # "hoodoo.gemspec" is correct and run "bundle install" (or "update").
14
14
  #
15
- VERSION = '1.0.4'
15
+ VERSION = '1.0.5'
16
16
 
17
17
  end
@@ -146,20 +146,22 @@ class RSpecClientTestTargetImplementation < Hoodoo::Services::Implementation
146
146
  # if adding things.
147
147
 
148
148
  {
149
- 'id' => context.request.ident ||
150
- context.request.body.try( :[], 'id' ) ||
151
- Hoodoo::UUID.generate(),
152
-
153
- 'created_at' => Time.now.utc.iso8601,
154
- 'kind' => 'RSpecClientTestTarget',
155
- 'language' => context.request.locale,
156
-
157
- 'embeds' => context.request.embeds,
158
- 'body_hash' => context.request.body,
159
- 'dated_at' => context.request.dated_at.nil? ? nil : Hoodoo::Utilities.nanosecond_iso8601( context.request.dated_at ),
160
- 'dated_from' => context.request.dated_from.nil? ? nil : Hoodoo::Utilities.nanosecond_iso8601( context.request.dated_from ),
161
- 'resource_uuid' => context.request.resource_uuid,
162
- 'deja_vu' => context.request.deja_vu,
149
+ 'id' => context.request.ident ||
150
+ context.request.body.try( :[], 'id' ) ||
151
+ Hoodoo::UUID.generate(),
152
+
153
+ 'created_at' => Time.now.utc.iso8601,
154
+ 'kind' => 'RSpecClientTestTarget',
155
+ 'language' => context.request.locale,
156
+
157
+ 'embeds' => context.request.embeds,
158
+ 'body_hash' => context.request.body,
159
+ 'dated_at' => context.request.dated_at.nil? ? nil : Hoodoo::Utilities.nanosecond_iso8601( context.request.dated_at ),
160
+ 'dated_from' => context.request.dated_from.nil? ? nil : Hoodoo::Utilities.nanosecond_iso8601( context.request.dated_from ),
161
+ 'resource_uuid' => context.request.resource_uuid,
162
+ 'deja_vu' => context.request.deja_vu,
163
+ 'assume_identity_of' => context.request.assume_identity_of,
164
+ 'actual_identity' => ( context.session.identity.to_h rescue nil ),
163
165
  }
164
166
  end
165
167
  end
@@ -213,6 +215,8 @@ describe Hoodoo::Client do
213
215
  @old_test_session = Hoodoo::Services::Middleware.test_session()
214
216
  @port = spec_helper_start_svc_app_in_thread_for( RSpecClientTestService )
215
217
  @https_port = spec_helper_start_svc_app_in_thread_for( RSpecClientTestService, true )
218
+ @authorised_identities = { 'member_id' => [ '23', '24' ] }
219
+ @example_authorised_identity = { 'member_id' => '23' }
216
220
  end
217
221
 
218
222
  after :all do
@@ -249,15 +253,17 @@ describe Hoodoo::Client do
249
253
  # "def option_based_expectations" later in this file. Be careful
250
254
  # to follow the naming convention evident below if adding things.
251
255
 
252
- @expected_dated_at = @dated_at.nil? ? nil : Hoodoo::Utilities.nanosecond_iso8601( @dated_at )
253
- @expected_dated_from = @dated_from.nil? ? nil : Hoodoo::Utilities.nanosecond_iso8601( @dated_from )
254
- @expected_resource_uuid = @resource_uuid
255
- @expected_deja_vu = @deja_vu != true ? nil : true
256
+ @expected_dated_at = @dated_at.nil? ? nil : Hoodoo::Utilities.nanosecond_iso8601( @dated_at )
257
+ @expected_dated_from = @dated_from.nil? ? nil : Hoodoo::Utilities.nanosecond_iso8601( @dated_from )
258
+ @expected_resource_uuid = @resource_uuid
259
+ @expected_assume_identity_of = @assume_identity_of
260
+ @expected_deja_vu = @deja_vu != true ? nil : true
256
261
 
257
- endpoint_opts[ :dated_at ] = @dated_at unless @dated_at.nil?
258
- endpoint_opts[ :dated_from ] = @dated_from unless @dated_from.nil?
259
- endpoint_opts[ :resource_uuid ] = @resource_uuid unless @resource_uuid.nil?
260
- endpoint_opts[ :deja_vu ] = @deja_vu if @deja_vu == true
262
+ endpoint_opts[ :dated_at ] = @dated_at unless @dated_at.nil?
263
+ endpoint_opts[ :dated_from ] = @dated_from unless @dated_from.nil?
264
+ endpoint_opts[ :resource_uuid ] = @resource_uuid unless @resource_uuid.nil?
265
+ endpoint_opts[ :assume_identity_of ] = @assume_identity_of unless @assume_identity_of.nil?
266
+ endpoint_opts[ :deja_vu ] = @deja_vu if @deja_vu == true
261
267
 
262
268
  if rand( 2 ) == 0
263
269
  override_locale = SecureRandom.urlsafe_base64( 2 )
@@ -508,6 +514,8 @@ describe Hoodoo::Client do
508
514
  case property
509
515
  when :resource_uuid
510
516
  @resource_uuid = Hoodoo::UUID.generate
517
+ when :assume_identity_of
518
+ @assume_identity_of = @example_authorised_identity
511
519
  else
512
520
  raise "Update client_spec.rb with new secured properties for test"
513
521
  end
@@ -725,8 +733,10 @@ describe Hoodoo::Client do
725
733
  context 'and with secured option' do
726
734
  before :each do
727
735
  test_session = @old_test_session.dup
736
+ test_session.identity = OpenStruct.new
728
737
  test_session.scoping = @old_test_session.scoping.dup
729
738
  test_session.scoping.authorised_http_headers = []
739
+ test_session.scoping.authorised_identities = @authorised_identities
730
740
 
731
741
  Hoodoo::Client::Headers::HEADER_TO_PROPERTY.each do | rack_header, description |
732
742
  next unless description[ :secured ] == true
@@ -750,6 +760,8 @@ describe Hoodoo::Client do
750
760
  case property
751
761
  when :resource_uuid
752
762
  @resource_uuid = Hoodoo::UUID.generate
763
+ when :assume_identity_of
764
+ @assume_identity_of = @example_authorised_identity
753
765
  else
754
766
  raise "Update client_spec.rb with new secured properties for test"
755
767
  end
@@ -763,6 +775,8 @@ describe Hoodoo::Client do
763
775
  result = @endpoint.show( mock_ident )
764
776
 
765
777
  expect( result.platform_errors.has_errors? ).to eq( false )
778
+
779
+ option_based_expectations( result )
766
780
  end
767
781
  end
768
782
 
@@ -781,6 +795,36 @@ describe Hoodoo::Client do
781
795
  expect( result.platform_errors.has_errors? ).to eq( false )
782
796
  expect( result[ 'id' ] ).to eq( @resource_uuid )
783
797
  end
798
+
799
+ context "'assume_identity_of' in use" do
800
+ it 'but invalid' do
801
+ @assume_identity_of = { 'invalid' => 'Hoodoo::UUID.generate' }
802
+
803
+ set_vars_for(
804
+ base_uri: "http://localhost:#{ @port }",
805
+ auto_session: false
806
+ )
807
+
808
+ result = @endpoint.create( { 'hello' => 'world' } )
809
+
810
+ expect( result.platform_errors.has_errors? ).to eq( true )
811
+ expect( result.platform_errors.errors[ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
812
+ end
813
+
814
+ it 'and valid' do
815
+ @assume_identity_of = @example_authorised_identity
816
+
817
+ set_vars_for(
818
+ base_uri: "http://localhost:#{ @port }",
819
+ auto_session: false
820
+ )
821
+
822
+ result = @endpoint.create( { 'hello' => 'world' } )
823
+
824
+ expect( result.platform_errors.has_errors? ).to eq( false )
825
+ expect( result[ 'actual_identity' ] ).to eq( @example_authorised_identity )
826
+ end
827
+ end
784
828
  end
785
829
  end
786
830
 
@@ -23,6 +23,35 @@ describe Hoodoo::Client::Headers do
23
23
  end
24
24
  end
25
25
 
26
+ context 'for URL encoded data' do
27
+ before :each do
28
+ @test_hash =
29
+ {
30
+ 'foo' => "hello, world; this & that = foo! \r\t",
31
+ 'bar' => "foo;bar=baz & this + UTF-8 / emoji 😀"
32
+ }
33
+
34
+ @test_string = URI::encode_www_form( @test_hash )
35
+ end
36
+
37
+ context 'KVP_PROPERTY_PROC' do
38
+ it 'converts valid values' do
39
+ expect( described_class::KVP_PROPERTY_PROC.call( @test_string ) ).to eq( @test_hash )
40
+ end
41
+
42
+ it 'does not raise exceptions for invalid values' do
43
+ expect( described_class::KVP_PROPERTY_PROC.call( '' ) ).to eq( {} )
44
+ expect( described_class::KVP_PROPERTY_PROC.call( 'hello' ) ).to eq( { 'hello' => '' } )
45
+ end
46
+ end
47
+
48
+ context 'KVP_HEADER_PROC' do
49
+ it 'converts values' do
50
+ expect( described_class::KVP_HEADER_PROC.call( @test_hash ) ).to eq( @test_string )
51
+ end
52
+ end
53
+ end
54
+
26
55
  context 'DATETIME_IN_PAST_ONLY_PROPERTY_PROC' do
27
56
  it 'converts valid values' do
28
57
  date_time = DateTime.now - 10.seconds