haveapi 0.3.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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/lib/haveapi/action.rb +521 -0
  3. data/lib/haveapi/actions/default.rb +55 -0
  4. data/lib/haveapi/actions/paginable.rb +12 -0
  5. data/lib/haveapi/api.rb +66 -0
  6. data/lib/haveapi/authentication/base.rb +37 -0
  7. data/lib/haveapi/authentication/basic/provider.rb +37 -0
  8. data/lib/haveapi/authentication/chain.rb +110 -0
  9. data/lib/haveapi/authentication/token/provider.rb +166 -0
  10. data/lib/haveapi/authentication/token/resources.rb +107 -0
  11. data/lib/haveapi/authorization.rb +108 -0
  12. data/lib/haveapi/common.rb +38 -0
  13. data/lib/haveapi/context.rb +78 -0
  14. data/lib/haveapi/example.rb +36 -0
  15. data/lib/haveapi/extensions/action_exceptions.rb +25 -0
  16. data/lib/haveapi/extensions/base.rb +9 -0
  17. data/lib/haveapi/extensions/resource_prefetch.rb +7 -0
  18. data/lib/haveapi/hooks.rb +190 -0
  19. data/lib/haveapi/metadata.rb +56 -0
  20. data/lib/haveapi/model_adapter.rb +119 -0
  21. data/lib/haveapi/model_adapters/active_record.rb +352 -0
  22. data/lib/haveapi/model_adapters/hash.rb +27 -0
  23. data/lib/haveapi/output_formatter.rb +57 -0
  24. data/lib/haveapi/output_formatters/base.rb +29 -0
  25. data/lib/haveapi/output_formatters/json.rb +9 -0
  26. data/lib/haveapi/params/param.rb +114 -0
  27. data/lib/haveapi/params/resource.rb +109 -0
  28. data/lib/haveapi/params.rb +314 -0
  29. data/lib/haveapi/public/css/bootstrap-theme.min.css +7 -0
  30. data/lib/haveapi/public/css/bootstrap.min.css +7 -0
  31. data/lib/haveapi/public/js/bootstrap.min.js +6 -0
  32. data/lib/haveapi/public/js/jquery-1.11.1.min.js +4 -0
  33. data/lib/haveapi/resource.rb +120 -0
  34. data/lib/haveapi/route.rb +22 -0
  35. data/lib/haveapi/server.rb +440 -0
  36. data/lib/haveapi/spec/helpers.rb +103 -0
  37. data/lib/haveapi/types.rb +24 -0
  38. data/lib/haveapi/version.rb +3 -0
  39. data/lib/haveapi/views/doc_layout.erb +27 -0
  40. data/lib/haveapi/views/doc_sidebars/create-client.erb +20 -0
  41. data/lib/haveapi/views/doc_sidebars/protocol.erb +42 -0
  42. data/lib/haveapi/views/index.erb +12 -0
  43. data/lib/haveapi/views/main_layout.erb +50 -0
  44. data/lib/haveapi/views/version_page.erb +195 -0
  45. data/lib/haveapi/views/version_sidebar.erb +42 -0
  46. data/lib/haveapi.rb +22 -0
  47. metadata +242 -0
@@ -0,0 +1,37 @@
1
+ module HaveAPI::Authentication
2
+ module Basic
3
+ # HTTP basic authentication provider.
4
+ #
5
+ # Example usage:
6
+ # class MyBasicAuth < HaveAPI::Authentication::Basic::Provider
7
+ # protected
8
+ # def find_user(request, username, password)
9
+ # ::User.find_by(login: username, password: password)
10
+ # end
11
+ # end
12
+ #
13
+ # Finally put the provider in the authentication chain:
14
+ # api = HaveAPI.new(...)
15
+ # ...
16
+ # api.auth_chain << MyBasicAuth
17
+ class Provider < Base
18
+ def authenticate(request)
19
+ user = nil
20
+
21
+ auth = Rack::Auth::Basic::Request.new(request.env)
22
+ if auth.provided? && auth.basic? && auth.credentials
23
+ user = find_user(request, *auth.credentials)
24
+ end
25
+
26
+ user
27
+ end
28
+
29
+ protected
30
+ # Reimplement this method. It has to return an authenticated
31
+ # user or nil.
32
+ def find_user(request, username, password)
33
+
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,110 @@
1
+ module HaveAPI::Authentication
2
+ # Authentication chain.
3
+ # At every request, #authenticate is called to authenticate user.
4
+ class Chain
5
+ def initialize(server)
6
+ @server = server
7
+ @chain = {}
8
+ @instances = {}
9
+ end
10
+
11
+ def setup(versions)
12
+ versions.each do |v|
13
+ @instances[v] ||= []
14
+
15
+ @chain[v] && @chain[v].each { |p| register_provider(v, p) }
16
+ end
17
+
18
+ if @chain[:all]
19
+ @chain[:all].each do |p|
20
+ @instances.each_key { |v| register_provider(v, p) }
21
+ end
22
+ end
23
+
24
+ # @chain.each do |p|
25
+ # @instances << p.new(@server)
26
+ #
27
+ # parts = p.to_s.split('::')
28
+ # mod = Kernel.const_get((parts[0..-2] << 'Resources').join('::'))
29
+ #
30
+ # @server.add_module(mod, prefix: parts[-2].tableize) if mod
31
+ # end
32
+ end
33
+
34
+ # Iterate through authentication providers registered for version +v+
35
+ # until authentication is successful or the end is reached and user
36
+ # is not authenticated.
37
+ # Authentication provider can deny the user access by calling Base#deny.
38
+ def authenticate(v, *args)
39
+ catch(:return) do
40
+ @instances[v].each do |provider|
41
+ u = provider.authenticate(*args)
42
+ return u if u
43
+ end
44
+ end
45
+
46
+ nil
47
+ end
48
+
49
+ def describe(context)
50
+ ret = {}
51
+
52
+ @instances[context.version].each do |provider|
53
+ ret[provider.name] = provider.describe
54
+
55
+ if provider.resources
56
+ ret[provider.name][:resources] = {}
57
+
58
+ @server.routes[context.version][:authentication][provider.name][:resources].each do |r, children|
59
+ ret[provider.name][:resources][r.to_s.demodulize.underscore.to_sym] = r.describe(children, context)
60
+ end
61
+ end
62
+ end
63
+
64
+ ret
65
+ end
66
+
67
+ # Return provider list for version +v+.
68
+ # Used for registering providers to specific version, e.g.
69
+ # api.auth_chain[1] << MyAuthProvider
70
+ def [](v)
71
+ @chain[v] ||= []
72
+ @chain[v]
73
+ end
74
+
75
+ # Register authentication +provider+ for all available API versions.
76
+ # +provider+ may also be an Array of providers.
77
+ def <<(provider)
78
+ @chain[:all] ||= []
79
+
80
+ if provider.is_a?(Array)
81
+ provider.each { |p| @chain[:all] << p }
82
+ else
83
+ @chain[:all] << provider
84
+ end
85
+
86
+ self
87
+ end
88
+
89
+ def empty?
90
+ @chain.empty?
91
+ end
92
+
93
+ protected
94
+ def register_provider(v, p)
95
+ instance = p.new(@server, v)
96
+ parts = p.superclass.name.split('::')
97
+
98
+ instance.name = parts[-2].underscore.to_sym
99
+
100
+ @instances[v] << instance
101
+
102
+ provider = Kernel.const_get(parts[0..-2].join('::'))
103
+
104
+ if provider.const_defined?('Resources')
105
+ instance.resources = provider.const_get('Resources')
106
+ @server.add_auth_module(v, instance.name, instance.resources, prefix: parts[-2].underscore)
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,166 @@
1
+ module HaveAPI::Authentication
2
+ module Token
3
+ # Exception that has to be raised when generated token already exists.
4
+ # Provider will catch it and generate another token.
5
+ class TokenExists < Exception
6
+
7
+ end
8
+
9
+ # Provider for token authentication. This class has to be subclassed
10
+ # and implemented.
11
+ #
12
+ # Token auth contains resource token. User can request a token by calling
13
+ # action Resources::Token::Request. Returned token is then used for
14
+ # authenticating the user. Client sends the token with each request
15
+ # in configured #http_header or #query_parameter.
16
+ #
17
+ # Token can be revoked by calling Resources::Token::Revoke.
18
+ #
19
+ # === \Example usage:
20
+ #
21
+ # \Token model:
22
+ # class ApiToken < ActiveRecord::Base
23
+ # belongs_to :user
24
+ #
25
+ # validates :user_id, :token, presence: true
26
+ # validates :token, length: {is: 100}
27
+ #
28
+ # enum lifetime: %i(fixed renewable_manual renewable_auto permanent)
29
+ #
30
+ # def renew
31
+ # self.valid_to = Time.now + interval
32
+ # end
33
+ # end
34
+ #
35
+ # Authentication provider:
36
+ # class MyTokenAuth < HaveAPI::Authentication::Token::Provider
37
+ # protected
38
+ # def save_token(request, user, token, lifetime, interval)
39
+ # user.tokens << ::Token.new(token: token, lifetime: lifetime,
40
+ # valid_to: (lifetime != 'permanent' ? Time.now + interval : nil),
41
+ # interval: interval, label: request.user_agent)
42
+ # end
43
+ #
44
+ # def revoke_token(request, user, token)
45
+ # user.tokens.delete(token: token)
46
+ # end
47
+ #
48
+ # def renew_token(request, user, token)
49
+ # t = ::Token.find_by(user: user, token: token)
50
+ #
51
+ # if t.lifetime.start_with('renewable')
52
+ # t.renew
53
+ # t.save
54
+ # t.valid_to
55
+ # end
56
+ # end
57
+ #
58
+ # def find_user_by_credentials(request, username, password)
59
+ # ::User.find_by(login: username, password: password)
60
+ # end
61
+ #
62
+ # def find_user_by_token(request, token)
63
+ # t = ::Token.find_by(token: token)
64
+ #
65
+ # if t
66
+ # # Renew the token if needed
67
+ # if t.lifetime == 'renewable_auto'
68
+ # t.renew
69
+ # t.save
70
+ # end
71
+ #
72
+ # t.user # return the user
73
+ # end
74
+ # end
75
+ # end
76
+ #
77
+ # Finally put the provider in the authentication chain:
78
+ # api = HaveAPI.new(...)
79
+ # ...
80
+ # api.auth_chain << MyTokenAuth
81
+ class Provider < Base
82
+ def setup
83
+ Resources::Token.token_instance ||= {}
84
+ Resources::Token.token_instance[@version] = self
85
+
86
+ @server.allow_header(http_header)
87
+ end
88
+
89
+ def authenticate(request)
90
+ t = token(request)
91
+
92
+ t && find_user_by_token(request, t)
93
+ end
94
+
95
+ def token(request)
96
+ request[query_parameter] || request.env[header_to_env]
97
+ end
98
+
99
+ def describe
100
+ {
101
+ http_header: http_header,
102
+ query_parameter: query_parameter,
103
+ }
104
+ end
105
+
106
+ protected
107
+ # HTTP header that is searched for token.
108
+ def http_header
109
+ 'X-HaveAPI-Auth-Token'
110
+ end
111
+
112
+ # Query parameter searched for token.
113
+ def query_parameter
114
+ :_auth_token
115
+ end
116
+
117
+ # Generate token. Implicit implementation returns token of 100 chars.
118
+ def generate_token
119
+ SecureRandom.hex(50)
120
+ end
121
+
122
+ # Save generated +token+ for +user+. Token has given +lifetime+
123
+ # and when not permanent, also a +interval+ of validity.
124
+ # Returns a date time which is token expiration.
125
+ # It is up to the implementation of this method to remember
126
+ # token lifetime and interval.
127
+ # Must be implemented.
128
+ def save_token(request, user, token, lifetime, interval)
129
+
130
+ end
131
+
132
+ # Revoke existing +token+ for +user+.
133
+ # Must be implemented.
134
+ def revoke_token(request, user, token)
135
+
136
+ end
137
+
138
+ # Renew existing +token+ for +user+.
139
+ # Returns a date time which is token expiration.
140
+ # Must be implemented.
141
+ def renew_token(request, user, token)
142
+
143
+ end
144
+
145
+ # Used by action Resources::Token::Request when the user is requesting
146
+ # a token. This method returns user object or nil.
147
+ # Must be implemented.
148
+ def find_user_by_credentials(request, username, password)
149
+
150
+ end
151
+
152
+ # Authenticate user by +token+. Return user object or nil.
153
+ # If the token was created as auto-renewable, this method
154
+ # is responsible for its renewal.
155
+ # Must be implemented.
156
+ def find_user_by_token(request, token)
157
+
158
+ end
159
+
160
+ private
161
+ def header_to_env
162
+ "HTTP_#{http_header.upcase.gsub(/\-/, '_')}"
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,107 @@
1
+ module HaveAPI::Authentication::Token
2
+ module Resources
3
+ class Token < HaveAPI::Resource
4
+ auth false
5
+ version :all
6
+
7
+ class << self
8
+ attr_accessor :token_instance
9
+ end
10
+
11
+ class Request < HaveAPI::Action
12
+ route ''
13
+ http_method :post
14
+
15
+ input(:hash) do
16
+ string :login, label: 'Login', required: true
17
+ string :password, label: 'Password', required: true
18
+ string :lifetime, label: 'Lifetime', required: true,
19
+ choices: %i(fixed renewable_manual renewable_auto permanent),
20
+ desc: <<END
21
+ fixed - the token has a fixed validity period, it cannot be renewed
22
+ renewable_manual - the token can be renewed, but it must be done manually via renew action
23
+ renewable_auto - the token is renewed automatically to now+interval every time it is used
24
+ permanent - the token will be valid forever, unless deleted
25
+ END
26
+ integer :interval, label: 'Interval',
27
+ desc: 'How long will requested token be valid, in seconds.',
28
+ default: 60*5, fill: true
29
+ end
30
+
31
+ output(:hash) do
32
+ string :token
33
+ datetime :valid_to
34
+ end
35
+
36
+ authorize do
37
+ allow
38
+ end
39
+
40
+ def exec
41
+ klass = self.class.resource.token_instance[@version]
42
+
43
+ user = klass.send(
44
+ :find_user_by_credentials,
45
+ request,
46
+ input[:login],
47
+ input[:password]
48
+ )
49
+ error('bad login or password') unless user
50
+
51
+ token = expiration = nil
52
+
53
+ loop do
54
+ begin
55
+ token = klass.send(:generate_token)
56
+ expiration = klass.send(:save_token,
57
+ @request,
58
+ user,
59
+ token,
60
+ params[:token][:lifetime],
61
+ params[:token][:interval])
62
+ break
63
+
64
+ rescue TokenExists
65
+ next
66
+ end
67
+ end
68
+
69
+ {token: token, valid_to: expiration}
70
+ end
71
+ end
72
+
73
+ class Revoke < HaveAPI::Action
74
+ # route ''
75
+ http_method :post
76
+ auth true
77
+
78
+ authorize do
79
+ allow
80
+ end
81
+
82
+ def exec
83
+ klass = self.class.resource.token_instance[@version]
84
+ klass.send(:revoke_token, request, current_user, klass.token(request))
85
+ end
86
+ end
87
+
88
+ class Renew < HaveAPI::Action
89
+ http_method :post
90
+ auth true
91
+
92
+ output(:hash) do
93
+ datetime :valid_to
94
+ end
95
+
96
+ authorize do
97
+ allow
98
+ end
99
+
100
+ def exec
101
+ klass = self.class.resource.token_instance[@version]
102
+ klass.send(:renew_token, request, current_user, klass.token(request))
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,108 @@
1
+ module HaveAPI
2
+ class Authorization
3
+ def initialize(&block)
4
+ @block = block
5
+ end
6
+
7
+ # Returns true if user is authorized.
8
+ # Block must call allow to authorize user, default rule is deny.
9
+ def authorized?(user)
10
+ @restrict = []
11
+
12
+ catch(:rule) do
13
+ instance_exec(user, &@block) if @block
14
+ deny # will not be called if block throws allow
15
+ end
16
+ end
17
+
18
+ # Apply restrictions on query which selects objects from database.
19
+ # Most common usage is restrict user to access only objects he owns.
20
+ def restrict(*args)
21
+ @restrict << args.first
22
+ end
23
+
24
+ # Restrict parameters client can set/change.
25
+ # [whitelist] allow only listed parameters
26
+ # [blacklist] allow all parameters except listed ones
27
+ def input(whitelist: nil, blacklist: nil)
28
+ @input = {
29
+ whitelist: whitelist,
30
+ blacklist: blacklist,
31
+ }
32
+ end
33
+
34
+ # Restrict parameters client can retrieve.
35
+ # [whitelist] allow only listed parameters
36
+ # [blacklist] allow all parameters except listed ones
37
+ def output(whitelist: nil, blacklist: nil)
38
+ @output = {
39
+ whitelist: whitelist,
40
+ blacklist: blacklist,
41
+ }
42
+ end
43
+
44
+ def allow
45
+ throw(:rule, true)
46
+ end
47
+
48
+ def deny
49
+ throw(:rule, false)
50
+ end
51
+
52
+ def restrictions
53
+ ret = {}
54
+
55
+ @restrict.each do |r|
56
+ ret.update(r)
57
+ end
58
+
59
+ ret
60
+ end
61
+
62
+ def filter_input(input, params)
63
+ filter_inner(input, @input, params, false)
64
+ end
65
+
66
+ def filter_output(output, params, format = false)
67
+ filter_inner(output, @output, params, format)
68
+ end
69
+
70
+ private
71
+ def filter_inner(allowed_params, direction, params, format)
72
+ allowed = {}
73
+
74
+ allowed_params.each do |p|
75
+ if params.has_param?(p.name)
76
+ allowed[p.name] = format ? p.format_output(params[p.name]) : params[p.name]
77
+
78
+ elsif params.has_param?(p.name.to_s) # FIXME: remove double checking
79
+ allowed[p.name] = format ? p.format_output(params[p.name.to_s]) : params[p.name.to_s]
80
+ end
81
+ end
82
+
83
+ return allowed unless direction
84
+
85
+ if direction[:whitelist]
86
+ ret = {}
87
+
88
+ direction[:whitelist].each do |p|
89
+ ret[p] = allowed[p] if allowed && allowed[p]
90
+ end
91
+
92
+ ret
93
+
94
+ elsif direction[:blacklist]
95
+ ret = allowed.dup
96
+
97
+ direction[:blacklist].each do |p|
98
+ ret.delete(p)
99
+ end
100
+
101
+ ret
102
+
103
+ else
104
+ allowed
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,38 @@
1
+ module HaveAPI
2
+ class Common
3
+ class << self
4
+ attr_accessor :custom_attrs
5
+
6
+ def has_attr(name, default=nil)
7
+ @custom_attrs ||= []
8
+ @custom_attrs << name
9
+
10
+ instance_variable_set("@#{name}", default)
11
+
12
+ self.class.send(:define_method, name) do |value=nil|
13
+ if value.nil?
14
+ instance_variable_get("@#{name}")
15
+ else
16
+ instance_variable_set("@#{name}", value)
17
+ end
18
+ end
19
+ end
20
+
21
+ # Called before subclass defines it's attributes (before has_attr or custom
22
+ # attr setting), so copy defaults from parent and let it override it.
23
+ def inherit_attrs(subclass)
24
+ return unless @custom_attrs
25
+
26
+ subclass.custom_attrs = []
27
+
28
+ @custom_attrs.each do |attr|
29
+ # puts "#{subclass}: Inherit #{attr} = #{instance_variable_get("@#{attr}")}"
30
+ subclass.method(attr).call(instance_variable_get("@#{attr}"))
31
+ subclass.custom_attrs << attr
32
+ end
33
+ end
34
+ end
35
+
36
+ has_attr :obj_type
37
+ end
38
+ end
@@ -0,0 +1,78 @@
1
+ module HaveAPI
2
+ class Context
3
+ attr_accessor :server, :version, :resource, :action, :url, :args,
4
+ :params, :current_user, :authorization, :endpoint,
5
+ :action_instance, :action_prepare, :layout
6
+
7
+ def initialize(server, version: nil, resource: [], action: nil,
8
+ url: nil, args: nil, params: nil, user: nil,
9
+ authorization: nil, endpoint: nil)
10
+ @server = server
11
+ @version = version
12
+ @resource = resource
13
+ @action = action
14
+ @url = url
15
+ @args = args
16
+ @params = params
17
+ @current_user = user
18
+ @authorization = authorization
19
+ @endpoint = endpoint
20
+ end
21
+
22
+ def resolved_url
23
+ return @url unless @args
24
+
25
+ ret = @url.dup
26
+
27
+ @args.each do |arg|
28
+ resolve_arg!(ret, arg)
29
+ end
30
+
31
+ ret
32
+ end
33
+
34
+ def url_for(action, args=nil)
35
+ top_module = Kernel
36
+ top_route = @server.routes[@version]
37
+
38
+ action.to_s.split('::').each do |name|
39
+ top_module = top_module.const_get(name)
40
+
41
+ begin
42
+ top_module.obj_type
43
+
44
+ rescue NoMethodError
45
+ next
46
+ end
47
+
48
+ if top_module.obj_type == :resource
49
+ top_route = top_route[:resources][top_module]
50
+ else
51
+ top_route = top_route[:actions][top_module]
52
+ end
53
+ end
54
+
55
+ ret = top_route.dup
56
+
57
+ args.each { |arg| resolve_arg!(ret, arg) } if args
58
+
59
+ ret
60
+ end
61
+
62
+ def call_url_params(action, obj)
63
+ ret = params && action.resolve.call(obj)
64
+
65
+ return [ret] if ret && !ret.is_a?(Array)
66
+ ret
67
+ end
68
+
69
+ def url_with_params(action, obj)
70
+ url_for(action, call_url_params(action, obj))
71
+ end
72
+
73
+ private
74
+ def resolve_arg!(url, arg)
75
+ url.sub!(/:[a-zA-Z\-_]+/, arg.to_s)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,36 @@
1
+ module HaveAPI
2
+ class Example
3
+ def initialize(title)
4
+ @title = title
5
+ end
6
+
7
+ def request(f)
8
+ @request = f
9
+ end
10
+
11
+ def response(f)
12
+ @response = f
13
+ end
14
+
15
+ def comment(str)
16
+ @comment = str
17
+ end
18
+
19
+ def provided?
20
+ @request || @response || @comment
21
+ end
22
+
23
+ def describe
24
+ if provided?
25
+ {
26
+ title: @title,
27
+ request: @request,
28
+ response: @response,
29
+ comment: @comment
30
+ }
31
+ else
32
+ {}
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,25 @@
1
+ module HaveAPI::Extensions
2
+ class ActionExceptions < Base
3
+ class << self
4
+ def enabled
5
+ HaveAPI::Action.connect_hook(:exec_exception) do |ret, action, e|
6
+ break(ret) unless @exceptions
7
+
8
+ @exceptions.each do |handler|
9
+ if e.is_a?(handler[:klass])
10
+ ret = handler[:block].call(ret, e)
11
+ break
12
+ end
13
+ end
14
+
15
+ ret
16
+ end
17
+ end
18
+
19
+ def rescue(klass, &block)
20
+ @exceptions ||= []
21
+ @exceptions << {klass: klass, block: block}
22
+ end
23
+ end
24
+ end
25
+ end