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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/haveapi.gemspec +1 -1
  4. data/lib/haveapi/action.rb +125 -36
  5. data/lib/haveapi/actions/paginable.rb +3 -1
  6. data/lib/haveapi/authentication/basic/provider.rb +2 -0
  7. data/lib/haveapi/authentication/chain.rb +11 -7
  8. data/lib/haveapi/authentication/oauth2/config.rb +25 -3
  9. data/lib/haveapi/authentication/oauth2/provider.rb +92 -11
  10. data/lib/haveapi/authentication/oauth2/revoke_endpoint.rb +44 -3
  11. data/lib/haveapi/authentication/token/provider.rb +53 -15
  12. data/lib/haveapi/authorization.rb +42 -18
  13. data/lib/haveapi/client_examples/php_client.rb +1 -1
  14. data/lib/haveapi/client_examples/ruby_client.rb +1 -1
  15. data/lib/haveapi/context.rb +10 -4
  16. data/lib/haveapi/example.rb +15 -16
  17. data/lib/haveapi/extensions/action_exceptions.rb +6 -6
  18. data/lib/haveapi/model_adapters/active_record.rb +150 -71
  19. data/lib/haveapi/model_adapters/hash.rb +1 -1
  20. data/lib/haveapi/parameters/resource.rb +50 -6
  21. data/lib/haveapi/parameters/typed.rb +40 -13
  22. data/lib/haveapi/params.rb +27 -8
  23. data/lib/haveapi/resource.rb +4 -1
  24. data/lib/haveapi/resources/action_state.rb +13 -5
  25. data/lib/haveapi/route.rb +2 -2
  26. data/lib/haveapi/server.rb +137 -45
  27. data/lib/haveapi/validator.rb +2 -2
  28. data/lib/haveapi/validator_chain.rb +1 -0
  29. data/lib/haveapi/validators/confirmation.rb +1 -0
  30. data/lib/haveapi/validators/format.rb +6 -2
  31. data/lib/haveapi/validators/length.rb +2 -0
  32. data/lib/haveapi/validators/numericality.rb +2 -0
  33. data/lib/haveapi/validators/presence.rb +1 -1
  34. data/lib/haveapi/version.rb +1 -1
  35. data/lib/haveapi/views/version_page/client_auth.erb +1 -1
  36. data/lib/haveapi/views/version_page/client_example.erb +3 -3
  37. data/lib/haveapi/views/version_page/client_init.erb +1 -1
  38. data/lib/haveapi/views/version_page.erb +2 -2
  39. data/lib/haveapi/views/version_sidebar.erb +4 -2
  40. data/spec/action/authorize_spec.rb +99 -0
  41. data/spec/action/runtime_spec.rb +426 -0
  42. data/spec/action_state_spec.rb +52 -0
  43. data/spec/authentication/basic_spec.rb +29 -0
  44. data/spec/authentication/oauth2_spec.rb +329 -0
  45. data/spec/authentication/token_spec.rb +195 -0
  46. data/spec/authentication/token_version_routes_spec.rb +164 -0
  47. data/spec/authorization_spec.rb +66 -0
  48. data/spec/documentation/auth_filtering_spec.rb +195 -1
  49. data/spec/documentation/current_user_html_escaping_spec.rb +47 -0
  50. data/spec/documentation/examples_spec.rb +97 -0
  51. data/spec/documentation/host_html_escaping_spec.rb +41 -0
  52. data/spec/documentation_spec.rb +13 -0
  53. data/spec/extensions/action_exceptions_spec.rb +30 -0
  54. data/spec/model_adapters/active_record_spec.rb +408 -3
  55. data/spec/parameters/typed_spec.rb +75 -7
  56. data/spec/params_spec.rb +41 -0
  57. data/spec/server/integration_spec.rb +90 -0
  58. data/spec/validator_chain_spec.rb +39 -0
  59. data/spec/validators/confirmation_spec.rb +14 -0
  60. data/spec/validators/format_spec.rb +7 -0
  61. data/spec/validators/length_spec.rb +6 -0
  62. data/spec/validators/numericality_spec.rb +7 -0
  63. data/spec/validators/presence_spec.rb +2 -0
  64. data/test_support/client_test_api.rb +31 -3
  65. metadata +8 -4
  66. data/shell.nix +0 -20
@@ -49,8 +49,12 @@ module HaveAPI
49
49
  return if @formatter
50
50
 
51
51
  @formatter = OutputFormatter.new
52
-
53
- unless @formatter.supports?(request.accept)
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.each do |k, v|
178
- ret += "<dt>#{k}</dt><dd>#{escape_html(v.to_s)}</dd>"
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
- authenticated?(settings.api_server.default_version)
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: current_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: current_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: current_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/([^\.]+)(\.md)?} do |f, _|
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
- @sidebar = erb :"doc_sidebars/#{f}"
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
- begin
491
- body = request.body.read
525
+ raw_body = request.body.read
526
+ body_method = !%i[get head options].include?(route.http_method.to_sym)
492
527
 
493
- body = if body.empty?
494
- nil
495
- else
496
- JSON.parse(body, symbolize_names: true)
497
- end
498
- rescue StandardError => e
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
- action = route.action.new(request, v, params, body, Context.new(
503
- settings.api_server,
504
- version: v,
505
- request: self,
506
- action: route.action,
507
- path: route.path,
508
- params:,
509
- user: current_user,
510
- endpoint: true,
511
- resource_path: route.resource_path
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
- context.version = @default_version
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
- default_version: @default_version,
584
- versions: { default: describe_version(context) }
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, "#{@root}_auth/#{prefix}")
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("#{@root}_auth/#{prefix}/", v, r, @routes[v][:authentication][name][:resources])
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
 
@@ -89,9 +89,9 @@ module HaveAPI
89
89
  # may use this information as it will.
90
90
  def validate(v, params)
91
91
  @params = params
92
- ret = valid?(v)
92
+ valid?(v)
93
+ ensure
93
94
  @params = nil
94
- ret
95
95
  end
96
96
 
97
97
  protected
@@ -69,6 +69,7 @@ module HaveAPI
69
69
  ret = []
70
70
 
71
71
  @validators.each do |validator|
72
+ validator = validator.clone
72
73
  next if validator.validate(value, params)
73
74
 
74
75
  ret << format(validator.message, value:)
@@ -20,6 +20,7 @@ module HaveAPI
20
20
 
21
21
  def setup
22
22
  @param = simple? ? take : take(:param)
23
+ @param = @param.to_sym if @param.is_a?(::String)
23
24
  @equal = take(:equal, true)
24
25
  @message = take(
25
26
  :message,
@@ -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
- @rx.match(v) ? true : false
41
+ matched
38
42
 
39
43
  else
40
- @rx.match(v) ? false : true
44
+ !matched
41
45
  end
42
46
  end
43
47
  end
@@ -62,6 +62,8 @@ module HaveAPI
62
62
  end
63
63
 
64
64
  def valid?(v)
65
+ return false unless v.respond_to?(:length)
66
+
65
67
  len = v.length
66
68
 
67
69
  return len == @equals if @equals
@@ -94,6 +94,8 @@ module HaveAPI
94
94
  v = v.to_i
95
95
  end
96
96
 
97
+ return false unless v.is_a?(::Numeric)
98
+
97
99
  ret = true
98
100
  ret = false if @min && v < @min
99
101
  ret = false if @max && v > @max
@@ -35,8 +35,8 @@ module HaveAPI
35
35
  def valid?(v)
36
36
  return false if v.nil?
37
37
  return !v.strip.empty? if !@empty && v.is_a?(::String)
38
+ return !v.empty? if !@empty && v.respond_to?(:empty?)
38
39
 
39
- # FIXME: other data types?
40
40
  true
41
41
  end
42
42
  end
@@ -1,4 +1,4 @@
1
1
  module HaveAPI
2
2
  PROTOCOL_VERSION = '2.0'.freeze
3
- VERSION = '0.27.2'.freeze
3
+ VERSION = '0.28.0'.freeze
4
4
  end
@@ -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 %>/#<%= escape(base_url) %>" target="_blank">connect to this API</a>)
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
- <a class="login btn btn-default" href="<%= url("#{root}_login") %>">Login</a>
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/')