haveapi 0.12.1 → 0.13.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/doc/protocol.md +27 -10
  3. data/haveapi.gemspec +1 -2
  4. data/lib/haveapi/action.rb +23 -7
  5. data/lib/haveapi/actions/default.rb +3 -3
  6. data/lib/haveapi/authentication/base.rb +19 -1
  7. data/lib/haveapi/authentication/basic/provider.rb +7 -1
  8. data/lib/haveapi/authentication/chain.rb +10 -12
  9. data/lib/haveapi/authentication/token/action_config.rb +53 -0
  10. data/lib/haveapi/authentication/token/action_request.rb +23 -0
  11. data/lib/haveapi/authentication/token/action_result.rb +42 -0
  12. data/lib/haveapi/authentication/token/config.rb +115 -0
  13. data/lib/haveapi/authentication/token/provider.rb +259 -81
  14. data/lib/haveapi/authentication/token.rb +9 -0
  15. data/lib/haveapi/client_examples/curl.rb +3 -3
  16. data/lib/haveapi/client_examples/fs_client.rb +3 -3
  17. data/lib/haveapi/client_examples/http.rb +7 -7
  18. data/lib/haveapi/client_examples/js_client.rb +1 -1
  19. data/lib/haveapi/client_examples/php_client.rb +1 -1
  20. data/lib/haveapi/client_examples/ruby_cli.rb +1 -1
  21. data/lib/haveapi/client_examples/ruby_client.rb +1 -1
  22. data/lib/haveapi/context.rb +13 -13
  23. data/lib/haveapi/example.rb +3 -3
  24. data/lib/haveapi/exceptions.rb +2 -0
  25. data/lib/haveapi/extensions/exception_mailer.rb +8 -1
  26. data/lib/haveapi/model_adapters/active_record.rb +6 -6
  27. data/lib/haveapi/parameters/resource.rb +10 -10
  28. data/lib/haveapi/params.rb +1 -1
  29. data/lib/haveapi/resource.rb +18 -10
  30. data/lib/haveapi/resources/action_state.rb +2 -2
  31. data/lib/haveapi/route.rb +4 -3
  32. data/lib/haveapi/server.rb +7 -7
  33. data/lib/haveapi/spec/mock_action.rb +3 -3
  34. data/lib/haveapi/spec/spec_methods.rb +8 -8
  35. data/lib/haveapi/version.rb +2 -2
  36. data/lib/haveapi/views/version_page.erb +2 -2
  37. data/shell.nix +1 -1
  38. metadata +9 -5
  39. data/lib/haveapi/authentication/token/resources.rb +0 -110
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c535654ced1007d57fa8d9c619ae9c17e9c3feea7ff45635579af316c2d18d85
4
- data.tar.gz: 6de601f8d1ccb5de8343a517172f91f439c84c854439914334cab6aba8905a7b
3
+ metadata.gz: 5e44e03fec908d742be4499f22c97f16453cd53bd4a23a14928af94d3456bea2
4
+ data.tar.gz: '0339228a0003baae1528050052b930acedbad51651893e4ce8329945bf98a5f0'
5
5
  SHA512:
6
- metadata.gz: f06faee46f829bc5c6facba8587317f3db4a08d293ed0981a3cabb9c521e877e886a90038f552974e14305333d18d5d7e4a21e31812c272f524eb871b102f67d
7
- data.tar.gz: 1876ae96a3979b8e34f4e3c582450f73c3573431ba671ef0964e866214e7d59e74e7e2ecc15e8a42a4925c89f2c9be730749f16d87dc0a6c9c6536b7c1823970
6
+ metadata.gz: 157bfeeaf24ea9304e96051c4a05e9454cdfbb818ac7476efbb7e587547305bbd1cb54bb9acfebf0f7ac3a8b553766b7527fa80a8af575d306df051e46394be6
7
+ data.tar.gz: 1dbfe70f5b5e7f15c1cc779df344a09afcc0912df4a74ac27ff51f45f65f3eef574dd66ff0fee80c7f6b44f093491c0242494fbd52af7b46a1674ad3971f1933
data/doc/protocol.md CHANGED
@@ -94,14 +94,29 @@ HTTP basic authentication needs no other configuration, only informs about its p
94
94
 
95
95
  "basic": {}
96
96
 
97
+ HTTP basic authentication does not support multi-factor authentication.
98
+
97
99
  ### Token authentication
98
- Token authentication contains a resource ``token``, that is used
99
- to acquire and revoke token.
100
+ Token authentication contains resource ``token``, that is used to acquire
101
+ and revoke tokens.
102
+
103
+ Tokens are acquired by action ``request``, in which the client provides arbitrary
104
+ login credentials. If the login credentials match, the server either concludes
105
+ the authentication process, or multiple authentication steps may be necessary.
106
+
107
+ For single-step authentication processses, action ``request`` returns the
108
+ token and its validity period. If multiple authentication steps are necessary,
109
+ the server signals this by returning ``complete = true``. The client then has
110
+ to call the next authentication step, which is an action on the ``token``
111
+ resource, as returned in ``next_action``.
100
112
 
101
- Token is acquired by action ``request``. The client provides login and password and gets a token
102
- that is used afterwards. Token has a validity period, which may also be infinity.
113
+ Tokens are returned in both cases. If the authentication is finished, the token
114
+ is then used for authenticating further requests. If the authentication continues,
115
+ the token is used to authenticate the next authentication request, which then
116
+ returns another token.
103
117
 
104
- Token can be revoked by calling the ``revoke`` action.
118
+ After the authentication is complete, acquired tokens can be renewed using action
119
+ ``renew`` and revoked by calling the ``revoke`` action.
105
120
 
106
121
  "token": {
107
122
  "http_header": "<name of HTTP header to transfer token in, by default X-HaveAPI-Auth-Token>",
@@ -113,7 +128,7 @@ Token can be revoked by calling the ``revoke`` action.
113
128
  "input": {
114
129
  ...
115
130
  "parameters": {
116
- "login": ...
131
+ "user": ...
117
132
  "password": ...
118
133
  "lifetime": ...
119
134
  "interval": ...
@@ -125,6 +140,8 @@ Token can be revoked by calling the ``revoke`` action.
125
140
  "parameters": {
126
141
  "token": ...
127
142
  "valid_to": ...
143
+ "complete": true/false
144
+ "next_action": ...
128
145
  },
129
146
  ...
130
147
  },
@@ -176,7 +193,7 @@ Every action is described as:
176
193
  ... list of examples ...
177
194
  ],
178
195
  "meta": ... metadata ...,
179
- "url": "URL for this action",
196
+ "path": "URL for this action",
180
197
  "method": "HTTP method to be used",
181
198
  "help": "URL to get this very description of the action"
182
199
  }
@@ -365,12 +382,12 @@ This is used for associations between resources, e.g. car has a wheel.
365
382
  "value_id": "<name of a parameter that is used as an id>",
366
383
  "value_label": "<name of a parameter that is used as a value>",
367
384
  "value": {
368
- "url": "URL to 'show' action of associated resource",
385
+ "path": "URL to 'show' action of associated resource",
369
386
  "method": "HTTP method to use",
370
387
  "help": "URL to get the associated resource's 'show' description"
371
388
  },
372
389
  "choices": {
373
- "url": "URL to action that returns a list of possible associations",
390
+ "path": "URL to action that returns a list of possible associations",
374
391
  "method": "HTTP method to use",
375
392
  "help": "URL to description of the list action"
376
393
  }
@@ -391,7 +408,7 @@ render them according to its syntax.
391
408
 
392
409
  {
393
410
  "title": "A title",
394
- "url_params: [ ... array of integers ... ],
411
+ "path_params: [ ... array of integers ... ],
395
412
  "request": {
396
413
  ... a hash of request parameters ...
397
414
  },
data/haveapi.gemspec CHANGED
@@ -5,7 +5,6 @@ require 'haveapi/version'
5
5
  Gem::Specification.new do |s|
6
6
  s.name = 'haveapi'
7
7
  s.version = HaveAPI::VERSION
8
- s.date = '2017-11-27'
9
8
  s.summary =
10
9
  s.description = 'Framework for creating self-describing APIs'
11
10
  s.authors = 'Jakub Skokan'
@@ -24,6 +23,6 @@ Gem::Specification.new do |s|
24
23
  s.add_runtime_dependency 'rake'
25
24
  s.add_runtime_dependency 'github-markdown'
26
25
  s.add_runtime_dependency 'nesty', '~> 1.0'
27
- s.add_runtime_dependency 'haveapi-client', '~> 0.12.0'
26
+ s.add_runtime_dependency 'haveapi-client', '~> 0.13.0'
28
27
  s.add_runtime_dependency 'mail'
29
28
  end
@@ -31,7 +31,8 @@ module HaveAPI
31
31
  attr_accessor :flags
32
32
 
33
33
  class << self
34
- attr_reader :resource, :authorization, :examples
34
+ attr_accessor :resource
35
+ attr_reader :authorization, :examples
35
36
 
36
37
  def inherited(subclass)
37
38
  # puts "Action.inherited called #{subclass} from #{to_s}"
@@ -45,7 +46,7 @@ module HaveAPI
45
46
  end
46
47
 
47
48
  def delayed_inherited(subclass)
48
- resource = Kernel.const_get(subclass.to_s.deconstantize)
49
+ resource = subclass.resource || Kernel.const_get(subclass.to_s.deconstantize)
49
50
 
50
51
  inherit_attrs(subclass)
51
52
  inherit_attrs_from_resource(subclass, resource, [:auth])
@@ -177,14 +178,29 @@ module HaveAPI
177
178
  @examples << e
178
179
  end
179
180
 
181
+ def action_name
182
+ (@action_name ? @action_name.to_s : to_s).demodulize
183
+ end
184
+
185
+ def action_name=(name)
186
+ @action_name = name
187
+ end
188
+
180
189
  def build_route(prefix)
181
- route = @route || to_s.demodulize.underscore
190
+ route = @route || action_name.underscore
191
+ if @route
192
+ @route
193
+ elsif action_name
194
+ action_name.to_s.demodulize.underscore
195
+ else
196
+ to_s.demodulize.underscore
197
+ end
182
198
 
183
199
  if !route.is_a?(String) && route.respond_to?(:call)
184
200
  route = route.call(self.resource)
185
201
  end
186
202
 
187
- prefix + route % {resource: self.resource.to_s.demodulize.underscore}
203
+ prefix + route % {resource: self.resource.resource_name.underscore}
188
204
  end
189
205
 
190
206
  def describe(context)
@@ -214,9 +230,9 @@ module HaveAPI
214
230
  output: @output ? @output.describe(context) : {parameters: {}},
215
231
  meta: @meta ? @meta.merge(@meta) { |_, v| v && v.describe(context) } : nil,
216
232
  examples: @examples ? @examples.describe(context) : [],
217
- url: context.resolved_url,
233
+ path: context.resolved_path,
218
234
  method: route_method,
219
- help: "#{context.url}?method=#{route_method}"
235
+ help: "#{context.path}?method=#{route_method}"
220
236
  }
221
237
  end
222
238
 
@@ -245,7 +261,7 @@ module HaveAPI
245
261
  ret
246
262
  end
247
263
 
248
- def resolve_url_params(object)
264
+ def resolve_path_params(object)
249
265
  if self.resolve
250
266
  self.resolve.call(object)
251
267
 
@@ -38,18 +38,18 @@ module HaveAPI
38
38
  end
39
39
 
40
40
  class Show < Action
41
- route ->(r){ r.singular ? '' : ':%{resource}_id' }
41
+ route ->(r){ r.singular ? '' : '{%{resource}_id}' }
42
42
  http_method :get
43
43
  aliases %i(find)
44
44
  end
45
45
 
46
46
  class Update < Action
47
- route ->(r){ r.singular ? '' : ':%{resource}_id' }
47
+ route ->(r){ r.singular ? '' : '{%{resource}_id}' }
48
48
  http_method :put
49
49
  end
50
50
 
51
51
  class Delete < Action
52
- route ->(r){ r.singular ? '' : ':%{resource}_id' }
52
+ route ->(r){ r.singular ? '' : '{%{resource}_id}' }
53
53
  http_method :delete
54
54
  aliases %i(destroy)
55
55
  end
@@ -2,14 +2,32 @@ module HaveAPI
2
2
  module Authentication
3
3
  # Base class for authentication providers.
4
4
  class Base
5
- attr_accessor :name, :resources
5
+ # Get or set auth method name
6
+ # @param v [Symbol, nil]
7
+ # @return [Symbol]
8
+ def self.auth_method(v = nil)
9
+ if v
10
+ @auth_method = v
11
+ else
12
+ @auth_method || name.split('::').last.underscore.to_sym
13
+ end
14
+ end
15
+
16
+ # @return [Symbol]
17
+ attr_reader :name
6
18
 
7
19
  def initialize(server, v)
20
+ @name = self.class.auth_method
8
21
  @server = server
9
22
  @version = v
10
23
  setup
11
24
  end
12
25
 
26
+ # @return [Module, nil]
27
+ def resource_module
28
+ nil
29
+ end
30
+
13
31
  # Reimplement this method in your authentication provider.
14
32
  # +request+ is passed directly from Sinatra.
15
33
  def authenticate(request)
@@ -17,12 +17,18 @@ module HaveAPI::Authentication
17
17
  # ...
18
18
  # api.auth_chain << MyBasicAuth
19
19
  class Provider < Base
20
+ auth_method :basic
21
+
20
22
  def authenticate(request)
21
23
  user = nil
22
24
 
23
25
  auth = Rack::Auth::Basic::Request.new(request.env)
24
26
  if auth.provided? && auth.basic? && auth.credentials
25
- user = find_user(request, *auth.credentials)
27
+ begin
28
+ user = find_user(request, *auth.credentials)
29
+ rescue HaveAPI::AuthenticationError
30
+ user = nil
31
+ end
26
32
  end
27
33
 
28
34
  user
@@ -50,17 +50,17 @@ module HaveAPI::Authentication
50
50
 
51
51
  def describe(context)
52
52
  ret = {}
53
-
53
+
54
54
  return ret unless @instances[context.version]
55
55
 
56
56
  @instances[context.version].each do |provider|
57
57
  ret[provider.name] = provider.describe
58
58
 
59
- if provider.resources
59
+ if provider.resource_module
60
60
  ret[provider.name][:resources] = {}
61
61
 
62
62
  @server.routes[context.version][:authentication][provider.name][:resources].each do |r, children|
63
- ret[provider.name][:resources][r.to_s.demodulize.underscore.to_sym] = r.describe(children, context)
63
+ ret[provider.name][:resources][r.resource_name.underscore.to_sym] = r.describe(children, context)
64
64
  end
65
65
  end
66
66
  end
@@ -97,17 +97,15 @@ module HaveAPI::Authentication
97
97
  protected
98
98
  def register_provider(v, p)
99
99
  instance = p.new(@server, v)
100
- parts = p.superclass.name.split('::')
101
-
102
- instance.name = parts[-2].underscore.to_sym
103
-
104
100
  @instances[v] << instance
105
101
 
106
- provider = Kernel.const_get(parts[0..-2].join('::'))
107
-
108
- if provider.const_defined?('Resources')
109
- instance.resources = provider.const_get('Resources')
110
- @server.add_auth_module(v, instance.name, instance.resources, prefix: parts[-2].underscore)
102
+ if resource_module = instance.resource_module
103
+ @server.add_auth_module(
104
+ v,
105
+ instance.name,
106
+ resource_module,
107
+ prefix: instance.name.to_s,
108
+ )
111
109
  end
112
110
  end
113
111
  end
@@ -0,0 +1,53 @@
1
+ module HaveAPI::Authentication
2
+ module Token
3
+ class ActionConfig
4
+ # @param block [Proc]
5
+ # @param opts [Hash]
6
+ # @option opts [Boolean] :input
7
+ # @option opts [Boolean] :handle
8
+ def initialize(block, opts = {})
9
+ @block = block
10
+ @opts = with_defaults(opts)
11
+ update(block)
12
+ end
13
+
14
+ # @param block [Proc]
15
+ def update(block)
16
+ instance_exec(&block)
17
+ end
18
+
19
+ # Configure input parameters in the context of {HaveAPI::Params}
20
+ def input(&block)
21
+ if block && check!(:input)
22
+ @input = block
23
+ else
24
+ @input
25
+ end
26
+ end
27
+
28
+ # Handle the action
29
+ # @yieldparam request [ActionRequest]
30
+ # @yieldparam result [ActionResult]
31
+ # @yieldreturn [ActionResult]
32
+ def handle(&block)
33
+ if block && check!(:handle)
34
+ @handle = block
35
+ else
36
+ @handle
37
+ end
38
+ end
39
+
40
+ private
41
+ def check!(name)
42
+ fail "#{name} cannot be configured" unless @opts[name]
43
+ true
44
+ end
45
+
46
+ def with_defaults(opts)
47
+ Hash[%i(input handle).map do |v|
48
+ [v, opts.has_key?(v) ? opts[v] : true]
49
+ end]
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,23 @@
1
+ module HaveAPI::Authentication
2
+ module Token
3
+ class ActionRequest
4
+ # @return [Sinatra::Request]
5
+ attr_reader :request
6
+
7
+ # @return [Hash]
8
+ attr_reader :input
9
+
10
+ # @return [Object, nil]
11
+ attr_reader :user
12
+
13
+ # @return [String, nil]
14
+ attr_reader :token
15
+
16
+ def initialize(opts = {})
17
+ opts.each do |k, v|
18
+ instance_variable_set(:"@#{k}", v)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,42 @@
1
+ module HaveAPI::Authentication
2
+ module Token
3
+ class ActionResult
4
+ # @param complete [Boolean]
5
+ # @return [Boolean]
6
+ attr_accessor :complete
7
+
8
+ # @param error [String]
9
+ # @return [String, nil]
10
+ attr_accessor :error
11
+
12
+ # @param token [String]
13
+ # @return [String, nil]
14
+ attr_accessor :token
15
+
16
+ # @param valid_to [Time]
17
+ # @return [Time, nil]
18
+ attr_accessor :valid_to
19
+
20
+ # @param next_action [String]
21
+ # @return [String, nil]
22
+ attr_accessor :next_action
23
+
24
+ def initialize
25
+ @ok = false
26
+ end
27
+
28
+ def ok
29
+ @ok = true
30
+ self
31
+ end
32
+
33
+ def ok?
34
+ @ok && @error.nil?
35
+ end
36
+
37
+ def complete?
38
+ @complete ? true : false
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,115 @@
1
+ module HaveAPI::Authentication
2
+ module Token
3
+ # Configuration for {HaveAPI::Authentication::Token::Provider}
4
+ #
5
+ # Create a subclass and use with {HaveAPI::Authentication::Token#with_config}.
6
+ class Config
7
+ class << self
8
+ # Configure token request action
9
+ def request(&block)
10
+ if block
11
+ if @request
12
+ @request.update(block)
13
+ else
14
+ @request = ActionConfig.new(block)
15
+ end
16
+ else
17
+ @request
18
+ end
19
+ end
20
+
21
+ %i(renew revoke).each do |name|
22
+ # Configuration method
23
+ define_method(name) do |&block|
24
+ var = :"@#{name}"
25
+ val = instance_variable_get(var)
26
+
27
+ if block
28
+ if val
29
+ val.update(block)
30
+ else
31
+ instance_variable_set(var, ActionConfig.new(block, input: false))
32
+ end
33
+ else
34
+ val
35
+ end
36
+ end
37
+ end
38
+
39
+ # @param name [Symbol]
40
+ def action(name, &block)
41
+ @actions ||= {}
42
+ @actions[name] = ActionConfig.new(block)
43
+ end
44
+
45
+ # @return [Hash]
46
+ def actions
47
+ @actions || {}
48
+ end
49
+
50
+ # HTTP header that is searched for token
51
+ # @param header [String, nil]
52
+ # @return [String]
53
+ def http_header(header = nil)
54
+ if header
55
+ @http_header = header
56
+ else
57
+ @http_header || 'X-HaveAPI-Auth-Token'
58
+ end
59
+ end
60
+
61
+ # Query parameter searched for token
62
+ # @param param [Symbol]
63
+ # @return [Symbol]
64
+ def query_parameter(param = nil)
65
+ if param
66
+ @query_param = param
67
+ else
68
+ @query_param || :_auth_token
69
+ end
70
+ end
71
+
72
+ def inherited(subclass)
73
+ # Default request
74
+ subclass.request do
75
+ input do
76
+ string :user, label: 'User', required: true
77
+ password :password, label: 'Password', required: true
78
+ end
79
+
80
+ handle do
81
+ raise NotImplementedError
82
+ end
83
+ end
84
+
85
+ # Default renew and revoke
86
+ %i(renew revoke).each do |name|
87
+ subclass.send(name) do
88
+ handle do
89
+ raise NotImplementedError
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ def initialize(server, v)
97
+ @server = server
98
+ @version = v
99
+ end
100
+
101
+ # Authenticate request by `token`
102
+ #
103
+ # Return user object or nil.
104
+ # If the token was created as auto-renewable, this method
105
+ # is responsible for its renewal.
106
+ # Must be implemented.
107
+ # @param request [Sinatra::Request]
108
+ # @param token [String]
109
+ # @return [Object, nil]
110
+ def find_user_by_token(request, token)
111
+
112
+ end
113
+ end
114
+ end
115
+ end