haveapi 0.12.1 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
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