Skip to content

Commit 0c22a02

Browse files
authored
Merge pull request #214 from rails/http-auth
Provide HTTP Basic authentication closed by default
2 parents 8336d0c + 689e8dd commit 0c22a02

File tree

13 files changed

+213
-9
lines changed

13 files changed

+213
-9
lines changed

README.md

+26-2
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,29 @@ RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
5656

5757
*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`.*
5858

59-
### Authentication and base controller class
59+
### Authentication
6060

61-
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:
61+
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:
62+
```yml
63+
mission_control:
64+
http_basic_auth_user: dev
65+
http_basic_auth_password: secret
66+
```
67+
68+
If no credentials are configured, Mission Control won't be accessible. To set these up, you can run the generator provided like this:
69+
70+
```
71+
bin/rails mission_control:jobs:authentication:configure
72+
```
73+
74+
To set them up for different environments you can use the `RAILS_ENV` environment variable, like this:
75+
```
76+
RAILS_ENV=production bin/rails mission_control:jobs:authentication:configure
77+
```
78+
79+
#### Custom authentication
80+
81+
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:
6282

6383
```ruby
6484
Rails.application.configure do
@@ -69,7 +89,11 @@ end
6989
Or, in your environment config or `application.rb`:
7090
```ruby
7191
config.mission_control.jobs.base_controller_class = "AdminController"
92+
```
7293

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

7599
### Other configuration settings
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module MissionControl::Jobs::BasicAuthentication
2+
extend ActiveSupport::Concern
3+
4+
included do
5+
before_action :authenticate_by_http_basic
6+
end
7+
8+
private
9+
def authenticate_by_http_basic
10+
if http_basic_authentication_enabled?
11+
if http_basic_authentication_configured?
12+
http_basic_authenticate_or_request_with(**http_basic_authentication_credentials)
13+
else
14+
head :unauthorized
15+
end
16+
end
17+
end
18+
19+
def http_basic_authentication_enabled?
20+
MissionControl::Jobs.http_basic_auth_enabled
21+
end
22+
23+
def http_basic_authentication_configured?
24+
http_basic_authentication_credentials.values.all?(&:present?)
25+
end
26+
27+
def http_basic_authentication_credentials
28+
{
29+
name: MissionControl::Jobs.http_basic_auth_user,
30+
password: MissionControl::Jobs.http_basic_auth_password
31+
}.transform_values(&:presence)
32+
end
33+
end

app/controllers/mission_control/jobs/application_controller.rb

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class MissionControl::Jobs::ApplicationController < MissionControl::Jobs.base_co
99
helper MissionControl::Jobs::ApplicationHelper unless self < MissionControl::Jobs::ApplicationHelper
1010
helper Importmap::ImportmapTagsHelper unless self < Importmap::ImportmapTagsHelper
1111

12+
include MissionControl::Jobs::BasicAuthentication
1213
include MissionControl::Jobs::ApplicationScoped, MissionControl::Jobs::NotFoundRedirections
1314
include MissionControl::Jobs::AdapterFeatures
1415

lib/mission_control/jobs.rb

+13-3
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,29 @@
77
loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
88
loader.push_dir(File.expand_path("..", __dir__))
99
loader.ignore("#{File.expand_path("..", __dir__)}/resque")
10+
loader.ignore("#{File.expand_path("..", __dir__)}/mission_control/jobs/tasks.rb")
11+
loader.ignore("#{File.expand_path("..", __dir__)}/generators")
1012
loader.setup
1113

1214
module MissionControl
1315
module Jobs
1416
mattr_accessor :adapters, default: Set.new
1517
mattr_accessor :applications, default: MissionControl::Jobs::Applications.new
1618
mattr_accessor :base_controller_class, default: "::ApplicationController"
19+
20+
mattr_accessor :internal_query_count_limit, default: 500_000 # Hard limit to keep unlimited count queries fast enough
1721
mattr_accessor :delay_between_bulk_operation_batches, default: 0
22+
mattr_accessor :scheduled_job_delay_threshold, default: 1.minute
23+
1824
mattr_accessor :logger, default: ActiveSupport::Logger.new(nil)
19-
mattr_accessor :internal_query_count_limit, default: 500_000 # Hard limit to keep unlimited count queries fast enough
25+
2026
mattr_accessor :show_console_help, default: true
21-
mattr_accessor :scheduled_job_delay_threshold, default: 1.minute
22-
mattr_accessor :importmap, default: Importmap::Map.new
2327
mattr_accessor :backtrace_cleaner
28+
29+
mattr_accessor :importmap, default: Importmap::Map.new
30+
31+
mattr_accessor :http_basic_auth_user
32+
mattr_accessor :http_basic_auth_password
33+
mattr_accessor :http_basic_auth_enabled, default: true
2434
end
2535
end
+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
class MissionControl::Jobs::Authentication < Rails::Command::Base
2+
def self.configure
3+
new.configure
4+
end
5+
6+
def configure
7+
if credentials_accessible?
8+
if authentication_configured?
9+
say "HTTP Basic Authentication is already configured for `#{Rails.env}`. You can edit it using `credentials:edit`"
10+
else
11+
say "Setting up credentials for HTTP Basic Authentication for `#{Rails.env}` environment."
12+
say ""
13+
14+
username = ask "Enter username: "
15+
password = SecureRandom.base58(64)
16+
17+
store_credentials(username, password)
18+
say "Username and password stored in Rails encrypted credentials."
19+
say ""
20+
say "You can now access Mission Control – Jobs with: "
21+
say ""
22+
say " - Username: #{username}"
23+
say " - password: #{password}"
24+
say ""
25+
say "You can also edit these in the future via `credentials:edit`"
26+
end
27+
else
28+
say "Rails credentials haven't been configured or aren't accessible. Configure them following the instructions in `credentials:help`"
29+
end
30+
end
31+
32+
private
33+
attr_reader :environment
34+
35+
def credentials_accessible?
36+
credentials.read.present?
37+
end
38+
39+
def authentication_configured?
40+
%i[ http_basic_auth_user http_basic_auth_password ].any? do |key|
41+
credentials.dig(:mission_control, key).present?
42+
end
43+
end
44+
45+
def store_credentials(username, password)
46+
content = credentials.read + "\n" + http_authentication_entry(username, password) + "\n"
47+
credentials.write(content)
48+
end
49+
50+
def credentials
51+
@credentials ||= Rails.application.encrypted(config.content_path, key_path: config.key_path)
52+
end
53+
54+
def config
55+
Rails.application.config.credentials
56+
end
57+
58+
def http_authentication_entry(username, password)
59+
<<~ENTRY
60+
mission_control:
61+
http_basic_auth_user: #{username}
62+
http_basic_auth_password: #{password}
63+
ENTRY
64+
end
65+
end

lib/mission_control/jobs/engine.rb

+9
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ module Jobs
77
class Engine < ::Rails::Engine
88
isolate_namespace MissionControl::Jobs
99

10+
rake_tasks do
11+
load "mission_control/jobs/tasks.rb"
12+
end
13+
1014
initializer "mission_control-jobs.middleware" do |app|
1115
if app.config.api_only
1216
config.middleware.use ActionDispatch::Flash
@@ -30,6 +34,11 @@ class Engine < ::Rails::Engine
3034
end
3135
end
3236

37+
initializer "mission_control-jobs.http_basic_auth" do |app|
38+
MissionControl::Jobs.http_basic_auth_user = app.credentials.dig(:mission_control, :http_basic_auth_user)
39+
MissionControl::Jobs.http_basic_auth_password = app.credentials.dig(:mission_control, :http_basic_auth_password)
40+
end
41+
3342
initializer "mission_control-jobs.active_job.extensions" do
3443
ActiveSupport.on_load :active_job do
3544
include ActiveJob::Querying

lib/mission_control/jobs/tasks.rb

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace :mission_control do
2+
namespace :jobs do
3+
desc "Configure HTTP Basic Authentication"
4+
task "authentication:configure" => :environment do
5+
MissionControl::Jobs::Authentication.configure
6+
end
7+
end
8+
end

lib/tasks/mission_control/jobs_tasks.rake

-4
This file was deleted.

mission_control-jobs.gemspec

+7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ Gem::Specification.new do |spec|
1212
spec.metadata["homepage_uri"] = spec.homepage
1313
spec.metadata["source_code_uri"] = "https://github.com/rails/mission_control-jobs"
1414

15+
spec.post_install_message = <<~MESSAGE
16+
Upgrading to Mission Control – Jobs 1.0.0? HTTP Basic authentication has been added by default, and it needs
17+
to be configured or disabled before you can access the dashboard.
18+
--> Check https://github.com/rails/mission_control-jobs?tab=readme-ov-file#authentication
19+
for more details and instructions.
20+
MESSAGE
21+
1522
spec.files = Dir.chdir(File.expand_path(__dir__)) do
1623
Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
1724
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
67f819f011ec672273c91cf789afb5d7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3wr+OnlAdcQJl0WURd7JXv+pleXbJVWozLH4JfPU6dGc9A0VlQ/kQosdPqDF7Yf/WrLtodre258ALf0ZHE2bQYgH3Eq0cJQ7xN8WwfGjBjXiL6uWaOHcfgcPVNg4E3Ag+YN3EOH8aquSttX7Uqyfv3tPlYQBQ7fs8lXjx3APfl3P8Vk2Yz6bhQcBgXhtFqH+--f7tDKb8EHxaT9l+Z--WIHpj/e3mEcqupnMrf5fvw==

test/dummy/config/environments/test.rb

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151

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

54+
config.mission_control.jobs.http_basic_auth_enabled = false
55+
5456
# Silence Solid Queue logging
5557
config.solid_queue.logger = ActiveSupport::Logger.new(nil)
5658
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
require "test_helper"
2+
3+
class MissionControl::Jobs::BasicAuthenticationTest < ActionDispatch::IntegrationTest
4+
test "unconfigured basic auth is closed" do
5+
with_http_basic_auth do
6+
get mission_control_jobs.application_queues_url(@application), headers: auth_headers("dev", "secret")
7+
assert_response :unauthorized
8+
end
9+
end
10+
11+
test "fail to authenticate without credentials" do
12+
with_http_basic_auth(user: "dev", password: "secret") do
13+
get mission_control_jobs.application_queues_url(@application)
14+
assert_response :unauthorized
15+
end
16+
end
17+
18+
test "fail to authenticate with wrong credentials" do
19+
with_http_basic_auth(user: "dev", password: "secret") do
20+
get mission_control_jobs.application_queues_url(@application), headers: auth_headers("dev", "wrong")
21+
assert_response :unauthorized
22+
end
23+
end
24+
25+
test "authenticate with correct credentials" do
26+
with_http_basic_auth(user: "dev", password: "secret") do
27+
get mission_control_jobs.application_queues_url(@application), headers: auth_headers("dev", "secret")
28+
assert_response :ok
29+
end
30+
end
31+
32+
private
33+
def with_http_basic_auth(enabled: true, user: nil, password: nil)
34+
previous_enabled, MissionControl::Jobs.http_basic_auth_enabled = MissionControl::Jobs.http_basic_auth_enabled, enabled
35+
previous_user, MissionControl::Jobs.http_basic_auth_user = MissionControl::Jobs.http_basic_auth_user, user
36+
previous_password, MissionControl::Jobs.http_basic_auth_password = MissionControl::Jobs.http_basic_auth_password, password
37+
yield
38+
ensure
39+
MissionControl::Jobs.http_basic_auth_enabled = previous_enabled
40+
MissionControl::Jobs.http_basic_auth_user = previous_user
41+
MissionControl::Jobs.http_basic_auth_password = previous_password
42+
end
43+
44+
def auth_headers(user, password)
45+
{ Authorization: ActionController::HttpAuthentication::Basic.encode_credentials(user, password) }
46+
end
47+
end

0 commit comments

Comments
 (0)