the_garage 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +162 -0
  4. data/Rakefile +33 -0
  5. data/app/assets/javascripts/garage/application.js +16 -0
  6. data/app/assets/javascripts/garage/docs/console.js.coffee +90 -0
  7. data/app/assets/javascripts/garage/docs/jquery.colorbox.js +1026 -0
  8. data/app/assets/stylesheets/garage/application.css +14 -0
  9. data/app/assets/stylesheets/garage/colorbox.scss +62 -0
  10. data/app/assets/stylesheets/garage/style.scss +59 -0
  11. data/app/assets/stylesheets/vendor/bootstrap.min.css +9 -0
  12. data/app/controllers/garage/application_controller.rb +4 -0
  13. data/app/controllers/garage/docs/resources_controller.rb +103 -0
  14. data/app/controllers/garage/meta/docs_controller.rb +20 -0
  15. data/app/controllers/garage/meta/services_controller.rb +20 -0
  16. data/app/helpers/garage/application_helper.rb +4 -0
  17. data/app/helpers/garage/docs/resources_helper.rb +24 -0
  18. data/app/models/garage/hash_representer.rb +11 -0
  19. data/app/views/garage/docs/resources/_layout_navigation.html.haml +5 -0
  20. data/app/views/garage/docs/resources/_navigation.html.haml +6 -0
  21. data/app/views/garage/docs/resources/callback.html.haml +5 -0
  22. data/app/views/garage/docs/resources/console.html.haml +45 -0
  23. data/app/views/garage/docs/resources/index.html.haml +2 -0
  24. data/app/views/garage/docs/resources/show.html.haml +16 -0
  25. data/app/views/layouts/garage/application.html.haml +26 -0
  26. data/config/routes.rb +0 -0
  27. data/lib/garage/app_responder.rb +22 -0
  28. data/lib/garage/authorizable.rb +26 -0
  29. data/lib/garage/config.rb +76 -0
  30. data/lib/garage/controller_helper.rb +110 -0
  31. data/lib/garage/docs/anchor_building.rb +28 -0
  32. data/lib/garage/docs/application.rb +24 -0
  33. data/lib/garage/docs/config.rb +61 -0
  34. data/lib/garage/docs/console_link_building.rb +14 -0
  35. data/lib/garage/docs/document.rb +141 -0
  36. data/lib/garage/docs/engine.rb +35 -0
  37. data/lib/garage/docs/example.rb +26 -0
  38. data/lib/garage/docs/renderer.rb +17 -0
  39. data/lib/garage/docs/toc_renderer.rb +14 -0
  40. data/lib/garage/docs.rb +9 -0
  41. data/lib/garage/exceptions.rb +49 -0
  42. data/lib/garage/hypermedia_filter.rb +44 -0
  43. data/lib/garage/hypermedia_responder.rb +120 -0
  44. data/lib/garage/meta/engine.rb +16 -0
  45. data/lib/garage/meta/remote_service.rb +78 -0
  46. data/lib/garage/meta.rb +6 -0
  47. data/lib/garage/meta_resource.rb +17 -0
  48. data/lib/garage/nested_field_query.rb +183 -0
  49. data/lib/garage/optional_response_body_responder.rb +16 -0
  50. data/lib/garage/paginating_responder.rb +113 -0
  51. data/lib/garage/permission.rb +13 -0
  52. data/lib/garage/permissions.rb +75 -0
  53. data/lib/garage/representer.rb +214 -0
  54. data/lib/garage/resource_casting_responder.rb +13 -0
  55. data/lib/garage/restful_actions.rb +219 -0
  56. data/lib/garage/strategy/access_token.rb +57 -0
  57. data/lib/garage/strategy/auth_server.rb +200 -0
  58. data/lib/garage/strategy/no_authentication.rb +13 -0
  59. data/lib/garage/strategy/test.rb +44 -0
  60. data/lib/garage/strategy.rb +4 -0
  61. data/lib/garage/test/migrator.rb +31 -0
  62. data/lib/garage/token_scope.rb +134 -0
  63. data/lib/garage/utils.rb +28 -0
  64. data/lib/garage/version.rb +3 -0
  65. data/lib/garage.rb +23 -0
  66. metadata +275 -0
@@ -0,0 +1,13 @@
1
+ module Garage::ResourceCastingResponder
2
+ def initialize(*args)
3
+ super
4
+ @caster = Garage.configuration.cast_resource
5
+ end
6
+
7
+ def display(resource, given_options={})
8
+ if @caster
9
+ resource = @caster.call(resource)
10
+ end
11
+ super(resource, given_options)
12
+ end
13
+ end
@@ -0,0 +1,219 @@
1
+ # Public: mixes in CRUD controller actions to your Action Controller
2
+ # classes to provide a simple RESTful actions that provides
3
+ # resource-based permissions with built-in integrations with
4
+ # Doorkeeper scopes.
5
+ #
6
+ # Examples
7
+ #
8
+ # class PostsController < ApiController
9
+ # include Garage::RestfulActions
10
+ #
11
+ # def require_resources
12
+ # @resources = Post.all
13
+ # end
14
+ #
15
+ # def require_resource
16
+ # @resource = Post.find(params[:id])
17
+ # end
18
+ # end
19
+ module Garage
20
+ module RestfulActions
21
+ extend ActiveSupport::Concern
22
+
23
+ included do
24
+ before_action :require_resource, :only => [:show, :update, :destroy]
25
+ before_action :require_resources, :only => [:index, :create]
26
+ before_action :require_action_permission_crud, :only => [:index, :create, :show, :update, :destroy], :if => -> (_) { verify_permission? }
27
+ end
28
+
29
+ module ClassMethods
30
+ def resource_class=(klass)
31
+ @resource_class = klass
32
+ end
33
+
34
+ def resource_class
35
+ @resource_class ||= name.sub(/Controller\z/, '').demodulize.singularize.constantize
36
+ end
37
+ end
38
+
39
+ # Public: List resources
40
+ # Renders `@resources` with options specified with `respond_with_resources_options`
41
+ # Requires `:read` permission on `resource_class` specified for `@resources`
42
+ def index
43
+ respond_with @resources, respond_with_resources_options
44
+ end
45
+
46
+ # Public: Get the resource
47
+ # Renders `@resource` with options specified with `respond_with_resource_options`
48
+ # Requries `:read` permission on `@resource`
49
+ def show
50
+ respond_with @resource, respond_with_resource_options
51
+ end
52
+
53
+ # Public: Create a new resource
54
+ # Calls `create_resource` in your controller to create a new resource
55
+ # Requires `:write` permission on `resource_class` specified for `@resources`
56
+ def create
57
+ @resource = create_resource
58
+ respond_with @resource, :location => location
59
+ end
60
+
61
+ # Public: Update the resource
62
+ # Calls `update_resource` in your controller to update `@resource`
63
+ # Requires `:write` permission on `@resource`
64
+ def update
65
+ @resource = update_resource
66
+ respond_with @resource, respond_with_resource_options
67
+ end
68
+
69
+ # Public: Delete the resource
70
+ # Calls `destroy_resource` in your controller to destroy `@resource`
71
+ # Requires `:write` permission on `@resource`
72
+ def destroy
73
+ @resource = destroy_resource
74
+ respond_with @resource, respond_with_resource_options
75
+ end
76
+
77
+ private
78
+
79
+ # Private: returns either `:read` or `:write`, depending on the current action name
80
+ def current_operation
81
+ if %w[create update destroy].include?(action_name)
82
+ :write
83
+ else
84
+ :read
85
+ end
86
+ end
87
+
88
+ # Private: Call this method to require additional permission on
89
+ # extra resource your controller handles. It will check if the
90
+ # current request user has permission to perform the operation
91
+ # (`:read` or `:write`) on the resource.
92
+ #
93
+ # Examples
94
+ #
95
+ # before_action :require_recipe
96
+ # def require_recipe
97
+ # @recipe = Recipe.find(params[:recipe_id])
98
+ # require_permission! @recipe, :read
99
+ # end
100
+ def require_permission!(resource, operation = nil)
101
+ operation ||= current_operation
102
+ resource.authorize!(current_resource_owner, operation)
103
+ end
104
+
105
+ # Private: Call this method to require additional access on extra
106
+ # resource class your controller needs access to. It will check if
107
+ # the current request token has an access permission (scope) to
108
+ # perform the operation (`:read` or `:write`) on the resource
109
+ # class.
110
+ #
111
+ # Examples
112
+ #
113
+ # before_action :require_stream
114
+ # def require_stream
115
+ # require_access! PostStream, :read
116
+ # end
117
+ def require_access!(resource, operation = nil)
118
+ operation ||= current_operation
119
+ ability_from_token.access!(resource.resource_class, operation)
120
+ end
121
+
122
+ # Private: Call this method to require additional access and
123
+ # permission on extra resource your controller performs operation
124
+ # on.
125
+ def require_access_and_permission!(resource, operation = nil)
126
+ require_permission!(resource, operation)
127
+ require_access!(resource, operation)
128
+ end
129
+
130
+ def require_action_permission_crud
131
+ if operated_resource
132
+ require_access_and_permission!(operated_resource, current_operation)
133
+ else
134
+ Rails.logger.debug "skipping permissions check since there's no @resource(s) set"
135
+ end
136
+ end
137
+
138
+ alias :require_action_permission :require_action_permission_crud
139
+
140
+ # Private: Call this method if you need to *change* the target
141
+ # resource to provision access and permission.
142
+ #
143
+ # def require_resources
144
+ # @resources = Post.where(user_id: @user.id)
145
+ # end
146
+ #
147
+ # By default, in `index` and `create` actions, Garage will check
148
+ # `:read` and `:write` access respectively on the default
149
+ # `resource_class` of `@resources`, in this case Post class. If
150
+ # you need more fine grained control than that, you should specify
151
+ # the optional parameters here, such as:
152
+ #
153
+ # def require_resources
154
+ # @resources = Post.where(user_id: @user.id)
155
+ # protect_source_as PrivatePost, user: @user
156
+ # end
157
+ #
158
+ # This way, the token should require access scope to `PrivatePost`
159
+ # (instead of `Post`), and the authorized user should have a
160
+ # permission to operate the action on resources owned by `@user`
161
+ # (instead of public). The `:user` option will be passed as
162
+ # parameters to `build_permissions` class method.
163
+ def protect_resource_as(klass, args = {})
164
+ if klass.is_a?(Hash)
165
+ klass, args = self.class.resource_class, klass
166
+ end
167
+ @operated_resource = MetaResource.new(klass, args)
168
+ end
169
+
170
+ def operated_resource
171
+ if @operated_resource
172
+ @operated_resource
173
+ elsif @resources
174
+ MetaResource.new(self.class.resource_class)
175
+ else
176
+ @resource
177
+ end
178
+ end
179
+
180
+ # Override to set @resource
181
+ def require_resource
182
+ raise NotImplementedError, "#{self.class}#require_resource is not implemented"
183
+ end
184
+
185
+ # Override to set @resources
186
+ def require_resources
187
+ raise NotImplementedError, "#{self.class}#require_resources is not implemented"
188
+ end
189
+
190
+ # Override to create a new resource
191
+ def create_resource
192
+ raise NotImplementedError, "#{self.class}#create_resource is not implemented"
193
+ end
194
+
195
+ # Override to update @resource
196
+ def update_resource
197
+ raise NotImplementedError, "#{self.class}#update_resource is not implemented"
198
+ end
199
+
200
+ # Override to destroy @resource
201
+ def destroy_resource
202
+ raise NotImplementedError, "#{self.class}#destroy_resource is not implemented"
203
+ end
204
+
205
+ # Override this if you want to pass options to respond_with in index action
206
+ def respond_with_resources_options
207
+ {}
208
+ end
209
+
210
+ # Override this if you want to pass options to respond_with in show, update and destroy actions
211
+ def respond_with_resource_options
212
+ {}
213
+ end
214
+
215
+ def location
216
+ { action: :show, id: @resource.id } if @resource.try(:respond_to?, :id)
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,57 @@
1
+ module Garage
2
+ module Strategy
3
+ class AccessToken
4
+ attr_reader :scope, :token, :token_type
5
+
6
+ def initialize(attrs)
7
+ @scope, @token, @token_type = attrs[:scope], attrs[:token], attrs[:token_type]
8
+ @application_id, @resource_owner_id = attrs[:application_id], attrs[:resource_owner_id]
9
+ @expired_at, @revoked_at = attrs[:expired_at], attrs[:revoked_at]
10
+ end
11
+
12
+ def application_id
13
+ @application_id.try(:to_i)
14
+ end
15
+
16
+ def resource_owner_id
17
+ @resource_owner_id.try(:to_i)
18
+ end
19
+
20
+ def expired_at
21
+ @expired_at.present? ? Time.zone.parse(@expired_at) : nil
22
+ rescue ArgumentError, TypeError
23
+ nil
24
+ end
25
+
26
+ def revoked_at
27
+ @revoked_at.present? ? Time.zone.parse(@revoked_at) : nil
28
+ rescue ArgumentError, TypeError
29
+ nil
30
+ end
31
+
32
+ def scopes
33
+ scope.try(:split, ' ')
34
+ end
35
+
36
+ def acceptable?(required_scopes)
37
+ accessible? && includes_scope?(required_scopes)
38
+ end
39
+
40
+ def accessible?
41
+ !expired? && !revoked?
42
+ end
43
+
44
+ def revoked?
45
+ !!revoked_at.try(:past?)
46
+ end
47
+
48
+ def expired?
49
+ !!expired_at.try(:past?)
50
+ end
51
+
52
+ def includes_scope?(required_scopes)
53
+ required_scopes.blank? || required_scopes.any? { |s| scopes.exists?(s) }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,200 @@
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'uri'
4
+
5
+ module Garage
6
+ module Strategy
7
+ module AuthServer
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ before_action :verify_auth, if: -> (_) { verify_permission? }
12
+ end
13
+
14
+ def access_token
15
+ if defined?(@access_token)
16
+ @access_token
17
+ else
18
+ @access_token = AccessTokenFetcher.fetch(request)
19
+ end
20
+ end
21
+
22
+ def verify_permission?
23
+ true
24
+ end
25
+
26
+ module Cache
27
+ def self.with_cache(key)
28
+ return yield unless Garage.configuration.cache_acceess_token_validation?
29
+
30
+ cached_token = Rails.cache.read(key)
31
+ return cached_token if cached_token && !cached_token.expired?
32
+
33
+ token = yield
34
+ Rails.cache.write(key, token, expires_in: default_ttl) if token && token.accessible?
35
+ token
36
+ end
37
+
38
+ def self.default_ttl
39
+ Garage.configuration.ttl_for_access_token_cache
40
+ end
41
+ end
42
+
43
+ # Returns an AccessToken from request object or returns nil if failed.
44
+ class AccessTokenFetcher
45
+ READ_TIMEOUT = 1
46
+ OPEN_TIMEOUT = 1
47
+ USER_AGENT = "Garage #{Garage::VERSION}"
48
+
49
+ def self.fetch(*args)
50
+ new(*args).fetch
51
+ end
52
+
53
+ def initialize(request)
54
+ @request = request
55
+ end
56
+
57
+ def fetch
58
+ if has_any_valid_credentials?
59
+ if has_cacheable_credentials?
60
+ fetch_with_cache
61
+ else
62
+ fetch_without_cache
63
+ end
64
+ else
65
+ nil
66
+ end
67
+ rescue Timeout::Error
68
+ raise AuthBackendTimeout.new(OPEN_TIMEOUT, read_timeout)
69
+ end
70
+
71
+ private
72
+
73
+ def get
74
+ raw = http_client.get(path_with_query, header)
75
+ Response.new(raw)
76
+ end
77
+
78
+ def header
79
+ {
80
+ 'Authorization' => @request.authorization,
81
+ 'Host' => Garage.configuration.auth_server_host,
82
+ 'Resource-Owner-Id' => @request.headers['Resource-Owner-Id'],
83
+ 'Scopes' => @request.headers['Scopes'],
84
+ 'User-Agent' => USER_AGENT,
85
+ }.reject {|_, v| v.nil? }
86
+ end
87
+
88
+ def path_with_query
89
+ result = uri.path
90
+ result << "?" + query unless query.empty?
91
+ result
92
+ end
93
+
94
+ def query
95
+ @query ||= @request.params.slice(:access_token, :bearer_token).to_query
96
+ end
97
+
98
+ def uri
99
+ @uri ||= URI.parse(auth_server_url)
100
+ end
101
+
102
+ def http_client
103
+ client = Net::HTTP.new(uri.host, uri.port)
104
+ client.use_ssl = true if uri.scheme == 'https'
105
+ client.read_timeout = read_timeout
106
+ client.open_timeout = OPEN_TIMEOUT
107
+ client
108
+ end
109
+
110
+ def auth_server_url
111
+ Garage.configuration.auth_server_url or raise NoUrlError
112
+ end
113
+
114
+ def read_timeout
115
+ Garage.configuration.auth_server_timeout or READ_TIMEOUT
116
+ end
117
+
118
+ def has_any_valid_credentials?
119
+ @request.authorization.present? ||
120
+ @request.params[:access_token].present? ||
121
+ @request.params[:bearer_token].present?
122
+ end
123
+
124
+ # Cacheable requests are:
125
+ # - Bearer token request with `Authorization` header.
126
+ #
127
+ # We don't cache these requests because they are less requested:
128
+ # - Bearer token request with query parameter which has been deprecated.
129
+ # - Any other token type.
130
+ def has_cacheable_credentials?
131
+ bearer_token.present?
132
+ end
133
+
134
+ def bearer_token
135
+ @bearer_token ||= @request.authorization.try {|o| o.slice(/\ABearer\s+(.+)\z/, 1) }
136
+ end
137
+
138
+ def fetch_with_cache
139
+ Cache.with_cache("garage_gem/token_cache/#{Garage::VERSION}/#{bearer_token}") do
140
+ fetch_without_cache
141
+ end
142
+ end
143
+
144
+ def fetch_without_cache
145
+ response = get
146
+ if response.valid?
147
+ Garage::Strategy::AccessToken.new(response.to_hash)
148
+ else
149
+ if response.status_code == 401
150
+ nil
151
+ else
152
+ raise AuthBackendError.new(response)
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ class Response
159
+ def initialize(raw)
160
+ @raw = raw
161
+ end
162
+
163
+ def valid?
164
+ status_code == 200 && json? && parsed_body.is_a?(Hash)
165
+ end
166
+
167
+ def to_hash
168
+ parsed_body.symbolize_keys
169
+ end
170
+
171
+ def status_code
172
+ @raw.code.to_i
173
+ end
174
+
175
+ def body
176
+ @raw.body
177
+ end
178
+
179
+ private
180
+
181
+ def json?
182
+ parsed_body
183
+ true
184
+ rescue JSON::ParserError
185
+ false
186
+ end
187
+
188
+ def parsed_body
189
+ @parsed_body ||= JSON.parse(body)
190
+ end
191
+ end
192
+
193
+ class NoUrlError < StandardError
194
+ def message
195
+ 'You must set Garage.configuration.auth_server_url'
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end