Skip to content

Commit 10128ad

Browse files
authored
Merge pull request #2563 from ericproulx/revisit_auth_middleware
Update Grape::Middleware::Auth::Base
2 parents 09e1bf5 + 615cdb2 commit 10128ad

File tree

9 files changed

+110
-100
lines changed

9 files changed

+110
-100
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* [#2558](https://github.com/ruby-grape/grape/pull/2558): Add Ruby's option `enable_frozen_string_literal` in CI - [@ericproulx](https://github.com/ericproulx).
1515
* [#2557](https://github.com/ruby-grape/grape/pull/2557): Add `lint!` - [@ericproulx](https://github.com/ericproulx).
1616
* [#2561](https://github.com/ruby-grape/grape/pull/2561): Optimize hash alloc for middleware's default options - [@ericproulx](https://github.com/ericproulx).
17+
* [#2563](https://github.com/ruby-grape/grape/pull/2563): Update `Grape::Middleware::Auth::Base` - [@ericproulx](https://github.com/ericproulx).
1718
* Your contribution here.
1819

1920
#### Fixes

UPGRADING.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ Upgrading Grape
33

44
### Upgrading to >= 2.4.0
55

6+
#### Grape::Middleware::Auth::Base
7+
`type` is now validated at compile time and will raise a `Grape::Exceptions::UnknownAuthStrategy` if unknown.
8+
69
#### Grape::Middleware::Base
710

811
- Second argument `options` is now a double splat (**) instead of single splat (*). If you're redefining `initialize` in your middleware and/or calling `super` in it, you might have to adapt the signature and the `super` call. Also, you might have to remove `{}` if you're pass `options` as a literal `Hash` or add `**` if you're using a variable.
@@ -18,7 +21,7 @@ Here are the notable changes:
1821
- `HTTP_HEADERS` has been moved to `Grape::Request` and renamed `KNOWN_HEADERS`. The last has been refreshed with new headers, and it's not lazy anymore.
1922
- `SUPPORTED_METHODS_WITHOUT_OPTIONS` and `find_supported_method` have been removed.
2023

21-
### Grape::Middleware::Base
24+
#### Grape::Middleware::Base
2225

2326
- Constant `TEXT_HTML` has been removed in favor of using literal string 'text/html'.
2427
- `rack_request` and `query_params` have been added. Feel free to call these in your middlewares.
@@ -34,7 +37,7 @@ Here are the notable changes:
3437

3538
Your API might act differently since it will strictly follow the [RFC2616 14.1](https://datatracker.ietf.org/doc/html/rfc2616#section-14.1) when interpreting the `Accept` header. Here are the differences:
3639

37-
###### Invalid or missing quality ranking
40+
##### Invalid or missing quality ranking
3841
The following used to yield `application/xml` and now will yield `application/json` as the preferred media type:
3942
- `application/json;q=invalid,application/xml;q=0.5`
4043
- `application/json,application/xml;q=1.0`
@@ -43,7 +46,7 @@ For the invalid case, the value `invalid` was automatically `to_f` and `invalid.
4346

4447
For the non provided case, 1.0 was automatically assigned and in a case of multiple best matches, the first was returned based on Ruby's sort_by `quality`. Now, 1.0 is still assigned and the last is returned in case of multiple best matches. See [Rack's implementation](https://github.com/rack/rack/blob/e8f47608668d507e0f231a932fa37c9ca551c0a5/lib/rack/utils.rb#L167) of the RFC.
4548

46-
###### Considering the closest generic when vendor tree
49+
##### Considering the closest generic when vendor tree
4750
Excluding the [header versioning strategy](https://github.com/ruby-grape/grape?tab=readme-ov-file#header), whenever a media type with the [vendor tree](https://datatracker.ietf.org/doc/html/rfc6838#section-3.2) leading facet `vnd.` like `application/vnd.api+json` was provided, Grape would also consider its closest generic when negotiating. In that case, `application/json` was added to the negotiation. Now, it will just consider the provided media types without considering any closest generics, and you'll need to [register](https://github.com/ruby-grape/grape?tab=readme-ov-file#api-formats) it.
4851
You can find the official vendor tree registrations on [IANA](https://www.iana.org/assignments/media-types/media-types.xhtml)
4952

lib/grape/api/instance.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ class API
55
# The API Instance class, is the engine behind Grape::API. Each class that inherits
66
# from this will represent a different API instance
77
class Instance
8+
extend Grape::Middleware::Auth::DSL
89
include Grape::DSL::API
910

1011
class << self

lib/grape/dsl/api.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ module DSL
55
module API
66
extend ActiveSupport::Concern
77

8-
include Grape::Middleware::Auth::DSL
9-
108
include Grape::DSL::Validations
119
include Grape::DSL::Callbacks
1210
include Grape::DSL::Configuration
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
module Grape
4+
module Exceptions
5+
class UnknownAuthStrategy < Base
6+
def initialize(strategy:)
7+
super(message: compose_message(:unknown_auth_strategy, strategy: strategy))
8+
end
9+
end
10+
end
11+
end

lib/grape/locale/en.yml

Lines changed: 44 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,59 @@
1+
---
12
en:
23
grape:
34
errors:
4-
format: ! '%{attributes} %{message}'
5+
format: '%{attributes} %{message}'
56
messages:
6-
coerce: 'is invalid'
7-
presence: 'is missing'
8-
regexp: 'is invalid'
7+
all_or_none: 'provide all or none of parameters'
8+
at_least_one: 'are missing, at least one parameter must be provided'
99
blank: 'is empty'
10-
values: 'does not have a valid value'
10+
coerce: 'is invalid'
11+
conflicting_types: 'query params contains conflicting types'
12+
empty_message_body: 'empty message body supplied with %{body_format} content-type'
13+
exactly_one: 'are missing, exactly one parameter must be provided'
1114
except_values: 'has a value not allowed'
12-
same_as: 'is not the same as %{parameter}'
15+
incompatible_option_values: '%{option1}: %{value1} is incompatible with %{option2}: %{value2}'
16+
invalid_accept_header:
17+
problem: 'invalid accept header'
18+
resolution: '%{message}'
19+
invalid_formatter: 'cannot convert %{klass} to %{to_format}'
20+
invalid_message_body:
21+
problem: 'message body does not match declared format'
22+
resolution: 'when specifying %{body_format} as content-type, you must pass valid %{body_format} in the request''s ''body'' '
23+
invalid_parameters: 'query params contains invalid format or byte sequence'
24+
invalid_response: 'Invalid response'
25+
invalid_version_header:
26+
problem: 'invalid version header'
27+
resolution: '%{message}'
28+
invalid_versioner_option:
29+
problem: 'unknown :using for versioner: %{strategy}'
30+
resolution: 'available strategy for :using is :path, :header, :accept_version_header, :param'
31+
invalid_with_option_for_represent:
32+
problem: 'you must specify an entity class in the :with option'
33+
resolution: 'eg: represent User, :with => Entity::User'
1334
length: 'is expected to have length within %{min} and %{max}'
1435
length_is: 'is expected to have length exactly equal to %{is}'
15-
length_min: 'is expected to have length greater than or equal to %{min}'
1636
length_max: 'is expected to have length less than or equal to %{max}'
17-
missing_vendor_option:
18-
problem: 'missing :vendor option'
19-
summary: 'when version using header, you must specify :vendor option'
20-
resolution: "eg: version 'v1', using: :header, vendor: 'twitter'"
37+
length_min: 'is expected to have length greater than or equal to %{min}'
38+
missing_group_type: 'group type is required'
2139
missing_mime_type:
2240
problem: 'missing mime type for %{new_format}'
23-
resolution:
24-
"you can choose existing mime type from Grape::ContentTypes::CONTENT_TYPES
25-
or add your own with content_type :%{new_format}, 'application/%{new_format}'
26-
"
27-
invalid_with_option_for_represent:
28-
problem: 'you must specify an entity class in the :with option'
29-
resolution: 'eg: represent User, :with => Entity::User'
41+
resolution: 'you can choose existing mime type from Grape::ContentTypes::CONTENT_TYPES or add your own with content_type :%{new_format}, ''application/%{new_format}'' '
3042
missing_option: 'you must specify :%{option} options'
31-
invalid_formatter: 'cannot convert %{klass} to %{to_format}'
32-
invalid_versioner_option:
33-
problem: 'unknown :using for versioner: %{strategy}'
34-
resolution: 'available strategy for :using is :path, :header, :accept_version_header, :param'
35-
unknown_validator: 'unknown validator: %{validator_type}'
36-
unknown_params_builder: 'unknown params_builder: %{params_builder_type}'
43+
missing_vendor_option:
44+
problem: 'missing :vendor option'
45+
resolution: 'eg: version ''v1'', using: :header, vendor: ''twitter'''
46+
summary: 'when version using header, you must specify :vendor option'
47+
mutual_exclusion: 'are mutually exclusive'
48+
presence: 'is missing'
49+
regexp: 'is invalid'
50+
same_as: 'is not the same as %{parameter}'
51+
too_deep_parameters: 'query params are recursively nested over the specified limit (%{limit})'
52+
too_many_multipart_files: 'the number of uploaded files exceeded the system''s configured limit (%{limit})'
53+
unknown_auth_strategy: 'unknown auth strategy: %{strategy}'
3754
unknown_options: 'unknown options: %{options}'
3855
unknown_parameter: 'unknown parameter: %{param}'
39-
incompatible_option_values: '%{option1}: %{value1} is incompatible with %{option2}: %{value2}'
40-
mutual_exclusion: 'are mutually exclusive'
41-
at_least_one: 'are missing, at least one parameter must be provided'
42-
exactly_one: 'are missing, exactly one parameter must be provided'
43-
all_or_none: 'provide all or none of parameters'
44-
missing_group_type: 'group type is required'
56+
unknown_params_builder: 'unknown params_builder: %{params_builder_type}'
57+
unknown_validator: 'unknown validator: %{validator_type}'
4558
unsupported_group_type: 'group type must be Array, Hash, JSON or Array[JSON]'
46-
invalid_message_body:
47-
problem: "message body does not match declared format"
48-
resolution:
49-
"when specifying %{body_format} as content-type, you must pass valid
50-
%{body_format} in the request's 'body'
51-
"
52-
empty_message_body: 'empty message body supplied with %{body_format} content-type'
53-
too_many_multipart_files: "the number of uploaded files exceeded the system's configured limit (%{limit})"
54-
invalid_accept_header:
55-
problem: 'invalid accept header'
56-
resolution: '%{message}'
57-
invalid_version_header:
58-
problem: 'invalid version header'
59-
resolution: '%{message}'
60-
invalid_response: 'Invalid response'
61-
conflicting_types: 'query params contains conflicting types'
62-
invalid_parameters: 'query params contains invalid format or byte sequence'
63-
too_deep_parameters: 'query params are recursively nested over the specified limit (%{limit})'
59+
values: 'does not have a valid value'

lib/grape/middleware/auth/base.rb

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,18 @@
33
module Grape
44
module Middleware
55
module Auth
6-
class Base
7-
attr_accessor :options, :app, :env
8-
6+
class Base < Grape::Middleware::Base
97
def initialize(app, **options)
10-
@app = app
11-
@options = options
12-
end
13-
14-
def call(env)
15-
dup._call(env)
8+
super
9+
@auth_strategy = Grape::Middleware::Auth::Strategies[options[:type]].tap do |auth_strategy|
10+
raise Grape::Exceptions::UnknownAuthStrategy.new(strategy: options[:type]) unless auth_strategy
11+
end
1612
end
1713

18-
def _call(env)
19-
self.env = env
20-
return app.call(env) unless options.key?(:type)
21-
22-
strategy_info = Grape::Middleware::Auth::Strategies[options[:type]]
23-
throw :error, status: 401, message: 'API Authorization Failed.' if strategy_info.blank?
24-
25-
strategy_info.create(@app, options) do |*args|
26-
env[Grape::Env::API_ENDPOINT].instance_exec(*args, &options[:proc])
14+
def call!(env)
15+
@env = env
16+
@auth_strategy.create(app, options) do |*args|
17+
context.instance_exec(*args, &options[:proc])
2718
end.call(env)
2819
end
2920
end

lib/grape/middleware/auth/dsl.rb

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,34 @@ module Grape
44
module Middleware
55
module Auth
66
module DSL
7-
extend ActiveSupport::Concern
8-
9-
module ClassMethods
10-
# Add an authentication type to the API. Currently
11-
# only `:http_basic`, `:http_digest` are supported.
12-
def auth(type = nil, options = {}, &block)
13-
if type
14-
namespace_inheritable(:auth, options.reverse_merge(type: type.to_sym, proc: block))
15-
use Grape::Middleware::Auth::Base, namespace_inheritable(:auth)
16-
else
17-
namespace_inheritable(:auth)
18-
end
19-
end
20-
21-
# Add HTTP Basic authorization to the API.
22-
#
23-
# @param [Hash] options A hash of options.
24-
# @option options [String] :realm "API Authorization" The HTTP Basic realm.
25-
def http_basic(options = {}, &block)
26-
options[:realm] ||= 'API Authorization'
27-
auth :http_basic, options, &block
7+
def auth(type = nil, options = {}, &block)
8+
if type
9+
namespace_inheritable(:auth, options.reverse_merge(type: type.to_sym, proc: block))
10+
use Grape::Middleware::Auth::Base, namespace_inheritable(:auth)
11+
else
12+
namespace_inheritable(:auth)
2813
end
14+
end
2915

30-
def http_digest(options = {}, &block)
31-
options[:realm] ||= 'API Authorization'
16+
# Add HTTP Basic authorization to the API.
17+
#
18+
# @param [Hash] options A hash of options.
19+
# @option options [String] :realm "API Authorization" The HTTP Basic realm.
20+
def http_basic(options = {}, &block)
21+
options[:realm] ||= 'API Authorization'
22+
auth :http_basic, options, &block
23+
end
3224

33-
if options[:realm].respond_to?(:values_at)
34-
options[:realm][:opaque] ||= 'secret'
35-
else
36-
options[:opaque] ||= 'secret'
37-
end
25+
def http_digest(options = {}, &block)
26+
options[:realm] ||= 'API Authorization'
3827

39-
auth :http_digest, options, &block
28+
if options[:realm].respond_to?(:values_at)
29+
options[:realm][:opaque] ||= 'secret'
30+
else
31+
options[:opaque] ||= 'secret'
4032
end
33+
34+
auth :http_digest, options, &block
4135
end
4236
end
4337
end

spec/grape/middleware/auth/strategies_spec.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,19 @@
2727
expect(last_response).to be_unauthorized
2828
end
2929
end
30+
31+
describe 'Unknown Auth' do
32+
context 'when type is not register' do
33+
let(:app) do
34+
Class.new(Grape::API) do
35+
use Grape::Middleware::Auth::Base, type: :unknown
36+
get('/whatever') { 'Hello there.' }
37+
end
38+
end
39+
40+
it 'throws a 401' do
41+
expect { get '/whatever' }.to raise_error(Grape::Exceptions::UnknownAuthStrategy, 'unknown auth strategy: unknown')
42+
end
43+
end
44+
end
3045
end

0 commit comments

Comments
 (0)