diff --git a/README.md b/README.md index 42511b0a..6c44f001 100644 --- a/README.md +++ b/README.md @@ -56,31 +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. - -#### HTTP authentication +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 +``` -You can set a simple HTTP authentication by either using +If no credentials are configured, Mission Control won't be accessible. To set these up, you can run the generator provided like this: -```ruby -Rails.application.configure do - config.mission_control.jobs.http_auth_user = "captain" - config.mission_control.jobs.http_auth_password = "topsecret" -end ``` -or you can set it via ENV +bin/rails mission_control:jobs:authentication:init +``` -```shell -ENV["MISSION_CONTROL_JOBS_HTTP_AUTH_USER"]=captain -ENV["MISSION_CONTROL_JOBS_HTTP_AUTH_password"]=topsecret +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:init ``` -If no value is provided (`nil`, `""` or `false`), authentication is skipped. #### Custom authentication -You might want to implement your own authentication. To make this easier, you can specify a different controller as the base class for Mission Control's controllers: +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 @@ -91,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 diff --git a/lib/generators/mission_control/jobs/authentication_generator.rb b/lib/generators/mission_control/jobs/authentication_generator.rb new file mode 100644 index 00000000..c29eb72a --- /dev/null +++ b/lib/generators/mission_control/jobs/authentication_generator.rb @@ -0,0 +1,59 @@ +class MissionControl::Jobs::AuthenticationGenerator < Rails::Generators::Base + def init + 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 + 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 diff --git a/lib/mission_control/jobs.rb b/lib/mission_control/jobs.rb index e70840dd..1dc12583 100644 --- a/lib/mission_control/jobs.rb +++ b/lib/mission_control/jobs.rb @@ -7,6 +7,8 @@ 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 diff --git a/lib/mission_control/jobs/engine.rb b/lib/mission_control/jobs/engine.rb index 1a1b6711..5b5dc293 100644 --- a/lib/mission_control/jobs/engine.rb +++ b/lib/mission_control/jobs/engine.rb @@ -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 @@ -31,8 +35,8 @@ class Engine < ::Rails::Engine end initializer "mission_control-jobs.http_basic_auth" do |app| - config.mission_control.jobs.http_basic_auth_user = app.credentials.dig(:mission_control, :http_basic_auth_user), - config.mission_control.jobs.http_basic_auth_password = app.credentials.dig(:mission_control, :http_basic_auth_password) + 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 diff --git a/lib/mission_control/jobs/tasks.rb b/lib/mission_control/jobs/tasks.rb new file mode 100644 index 00000000..b1fd03ee --- /dev/null +++ b/lib/mission_control/jobs/tasks.rb @@ -0,0 +1,8 @@ +namespace :mission_control do + namespace :jobs do + desc "Configure HTTP Basic Authentication" + task "authentication:init" do + Rails::Command.invoke :generate, [ "mission_control:jobs:authentication" ] + end + end +end diff --git a/lib/tasks/mission_control/jobs_tasks.rake b/lib/tasks/mission_control/jobs_tasks.rake deleted file mode 100644 index 782dc2b3..00000000 --- a/lib/tasks/mission_control/jobs_tasks.rake +++ /dev/null @@ -1,4 +0,0 @@ -# desc "Explaining what the task does" -# task :mission_control_jobs do -# # Task goes here -# end diff --git a/test/dummy/config/credentials/development.key b/test/dummy/config/credentials/development.key new file mode 100644 index 00000000..c0bed95e --- /dev/null +++ b/test/dummy/config/credentials/development.key @@ -0,0 +1 @@ +67f819f011ec672273c91cf789afb5d7 \ No newline at end of file diff --git a/test/dummy/config/credentials/development.yml.enc b/test/dummy/config/credentials/development.yml.enc new file mode 100644 index 00000000..6def87d1 --- /dev/null +++ b/test/dummy/config/credentials/development.yml.enc @@ -0,0 +1 @@ +3wr+OnlAdcQJl0WURd7JXv+pleXbJVWozLH4JfPU6dGc9A0VlQ/kQosdPqDF7Yf/WrLtodre258ALf0ZHE2bQYgH3Eq0cJQ7xN8WwfGjBjXiL6uWaOHcfgcPVNg4E3Ag+YN3EOH8aquSttX7Uqyfv3tPlYQBQ7fs8lXjx3APfl3P8Vk2Yz6bhQcBgXhtFqH+--f7tDKb8EHxaT9l+Z--WIHpj/e3mEcqupnMrf5fvw== \ No newline at end of file