From 75799f15d5edc3df9809042e39712c1e5c8281d2 Mon Sep 17 00:00:00 2001 From: Arne De Herdt Date: Fri, 16 Sep 2022 09:31:45 +0200 Subject: [PATCH 1/2] [CIOPS-1084] Bearer Token Support This pull request adds support for Basic Auth on the endpoint. This allows projects implementing this gem to secure their endpoint when they are public facing. That way the Rails stack is safe from being hammered with requests and prevents the application from being taken down. --- CHANGELOG.md | 3 ++ README.md | 6 +++ .../is_it_ready/application_controller.rb | 9 ----- .../is_it_ready/health_check_controller.rb | 37 +++++++++++++++++++ config/routes.rb | 2 +- lib/is_it_ready.rb | 6 +++ lib/is_it_ready/version.rb | 2 +- .../dummy/config/initializers/secret_token.rb | 2 +- test/dummy/config/initializers/sqlite3.rb | 8 ++++ .../integration/navigation_with_token_test.rb | 36 ++++++++++++++++++ test/test_helper.rb | 2 +- 11 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 app/controllers/is_it_ready/health_check_controller.rb create mode 100644 test/dummy/config/initializers/sqlite3.rb create mode 100644 test/integration/navigation_with_token_test.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index c00b197..fcb9a6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Is It Ready? CHANGELOG +## 0.0.4 +* Add support for HTTP Authorization Bearer tokens + ## 0.0.3 * Make the mounting of the engine dynamic based upon the configuration diff --git a/README.md b/README.md index da6049c..3b717df 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,12 @@ or conflicts with another plugin. In this case, creating an initializer under `c # * https://your-domain/something_else # This is more for cosmetic purposes, or when mountain multiple engines under the same endpoint with distinct routes. ::IsItReady.endpoint = '/something_else' + +# Setting the bearer token will protect the endpoint with Basic HTTP Auth. +# When this value is set, every incoming request on this endpoint must provided the +# AUTHORIZATION header with a Bearer or Token associated to it. +# Failure to do so, will return in an HTTP UNAUTHORIZED response. +::IsItReady.bearer_token = 'my-secret-token' ``` ## Contributing diff --git a/app/controllers/is_it_ready/application_controller.rb b/app/controllers/is_it_ready/application_controller.rb index 37ec6e8..6ae51bf 100644 --- a/app/controllers/is_it_ready/application_controller.rb +++ b/app/controllers/is_it_ready/application_controller.rb @@ -11,14 +11,5 @@ module IsItReady # See the general configuration options to enable this protection in the controller. class ApplicationController < ActionController::Base protect_from_forgery :with => :exception - - # GET /is_it_ready - # - # Returns the desired output, running through the entire Ruby on Rails stack to indicate that - # this application is able to serve requests. The routing is controlled through the Engine, - # but we might be mounted under a specific endpoint or with a custom path. - def is_it_ready - render :json => { :status => 'ok', :code => 200 } - end end end diff --git a/app/controllers/is_it_ready/health_check_controller.rb b/app/controllers/is_it_ready/health_check_controller.rb new file mode 100644 index 0000000..8a442b9 --- /dev/null +++ b/app/controllers/is_it_ready/health_check_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module IsItReady + class HealthCheckController < ::IsItReady::ApplicationController + # Disable the CSRF security checks, since this controller will receive calls from external + # services that do not have the ability to generate the required CSRF token + skip_before_action :verify_authenticity_token + + # Ensure that all incoming requests supply a valid Bearer token in their Authorization header. + # The function will determine whether the check is required or not based upon the configuration. + before_action :authenticate! + + AUTHORIZATION_HEADER = 'HTTP_AUTHORIZATION' + + # GET /is_it_ready + # + # Returns the desired output, running through the entire Ruby on Rails stack to indicate that + # this application is able to serve requests. The routing is controlled through the Engine, + # but we might be mounted under a specific endpoint or with a custom path. + def is_it_ready + render :json => { :status => 'ok', :code => 200 } + end + + private + + # This action will look up the HTTP Authorization header when the configuration has a Bearer token set. + # When the token is set, the incoming requests must provide this as a Bearer token, otherwise the request + # will be refused with an HTTP UNAUTHORIZED response. + def authenticate! + return unless ::IsItReady.bearer_token.present? + + authenticate_or_request_with_http_token do |token, _options| + ::IsItReady.bearer_token == token + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 1422781..4e174a9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,5 +4,5 @@ # This follows the same pattern as standard Ruby on Rails routing, but is scoped to # just the Rails Engine. ::IsItReady::Engine.routes.draw do - root :to => 'application#is_it_ready' + root :to => 'health_check#is_it_ready' end diff --git a/lib/is_it_ready.rb b/lib/is_it_ready.rb index 4051134..5a6b2e3 100644 --- a/lib/is_it_ready.rb +++ b/lib/is_it_ready.rb @@ -13,4 +13,10 @@ module IsItReady # but if this is already claimed by an application, it can be overwritten. mattr_accessor :endpoint @@endpoint = DEFAULT_PATH + + # Sets the Bearer token to be used when securing the endpoint. + # By default this is left blank, and disables the security verification of the controller. + # When enabled however, the request will need to provide the required token as a Bearer value + # in the AUTHORIZATION header of the request. + mattr_accessor :bearer_token end diff --git a/lib/is_it_ready/version.rb b/lib/is_it_ready/version.rb index e11147a..ea8cb1c 100644 --- a/lib/is_it_ready/version.rb +++ b/lib/is_it_ready/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module IsItReady - VERSION = '0.0.3' + VERSION = '0.0.4' end diff --git a/test/dummy/config/initializers/secret_token.rb b/test/dummy/config/initializers/secret_token.rb index ddcc2bf..d4606b3 100644 --- a/test/dummy/config/initializers/secret_token.rb +++ b/test/dummy/config/initializers/secret_token.rb @@ -4,4 +4,4 @@ # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. -Dummy::Application.config.secret_token = 'f72b0ec7b89beb76a5a4782594beaa709ced5f43d2a078c71cd4ce26ed2775dffc0e65babff2783e0073be64dabff10ee03e9eca67d7b6d2787111da8ed615a5' +Dummy::Application.config.secret_key_base = 'f72b0ec7b89beb76a5a4782594beaa709ced5f43d2a078c71cd4ce26ed2775dffc0e65babff2783e0073be64dabff10ee03e9eca67d7b6d2787111da8ed615a5' diff --git a/test/dummy/config/initializers/sqlite3.rb b/test/dummy/config/initializers/sqlite3.rb new file mode 100644 index 0000000..2d460c9 --- /dev/null +++ b/test/dummy/config/initializers/sqlite3.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# This initializer configures SQLite to silence warnings and make sure the "database" is usable +# across all Rails versions the way a normal database is used during testing. + +# Enforce booleans to represented as integers in the database. +# This was an old SQLite feature that we do not want to support anymore. +::Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true \ No newline at end of file diff --git a/test/integration/navigation_with_token_test.rb b/test/integration/navigation_with_token_test.rb new file mode 100644 index 0000000..e116c18 --- /dev/null +++ b/test/integration/navigation_with_token_test.rb @@ -0,0 +1,36 @@ +require 'test_helper' +require 'securerandom' + +module IsItReady + class NavigationWithTokenTest < ActionDispatch::IntegrationTest + include Engine.routes.url_helpers + + setup do + ::IsItReady.bearer_token = ::SecureRandom.hex(15) + end + + teardown do + ::IsItReady.bearer_token = nil + end + + test('it returns the correct response status on the root') do + get root_url, headers: { 'HTTP_AUTHORIZATION' => "Bearer token=#{::IsItReady.bearer_token}" } + + assert_response :success + end + + test('it returns the correct output on the root') do + get root_url, headers: { 'HTTP_AUTHORIZATION' => "Bearer token=#{::IsItReady.bearer_token}" } + + response = ::JSON.parse(@response.body, symbolize_names: true) + + assert_equal({ :status => "ok", :code => 200 }, response) + end + + test('it returns the correct response status on the root without token') do + get root_url + + assert_response :unauthorized + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 1e26a31..5793b41 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -2,7 +2,7 @@ ENV["RAILS_ENV"] = "test" require File.expand_path("../dummy/config/environment.rb", __FILE__) -require "rails/test_help" +require "minitest/autorun" Rails.backtrace_cleaner.remove_silencers! From 463c6d1d62eca04469e5e729afa85fffb087b491 Mon Sep 17 00:00:00 2001 From: Arne De Herdt Date: Fri, 16 Sep 2022 09:39:35 +0200 Subject: [PATCH 2/2] [Ruby on Rails] Wrap the SQLite configuration for supported versions --- test/dummy/config/initializers/sqlite3.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/dummy/config/initializers/sqlite3.rb b/test/dummy/config/initializers/sqlite3.rb index 2d460c9..b80ef28 100644 --- a/test/dummy/config/initializers/sqlite3.rb +++ b/test/dummy/config/initializers/sqlite3.rb @@ -5,4 +5,7 @@ # Enforce booleans to represented as integers in the database. # This was an old SQLite feature that we do not want to support anymore. -::Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true \ No newline at end of file +# Note: This is only for older versions of Sqlite3, the configuration option was dropped at some point +if ::Rails.application.config.active_record.sqlite3.respond_to?(:represent_boolean_as_integer) + ::Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true +end