haveapi 0.27.2 → 0.28.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/Gemfile +1 -1
- data/haveapi.gemspec +1 -1
- data/lib/haveapi/action.rb +125 -36
- data/lib/haveapi/actions/paginable.rb +3 -1
- data/lib/haveapi/authentication/basic/provider.rb +2 -0
- data/lib/haveapi/authentication/chain.rb +11 -7
- data/lib/haveapi/authentication/oauth2/config.rb +25 -3
- data/lib/haveapi/authentication/oauth2/provider.rb +92 -11
- data/lib/haveapi/authentication/oauth2/revoke_endpoint.rb +44 -3
- data/lib/haveapi/authentication/token/provider.rb +53 -15
- data/lib/haveapi/authorization.rb +42 -18
- data/lib/haveapi/client_examples/php_client.rb +1 -1
- data/lib/haveapi/client_examples/ruby_client.rb +1 -1
- data/lib/haveapi/context.rb +10 -4
- data/lib/haveapi/example.rb +15 -16
- data/lib/haveapi/extensions/action_exceptions.rb +6 -6
- data/lib/haveapi/model_adapters/active_record.rb +150 -71
- data/lib/haveapi/model_adapters/hash.rb +1 -1
- data/lib/haveapi/parameters/resource.rb +50 -6
- data/lib/haveapi/parameters/typed.rb +40 -13
- data/lib/haveapi/params.rb +27 -8
- data/lib/haveapi/resource.rb +4 -1
- data/lib/haveapi/resources/action_state.rb +13 -5
- data/lib/haveapi/route.rb +2 -2
- data/lib/haveapi/server.rb +137 -45
- data/lib/haveapi/validator.rb +2 -2
- data/lib/haveapi/validator_chain.rb +1 -0
- data/lib/haveapi/validators/confirmation.rb +1 -0
- data/lib/haveapi/validators/format.rb +6 -2
- data/lib/haveapi/validators/length.rb +2 -0
- data/lib/haveapi/validators/numericality.rb +2 -0
- data/lib/haveapi/validators/presence.rb +1 -1
- data/lib/haveapi/version.rb +1 -1
- data/lib/haveapi/views/version_page/client_auth.erb +1 -1
- data/lib/haveapi/views/version_page/client_example.erb +3 -3
- data/lib/haveapi/views/version_page/client_init.erb +1 -1
- data/lib/haveapi/views/version_page.erb +2 -2
- data/lib/haveapi/views/version_sidebar.erb +4 -2
- data/spec/action/authorize_spec.rb +99 -0
- data/spec/action/runtime_spec.rb +426 -0
- data/spec/action_state_spec.rb +52 -0
- data/spec/authentication/basic_spec.rb +29 -0
- data/spec/authentication/oauth2_spec.rb +329 -0
- data/spec/authentication/token_spec.rb +195 -0
- data/spec/authentication/token_version_routes_spec.rb +164 -0
- data/spec/authorization_spec.rb +66 -0
- data/spec/documentation/auth_filtering_spec.rb +195 -1
- data/spec/documentation/current_user_html_escaping_spec.rb +47 -0
- data/spec/documentation/examples_spec.rb +97 -0
- data/spec/documentation/host_html_escaping_spec.rb +41 -0
- data/spec/documentation_spec.rb +13 -0
- data/spec/extensions/action_exceptions_spec.rb +30 -0
- data/spec/model_adapters/active_record_spec.rb +408 -3
- data/spec/parameters/typed_spec.rb +75 -7
- data/spec/params_spec.rb +41 -0
- data/spec/server/integration_spec.rb +90 -0
- data/spec/validator_chain_spec.rb +39 -0
- data/spec/validators/confirmation_spec.rb +14 -0
- data/spec/validators/format_spec.rb +7 -0
- data/spec/validators/length_spec.rb +6 -0
- data/spec/validators/numericality_spec.rb +7 -0
- data/spec/validators/presence_spec.rb +2 -0
- data/test_support/client_test_api.rb +31 -3
- metadata +8 -4
- data/shell.nix +0 -20
data/lib/haveapi/server.rb
CHANGED
|
@@ -49,8 +49,12 @@ module HaveAPI
|
|
|
49
49
|
return if @formatter
|
|
50
50
|
|
|
51
51
|
@formatter = OutputFormatter.new
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
accept = request.accept
|
|
53
|
+
rescue ArgumentError, EncodingError
|
|
54
|
+
@formatter.supports?([])
|
|
55
|
+
report_error(400, {}, 'Bad Accept header')
|
|
56
|
+
else
|
|
57
|
+
unless @formatter.supports?(accept)
|
|
54
58
|
@halted = true
|
|
55
59
|
halt 406, "Not Acceptable\n"
|
|
56
60
|
end
|
|
@@ -79,6 +83,19 @@ module HaveAPI
|
|
|
79
83
|
@current_user
|
|
80
84
|
end
|
|
81
85
|
|
|
86
|
+
def authenticated_versions
|
|
87
|
+
settings.api_server.versions.each_with_object({}) do |v, ret|
|
|
88
|
+
ret[v] = settings.api_server.send(:do_authenticate, v, request)
|
|
89
|
+
rescue HaveAPI::Authentication::TokenConflict => e
|
|
90
|
+
unless @formatter
|
|
91
|
+
@formatter = OutputFormatter.new
|
|
92
|
+
@formatter.supports?([])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
report_error(400, {}, e.message)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
82
99
|
def access_control
|
|
83
100
|
return unless request.env['HTTP_ORIGIN'] && request.env['HTTP_ACCESS_CONTROL_REQUEST_METHOD']
|
|
84
101
|
|
|
@@ -110,6 +127,10 @@ module HaveAPI
|
|
|
110
127
|
|
|
111
128
|
def report_error(code, headers, msg)
|
|
112
129
|
@halted = true
|
|
130
|
+
unless @formatter
|
|
131
|
+
@formatter = OutputFormatter.new
|
|
132
|
+
@formatter.supports?([])
|
|
133
|
+
end
|
|
113
134
|
|
|
114
135
|
content_type @formatter.content_type, charset: 'utf-8'
|
|
115
136
|
halt code, headers, @formatter.format(false, nil, msg, version: false)
|
|
@@ -172,10 +193,14 @@ module HaveAPI
|
|
|
172
193
|
return ret if validators.nil?
|
|
173
194
|
|
|
174
195
|
validators.each do |name, opts|
|
|
175
|
-
ret += "<h5>#{name.to_s.capitalize}</h5>"
|
|
196
|
+
ret += "<h5>#{escape_html(name.to_s.capitalize)}</h5>"
|
|
176
197
|
ret += '<dl>'
|
|
177
|
-
opts.
|
|
178
|
-
|
|
198
|
+
if opts.respond_to?(:each_pair)
|
|
199
|
+
opts.each_pair do |k, v|
|
|
200
|
+
ret += "<dt>#{escape_html(k)}</dt><dd>#{escape_html(v.to_s)}</dd>"
|
|
201
|
+
end
|
|
202
|
+
else
|
|
203
|
+
ret += "<dt>description</dt><dd>#{escape_html(opts.to_s)}</dd>"
|
|
179
204
|
end
|
|
180
205
|
ret += '</dl>'
|
|
181
206
|
end
|
|
@@ -270,12 +295,13 @@ module HaveAPI
|
|
|
270
295
|
|
|
271
296
|
# Mount root
|
|
272
297
|
@sinatra.get @root do
|
|
273
|
-
|
|
298
|
+
auth_users_by_version = authenticated_versions
|
|
274
299
|
|
|
275
300
|
@api = settings.api_server.describe(Context.new(
|
|
276
301
|
settings.api_server,
|
|
277
|
-
user:
|
|
278
|
-
params
|
|
302
|
+
user: auth_users_by_version[settings.api_server.default_version],
|
|
303
|
+
params:,
|
|
304
|
+
auth_users_by_version:
|
|
279
305
|
))
|
|
280
306
|
|
|
281
307
|
content_type 'text/html'
|
|
@@ -285,7 +311,6 @@ module HaveAPI
|
|
|
285
311
|
@sinatra.options @root do
|
|
286
312
|
setup_formatter
|
|
287
313
|
access_control
|
|
288
|
-
authenticated?(settings.api_server.default_version)
|
|
289
314
|
ret = nil
|
|
290
315
|
|
|
291
316
|
ret = case params[:describe]
|
|
@@ -296,20 +321,26 @@ module HaveAPI
|
|
|
296
321
|
}
|
|
297
322
|
|
|
298
323
|
when 'default'
|
|
324
|
+
auth_users_by_version = authenticated_versions
|
|
325
|
+
|
|
299
326
|
settings.api_server.describe_version(Context.new(
|
|
300
327
|
settings.api_server,
|
|
301
328
|
version: settings.api_server.default_version,
|
|
302
|
-
user:
|
|
329
|
+
user: auth_users_by_version[settings.api_server.default_version],
|
|
303
330
|
doc: true,
|
|
304
|
-
params
|
|
331
|
+
params:,
|
|
332
|
+
auth_users_by_version:
|
|
305
333
|
))
|
|
306
334
|
|
|
307
335
|
else
|
|
336
|
+
auth_users_by_version = authenticated_versions
|
|
337
|
+
|
|
308
338
|
settings.api_server.describe(Context.new(
|
|
309
339
|
settings.api_server,
|
|
310
|
-
user:
|
|
340
|
+
user: auth_users_by_version[settings.api_server.default_version],
|
|
311
341
|
doc: true,
|
|
312
|
-
params
|
|
342
|
+
params:,
|
|
343
|
+
auth_users_by_version:
|
|
313
344
|
))
|
|
314
345
|
end
|
|
315
346
|
|
|
@@ -340,7 +371,7 @@ module HaveAPI
|
|
|
340
371
|
end
|
|
341
372
|
end
|
|
342
373
|
|
|
343
|
-
@sinatra.get %r{#{@root}doc/([
|
|
374
|
+
@sinatra.get %r{#{@root}doc/([^.]+)(\.md)?} do |f, _|
|
|
344
375
|
content_type 'text/html'
|
|
345
376
|
erb :doc_layout, layout: :main_layout do
|
|
346
377
|
begin
|
|
@@ -349,7 +380,11 @@ module HaveAPI
|
|
|
349
380
|
halt 404
|
|
350
381
|
end
|
|
351
382
|
|
|
352
|
-
|
|
383
|
+
begin
|
|
384
|
+
@sidebar = erb :"doc_sidebars/#{f}"
|
|
385
|
+
rescue Errno::ENOENT
|
|
386
|
+
@sidebar = ''
|
|
387
|
+
end
|
|
353
388
|
end
|
|
354
389
|
end
|
|
355
390
|
|
|
@@ -481,35 +516,45 @@ module HaveAPI
|
|
|
481
516
|
@sinatra.method(route.http_method).call(route.sinatra_path) do
|
|
482
517
|
setup_formatter
|
|
483
518
|
|
|
484
|
-
if route.action.auth
|
|
519
|
+
if route.action.auth || settings.api_server.action_state_auth_required?(route)
|
|
485
520
|
authenticate!(v)
|
|
486
521
|
else
|
|
487
522
|
authenticated?(v)
|
|
488
523
|
end
|
|
489
524
|
|
|
490
|
-
|
|
491
|
-
|
|
525
|
+
raw_body = request.body.read
|
|
526
|
+
body_method = !%i[get head options].include?(route.http_method.to_sym)
|
|
492
527
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
528
|
+
if body_method && !raw_body.empty? && !settings.api_server.send(:json_content_type?, request)
|
|
529
|
+
report_error(415, {}, 'Unsupported Content-Type')
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
begin
|
|
533
|
+
body = raw_body.empty? ? nil : JSON.parse(raw_body, symbolize_names: true)
|
|
534
|
+
rescue JSON::ParserError
|
|
499
535
|
report_error(400, {}, 'Bad JSON syntax')
|
|
500
536
|
end
|
|
501
537
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
538
|
+
if !raw_body.empty? && !body.is_a?(Hash)
|
|
539
|
+
report_error(400, {}, 'JSON body must be an object')
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
action_params = body_method ? settings.api_server.send(:path_params, route, params) : params
|
|
543
|
+
context_params = body ? action_params.merge(body) : action_params
|
|
544
|
+
|
|
545
|
+
context = Context.new(
|
|
546
|
+
settings.api_server,
|
|
547
|
+
version: v,
|
|
548
|
+
request: self,
|
|
549
|
+
action: route.action,
|
|
550
|
+
path: route.path,
|
|
551
|
+
params: context_params,
|
|
552
|
+
user: current_user,
|
|
553
|
+
endpoint: true,
|
|
554
|
+
resource_path: route.resource_path
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
action = route.action.new(request, v, action_params, body, context)
|
|
513
558
|
|
|
514
559
|
unless action.authorized?(current_user)
|
|
515
560
|
report_error(403, {}, 'Access denied. Insufficient permissions.')
|
|
@@ -537,7 +582,7 @@ module HaveAPI
|
|
|
537
582
|
|
|
538
583
|
pass if params[:method] && params[:method] != route_method
|
|
539
584
|
|
|
540
|
-
if route.action.auth
|
|
585
|
+
if route.action.auth || settings.api_server.action_state_auth_required?(route)
|
|
541
586
|
authenticate!(v)
|
|
542
587
|
else
|
|
543
588
|
authenticated?(v)
|
|
@@ -563,6 +608,8 @@ module HaveAPI
|
|
|
563
608
|
unless desc
|
|
564
609
|
report_error(403, {}, 'Access denied. Insufficient permissions.')
|
|
565
610
|
end
|
|
611
|
+
rescue ValidationError => e
|
|
612
|
+
report_error(400, e.to_hash, e.message)
|
|
566
613
|
rescue StandardError => e
|
|
567
614
|
tmp = settings.api_server.call_hooks_for(:description_exception, args: [ctx, e])
|
|
568
615
|
report_error(
|
|
@@ -577,19 +624,28 @@ module HaveAPI
|
|
|
577
624
|
end
|
|
578
625
|
|
|
579
626
|
def describe(context)
|
|
580
|
-
|
|
627
|
+
original_user = context.current_user
|
|
628
|
+
auth_users_by_version = context.auth_users_by_version
|
|
629
|
+
authenticated_description = auth_users_by_version&.values&.any?
|
|
581
630
|
|
|
582
|
-
ret = {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
631
|
+
ret = { default_version: @default_version, versions: {} }
|
|
632
|
+
|
|
633
|
+
context.version = @default_version
|
|
634
|
+
context.current_user = auth_users_by_version ? auth_users_by_version[@default_version] : original_user
|
|
635
|
+
ret[:versions][:default] = describe_version(context) unless authenticated_description && context.current_user.nil?
|
|
586
636
|
|
|
587
637
|
@versions.each do |v|
|
|
638
|
+
user = auth_users_by_version ? auth_users_by_version[v] : original_user
|
|
639
|
+
next if authenticated_description && user.nil?
|
|
640
|
+
|
|
588
641
|
context.version = v
|
|
642
|
+
context.current_user = user
|
|
589
643
|
ret[:versions][v] = describe_version(context)
|
|
590
644
|
end
|
|
591
645
|
|
|
592
646
|
ret
|
|
647
|
+
ensure
|
|
648
|
+
context.current_user = original_user
|
|
593
649
|
end
|
|
594
650
|
|
|
595
651
|
def describe_version(context)
|
|
@@ -618,6 +674,12 @@ module HaveAPI
|
|
|
618
674
|
r.describe(hash, context)
|
|
619
675
|
end
|
|
620
676
|
|
|
677
|
+
def action_state_auth_required?(route)
|
|
678
|
+
return false if @auth_chain.empty?
|
|
679
|
+
|
|
680
|
+
route.action.resource == HaveAPI::Resources::ActionState
|
|
681
|
+
end
|
|
682
|
+
|
|
621
683
|
def version_prefix(v)
|
|
622
684
|
"#{@root}v#{v}/"
|
|
623
685
|
end
|
|
@@ -625,15 +687,45 @@ module HaveAPI
|
|
|
625
687
|
# @param v [String] API version
|
|
626
688
|
# @param provider [Authentication::Base]
|
|
627
689
|
# @param prefix [String]
|
|
628
|
-
def add_auth_routes(v, provider, prefix: '')
|
|
629
|
-
provider.register_routes(@sinatra,
|
|
690
|
+
def add_auth_routes(v, provider, prefix: '', global: false)
|
|
691
|
+
provider.register_routes(@sinatra, auth_prefix(v, prefix, global:))
|
|
630
692
|
end
|
|
631
693
|
|
|
632
|
-
def add_auth_module(v, name, mod, prefix: '')
|
|
694
|
+
def add_auth_module(v, name, mod, prefix: '', global: false)
|
|
633
695
|
@routes[v] ||= { authentication: { name => { resources: {} } } }
|
|
634
696
|
|
|
635
697
|
HaveAPI.get_version_resources(mod, v).each do |r|
|
|
636
|
-
mount_resource("#{
|
|
698
|
+
mount_resource("#{auth_prefix(v, prefix, global:)}/", v, r, @routes[v][:authentication][name][:resources])
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def auth_prefix(v, prefix, global:)
|
|
703
|
+
root = global ? "#{@root}_auth" : "#{version_prefix(v)}_auth"
|
|
704
|
+
"#{root}/#{prefix}"
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
def json_content_type?(request)
|
|
708
|
+
media_type = if request.respond_to?(:media_type)
|
|
709
|
+
request.media_type
|
|
710
|
+
else
|
|
711
|
+
request.content_type.to_s.split(';').first
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
media_type == 'application/json' || media_type.to_s.end_with?('+json')
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
def path_params(route, params)
|
|
718
|
+
route.action.path_param_names(route.path).each_with_object({}) do |name, ret|
|
|
719
|
+
value = if params.has_key?(name.to_sym)
|
|
720
|
+
params[name.to_sym]
|
|
721
|
+
elsif params.has_key?(name)
|
|
722
|
+
params[name]
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
next if value.nil?
|
|
726
|
+
|
|
727
|
+
ret[name] = value
|
|
728
|
+
ret[name.to_sym] = value
|
|
637
729
|
end
|
|
638
730
|
end
|
|
639
731
|
|
data/lib/haveapi/validator.rb
CHANGED
|
@@ -33,11 +33,15 @@ module HaveAPI
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def valid?(v)
|
|
36
|
+
return false unless v.respond_to?(:to_str)
|
|
37
|
+
|
|
38
|
+
matched = @rx.match?(v.to_str)
|
|
39
|
+
|
|
36
40
|
if @match
|
|
37
|
-
|
|
41
|
+
matched
|
|
38
42
|
|
|
39
43
|
else
|
|
40
|
-
|
|
44
|
+
!matched
|
|
41
45
|
end
|
|
42
46
|
end
|
|
43
47
|
end
|
data/lib/haveapi/version.rb
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
<h4><%= client.label %></h4>
|
|
2
|
-
<pre><code class="<%= client.code %>"><%= client.auth(host, base_url, api_version, method, desc) %></code></pre>
|
|
2
|
+
<pre><code class="<%= client.code %>"><%= escape_html(client.auth(host, base_url, api_version, method, desc)) %></code></pre>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<h6><%= client.label %></h6>
|
|
2
2
|
<% sample = client.new(host, base_url, api_version, r_name, resource, a_name, action) %>
|
|
3
3
|
<% if sample.respond_to?(:example) %>
|
|
4
|
-
<pre><code class="<%= client.code %>"><%= sample.example(example) %></code></pre>
|
|
4
|
+
<pre><code class="<%= client.code %>"><%= escape_html(sample.example(example)) %></code></pre>
|
|
5
5
|
|
|
6
6
|
<% else %>
|
|
7
7
|
<h6>Request</h6>
|
|
8
|
-
<pre><code class="<%= client.code %>"><%= sample.request(example) %></code></pre>
|
|
8
|
+
<pre><code class="<%= client.code %>"><%= escape_html(sample.request(example)) %></code></pre>
|
|
9
9
|
<h6>Response</h6>
|
|
10
|
-
<pre><code class="<%= client.code %>"><%= sample.response(example) %></code></pre>
|
|
10
|
+
<pre><code class="<%= client.code %>"><%= escape_html(sample.response(example)) %></code></pre>
|
|
11
11
|
<% end %>
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
<h4><%= client.label %></h4>
|
|
2
|
-
<pre><code class="<%= client.code %>"><%= client.init(host, base_url, api_version) %></code></pre>
|
|
2
|
+
<pre><code class="<%= client.code %>"><%= escape_html(client.init(host, base_url, api_version)) %></code></pre>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<h1 id="api">API v<%= @v %></h1>
|
|
2
2
|
|
|
3
3
|
<ol class="breadcrumb">
|
|
4
|
-
<li><a href="<%= root %>"><%= host %></a></li>
|
|
4
|
+
<li><a href="<%= escape_html(root) %>"><%= escape_html(host) %></a></li>
|
|
5
5
|
<li class="active">v<%= @v %></li>
|
|
6
6
|
</ol>
|
|
7
7
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
<li><a href="https://github.com/vpsfreecz/haveapi-client-php" target="_blank">PHP</a></li>
|
|
22
22
|
<li>
|
|
23
23
|
<a href="https://github.com/vpsfreecz/haveapi-webui" target="_blank">Generic web interface</a>
|
|
24
|
-
(<a href="https://webui.haveapi.org/v<%= version %>/#<%=
|
|
24
|
+
(<a href="https://webui.haveapi.org/v<%= version %>/#<%= urlescape(base_url) %>" target="_blank">connect to this API</a>)
|
|
25
25
|
</li>
|
|
26
26
|
<li><a href="https://github.com/vpsfreecz/haveapi-fs" target="_blank">FUSE-based file system</a></li>
|
|
27
27
|
</ul>
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<h1>Authentication</h1>
|
|
2
2
|
<p class="authentication">
|
|
3
3
|
<% if current_user %>
|
|
4
|
-
Logged as <%= current_user.login %> [<a class="logout" href="<%= logout_url %>">logout</a>]
|
|
4
|
+
Logged as <%= escape_html(current_user.login) %> [<a class="logout" href="<%= escape_html(logout_url) %>">logout</a>]
|
|
5
|
+
<% elsif @help[:authentication].any? %>
|
|
6
|
+
<a class="login btn btn-default" href="<%= escape_html(url("#{root}_login")) %>">Login</a>
|
|
5
7
|
<% else %>
|
|
6
|
-
|
|
8
|
+
Authentication disabled.
|
|
7
9
|
<% end %>
|
|
8
10
|
</p>
|
|
9
11
|
<p>
|
|
@@ -9,6 +9,10 @@ module AuthorizeSpec
|
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
class << self
|
|
13
|
+
attr_accessor :shared_hash_list
|
|
14
|
+
end
|
|
15
|
+
|
|
12
16
|
class BasicProvider < HaveAPI::Authentication::Basic::Provider
|
|
13
17
|
protected
|
|
14
18
|
|
|
@@ -120,6 +124,46 @@ describe AuthorizeSpec do
|
|
|
120
124
|
items.select { |item| item[:owner_id] == restrictions[:owner_id] }
|
|
121
125
|
end
|
|
122
126
|
end
|
|
127
|
+
|
|
128
|
+
define_action(:OwnerList) do
|
|
129
|
+
route 'owners/{owner_id}/list'
|
|
130
|
+
http_method :get
|
|
131
|
+
|
|
132
|
+
authorize do |_user, path_params|
|
|
133
|
+
restrict owner_id: path_params['owner_id'].to_i
|
|
134
|
+
allow
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
output(:object_list) do
|
|
138
|
+
integer :id
|
|
139
|
+
integer :owner_id
|
|
140
|
+
string :name
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
define_method(:exec) do
|
|
144
|
+
restrictions = with_restricted
|
|
145
|
+
items.select { |item| item[:owner_id] == restrictions[:owner_id] }
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
define_action(:HashList) do
|
|
150
|
+
route 'hash_list'
|
|
151
|
+
http_method :get
|
|
152
|
+
|
|
153
|
+
authorize do |user|
|
|
154
|
+
output blacklist: [:secret] unless user.admin?
|
|
155
|
+
allow
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
output(:hash_list) do
|
|
159
|
+
string :public
|
|
160
|
+
string :secret
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def exec
|
|
164
|
+
AuthorizeSpec.shared_hash_list
|
|
165
|
+
end
|
|
166
|
+
end
|
|
123
167
|
end
|
|
124
168
|
end
|
|
125
169
|
|
|
@@ -137,11 +181,32 @@ describe AuthorizeSpec do
|
|
|
137
181
|
}
|
|
138
182
|
end
|
|
139
183
|
|
|
184
|
+
let(:full_hash_list) do
|
|
185
|
+
[
|
|
186
|
+
{
|
|
187
|
+
public: 'visible',
|
|
188
|
+
secret: 'admin-only'
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
end
|
|
192
|
+
|
|
140
193
|
def call_get_action(resource, action, params = {})
|
|
141
194
|
env 'rack.input', StringIO.new('')
|
|
142
195
|
call_api(resource, action, params)
|
|
143
196
|
end
|
|
144
197
|
|
|
198
|
+
def with_pre_authorize(listener)
|
|
199
|
+
hooks = HaveAPI::Hooks.hooks
|
|
200
|
+
action_hooks = hooks[HaveAPI::Action][:pre_authorize]
|
|
201
|
+
original = action_hooks[:listeners].dup
|
|
202
|
+
|
|
203
|
+
HaveAPI::Action.connect_hook(:pre_authorize, &listener)
|
|
204
|
+
|
|
205
|
+
yield
|
|
206
|
+
ensure
|
|
207
|
+
action_hooks[:listeners] = original
|
|
208
|
+
end
|
|
209
|
+
|
|
145
210
|
it 'denies non-admins from admin-only action' do
|
|
146
211
|
login('user', 'pass')
|
|
147
212
|
call_get_action([:Item], :admin_only, {})
|
|
@@ -226,6 +291,25 @@ describe AuthorizeSpec do
|
|
|
226
291
|
expect(api_response[:item]).to have_key(:secret)
|
|
227
292
|
end
|
|
228
293
|
|
|
294
|
+
it 'does not mutate shared hash list output while filtering fields' do
|
|
295
|
+
described_class.shared_hash_list = full_hash_list.map(&:dup)
|
|
296
|
+
|
|
297
|
+
login('user', 'pass')
|
|
298
|
+
call_get_action([:Item], :hash_list, {})
|
|
299
|
+
|
|
300
|
+
expect(last_response.status).to eq(200)
|
|
301
|
+
expect(api_response).to be_ok
|
|
302
|
+
expect(api_response[:items]).to eq([{ public: 'visible' }])
|
|
303
|
+
expect(described_class.shared_hash_list).to eq(full_hash_list)
|
|
304
|
+
|
|
305
|
+
login('admin', 'pass')
|
|
306
|
+
call_get_action([:Item], :hash_list, {})
|
|
307
|
+
|
|
308
|
+
expect(last_response.status).to eq(200)
|
|
309
|
+
expect(api_response).to be_ok
|
|
310
|
+
expect(api_response[:items]).to eq(full_hash_list)
|
|
311
|
+
end
|
|
312
|
+
|
|
229
313
|
it 'restricts list results to the current user' do
|
|
230
314
|
login('user', 'pass')
|
|
231
315
|
call_get_action([:Item], :list, {})
|
|
@@ -259,6 +343,21 @@ describe AuthorizeSpec do
|
|
|
259
343
|
expect(owners).to eq([1])
|
|
260
344
|
end
|
|
261
345
|
|
|
346
|
+
it 'fails closed when action restrictions conflict with global restrictions' do
|
|
347
|
+
with_pre_authorize(proc do |ret, _context|
|
|
348
|
+
ret[:blocks] << proc do |user|
|
|
349
|
+
restrict owner_id: user.id
|
|
350
|
+
end
|
|
351
|
+
ret
|
|
352
|
+
end) do
|
|
353
|
+
login('user', 'pass')
|
|
354
|
+
get '/v1/items/owners/2/list', {}, input: ''
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
expect(last_response.status).to eq(403)
|
|
358
|
+
expect(api_response).not_to be_ok
|
|
359
|
+
end
|
|
360
|
+
|
|
262
361
|
it 'hides admin-only actions from non-admin documentation' do
|
|
263
362
|
login('user', 'pass')
|
|
264
363
|
call_api(:options, '/v1/')
|