diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e5929f..e9a3961 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,11 +15,10 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: ruby-3.4.4 bundler-cache: true - name: Lint code for consistent style - run: bin/rubocop -f github + run: bundle exec standardrb test: runs-on: ubuntu-latest @@ -34,12 +33,6 @@ jobs: - 5432:5432 options: --health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=3 - # redis: - # image: redis - # ports: - # - 6379:6379 - # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 - steps: - name: Install packages run: sudo apt-get update && sudo apt-get install --no-install-recommends -y build-essential git libpq-dev libyaml-dev pkg-config google-chrome-stable @@ -50,20 +43,18 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: ruby-3.4.4 bundler-cache: true - name: Run tests env: RAILS_ENV: test DATABASE_URL: postgres://postgres:postgres@localhost:5432 - # REDIS_URL: redis://localhost:6379/0 - run: bin/rails db:test:prepare test + run: bundle exec rake app:db:test:prepare test test:system - name: Keep screenshots from failed system tests uses: actions/upload-artifact@v4 if: failure() with: name: screenshots - path: ${{ github.workspace }}/tmp/screenshots + path: ${{ github.workspace }}/test/dummy/tmp/screenshots if-no-files-found: ignore diff --git a/.rubocop.yml b/.rubocop.yml deleted file mode 100644 index f9d86d4..0000000 --- a/.rubocop.yml +++ /dev/null @@ -1,8 +0,0 @@ -# Omakase Ruby styling for Rails -inherit_gem: { rubocop-rails-omakase: rubocop.yml } - -# Overwrite or add rules to create your own house style -# -# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` -# Layout/SpaceInsideArrayLiteralBrackets: -# Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..f989260 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.4.4 diff --git a/.standard.yml b/.standard.yml new file mode 100644 index 0000000..95f7716 --- /dev/null +++ b/.standard.yml @@ -0,0 +1,2 @@ +ignore: + - test/dummy/db/schema.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index de3b08a..2c1df9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.1.6] - 2026-07-10 + +### Security +- Production Basic authentication moved from the controller into a Rack + middleware inserted in front of `WebConsole::Middleware`, so it now covers + web-console's evaluator endpoints (`/__web_console/repl_sessions/:id`) as + well as the console page. Previously a console session ID acted as an + unauthenticated bearer token for code execution until process restart. + +### Fixed +- The console renders under a strict nonce-based Content Security Policy. + The page stylesheet carries Rails' per-request nonce, and requesting the + nonce during rendering makes it available to web-console's injected + scripts even when the app hoists the CSP middleware above web-console + (previously such apps got a nonempty header nonce but `nonce=""` scripts, + i.e. a blank console). +- Console input evaluates in a fresh top-level binding, so constants + resolve the same way as in `bin/rails console` instead of inside the + engine's namespace (`ApplicationController` no longer resolves to + `SlashConsole::ApplicationController`, and app constants no longer need a + leading `::`). Local variables persist within a console session but not + across sessions. +- Removed the duplicate `WebConsole::Middleware` the engine inserted + alongside the copy web-console's own railtie already adds. + ## [0.1.5] - 2025-09-15 ### Fixed @@ -53,5 +78,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Development mode with no authentication required - Standard Ruby style guide compliance -[Unreleased]: https://github.com/firstdraft/slash_console/compare/v0.1.0...HEAD -[0.1.0]: https://github.com/firstdraft/slash_console/releases/tag/v0.1.0 \ No newline at end of file +[Unreleased]: https://github.com/firstdraft/slash_console/compare/v0.1.6...HEAD +[0.1.6]: https://github.com/firstdraft/slash_console/compare/v0.1.0...v0.1.6 +[0.1.0]: https://github.com/firstdraft/slash_console/releases/tag/v0.1.0 diff --git a/Gemfile b/Gemfile index e13dc1b..6497867 100644 --- a/Gemfile +++ b/Gemfile @@ -9,8 +9,10 @@ gem "pg" gem "propshaft" -# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] -gem "rubocop-rails-omakase", require: false +group :test do + gem "capybara" + gem "selenium-webdriver" +end # Start debugger with binding.b [https://github.com/ruby/debug] # gem "debug", ">= 1.0.0" diff --git a/Gemfile.lock b/Gemfile.lock index ebec208..e26a22c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - slash_console (0.1.0) + slash_console (0.1.6) rails (>= 7.0) web-console (>= 4.0) @@ -79,12 +79,23 @@ GEM securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) base64 (0.3.0) benchmark (0.4.1) bigdecimal (3.2.3) bindex (0.8.1) builder (3.3.0) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) concurrent-ruby (1.3.5) connection_pool (2.5.4) crass (1.0.6) @@ -114,6 +125,7 @@ GEM net-pop net-smtp marcel (1.0.4) + matrix (0.4.3) mini_mime (1.1.5) minitest (5.25.5) net-imap (0.5.10) @@ -128,11 +140,14 @@ GEM nio4r (2.7.4) nokogiri (1.18.10-arm64-darwin) racc (~> 1.4) + nokogiri (1.18.10-x86_64-linux-gnu) + racc (~> 1.4) parallel (1.27.0) parser (3.3.9.0) ast (~> 2.4.1) racc pg (1.6.2-arm64-darwin) + pg (1.6.2-x86_64-linux) pp (0.6.2) prettyprint prettyprint (0.2.0) @@ -144,6 +159,7 @@ GEM psych (5.2.6) date stringio + public_suffix (7.0.5) puma (7.0.3) nio4r (~> 2.0) racc (1.8.1) @@ -192,6 +208,7 @@ GEM regexp_parser (2.11.3) reline (0.6.2) io-console (~> 0.5) + rexml (3.4.4) rubocop (1.80.2) json (~> 2.3) language_server-protocol (~> 3.17.0.2) @@ -210,18 +227,15 @@ GEM lint_roller (~> 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.38.0, < 2.0) - rubocop-rails (2.33.3) - activesupport (>= 4.2.0) - lint_roller (~> 1.1) - rack (>= 1.1) - rubocop (>= 1.75.0, < 2.0) - rubocop-ast (>= 1.44.0, < 2.0) - rubocop-rails-omakase (1.1.0) - rubocop (>= 1.72) - rubocop-performance (>= 1.24) - rubocop-rails (>= 2.30) ruby-progressbar (1.13.0) + rubyzip (3.4.1) securerandom (0.4.1) + selenium-webdriver (4.45.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 4.0) + websocket (~> 1.0) standard (1.51.1) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) @@ -249,20 +263,25 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) + websocket (1.2.11) websocket-driver (0.8.0) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) zeitwerk (2.7.3) PLATFORMS arm64-darwin + x86_64-linux DEPENDENCIES + capybara pg propshaft puma - rubocop-rails-omakase + selenium-webdriver slash_console! standard (~> 1.0) diff --git a/README.md b/README.md index cddcb75..58f169a 100644 --- a/README.md +++ b/README.md @@ -46,19 +46,24 @@ Without these environment variables, you'll see an error message explaining what SlashConsole is a lightweight wrapper around [the excellent `web-console` gem](https://github.com/rails/web-console). It: 1. Provides a dedicated route for console access (instead of only on error pages). -2. Renders a full-page console interface. -3. Uses basic authentication when in production. +2. Renders a full-page console interface, including in apps that enforce a strict nonce-based Content Security Policy. +3. In production, requires basic authentication via a Rack middleware that protects both the console page and web-console's code-evaluation endpoints (`/__web_console/repl_sessions/:id`). +4. Evaluates console input at the top level, so constants resolve the same way as in `bin/rails console`. ## Development -After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. +After checking out the repo, run `bundle install` to install dependencies. To install this gem onto your local machine, run `bundle exec rake install`. ### Running Tests +The test suite needs a local PostgreSQL server. Prepare the test database once, then: + ```bash -bundle exec rake test +bundle exec rake app:db:test:prepare +bundle exec rake test # integration + unit tests +bundle exec rake test:system # browser smoke tests (needs Chrome) ``` ### Linting diff --git a/Rakefile b/Rakefile index e7793b5..da8ad9a 100644 --- a/Rakefile +++ b/Rakefile @@ -6,3 +6,19 @@ load "rails/tasks/engine.rake" load "rails/tasks/statistics.rake" require "bundler/gem_tasks" + +require "rake/testtask" + +Rake::TestTask.new(:test) do |t| + t.libs << "test" + t.test_files = FileList["test/**/*_test.rb"].exclude("test/system/**/*", "test/dummy/**/*") + t.verbose = false +end + +Rake::TestTask.new("test:system") do |t| + t.libs << "test" + t.test_files = FileList["test/system/**/*_test.rb"] + t.verbose = false +end + +task default: :test diff --git a/app/controllers/slash_console/console_controller.rb b/app/controllers/slash_console/console_controller.rb index f32ec94..d2f1ea4 100644 --- a/app/controllers/slash_console/console_controller.rb +++ b/app/controllers/slash_console/console_controller.rb @@ -4,27 +4,30 @@ class ConsoleController < ApplicationController skip_before_action :verify_authenticity_token, if: -> { defined?(verify_authenticity_token) } + # BasicAuthMiddleware is the primary guard: it protects both this page + # and web-console's evaluator endpoints, but only at the engine's + # standard /rails mount point. These filters remain as a second layer + # so the page stays protected if an application mounts the engine at a + # custom path. before_action :ensure_credentials_configured, if: -> { Rails.env.production? } before_action :authenticate_user, if: -> { Rails.env.production? } def index - console + console(SlashConsole.console_binding) render :index end private def ensure_credentials_configured - if ENV["ADMIN_USERNAME"].blank? || ENV["ADMIN_PASSWORD"].blank? - render plain: 'Before you can access the console, you must set environment variables called "ADMIN_USERNAME" and "ADMIN_PASSWORD".', - status: :service_unavailable + unless BasicAuthMiddleware.credentials_configured? + render plain: BasicAuthMiddleware::CREDENTIALS_MESSAGE, status: :service_unavailable end end def authenticate_user - authenticate_or_request_with_http_basic("Rails Console") do |username, password| - ActiveSupport::SecurityUtils.secure_compare(username, ENV["ADMIN_USERNAME"]) && - ActiveSupport::SecurityUtils.secure_compare(password, ENV["ADMIN_PASSWORD"]) + authenticate_or_request_with_http_basic(BasicAuthMiddleware::REALM) do |username, password| + BasicAuthMiddleware.authorized?(username, password) end end end diff --git a/app/views/slash_console/console/index.html.erb b/app/views/slash_console/console/index.html.erb index 63dca4e..4fe04ad 100644 --- a/app/views/slash_console/console/index.html.erb +++ b/app/views/slash_console/console/index.html.erb @@ -3,7 +3,9 @@ Rails Console -