hoodoo 1.0.4 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
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