Skip to content

Commit

Permalink
Merge pull request #214 from rails/http-auth
Browse files Browse the repository at this point in the history
Provide HTTP Basic authentication closed by default
  • Loading branch information
rosa authored Dec 3, 2024
2 parents 8336d0c + 689e8dd commit 0c22a02
Show file tree
Hide file tree
Showing 13 changed files with 213 additions and 9 deletions.
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,29 @@ RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile

*Note: Legacy CSS bundlers `sass-rails` and `sassc-rails` may fail to compile some of the CSS vendored into this library from [Bulma](https://github.com/jgthms/bulma), which was created in [Dart SASS](https://sass-lang.com/dart-sass/). You will therefore need to upgrade to `dartsass-rails` or some library that relies on it, like `cssbundling-rails`.*

### Authentication and base controller class
### Authentication

By default, Mission Control's controllers will extend the host app's `ApplicationController`. If no authentication is enforced, `/jobs` will be available to everyone. You might want to implement some kind of authentication for this in your app. To make this easier, you can specify a different controller as the base class for Mission Control's controllers:
Mission Control comes with **HTTP basic authentication enabled and closed** by default. Credentials are stored in [Rails's credentials](https://edgeguides.rubyonrails.org/security.html#custom-credentials) like this:
```yml
mission_control:
http_basic_auth_user: dev
http_basic_auth_password: secret
```
If no credentials are configured, Mission Control won't be accessible. To set these up, you can run the generator provided like this:
```
bin/rails mission_control:jobs:authentication:configure
```

To set them up for different environments you can use the `RAILS_ENV` environment variable, like this:
```
RAILS_ENV=production bin/rails mission_control:jobs:authentication:configure
```

#### Custom authentication

You can provide your own authentication mechanism, for example, if you have a certain type of admin user in your app that can access Mission Control. To make this easier, you can specify a different controller as the base class for Mission Control's controllers. By default, Mission Control's controllers will extend the host app's `ApplicationController`, but you can change this easily:

```ruby
Rails.application.configure do
Expand All @@ -69,7 +89,11 @@ end
Or, in your environment config or `application.rb`:
```ruby
config.mission_control.jobs.base_controller_class = "AdminController"
```

If you do this, you can disable the default HTTP Basic Authentication using the following option:
```ruby
config.mission_control.jobs.http_basic_auth_enabled = false
```

### Other configuration settings
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module MissionControl::Jobs::BasicAuthentication
extend ActiveSupport::Concern

included do
before_action :authenticate_by_http_basic
end

private
def authenticate_by_http_basic
if http_basic_authentication_enabled?
if http_basic_authentication_configured?
http_basic_authenticate_or_request_with(**http_basic_authentication_credentials)
else
head :unauthorized
end
end
end

def http_basic_authentication_enabled?
MissionControl::Jobs.http_basic_auth_enabled
end

def http_basic_authentication_configured?
http_basic_authentication_credentials.values.all?(&:present?)
end

def http_basic_authentication_credentials
{
name: MissionControl::Jobs.http_basic_auth_user,
password: MissionControl::Jobs.http_basic_auth_password
}.transform_values(&:presence)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class MissionControl::Jobs::ApplicationController < MissionControl::Jobs.base_co
helper MissionControl::Jobs::ApplicationHelper unless self < MissionControl::Jobs::ApplicationHelper
helper Importmap::ImportmapTagsHelper unless self < Importmap::ImportmapTagsHelper

include MissionControl::Jobs::BasicAuthentication
include MissionControl::Jobs::ApplicationScoped, MissionControl::Jobs::NotFoundRedirections
include MissionControl::Jobs::AdapterFeatures

Expand Down
16 changes: 13 additions & 3 deletions lib/mission_control/jobs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,29 @@
loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
loader.push_dir(File.expand_path("..", __dir__))
loader.ignore("#{File.expand_path("..", __dir__)}/resque")
loader.ignore("#{File.expand_path("..", __dir__)}/mission_control/jobs/tasks.rb")
loader.ignore("#{File.expand_path("..", __dir__)}/generators")
loader.setup

module MissionControl
module Jobs
mattr_accessor :adapters, default: Set.new
mattr_accessor :applications, default: MissionControl::Jobs::Applications.new
mattr_accessor :base_controller_class, default: "::ApplicationController"

mattr_accessor :internal_query_count_limit, default: 500_000 # Hard limit to keep unlimited count queries fast enough
mattr_accessor :delay_between_bulk_operation_batches, default: 0
mattr_accessor :scheduled_job_delay_threshold, default: 1.minute

mattr_accessor :logger, default: ActiveSupport::Logger.new(nil)
mattr_accessor :internal_query_count_limit, default: 500_000 # Hard limit to keep unlimited count queries fast enough

mattr_accessor :show_console_help, default: true
mattr_accessor :scheduled_job_delay_threshold, default: 1.minute
mattr_accessor :importmap, default: Importmap::Map.new
mattr_accessor :backtrace_cleaner

mattr_accessor :importmap, default: Importmap::Map.new

mattr_accessor :http_basic_auth_user
mattr_accessor :http_basic_auth_password
mattr_accessor :http_basic_auth_enabled, default: true
end
end
65 changes: 65 additions & 0 deletions lib/mission_control/jobs/authentication.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
class MissionControl::Jobs::Authentication < Rails::Command::Base
def self.configure
new.configure
end

def configure
if credentials_accessible?
if authentication_configured?
say "HTTP Basic Authentication is already configured for `#{Rails.env}`. You can edit it using `credentials:edit`"
else
say "Setting up credentials for HTTP Basic Authentication for `#{Rails.env}` environment."
say ""

username = ask "Enter username: "
password = SecureRandom.base58(64)

store_credentials(username, password)
say "Username and password stored in Rails encrypted credentials."
say ""
say "You can now access Mission Control – Jobs with: "
say ""
say " - Username: #{username}"
say " - password: #{password}"
say ""
say "You can also edit these in the future via `credentials:edit`"
end
else
say "Rails credentials haven't been configured or aren't accessible. Configure them following the instructions in `credentials:help`"
end
end

private
attr_reader :environment

def credentials_accessible?
credentials.read.present?
end

def authentication_configured?
%i[ http_basic_auth_user http_basic_auth_password ].any? do |key|
credentials.dig(:mission_control, key).present?
end
end

def store_credentials(username, password)
content = credentials.read + "\n" + http_authentication_entry(username, password) + "\n"
credentials.write(content)
end

def credentials
@credentials ||= Rails.application.encrypted(config.content_path, key_path: config.key_path)
end

def config
Rails.application.config.credentials
end

def http_authentication_entry(username, password)
<<~ENTRY
mission_control:
http_basic_auth_user: #{username}
http_basic_auth_password: #{password}
ENTRY
end
end
9 changes: 9 additions & 0 deletions lib/mission_control/jobs/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ module Jobs
class Engine < ::Rails::Engine
isolate_namespace MissionControl::Jobs

rake_tasks do
load "mission_control/jobs/tasks.rb"
end

initializer "mission_control-jobs.middleware" do |app|
if app.config.api_only
config.middleware.use ActionDispatch::Flash
Expand All @@ -30,6 +34,11 @@ class Engine < ::Rails::Engine
end
end

initializer "mission_control-jobs.http_basic_auth" do |app|
MissionControl::Jobs.http_basic_auth_user = app.credentials.dig(:mission_control, :http_basic_auth_user)
MissionControl::Jobs.http_basic_auth_password = app.credentials.dig(:mission_control, :http_basic_auth_password)
end

initializer "mission_control-jobs.active_job.extensions" do
ActiveSupport.on_load :active_job do
include ActiveJob::Querying
Expand Down
8 changes: 8 additions & 0 deletions lib/mission_control/jobs/tasks.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace :mission_control do
namespace :jobs do
desc "Configure HTTP Basic Authentication"
task "authentication:configure" => :environment do
MissionControl::Jobs::Authentication.configure
end
end
end
4 changes: 0 additions & 4 deletions lib/tasks/mission_control/jobs_tasks.rake

This file was deleted.

7 changes: 7 additions & 0 deletions mission_control-jobs.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ Gem::Specification.new do |spec|
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "https://github.com/rails/mission_control-jobs"

spec.post_install_message = <<~MESSAGE
Upgrading to Mission Control – Jobs 1.0.0? HTTP Basic authentication has been added by default, and it needs
to be configured or disabled before you can access the dashboard.
--> Check https://github.com/rails/mission_control-jobs?tab=readme-ov-file#authentication
for more details and instructions.
MESSAGE

spec.files = Dir.chdir(File.expand_path(__dir__)) do
Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
end
Expand Down
1 change: 1 addition & 0 deletions test/dummy/config/credentials/development.key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
67f819f011ec672273c91cf789afb5d7
1 change: 1 addition & 0 deletions test/dummy/config/credentials/development.yml.enc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3wr+OnlAdcQJl0WURd7JXv+pleXbJVWozLH4JfPU6dGc9A0VlQ/kQosdPqDF7Yf/WrLtodre258ALf0ZHE2bQYgH3Eq0cJQ7xN8WwfGjBjXiL6uWaOHcfgcPVNg4E3Ag+YN3EOH8aquSttX7Uqyfv3tPlYQBQ7fs8lXjx3APfl3P8Vk2Yz6bhQcBgXhtFqH+--f7tDKb8EHxaT9l+Z--WIHpj/e3mEcqupnMrf5fvw==
2 changes: 2 additions & 0 deletions test/dummy/config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@

config.solid_queue.connects_to = { database: { writing: :queue } }

config.mission_control.jobs.http_basic_auth_enabled = false

# Silence Solid Queue logging
config.solid_queue.logger = ActiveSupport::Logger.new(nil)
end
47 changes: 47 additions & 0 deletions test/mission_control/jobs/basic_authentication_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require "test_helper"

class MissionControl::Jobs::BasicAuthenticationTest < ActionDispatch::IntegrationTest
test "unconfigured basic auth is closed" do
with_http_basic_auth do
get mission_control_jobs.application_queues_url(@application), headers: auth_headers("dev", "secret")
assert_response :unauthorized
end
end

test "fail to authenticate without credentials" do
with_http_basic_auth(user: "dev", password: "secret") do
get mission_control_jobs.application_queues_url(@application)
assert_response :unauthorized
end
end

test "fail to authenticate with wrong credentials" do
with_http_basic_auth(user: "dev", password: "secret") do
get mission_control_jobs.application_queues_url(@application), headers: auth_headers("dev", "wrong")
assert_response :unauthorized
end
end

test "authenticate with correct credentials" do
with_http_basic_auth(user: "dev", password: "secret") do
get mission_control_jobs.application_queues_url(@application), headers: auth_headers("dev", "secret")
assert_response :ok
end
end

private
def with_http_basic_auth(enabled: true, user: nil, password: nil)
previous_enabled, MissionControl::Jobs.http_basic_auth_enabled = MissionControl::Jobs.http_basic_auth_enabled, enabled
previous_user, MissionControl::Jobs.http_basic_auth_user = MissionControl::Jobs.http_basic_auth_user, user
previous_password, MissionControl::Jobs.http_basic_auth_password = MissionControl::Jobs.http_basic_auth_password, password
yield
ensure
MissionControl::Jobs.http_basic_auth_enabled = previous_enabled
MissionControl::Jobs.http_basic_auth_user = previous_user
MissionControl::Jobs.http_basic_auth_password = previous_password
end

def auth_headers(user, password)
{ Authorization: ActionController::HttpAuthentication::Basic.encode_credentials(user, password) }
end
end

0 comments on commit 0c22a02

Please sign in to comment.