Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading