haveapi 0.27.3 → 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 +140 -68
- data/lib/haveapi/model_adapters/hash.rb +1 -1
- data/lib/haveapi/parameters/resource.rb +35 -3
- data/lib/haveapi/parameters/typed.rb +26 -7
- data/lib/haveapi/params.rb +27 -8
- data/lib/haveapi/resource.rb +4 -1
- data/lib/haveapi/resources/action_state.rb +8 -1
- 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 +406 -1
- data/spec/parameters/typed_spec.rb +42 -0
- 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 +28 -0
- metadata +8 -4
- data/shell.nix +0 -20
|
@@ -97,7 +97,7 @@ module HaveAPI::Parameters
|
|
|
97
97
|
attrs.each { |k, v| instance_variable_set("@#{k}", v) }
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
-
def clean(raw)
|
|
100
|
+
def clean(raw, context = nil)
|
|
101
101
|
if raw.nil?
|
|
102
102
|
return nil if nullable?
|
|
103
103
|
|
|
@@ -105,15 +105,19 @@ module HaveAPI::Parameters
|
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
if raw.is_a?(String)
|
|
108
|
-
stripped = raw
|
|
108
|
+
stripped = strip_string(raw)
|
|
109
109
|
return nil if stripped.empty? && nullable?
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
extra = @extra.merge(optional: optional?, nullable: nullable?)
|
|
113
113
|
|
|
114
|
-
::HaveAPI::ModelAdapter.for(
|
|
114
|
+
ret = ::HaveAPI::ModelAdapter.for(
|
|
115
115
|
show_action.input.layout, @resource.model
|
|
116
116
|
).input_clean(@resource.model, raw, extra)
|
|
117
|
+
|
|
118
|
+
authorize_record!(ret, context)
|
|
119
|
+
|
|
120
|
+
ret
|
|
117
121
|
end
|
|
118
122
|
|
|
119
123
|
def validate(v, params)
|
|
@@ -126,6 +130,12 @@ module HaveAPI::Parameters
|
|
|
126
130
|
|
|
127
131
|
private
|
|
128
132
|
|
|
133
|
+
def strip_string(value)
|
|
134
|
+
value.strip
|
|
135
|
+
rescue ArgumentError, Encoding::CompatibilityError
|
|
136
|
+
raise HaveAPI::ValidationError, 'invalid string encoding'
|
|
137
|
+
end
|
|
138
|
+
|
|
129
139
|
def build_resource_path(r)
|
|
130
140
|
path = []
|
|
131
141
|
top_module = Kernel
|
|
@@ -144,5 +154,27 @@ module HaveAPI::Parameters
|
|
|
144
154
|
|
|
145
155
|
path
|
|
146
156
|
end
|
|
157
|
+
|
|
158
|
+
def authorize_record!(record, context)
|
|
159
|
+
return if record.nil? || context.nil?
|
|
160
|
+
return unless show_action.authorization
|
|
161
|
+
|
|
162
|
+
path = show_action.build_route('')
|
|
163
|
+
path_params = show_action.path_params(path, show_action.resolve_path_params(record))
|
|
164
|
+
child_context = HaveAPI::Context.new(
|
|
165
|
+
context.server,
|
|
166
|
+
version: context.version,
|
|
167
|
+
request: context.request,
|
|
168
|
+
action: show_action,
|
|
169
|
+
path:,
|
|
170
|
+
params: path_params,
|
|
171
|
+
user: context.current_user,
|
|
172
|
+
endpoint: context.endpoint
|
|
173
|
+
)
|
|
174
|
+
action = show_action.new(context.request, context.version, path_params, nil, child_context)
|
|
175
|
+
return if action.authorized?(context.current_user)
|
|
176
|
+
|
|
177
|
+
raise HaveAPI::ValidationError, 'resource not found'
|
|
178
|
+
end
|
|
147
179
|
end
|
|
148
180
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'date'
|
|
2
|
+
require 'time'
|
|
2
3
|
|
|
3
4
|
module HaveAPI::Parameters
|
|
4
5
|
class Typed
|
|
@@ -78,7 +79,7 @@ module HaveAPI::Parameters
|
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
def clean(raw)
|
|
81
|
-
return instance_exec(raw, &@clean) if @clean
|
|
82
|
+
return validate_cleaned_value(instance_exec(raw, &@clean)) if @clean
|
|
82
83
|
|
|
83
84
|
if raw.nil?
|
|
84
85
|
return nil if nullable?
|
|
@@ -87,7 +88,7 @@ module HaveAPI::Parameters
|
|
|
87
88
|
end
|
|
88
89
|
|
|
89
90
|
if raw.is_a?(String)
|
|
90
|
-
stripped = raw
|
|
91
|
+
stripped = strip_string(raw)
|
|
91
92
|
return nil if stripped.empty? && nullable?
|
|
92
93
|
end
|
|
93
94
|
|
|
@@ -144,6 +145,20 @@ module HaveAPI::Parameters
|
|
|
144
145
|
|
|
145
146
|
private
|
|
146
147
|
|
|
148
|
+
def validate_cleaned_value(value)
|
|
149
|
+
if value.nil? && !nullable?
|
|
150
|
+
raise HaveAPI::ValidationError, 'cannot be null'
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
value
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def strip_string(value)
|
|
157
|
+
value.strip
|
|
158
|
+
rescue ArgumentError, Encoding::CompatibilityError
|
|
159
|
+
raise HaveAPI::ValidationError, 'invalid string encoding'
|
|
160
|
+
end
|
|
161
|
+
|
|
147
162
|
def coerce_integer(raw)
|
|
148
163
|
case raw
|
|
149
164
|
when Integer
|
|
@@ -155,7 +170,7 @@ module HaveAPI::Parameters
|
|
|
155
170
|
|
|
156
171
|
raw.to_i
|
|
157
172
|
when String
|
|
158
|
-
s = raw
|
|
173
|
+
s = strip_string(raw)
|
|
159
174
|
|
|
160
175
|
if s.empty? || !s.match?(/\A[+-]?\d+\z/)
|
|
161
176
|
raise HaveAPI::ValidationError, "not a valid integer #{raw.inspect}"
|
|
@@ -172,7 +187,7 @@ module HaveAPI::Parameters
|
|
|
172
187
|
f = raw.to_f
|
|
173
188
|
|
|
174
189
|
elsif raw.is_a?(String)
|
|
175
|
-
s = raw
|
|
190
|
+
s = strip_string(raw)
|
|
176
191
|
raise HaveAPI::ValidationError, "not a valid float #{raw.inspect}" if s.empty?
|
|
177
192
|
|
|
178
193
|
begin
|
|
@@ -199,7 +214,7 @@ module HaveAPI::Parameters
|
|
|
199
214
|
return true if raw == 1
|
|
200
215
|
|
|
201
216
|
elsif raw.is_a?(String)
|
|
202
|
-
s = raw
|
|
217
|
+
s = strip_string(raw)
|
|
203
218
|
raise HaveAPI::ValidationError, "not a valid boolean #{raw.inspect}" if s.empty?
|
|
204
219
|
|
|
205
220
|
return true if %w[true t yes y 1].include?(s.downcase)
|
|
@@ -210,12 +225,16 @@ module HaveAPI::Parameters
|
|
|
210
225
|
end
|
|
211
226
|
|
|
212
227
|
def coerce_datetime(raw)
|
|
213
|
-
|
|
228
|
+
unless raw.is_a?(String)
|
|
229
|
+
raise HaveAPI::ValidationError, "not in ISO 8601 format '#{raw}'"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
if strip_string(raw).empty?
|
|
214
233
|
raise HaveAPI::ValidationError, "not in ISO 8601 format '#{raw}'"
|
|
215
234
|
end
|
|
216
235
|
|
|
217
236
|
DateTime.iso8601(raw).to_time
|
|
218
|
-
rescue ArgumentError
|
|
237
|
+
rescue ArgumentError, TypeError
|
|
219
238
|
raise HaveAPI::ValidationError, "not in ISO 8601 format '#{raw}'"
|
|
220
239
|
end
|
|
221
240
|
|
data/lib/haveapi/params.rb
CHANGED
|
@@ -134,12 +134,12 @@ module HaveAPI
|
|
|
134
134
|
|
|
135
135
|
if include
|
|
136
136
|
@include ||= []
|
|
137
|
-
@include.concat(include)
|
|
137
|
+
@include.concat(normalize_names(include))
|
|
138
138
|
end
|
|
139
139
|
|
|
140
140
|
if exclude
|
|
141
141
|
@exclude ||= []
|
|
142
|
-
@exclude.concat(exclude)
|
|
142
|
+
@exclude.concat(normalize_names(exclude))
|
|
143
143
|
end
|
|
144
144
|
|
|
145
145
|
instance_eval(&block)
|
|
@@ -209,10 +209,17 @@ module HaveAPI
|
|
|
209
209
|
# First step of validation. Check if input is in correct namespace
|
|
210
210
|
# and has a correct layout.
|
|
211
211
|
def check_layout(params)
|
|
212
|
-
|
|
212
|
+
value = namespace ? params[namespace] : params
|
|
213
|
+
|
|
214
|
+
if value.nil?
|
|
215
|
+
raise ValidationError.new('invalid input layout', {}) if any_required_params?
|
|
216
|
+
|
|
217
|
+
elsif !valid_layout?(value)
|
|
213
218
|
raise ValidationError.new('invalid input layout', {})
|
|
214
219
|
end
|
|
215
220
|
|
|
221
|
+
return unless namespace
|
|
222
|
+
|
|
216
223
|
case layout
|
|
217
224
|
when :object, :hash
|
|
218
225
|
params[namespace] ||= {}
|
|
@@ -224,12 +231,15 @@ module HaveAPI
|
|
|
224
231
|
|
|
225
232
|
# Third step of validation. Check if all required params are present,
|
|
226
233
|
# convert params to correct data types, set default values if necessary.
|
|
227
|
-
def validate(params)
|
|
234
|
+
def validate(params, context: nil, only: nil)
|
|
228
235
|
errors = {}
|
|
236
|
+
permitted = only && only.map(&:to_sym)
|
|
229
237
|
|
|
230
238
|
layout_aware(params) do |input|
|
|
231
239
|
# First run - coerce values to correct types
|
|
232
240
|
@params.each do |p|
|
|
241
|
+
next if permitted && !permitted.include?(p.name)
|
|
242
|
+
|
|
233
243
|
if p.required? && input[p.name].nil?
|
|
234
244
|
errors[p.name] = ['required parameter missing']
|
|
235
245
|
next
|
|
@@ -241,7 +251,11 @@ module HaveAPI
|
|
|
241
251
|
end
|
|
242
252
|
|
|
243
253
|
begin
|
|
244
|
-
cleaned = p.clean
|
|
254
|
+
cleaned = if p.method(:clean).arity.abs > 1
|
|
255
|
+
p.clean(input[p.name], context)
|
|
256
|
+
else
|
|
257
|
+
p.clean(input[p.name])
|
|
258
|
+
end
|
|
245
259
|
rescue ValidationError => e
|
|
246
260
|
errors[p.name] ||= []
|
|
247
261
|
errors[p.name] << e.message
|
|
@@ -253,6 +267,7 @@ module HaveAPI
|
|
|
253
267
|
|
|
254
268
|
# Second run - validate parameters
|
|
255
269
|
@params.each do |p|
|
|
270
|
+
next if permitted && !permitted.include?(p.name)
|
|
256
271
|
next if errors.has_key?(p.name)
|
|
257
272
|
next if input[p.name].nil?
|
|
258
273
|
|
|
@@ -305,13 +320,17 @@ module HaveAPI
|
|
|
305
320
|
kwargs
|
|
306
321
|
end
|
|
307
322
|
|
|
308
|
-
def
|
|
323
|
+
def normalize_names(names)
|
|
324
|
+
names.map { |v| v.is_a?(String) ? v.to_sym : v }
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def valid_layout?(value)
|
|
309
328
|
case layout
|
|
310
329
|
when :object, :hash
|
|
311
|
-
|
|
330
|
+
value.is_a?(Hash)
|
|
312
331
|
|
|
313
332
|
when :object_list, :hash_list
|
|
314
|
-
|
|
333
|
+
value.is_a?(Array) && value.all?(Hash)
|
|
315
334
|
|
|
316
335
|
else
|
|
317
336
|
false
|
data/lib/haveapi/resource.rb
CHANGED
|
@@ -98,7 +98,10 @@ module HaveAPI
|
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
hash[:resources].each do |resource, children|
|
|
101
|
-
|
|
101
|
+
child = resource.describe(children, context)
|
|
102
|
+
next if child[:actions].empty? && child[:resources].empty?
|
|
103
|
+
|
|
104
|
+
ret[:resources][resource.resource_name.underscore] = child
|
|
102
105
|
end
|
|
103
106
|
|
|
104
107
|
context.resource_path = orig_resource_path
|
|
@@ -77,12 +77,15 @@ module HaveAPI::Resources
|
|
|
77
77
|
class Poll < HaveAPI::Action
|
|
78
78
|
include Mixin
|
|
79
79
|
|
|
80
|
+
MAX_TIMEOUT = 30
|
|
81
|
+
|
|
80
82
|
desc 'Returns when the action is completed or timeout occurs'
|
|
81
83
|
http_method :get
|
|
82
84
|
route '{%{resource}_id}/poll'
|
|
83
85
|
|
|
84
86
|
input(:hash) do
|
|
85
|
-
float :timeout, label: 'Timeout', desc: 'in seconds', default: 15, fill: true
|
|
87
|
+
float :timeout, label: 'Timeout', desc: 'in seconds', default: 15, fill: true,
|
|
88
|
+
number: { min: 0, max: MAX_TIMEOUT }
|
|
86
89
|
float :update_in, label: 'Progress',
|
|
87
90
|
desc: 'number of seconds after which the state is returned if the progress ' \
|
|
88
91
|
'has changed',
|
|
@@ -99,6 +102,10 @@ module HaveAPI::Resources
|
|
|
99
102
|
authorize { allow }
|
|
100
103
|
|
|
101
104
|
def exec
|
|
105
|
+
if input[:timeout] > MAX_TIMEOUT
|
|
106
|
+
error!("timeout has to be maximally #{MAX_TIMEOUT}", {}, http_status: 400)
|
|
107
|
+
end
|
|
108
|
+
|
|
102
109
|
t = Time.now
|
|
103
110
|
|
|
104
111
|
loop do
|
data/lib/haveapi/route.rb
CHANGED
|
@@ -3,8 +3,8 @@ module HaveAPI
|
|
|
3
3
|
attr_reader :path, :sinatra_path, :action, :resource_path
|
|
4
4
|
|
|
5
5
|
def initialize(path, action, resource_path)
|
|
6
|
-
@
|
|
7
|
-
@
|
|
6
|
+
@sinatra_path = path.gsub(/:([a-zA-Z0-9\-_]+)/, '{\1}')
|
|
7
|
+
@path = @sinatra_path
|
|
8
8
|
@action = action
|
|
9
9
|
@resource_path = resource_path
|
|
10
10
|
end
|
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
|