Skip to content

Commit 5babd3f

Browse files
authored
Merge pull request #208 from MITLibraries/timx-480-cloudflare-turnstile
CloudFlare Turnstile
2 parents 34c5629 + 73d9b3a commit 5babd3f

File tree

10 files changed

+125
-20
lines changed

10 files changed

+125
-20
lines changed

Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ 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'
17+
gem 'redis'
1518
gem 'sentry-rails'
1619
gem 'sentry-ruby'
1720
gem 'sprockets-rails'

Gemfile.lock

Lines changed: 13 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)
@@ -279,6 +285,10 @@ GEM
279285
rake (13.2.1)
280286
rdoc (6.12.0)
281287
psych (>= 4.0.0)
288+
redis (5.4.0)
289+
redis-client (>= 0.22.0)
290+
redis-client (0.24.0)
291+
connection_pool
282292
regexp_parser (2.10.0)
283293
reline (0.6.0)
284294
io-console (~> 0.5)
@@ -395,6 +405,7 @@ DEPENDENCIES
395405
better_errors
396406
binding_of_caller
397407
bootsnap
408+
bot_challenge_page
398409
capybara
399410
climate_control
400411
debug
@@ -410,7 +421,9 @@ DEPENDENCIES
410421
mocha
411422
pg
412423
puma
424+
rack-attack
413425
rails (~> 7.1.0)
426+
redis
414427
rubocop
415428
rubocop-rails
416429
selenium-webdriver

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

config/environments/production.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666

6767
# Use a different cache store in production.
6868
# config.cache_store = :mem_cache_store
69+
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'], ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE } }
6970

7071
# Use a real queuing backend for Active Job (and separate queues per environment).
7172
# config.active_job.queue_adapter = :resque
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/initializers/content_security_policy.rb

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,22 @@
44
# See the Securing Rails Applications Guide for more information:
55
# https://guides.rubyonrails.org/security.html#content-security-policy-header
66

7-
# Rails.application.configure do
8-
# config.content_security_policy do |policy|
9-
# policy.default_src :self, :https
10-
# policy.font_src :self, :https, :data
11-
# policy.img_src :self, :https, :data
12-
# policy.object_src :none
13-
# policy.script_src :self, :https
14-
# policy.style_src :self, :https
15-
# # Specify URI for violation reports
16-
# # policy.report_uri "/csp-violation-report-endpoint"
17-
# end
18-
#
19-
# # Generate session nonces for permitted importmap, inline scripts, and inline styles.
20-
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
21-
# config.content_security_policy_nonce_directives = %w(script-src style-src)
22-
#
23-
# # Report violations without enforcing the policy.
24-
# # config.content_security_policy_report_only = true
25-
# end
7+
Rails.application.configure do
8+
config.content_security_policy do |policy|
9+
policy.default_src :self, :https
10+
policy.font_src :self, :https, :data
11+
policy.img_src :self, :https, :data
12+
policy.object_src :none
13+
policy.script_src :self, :https, :unsafe_inline
14+
policy.style_src :self, :https, :unsafe_inline
15+
# Specify URI for violation reports
16+
# policy.report_uri "/csp-violation-report-endpoint"
17+
end
18+
#
19+
# Generate session nonces for permitted importmap, inline scripts, and inline styles.
20+
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
21+
# config.content_security_policy_nonce_directives = %w(script-src style-src)
22+
#
23+
# Report violations without enforcing the policy.
24+
# config.content_security_policy_report_only = true
25+
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)