the_garage 2.0.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 (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