Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v6.2 auto-load prevents specific middleware placement #459

Open
zarqman opened this issue Nov 8, 2019 · 15 comments
Open

v6.2 auto-load prevents specific middleware placement #459

zarqman opened this issue Nov 8, 2019 · 15 comments

Comments

@zarqman
Copy link
Contributor

zarqman commented Nov 8, 2019

Upon upgrading to 6.2.1 (from 6.1.0), we end up with Rack::Attack twice in our middleware stack.

Actual behavior:

$ rails middleware
...
use Rack::Head
use Rack::ConditionalGet
use Authenticator::PartOne
use Rack::Attack     # <--- our intended Rack::Attack instance
use Authenticator::PartTwo
use Rack::Attack     # <--- the duplicate now added in 6.2 that we don't want
run OurApp::Application.routes

Expected behavior:

$ rails middleware
...
use Authenticator::PartOne
use Rack::Attack     # <--- our intended Rack::Attack instance
use Authenticator::PartTwo
run OurApp::Application.routes

As you can see, our specific case involves a two-phase authentication middleware stack that is specifically designed to surround Rack::Attack, so we cannot move Rack::Attack to the end of the stack, where it installs itself by default.

I see that 6.2.1 was intended to turn the second instance into a no-op, but we'd much prefer avoiding the double load to begin with.

Given the need to configure Rack::Attack anyway, one could argue against auto-loading the middleware. However, I'd like to instead propose a couple of ways to allow disabling the auto-load for advanced configurations like ours, while preserving it as the new default behavior.

  1. Add a config option Rack::Attack.auto_load = true. Defaults to true. When false, the initializer would skip the auto-load.

  2. Rework the gem's default require by moving most of lib/rack/attack.rb into a new file. Replace the original file with nothing but:

require 'rack/attack/<newfile>'
require 'rack/attack/railtie' if defined?(::Rails)

This would allow advanced configurations to set the Gemfile as

gem 'rack-attack', require: 'rack/attack/<newfile>'

which would implicitly disable the auto-load railtie.

Either way preserves your new default while adding a path for more specific configuration for those of us with apps with advanced middleware configurations.

Are you open to either of the above solutions? If at least one is acceptable, I'm happy to prepare a PR accordingly if that'd be helpful.

FWIW, either of the above would also allow the removal of the already-called condition (return ... if ... env["rack.attack.called"]) added in #457, which I think may break/prevent PR #442.

@zarqman zarqman changed the title v6.2 auto-load of middleware creates side-effects v6.2 auto-load prevents specific middleware placement Nov 8, 2019
@grzuy
Copy link
Collaborator

grzuy commented Nov 8, 2019

Hi @zarqman,

Thank you for the detailed issue report!

How are you inserting Authenticator::PartTwo middleware?

@zarqman
Copy link
Contributor Author

zarqman commented Nov 8, 2019

@grzuy, appreciate the quick response.

As added background, part of our auth process dynamically affects Rack::Attack's rate limits. Because of that tight relationship, we use the following to assure everything remains adjacent. From application.rb:

config.middleware.use Rack::Attack
config.middleware.insert_before Rack::Attack, Authenticator::PartOne
config.middleware.insert_after Rack::Attack, Authenticator::PartTwo

@fatkodima
Copy link
Contributor

use Rack::Attack # <--- the duplicate now added in 6.2 that we don't want

I see that 6.2.1 was intended to turn the second instance into a no-op, but we'd much prefer avoiding the double load to begin with.

From reading the issue description, I didn't get what the problem with this:

$ rails middleware
...
use Rack::Head
use Rack::ConditionalGet
use Authenticator::PartOne
use Rack::Attack     # <--- our intended Rack::Attack instance
use Authenticator::PartTwo
use Rack::Attack     # <--- the duplicate now added in 6.2 that we don't want
run OurApp::Application.routes

Can you elaborate more what is the problem with second no-op Rack::Attack middleware?

@zarqman
Copy link
Contributor Author

zarqman commented Nov 9, 2019

@fatkodima, there are a few reasons we'd rather not have duplicate copies of Rack::Attack in the middleware stack:

  • It adds confusion to any developer who looks at the stack in the future, raising questions like, "Is it actually executing twice?" That Rack::Attack will self no-op on the second execution is non-obvious.
  • It adds an extra few lines to stack traces. Minor, but one more bit of stuff to sift through.
  • We're using this in an API (probably not uncommon for Rack::Attack) and every bit of performance counts, especially in the middleware chain. We've carefully deleted several unneeded default middlewares for the same reason.
  • The self no-op also breaks PR Add ability to use multiple instances of middleware #442 or any other path of intentionally running multiple instances of Rack::Attack. This isn't our need, but I did see that it was something being explored.

Are you open to adding something like the Rack::Attack.auto_load parameter proposed above?

@fatkodima
Copy link
Contributor

  1. yes, valid reason, but not critical
  2. agree, but minor
  3. avoiding calling basically empty method won't save anything, unless in app with tens of thousands requests per second, I guess
  4. yes, agree, after merging of that PR, auto-including middleware will not make sense if somebody will start using multiple instances of rack-attack or a new proposed syntax for configuring. But will still work for current users usecases (monkey-patching Rack::Attack class and defining configuration there).

I would rather prefer to revert to original explicit need to include rack-attack middleware, than introducing any more flags or parameters. But let's wait for @grzuy thoughts.

@grzuy grzuy self-assigned this Dec 17, 2019
@mt-kelvintaywl
Copy link

mt-kelvintaywl commented Jan 6, 2020

moving forward, is there any way we can configure Rack::Attack to not auto-load?
I have a use case in where I'd like to specify the exact position of this middleware in the chain of my middleware as above, and i'd prefer not having a duplicated (2nd) Rack::Attack middleware if so.

Thank you!

@grzuy
Copy link
Collaborator

grzuy commented Jan 6, 2020

@fatkodima, there are a few reasons we'd rather not have duplicate copies of Rack::Attack in the middleware stack:

* It adds confusion to any developer who looks at the stack in the future, raising questions like, "Is it actually executing twice?" That Rack::Attack will self no-op on the second execution is non-obvious.

* It adds an extra few lines to stack traces. Minor, but one more bit of stuff to sift through.

* We're using this in an API (probably not uncommon for Rack::Attack) and every bit of performance counts, especially in the middleware chain. We've carefully deleted several unneeded default middlewares for the same reason.

* The self no-op also breaks PR #442 or any other path of intentionally running multiple instances of Rack::Attack. This isn't our need, but I did see that it was something being explored.

@zarqman Aside from these 4 listed concerns, is your custom "2 step authentication" still working with rack-attack v6.2 then?

Are you open to adding something like the Rack::Attack.auto_load parameter proposed above?

@zarqman
Copy link
Contributor Author

zarqman commented Jan 6, 2020

@grzuy Correct, no other changes in 6.2 have broken anything else.

@grzuy
Copy link
Collaborator

grzuy commented Jan 14, 2020

I would rather prefer to revert to original explicit need to include rack-attack middleware, than introducing any more flags or parameters. But let's wait for @grzuy thoughts.

Agree with @fatkodima I would prefer not to add a new setting/flag for this. Not sure how we would eventually let users set a config option before even loading the gem... Feels like we could end up with something too "hacky" and brittle.

I don't think "Revert to original explicit need to include rack-attack middleware" is the way to go either. To put this issue into perspective, rack-attack has been downloaded ~1,4 million times from RubyGems.org since we released this "auto-load" feature and we heard only 2 users asking for disabling so far. Not that we don't want everyone to be happy :-), but just pointing this out to understand the real magnitude of this. I prefer to keep this feature enabled by default for the benefit of the majority of the user base.

I think the most feasible option so far, if we end up addressing this, is the gem 'rack-attack', require: 'rack/attack/<newfile>' proposed by @zarqman.

@mt-kelvintaywl
Copy link

for what it's worth,

Rack::Timeout is doing something similar with the require: to prevent auto-loading of the middleware.
https://github.com/sharpstone/rack-timeout#rails-apps-manually

@jarthod
Copy link

jarthod commented Apr 14, 2021

Hi, I just wanted to add one more case here ^^ I first encountered this problem and reported it in #457 and by lack of better option, decided to lock the version below 6.2 to avoid the trouble. And now I have the same problem again on another project where I need to have one middleware inserted after Rack::Attack (because it's slow and needs to be protected) and I can't again figure out how to do that cleanly.

I totally understand the default to silently add the middleware the the end and for me the best fix here would also be something like:

# Gemfile
gem 'rack-attack', require: 'rack/attack/base'
# application.rb
config.middleware.use Rack::Attack

Or small variant:

# Gemfile
gem 'rack-attack', require: false
# application.rb
require 'rack/attack/base'
config.middleware.use Rack::Attack

For people who prefer to keep the require where it's used or conditionally load it, stuff like that.

Basically a way to NOT require the rails autoload code and manually insert the middleware where we want.
(I took the "base" naming from https://github.com/sharpstone/rack-timeout#rails-apps-manually but I don't have a strong preference here ^^ "middleware", "minimal", "manual", ... works too)

If this is something you are open to, I can submit a PR.

Also for the record there's more people interested in this outside of these issues ^^ #491

@zarqman
Copy link
Contributor Author

zarqman commented Apr 14, 2021

@jarthod We found a way around this. It's super hacky, and a directly supported path with an alternate require would still be preferred, but it is working for us.

# in application.rb, where you'd add any other middleware:
initializer 'run after engine initializers' do
  config.middleware.insert_after Rack::Attack, SomeLastMiddleware
end

It's also possible to relocate Rack::Attack itself, but requires a more advanced hack:

initializer 'run after engine initializers' do
  class Dummy ; end
  config.middleware.swap Rack::Attack, Dummy
  config.middleware.delete Dummy
  config.middleware.insert_after ActionDispatch::DebugExceptions, Rack::Attack
end

The rename/swap is required because all delete ops are processed after everything else. Deleting Rack::Attack directly will delete all copies, including the newly added one.

@jarthod
Copy link

jarthod commented Apr 15, 2021

Thanks @zarqman! That's what I was starting to do indeed, I'm gonna use this workaround until some decision is reached about a potential future cleaner solution.

@TLischkaRemerge
Copy link

Is there still a possibility that this might be solved in a future version? We have a use case where one of the middlewares does takes some actions on particular requests to track and record them, which is fine for most cases but if a user is being rate-limited then it would negate the performance gains offered by rack attack since the middleware would run before rack attack. We can use one of the suggested workarounds for now but it'd be really useful to have the option to load and initialize the Rack::Attack middleware manually if required.

@robotfelix
Copy link

I just happened to come across this thread. I'm not encountering the problem, but I think if you want to skip the railtie initializer inserting the middleware automatically (eg: so you can insert it manually into a different position) then I think you should be able to do disable with this one-liner rather than fighting to undo its effects afterwards?

# Put this anywhere in application.rb
Rack::Attack::Railtie.initializers.clear

More nuance will be needed if this gem ever starts using other initialisers that we don't want to skip, but it should work fine for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants