haveapi 0.3.0

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