Skip to content

Commit a1fbf1a

Browse files
committed
CloudFlare Turnstile
Why are these changes being introduced: * Bots are using more resources than desired Relevant ticket(s): * https://mitlibraries.atlassian.net/browse/TIMX-480 How does this address that need: * Adds rack-attack and bot_challenge_page gems * Adds redis to heroku app.json (will need to manually enable in staging and production environments before merge)
1 parent 34c5629 commit a1fbf1a

File tree

8 files changed

+99
-1
lines changed

8 files changed

+99
-1
lines changed

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
44
ruby '3.2.7'
55

66
gem 'bootsnap', require: false
7+
gem 'bot_challenge_page'
78
gem 'graphql'
89
gem 'graphql-client'
910
gem 'http'
1011
gem 'importmap-rails'
1112
gem 'jbuilder'
1213
gem 'mitlibraries-theme', git: 'https://github.com/mitlibraries/mitlibraries-theme', tag: 'v1.4'
1314
gem 'puma'
15+
gem 'rack-attack'
1416
gem 'rails', '~> 7.1.0'
1517
gem 'sentry-rails'
1618
gem 'sentry-ruby'

Gemfile.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ GEM
106106
debug_inspector (>= 1.2.0)
107107
bootsnap (1.18.4)
108108
msgpack (~> 1.2)
109+
bot_challenge_page (0.3.0)
110+
http (~> 5.2)
111+
rack-attack (~> 6.7)
112+
rails (>= 7.1, < 8.1)
109113
builder (3.3.0)
110114
capybara (3.40.0)
111115
addressable
@@ -239,6 +243,8 @@ GEM
239243
nio4r (~> 2.0)
240244
racc (1.8.1)
241245
rack (3.1.12)
246+
rack-attack (6.7.0)
247+
rack (>= 1.0, < 4)
242248
rack-session (2.1.0)
243249
base64 (>= 0.1.0)
244250
rack (>= 3.0.0)
@@ -395,6 +401,7 @@ DEPENDENCIES
395401
better_errors
396402
binding_of_caller
397403
bootsnap
404+
bot_challenge_page
398405
capybara
399406
climate_control
400407
debug
@@ -410,6 +417,7 @@ DEPENDENCIES
410417
mocha
411418
pg
412419
puma
420+
rack-attack
413421
rails (~> 7.1.0)
414422
rubocop
415423
rubocop-rails

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,22 @@ If the `flipflop` gem has been updated, check that the `:gdt` feature is working
6767
UI elements specific to GDT (e.g., geospatial search fields or the 'Ask GIS' link) appear with the
6868
feature flag enabled, and do not when it is disabled.
6969

70+
### CloudFlare Turnstile
71+
72+
This application uses [CloudFlare Turnstile](https://www.cloudflare.com/application-services/products/turnstile/) via
73+
the [Bot Challenge Page](https://github.com/samvera-labs/bot_challenge_page) gem.
74+
75+
In development, you can enable/disable this by running `rails dev:cache`. When `dev:cache` is not enabled, the cache is
76+
set to `null` so no enforcement is in place. As we do not register `localhost` with CloudFlare, if you have `dev:cache`
77+
enabled locally, you won't actually see the Turnstile challenge and instead will see a message saying you have been
78+
blocked. This is what users would also see if a deployed app is not registered with CloudFlare so we need to ensure all
79+
apps we intend to protect are registered with the site key we have enabled.
80+
81+
`Bot Challenge Page` uses [rack-attack](https://github.com/rack/rack-attack). On Heroku deployed apps, we'll be using
82+
Redis to track requests.
83+
84+
See `Optional Environment Variables` for more information.
85+
7086
### Required Environment Variables
7187

7288
- `TIMDEX_GRAPHQL`: Set this to the URL of the GraphQL endpoint. There is no default value in the application.
@@ -79,6 +95,10 @@ feature flag enabled, and do not when it is disabled.
7995
- `BOOLEAN_OPTIONS`: comma separated list of values to present to testers on instances where `BOOLEAN_PICKER` feature is enabled.
8096
- `BOOLEAN_PICKER`: feature to allow users to select their preferred boolean type. If set, feature is enabled. This feature is only intended for internal team
8197
testing and should never be enabled in production (mostly because the UI is a mess more than it would cause harm).
98+
- `CLOUDFLARE_SITE_KEY`: obtained through our cloudflare account (see lastpass for account info)
99+
- `CLOUDFLARE_SECRET_KEY`: obtained through our cloudflare account (see lastpass for account info)
100+
- `CLOUDFLARE_REQUEST_PERIOD_IN_HOURS`: integer in hours we use for grouping requests. Combined with `CLOUDFLARE_REQUESTS_PER_PERIOD` this makes up the "requests allowed per time period". Defaults to 12.
101+
- `CLOUDFLARE_REQUESTS_PER_PERIOD`: integer representing number of results and records pages allowed in the period defined in `CLOUDFLARE_REQUEST_PERIOD_IN_HOURS`. Defaults to 10.
82102
- `FACT_PANELS_ENABLED`: Comma separated list of enabled fact panels. See `/views/results.html.erb` for implemented panels/valid options. Leave unset to disable all.
83103
- `FILTER_ACCESS_TO_FILES`: The name to use instead of "Access to files" for that filter / aggregation.
84104
- `FILTER_CONTENT_TYPE`: The name to use instead of "Content type" for that filter / aggregation.

app.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
{
22
"name": "timdex-ui",
3-
"stack": "heroku-22"
3+
"stack": "heroku-22",
4+
"addons": [
5+
"heroku-redis"
6+
],
7+
"buildpacks": [
8+
{
9+
"url": "heroku/ruby"
10+
}
11+
]
412
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
class ApplicationController < ActionController::Base
2+
# This will only protect CONFIGURED routes, but also could be put on just certain
3+
# controllers, it does not need to be in ApplicationController
4+
before_action do |controller|
5+
BotChallengePage::BotChallengePageController.bot_challenge_enforce_filter(controller)
6+
end
7+
28
helper Mitlibraries::Theme::Engine.helpers
39
end

config/application.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
require_relative "boot"
22

33
require "rails/all"
4+
5+
require 'rack/attack'
46
require 'sprockets/railtie'
57

68
# Require the gems listed in Gemfile, including any gems
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
Rails.application.config.to_prepare do
2+
3+
BotChallengePage::BotChallengePageController.bot_challenge_config.enabled = true
4+
5+
# Get from CloudFlare Turnstile: https://www.cloudflare.com/application-services/products/turnstile/
6+
# Some testing keys are also available: https://developers.cloudflare.com/turnstile/troubleshooting/testing/
7+
#
8+
# Always pass testing sitekey: "1x00000000000000000000AA"
9+
BotChallengePage::BotChallengePageController.bot_challenge_config.cf_turnstile_sitekey = ENV.fetch('CLOUDFLARE_SITE_KEY', "NOT SET")
10+
# Always pass testing secret_key: "1x0000000000000000000000000000000AA"
11+
BotChallengePage::BotChallengePageController.bot_challenge_config.cf_turnstile_secret_key = ENV.fetch('CLOUDFLARE_SECRET_KEY', "NOT SET")
12+
13+
BotChallengePage::BotChallengePageController.bot_challenge_config.redirect_for_challenge = false
14+
15+
# What paths do you want to protect?
16+
#
17+
# You can use path prefixes: "/catalog" or even "/"
18+
#
19+
# Or hashes with controller and/or action:
20+
#
21+
# { controller: "catalog" }
22+
# { controller: "catalog", action: "index" }
23+
#
24+
# Note that we can only protect GET paths, and also think about making sure you DON'T protect
25+
# any path your front-end needs JS `fetch` access to, as this would block it (at least
26+
# without custom front-end code we haven't really explored)
27+
28+
BotChallengePage::BotChallengePageController.bot_challenge_config.rate_limited_locations = ['/results', '/record']
29+
30+
# allow rate_limit_count requests in rate_limit_period, before issuing challenge
31+
BotChallengePage::BotChallengePageController.bot_challenge_config.rate_limit_period = ENV.fetch('CLOUDFLARE_REQUEST_PERIOD_IN_HOURS', 12).to_i.hour
32+
BotChallengePage::BotChallengePageController.bot_challenge_config.rate_limit_count = ENV.fetch('CLOUDFLARE_REQUESTS_PER_PERIOD', 10).to_i
33+
34+
# How long will a challenge success exempt a session from further challenges?
35+
# BotChallengePage::BotChallengePageController.bot_challenge_config.session_passed_good_for = 36.hours
36+
37+
# Exempt some requests from bot challenge protection
38+
# BotChallengePage::BotChallengePageController.bot_challenge_config.allow_exempt = ->(controller) {
39+
# # controller.params
40+
# # controller.request
41+
# # controller.session
42+
43+
# # Here's a way to identify browser `fetch` API requests; note
44+
# # it can be faked by an "attacker"
45+
# controller.request.headers["sec-fetch-dest"] == "empty"
46+
# }
47+
48+
# More configuration is available
49+
50+
BotChallengePage::BotChallengePageController.rack_attack_init
51+
end

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
Rails.application.routes.draw do
2+
post "/challenge", to: "bot_challenge_page/bot_challenge_page#verify_challenge", as: :bot_detect_challenge
23
mount Flipflop::Engine => "/flipflop"
34
root "basic_search#index"
45

0 commit comments

Comments
 (0)