the_garage 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +162 -0
- data/Rakefile +33 -0
- data/app/assets/javascripts/garage/application.js +16 -0
- data/app/assets/javascripts/garage/docs/console.js.coffee +90 -0
- data/app/assets/javascripts/garage/docs/jquery.colorbox.js +1026 -0
- data/app/assets/stylesheets/garage/application.css +14 -0
- data/app/assets/stylesheets/garage/colorbox.scss +62 -0
- data/app/assets/stylesheets/garage/style.scss +59 -0
- data/app/assets/stylesheets/vendor/bootstrap.min.css +9 -0
- data/app/controllers/garage/application_controller.rb +4 -0
- data/app/controllers/garage/docs/resources_controller.rb +103 -0
- data/app/controllers/garage/meta/docs_controller.rb +20 -0
- data/app/controllers/garage/meta/services_controller.rb +20 -0
- data/app/helpers/garage/application_helper.rb +4 -0
- data/app/helpers/garage/docs/resources_helper.rb +24 -0
- data/app/models/garage/hash_representer.rb +11 -0
- data/app/views/garage/docs/resources/_layout_navigation.html.haml +5 -0
- data/app/views/garage/docs/resources/_navigation.html.haml +6 -0
- data/app/views/garage/docs/resources/callback.html.haml +5 -0
- data/app/views/garage/docs/resources/console.html.haml +45 -0
- data/app/views/garage/docs/resources/index.html.haml +2 -0
- data/app/views/garage/docs/resources/show.html.haml +16 -0
- data/app/views/layouts/garage/application.html.haml +26 -0
- data/config/routes.rb +0 -0
- data/lib/garage/app_responder.rb +22 -0
- data/lib/garage/authorizable.rb +26 -0
- data/lib/garage/config.rb +76 -0
- data/lib/garage/controller_helper.rb +110 -0
- data/lib/garage/docs/anchor_building.rb +28 -0
- data/lib/garage/docs/application.rb +24 -0
- data/lib/garage/docs/config.rb +61 -0
- data/lib/garage/docs/console_link_building.rb +14 -0
- data/lib/garage/docs/document.rb +141 -0
- data/lib/garage/docs/engine.rb +35 -0
- data/lib/garage/docs/example.rb +26 -0
- data/lib/garage/docs/renderer.rb +17 -0
- data/lib/garage/docs/toc_renderer.rb +14 -0
- data/lib/garage/docs.rb +9 -0
- data/lib/garage/exceptions.rb +49 -0
- data/lib/garage/hypermedia_filter.rb +44 -0
- data/lib/garage/hypermedia_responder.rb +120 -0
- data/lib/garage/meta/engine.rb +16 -0
- data/lib/garage/meta/remote_service.rb +78 -0
- data/lib/garage/meta.rb +6 -0
- data/lib/garage/meta_resource.rb +17 -0
- data/lib/garage/nested_field_query.rb +183 -0
- data/lib/garage/optional_response_body_responder.rb +16 -0
- data/lib/garage/paginating_responder.rb +113 -0
- data/lib/garage/permission.rb +13 -0
- data/lib/garage/permissions.rb +75 -0
- data/lib/garage/representer.rb +214 -0
- data/lib/garage/resource_casting_responder.rb +13 -0
- data/lib/garage/restful_actions.rb +219 -0
- data/lib/garage/strategy/access_token.rb +57 -0
- data/lib/garage/strategy/auth_server.rb +200 -0
- data/lib/garage/strategy/no_authentication.rb +13 -0
- data/lib/garage/strategy/test.rb +44 -0
- data/lib/garage/strategy.rb +4 -0
- data/lib/garage/test/migrator.rb +31 -0
- data/lib/garage/token_scope.rb +134 -0
- data/lib/garage/utils.rb +28 -0
- data/lib/garage/version.rb +3 -0
- data/lib/garage.rb +23 -0
- 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
|