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 +8 -8
- data/lib/hoodoo/client/headers.rb +33 -0
- data/lib/hoodoo/services/middleware/middleware.rb +172 -3
- data/lib/hoodoo/version.rb +1 -1
- data/spec/client/client_spec.rb +66 -22
- data/spec/client/headers_spec.rb +29 -0
- data/spec/services/middleware/middleware_assumed_identity_spec.rb +651 -0
- data/spec/services/middleware/middleware_multi_local_spec.rb +145 -13
- data/spec/services/middleware/middleware_multi_remote_spec.rb +145 -2
- metadata +4 -2
@@ -0,0 +1,651 @@
|
|
1
|
+
# The X-Assume-Identity-Of secured HTTP header allows a caller to
|
2
|
+
# masquerade as some other identity according to scoping limitations.
|
3
|
+
# Since this bypasses some of the deliberate restrictions on identity
|
4
|
+
# through a Caller, it's important to test it extensively so this
|
5
|
+
# file exists for that purpose alone.
|
6
|
+
#
|
7
|
+
# The inter-resource local and remote specs are also involved to make
|
8
|
+
# sure that the header value is auto-propagated across inter-resource
|
9
|
+
# calls. This is obviously vital, else the identity of a caller may
|
10
|
+
# seem to change across the inter-resource boundary.
|
11
|
+
|
12
|
+
require 'spec_helper'
|
13
|
+
|
14
|
+
# ============================================================================
|
15
|
+
|
16
|
+
class RSpecAssumedIdentityImplementation < Hoodoo::Services::Implementation
|
17
|
+
|
18
|
+
# This implementation returns the identity hash from the Hoodoo session
|
19
|
+
# information under the body data key of "identity". If there is a
|
20
|
+
# "caller_id" key present in the Hoodoo identity is it removed first.
|
21
|
+
#
|
22
|
+
def show( context )
|
23
|
+
identity_hash = Hoodoo::Utilities.stringify( context.session.identity.to_h )
|
24
|
+
identity_hash.delete( 'caller_id' )
|
25
|
+
|
26
|
+
context.response.body = {
|
27
|
+
'identity' => identity_hash
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class RSpecAssumedIdentityInterface < Hoodoo::Services::Interface
|
33
|
+
interface :RSpecAssumedIdentity do
|
34
|
+
endpoint :rspec_assumed_identity, RSpecAssumedIdentityImplementation
|
35
|
+
actions :show
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class RSpecAssumedIdentityService < Hoodoo::Services::Service
|
40
|
+
comprised_of RSpecAssumedIdentityInterface
|
41
|
+
end
|
42
|
+
|
43
|
+
# ============================================================================
|
44
|
+
|
45
|
+
describe Hoodoo::Services::Middleware do
|
46
|
+
|
47
|
+
before :each do
|
48
|
+
@old_test_session = Hoodoo::Services::Middleware.test_session()
|
49
|
+
|
50
|
+
# Set up a permissive test session that's ready to have scoping and
|
51
|
+
# identity information updated.
|
52
|
+
#
|
53
|
+
@test_session = @old_test_session.dup
|
54
|
+
|
55
|
+
permissions = Hoodoo::Services::Permissions.new # (this is "default-else-deny")
|
56
|
+
permissions.set_default_fallback( Hoodoo::Services::Permissions::ALLOW )
|
57
|
+
|
58
|
+
@test_session.permissions = permissions
|
59
|
+
@test_session.identity = OpenStruct.new
|
60
|
+
@test_session.scoping = @test_session.scoping.dup
|
61
|
+
end
|
62
|
+
|
63
|
+
after :each do
|
64
|
+
Hoodoo::Services::Middleware.set_test_session( @old_test_session )
|
65
|
+
end
|
66
|
+
|
67
|
+
def app
|
68
|
+
Rack::Builder.new do
|
69
|
+
use Hoodoo::Services::Middleware
|
70
|
+
run RSpecAssumedIdentityService.new
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Calls the test service using a given identity Hash (which it encodes
|
75
|
+
# for the HTTP header) and expecting the given HTTP status code (as a
|
76
|
+
# String or Integer). If expecting 200, the response from the test
|
77
|
+
# service is examined as the "identity" key in the response body should
|
78
|
+
# yield the same identity hash that was given on input, if the
|
79
|
+
# middleware correctly decoded and transferred the encoded value all
|
80
|
+
# the way through to the called service.
|
81
|
+
#
|
82
|
+
# +identity_hash+:: Hash for the 'assume identity' header, String keys
|
83
|
+
# and values only
|
84
|
+
#
|
85
|
+
# +expected_status+:: HTTP status to expect(), as a String or Integer.
|
86
|
+
#
|
87
|
+
# Always returns the JSON-parsed response body for further examination.
|
88
|
+
#
|
89
|
+
def show( identity_hash, expected_status )
|
90
|
+
get(
|
91
|
+
'/v1/rspec_assumed_identity/hello',
|
92
|
+
nil,
|
93
|
+
{
|
94
|
+
'CONTENT_TYPE' => 'application/json; charset=utf-8',
|
95
|
+
'HTTP_X_ASSUME_IDENTITY_OF' => URI.encode_www_form( identity_hash )
|
96
|
+
}
|
97
|
+
)
|
98
|
+
|
99
|
+
expect( last_response.status ).to eq( expected_status )
|
100
|
+
result = JSON.parse( last_response.body )
|
101
|
+
|
102
|
+
if ( expected_status == 200 )
|
103
|
+
expect( result[ 'identity' ] ).to eq( identity_hash )
|
104
|
+
end
|
105
|
+
|
106
|
+
return result
|
107
|
+
end
|
108
|
+
|
109
|
+
# ==========================================================================
|
110
|
+
|
111
|
+
# Tested implicitly elsewhere and explicitly here to make sure.
|
112
|
+
#
|
113
|
+
context 'X-Assume-Identity-Of prohibited' do
|
114
|
+
before :each do
|
115
|
+
@test_session.scoping.authorised_http_headers = []
|
116
|
+
Hoodoo::Services::Middleware.set_test_session( @test_session )
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'rejects usage attempts' do
|
120
|
+
result = show( { 'account_id' => 'bad' }, 403 )
|
121
|
+
|
122
|
+
# We expect a generic - arguably even confusing - 'platform.forbidden'
|
123
|
+
# message to avoid information disclosure that someone has guessed /
|
124
|
+
# potentially correctly tried to use a secured header, but for their
|
125
|
+
# caller credentials prohibiting its use.
|
126
|
+
#
|
127
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
128
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
129
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'Action not authorized' )
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# ==========================================================================
|
134
|
+
|
135
|
+
context 'X-Assume-Identity-Of allowed' do
|
136
|
+
context 'with empty rules' do
|
137
|
+
before :each do
|
138
|
+
@test_session.scoping.authorised_http_headers = [ 'X-Assume-Identity-Of' ]
|
139
|
+
@test_session.scoping.authorised_identities = {}
|
140
|
+
|
141
|
+
Hoodoo::Services::Middleware.set_test_session( @test_session )
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'rejects any values' do
|
145
|
+
result = show( { 'account_id' => '1', 'member_id' => 1 }, 403 )
|
146
|
+
|
147
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
148
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
149
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests prohibited identity name(s)' )
|
150
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'account_id\\,member_id' )
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context 'with flat rules' do
|
155
|
+
before :each do
|
156
|
+
@test_session.scoping.authorised_http_headers = [ 'X-Assume-Identity-Of' ]
|
157
|
+
@test_session.scoping.authorised_identities =
|
158
|
+
{
|
159
|
+
'account_id' => [ '20', '21', '22' ],
|
160
|
+
'member_id' => [ '1', '2', '3', '4', '5', '6' ],
|
161
|
+
'device_id' => [ 'A', 'B' ]
|
162
|
+
}
|
163
|
+
|
164
|
+
Hoodoo::Services::Middleware.set_test_session( @test_session )
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'rejects bad account ID' do
|
168
|
+
result = show( { 'account_id' => 'bad' }, 403 )
|
169
|
+
|
170
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
171
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
172
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests a prohibited identity quantity' )
|
173
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'account_id,bad' )
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'rejects bad member ID' do
|
177
|
+
result = show( { 'member_id' => 'bad' }, 403 )
|
178
|
+
|
179
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
180
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
181
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests a prohibited identity quantity' )
|
182
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'member_id,bad' )
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'rejects bad device ID' do
|
186
|
+
result = show( { 'device_id' => 'bad' }, 403 )
|
187
|
+
|
188
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
189
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
190
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests a prohibited identity quantity' )
|
191
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'device_id,bad' )
|
192
|
+
end
|
193
|
+
|
194
|
+
# Belt-and-braces check that multiple bad items are still rejected,
|
195
|
+
# but don't have any expectations about which one gets picked out
|
196
|
+
# in the 'reference' field.
|
197
|
+
#
|
198
|
+
it 'rejects bad combinations' do
|
199
|
+
result = show( { 'account_id' => 'bad', 'member_id' => 'bad', 'device_id' => 'bad' }, 403 )
|
200
|
+
|
201
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
202
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
203
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests a prohibited identity quantity' )
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'rejects bad IDs amongst good' do
|
207
|
+
result = show( { 'account_id' => '21', 'member_id' => 'bad', 'device_id' => 'A' }, 403 )
|
208
|
+
|
209
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
210
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
211
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests a prohibited identity quantity' )
|
212
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'member_id,bad' )
|
213
|
+
end
|
214
|
+
|
215
|
+
# Each 'show' must be in its own test so that the test session data
|
216
|
+
# gets reset in between; otherwise, the *same* session identity is
|
217
|
+
# being successively merged/updated under test, since it's a single
|
218
|
+
# object that's reused rather than a new loaded-in session.
|
219
|
+
#
|
220
|
+
it 'accepts one good ID (1)' do
|
221
|
+
result = show( { 'account_id' => '22' }, 200 )
|
222
|
+
end
|
223
|
+
it 'accepts one good ID (2)' do
|
224
|
+
result = show( { 'member_id' => '1' }, 200 )
|
225
|
+
end
|
226
|
+
it 'accepts one good ID (3)' do
|
227
|
+
result = show( { 'device_id' => 'B' }, 200 )
|
228
|
+
end
|
229
|
+
|
230
|
+
it 'accepts many good IDs' do
|
231
|
+
result = show( { 'account_id' => '22', 'member_id' => '1', 'device_id' => 'B' }, 200 )
|
232
|
+
end
|
233
|
+
|
234
|
+
it 'accepts encoded names' do
|
235
|
+
get(
|
236
|
+
'/v1/rspec_assumed_identity/hello',
|
237
|
+
nil,
|
238
|
+
{
|
239
|
+
'CONTENT_TYPE' => 'application/json; charset=utf-8',
|
240
|
+
'HTTP_X_ASSUME_IDENTITY_OF' => 'a%63%63ount_id=22'
|
241
|
+
}
|
242
|
+
)
|
243
|
+
|
244
|
+
expect( last_response.status ).to eq( 200 )
|
245
|
+
end
|
246
|
+
|
247
|
+
it 'accepts encoded values' do
|
248
|
+
get(
|
249
|
+
'/v1/rspec_assumed_identity/hello',
|
250
|
+
nil,
|
251
|
+
{
|
252
|
+
'CONTENT_TYPE' => 'application/json; charset=utf-8',
|
253
|
+
'HTTP_X_ASSUME_IDENTITY_OF' => 'account_id=%32%32'
|
254
|
+
}
|
255
|
+
)
|
256
|
+
|
257
|
+
expect( last_response.status ).to eq( 200 )
|
258
|
+
end
|
259
|
+
|
260
|
+
it 'rejects an unknown name' do
|
261
|
+
result = show( { 'another_id' => 'A155C' }, 403 )
|
262
|
+
|
263
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
264
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
265
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests prohibited identity name(s)' )
|
266
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'another_id' )
|
267
|
+
end
|
268
|
+
|
269
|
+
it 'rejects unknown names' do
|
270
|
+
result = show( { 'another_id' => 'A155C', 'additional_id' => 'iiv' }, 403 )
|
271
|
+
|
272
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
273
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
274
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests prohibited identity name(s)' )
|
275
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'additional_id\\,another_id' )
|
276
|
+
end
|
277
|
+
|
278
|
+
it 'rejects an unknown name amongst a known name' do
|
279
|
+
result = show( { 'another_id' => 'A155C', 'account_id' => '22' }, 403 )
|
280
|
+
|
281
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
282
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
283
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests prohibited identity name(s)' )
|
284
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'another_id' )
|
285
|
+
end
|
286
|
+
|
287
|
+
it 'rejects an unknown name amongst known names' do
|
288
|
+
result = show( { 'another_id' => 'A155C', 'account_id' => '22', 'member_id' => '1' }, 403 )
|
289
|
+
|
290
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
291
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
292
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests prohibited identity name(s)' )
|
293
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'another_id' )
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
context 'with deep rules' do
|
298
|
+
before :each do
|
299
|
+
@test_session.scoping.authorised_http_headers = [ 'X-Assume-Identity-Of' ]
|
300
|
+
@test_session.scoping.authorised_identities =
|
301
|
+
{
|
302
|
+
'account_id' =>
|
303
|
+
{
|
304
|
+
'20' => { 'member_id' => [ '1', '2' ] },
|
305
|
+
'21' => { 'member_id' => [ '3', '4' ] },
|
306
|
+
'22' =>
|
307
|
+
{
|
308
|
+
'member_id' =>
|
309
|
+
{
|
310
|
+
'5' => { 'device_id' => [ 'A' ] },
|
311
|
+
'6' => { 'device_id' => [ 'B' ] }
|
312
|
+
}
|
313
|
+
}
|
314
|
+
}
|
315
|
+
}
|
316
|
+
|
317
|
+
Hoodoo::Services::Middleware.set_test_session( @test_session )
|
318
|
+
end
|
319
|
+
|
320
|
+
it 'rejects bad account ID' do
|
321
|
+
result = show( { 'account_id' => 'bad' }, 403 )
|
322
|
+
|
323
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
324
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
325
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests a prohibited identity quantity' )
|
326
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'account_id,bad' )
|
327
|
+
end
|
328
|
+
|
329
|
+
it 'rejects bad member ID' do
|
330
|
+
result = show( { 'account_id' => '20', 'member_id' => 'bad' }, 403 )
|
331
|
+
|
332
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
333
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
334
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests a prohibited identity quantity' )
|
335
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'member_id,bad' )
|
336
|
+
end
|
337
|
+
|
338
|
+
it 'rejects bad device ID' do
|
339
|
+
result = show( { 'account_id' => '22', 'member_id' => '5', 'device_id' => 'bad' }, 403 )
|
340
|
+
|
341
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
342
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
343
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests a prohibited identity quantity' )
|
344
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'device_id,bad' )
|
345
|
+
end
|
346
|
+
|
347
|
+
it 'rejects attempt to use device ID when not listed in rules' do
|
348
|
+
result = show( { 'account_id' => '21', 'member_id' => '4', 'device_id' => 'A' }, 403 )
|
349
|
+
|
350
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
351
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
352
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests prohibited identity name(s)' )
|
353
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'device_id' )
|
354
|
+
end
|
355
|
+
|
356
|
+
it 'rejects an ID that is present but listed under a different key' do
|
357
|
+
result = show( { 'account_id' => '20', 'member_id' => '4' }, 403 )
|
358
|
+
|
359
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
360
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
361
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests a prohibited identity quantity' )
|
362
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'member_id,4' )
|
363
|
+
end
|
364
|
+
|
365
|
+
it 'rejects an ID that is present but not top-level' do
|
366
|
+
result = show( { 'member_id' => '1' }, 403 )
|
367
|
+
|
368
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
369
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
370
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests prohibited identity name(s)' )
|
371
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'member_id' )
|
372
|
+
end
|
373
|
+
|
374
|
+
# Each 'show' must be in its own test so that the test session data
|
375
|
+
# gets reset in between; otherwise, the *same* session identity is
|
376
|
+
# being successively merged/updated under test, since it's a single
|
377
|
+
# object that's reused rather than a new loaded-in session.
|
378
|
+
#
|
379
|
+
it 'accepts a subset of good IDs (1)' do
|
380
|
+
result = show( { 'account_id' => '22' }, 200 )
|
381
|
+
end
|
382
|
+
it 'accepts a subset of good IDs (2)' do
|
383
|
+
result = show( { 'account_id' => '22', 'member_id' => '5' }, 200 )
|
384
|
+
end
|
385
|
+
it 'accepts many good IDs (1)' do
|
386
|
+
result = show( { 'account_id' => '20', 'member_id' => '2' }, 200 )
|
387
|
+
end
|
388
|
+
it 'accepts many good IDs (2)' do
|
389
|
+
result = show( { 'account_id' => '22', 'member_id' => '6', 'device_id' => 'B' }, 200 )
|
390
|
+
end
|
391
|
+
|
392
|
+
it 'rejects an unknown name' do
|
393
|
+
result = show( { 'another_id' => 'A155C' }, 403 )
|
394
|
+
|
395
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
396
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
397
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests prohibited identity name(s)' )
|
398
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'another_id' )
|
399
|
+
end
|
400
|
+
|
401
|
+
it 'rejects unknown names' do
|
402
|
+
result = show( { 'another_id' => 'A155C', 'additional_id' => 'iiv' }, 403 )
|
403
|
+
|
404
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
405
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
406
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests prohibited identity name(s)' )
|
407
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'additional_id\\,another_id' )
|
408
|
+
end
|
409
|
+
|
410
|
+
it 'rejects an unknown name amongst a known name' do
|
411
|
+
result = show( { 'another_id' => 'A155C', 'account_id' => '22' }, 403 )
|
412
|
+
|
413
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
414
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
415
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests prohibited identity name(s)' )
|
416
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'another_id' )
|
417
|
+
end
|
418
|
+
|
419
|
+
it 'rejects an unknown name amongst known names' do
|
420
|
+
result = show( { 'another_id' => 'A155C', 'account_id' => '22', 'member_id' => '6' }, 403 )
|
421
|
+
|
422
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
423
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'platform.forbidden' )
|
424
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'X-Assume-Identity-Of header value requests prohibited identity name(s)' )
|
425
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'another_id' )
|
426
|
+
end
|
427
|
+
|
428
|
+
end
|
429
|
+
|
430
|
+
context 'with malformed rules' do
|
431
|
+
def set_rules( rules )
|
432
|
+
@test_session.scoping.authorised_http_headers = [ 'X-Assume-Identity-Of' ]
|
433
|
+
@test_session.scoping.authorised_identities = rules
|
434
|
+
Hoodoo::Services::Middleware.set_test_session( @test_session )
|
435
|
+
end
|
436
|
+
|
437
|
+
def expect_malformed( result )
|
438
|
+
expect( result[ 'kind' ] ).to eq( 'Errors' )
|
439
|
+
expect( result[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'generic.malformed' )
|
440
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( "X-Assume-Identity-Of header cannot be processed because of malformed scoping rules in Session's associated Caller" )
|
441
|
+
end
|
442
|
+
|
443
|
+
it 'rejects top-level non-Hash (1)' do
|
444
|
+
set_rules( [ '1', '2', '3' ] )
|
445
|
+
result = show( { 'account_id' => '21' }, 422 )
|
446
|
+
expect_malformed( result )
|
447
|
+
end
|
448
|
+
|
449
|
+
it 'rejects top-level non-Hash (2)' do
|
450
|
+
set_rules( 'String' )
|
451
|
+
result = show( { 'account_id' => '21' }, 422 )
|
452
|
+
expect_malformed( result )
|
453
|
+
end
|
454
|
+
|
455
|
+
it 'rejects top-level value where array was expected' do
|
456
|
+
set_rules( {
|
457
|
+
'account_id' => '21'
|
458
|
+
} )
|
459
|
+
|
460
|
+
result = show( { 'account_id' => '21' }, 422 )
|
461
|
+
expect_malformed( result )
|
462
|
+
end
|
463
|
+
|
464
|
+
it 'rejects intermediate value where Hash was expected' do
|
465
|
+
set_rules( {
|
466
|
+
'account_id' =>
|
467
|
+
{
|
468
|
+
'20' => 'member_id'
|
469
|
+
}
|
470
|
+
} )
|
471
|
+
|
472
|
+
result = show( { 'account_id' => '20', 'member_id' => '1' }, 422 )
|
473
|
+
expect_malformed( result )
|
474
|
+
end
|
475
|
+
|
476
|
+
it 'rejects deep value where Array was expected' do
|
477
|
+
set_rules( {
|
478
|
+
'account_id' =>
|
479
|
+
{
|
480
|
+
'22' =>
|
481
|
+
{
|
482
|
+
'member_id' =>
|
483
|
+
{
|
484
|
+
'5' => { 'device_id' => 'A' }
|
485
|
+
}
|
486
|
+
}
|
487
|
+
}
|
488
|
+
} )
|
489
|
+
|
490
|
+
result = show( { 'account_id' => '22', 'member_id' => '5', 'device_id' => 'A' }, 422 )
|
491
|
+
expect_malformed( result )
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
# All of these should result in 403 cases because they parse into
|
496
|
+
# identity hashes with either an empty key or empty value, or a
|
497
|
+
# key or value that's not allowed by the rules. The exception is
|
498
|
+
# for the pure empty string header value or empty Hash, which
|
499
|
+
# gets caught explicitly as malformed input.
|
500
|
+
#
|
501
|
+
context 'but header value is malformed' do
|
502
|
+
before :each do
|
503
|
+
@test_session.scoping.authorised_http_headers = [ 'X-Assume-Identity-Of' ]
|
504
|
+
@test_session.scoping.authorised_identities =
|
505
|
+
{
|
506
|
+
'account_id' => [ '20', '21', '22' ],
|
507
|
+
'member_id' => [ '1', '2', '3', '4', '5', '6' ],
|
508
|
+
'device_id' => [ 'A', 'B' ]
|
509
|
+
}
|
510
|
+
|
511
|
+
Hoodoo::Services::Middleware.set_test_session( @test_session )
|
512
|
+
end
|
513
|
+
|
514
|
+
it 'rejects explicit empty hashes (1)' do
|
515
|
+
get(
|
516
|
+
'/v1/rspec_assumed_identity/hello',
|
517
|
+
nil,
|
518
|
+
{
|
519
|
+
'CONTENT_TYPE' => 'application/json; charset=utf-8',
|
520
|
+
'HTTP_X_ASSUME_IDENTITY_OF' => ''
|
521
|
+
}
|
522
|
+
)
|
523
|
+
|
524
|
+
expect( last_response.status ).to eq( 422 )
|
525
|
+
end
|
526
|
+
|
527
|
+
# Likewise via a Hash.
|
528
|
+
#
|
529
|
+
it 'rejects explicit empty hashes (2)' do
|
530
|
+
result = show( {}, 422 )
|
531
|
+
end
|
532
|
+
|
533
|
+
it 'rejects no-key headers (1)' do
|
534
|
+
get(
|
535
|
+
'/v1/rspec_assumed_identity/hello',
|
536
|
+
nil,
|
537
|
+
{
|
538
|
+
'CONTENT_TYPE' => 'application/json; charset=utf-8',
|
539
|
+
'HTTP_X_ASSUME_IDENTITY_OF' => '=22'
|
540
|
+
}
|
541
|
+
)
|
542
|
+
|
543
|
+
expect( last_response.status ).to eq( 403 )
|
544
|
+
|
545
|
+
result = JSON.parse( last_response.body )
|
546
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( '' )
|
547
|
+
end
|
548
|
+
|
549
|
+
# Likewise via a Hash.
|
550
|
+
#
|
551
|
+
it 'rejects no-key headers (2)' do
|
552
|
+
result = show( { '' => '22' }, 403 )
|
553
|
+
end
|
554
|
+
|
555
|
+
it 'rejects no-key value (1)' do
|
556
|
+
get(
|
557
|
+
'/v1/rspec_assumed_identity/hello',
|
558
|
+
nil,
|
559
|
+
{
|
560
|
+
'CONTENT_TYPE' => 'application/json; charset=utf-8',
|
561
|
+
'HTTP_X_ASSUME_IDENTITY_OF' => 'account_id='
|
562
|
+
}
|
563
|
+
)
|
564
|
+
|
565
|
+
expect( last_response.status ).to eq( 403 )
|
566
|
+
|
567
|
+
result = JSON.parse( last_response.body )
|
568
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'account_id,' )
|
569
|
+
end
|
570
|
+
|
571
|
+
# Likewise via a Hash.
|
572
|
+
#
|
573
|
+
it 'rejects no-key value (2)' do
|
574
|
+
result = show( { 'account_id' => '' }, 403 )
|
575
|
+
end
|
576
|
+
|
577
|
+
it 'rejects a non-KVP value' do
|
578
|
+
get(
|
579
|
+
'/v1/rspec_assumed_identity/hello',
|
580
|
+
nil,
|
581
|
+
{
|
582
|
+
'CONTENT_TYPE' => 'application/json; charset=utf-8',
|
583
|
+
'HTTP_X_ASSUME_IDENTITY_OF' => 'foo'
|
584
|
+
}
|
585
|
+
)
|
586
|
+
|
587
|
+
expect( last_response.status ).to eq( 403 )
|
588
|
+
|
589
|
+
result = JSON.parse( last_response.body )
|
590
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'foo' )
|
591
|
+
end
|
592
|
+
|
593
|
+
it 'rejects a badly encoded value' do
|
594
|
+
get(
|
595
|
+
'/v1/rspec_assumed_identity/hello',
|
596
|
+
nil,
|
597
|
+
{
|
598
|
+
'CONTENT_TYPE' => 'application/json; charset=utf-8',
|
599
|
+
'HTTP_X_ASSUME_IDENTITY_OF' => 'account_id=%4'
|
600
|
+
}
|
601
|
+
)
|
602
|
+
|
603
|
+
expect( last_response.status ).to eq( 403 )
|
604
|
+
|
605
|
+
result = JSON.parse( last_response.body )
|
606
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'account_id,%4' )
|
607
|
+
end
|
608
|
+
|
609
|
+
it 'rejects an encoded "=" because it is taken as a literal, not a separator' do
|
610
|
+
get(
|
611
|
+
'/v1/rspec_assumed_identity/hello',
|
612
|
+
nil,
|
613
|
+
{
|
614
|
+
'CONTENT_TYPE' => 'application/json; charset=utf-8',
|
615
|
+
'HTTP_X_ASSUME_IDENTITY_OF' => 'account_id%3d22'
|
616
|
+
}
|
617
|
+
)
|
618
|
+
|
619
|
+
expect( last_response.status ).to eq( 403 )
|
620
|
+
|
621
|
+
result = JSON.parse( last_response.body )
|
622
|
+
expect( result[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'account_id=22' )
|
623
|
+
end
|
624
|
+
end
|
625
|
+
end
|
626
|
+
|
627
|
+
# ==========================================================================
|
628
|
+
|
629
|
+
context 'code coverage' do
|
630
|
+
before :each do
|
631
|
+
@test_session.scoping.authorised_http_headers = [ 'X-Assume-Identity-Of' ]
|
632
|
+
@test_session.scoping.authorised_identities = { 'account_id' => [ '1' ] }
|
633
|
+
Hoodoo::Services::Middleware.set_test_session( @test_session )
|
634
|
+
end
|
635
|
+
|
636
|
+
it 'internally self-checks' do
|
637
|
+
allow_any_instance_of( Hoodoo::Services::Middleware ).to receive( :deal_with_x_assume_identity_of ) do | instance, interaction |
|
638
|
+
instance.send(
|
639
|
+
:validate_x_assume_identity_of,
|
640
|
+
interaction,
|
641
|
+
{ 'account_id' => 23 },
|
642
|
+
@test_session.scoping.authorised_identities
|
643
|
+
)
|
644
|
+
end
|
645
|
+
|
646
|
+
result = show( { 'account_id' => '1' }, 500 )
|
647
|
+
expect( result[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'Internal error - internal validation input value for X-Assume-Identity-Of is not a String' )
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
end
|