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
15 changes: 3 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
8 changes: 0 additions & 8 deletions .rubocop.yml

This file was deleted.

1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.4.4
2 changes: 2 additions & 0 deletions .standard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ignore:
- test/dummy/db/schema.rb
30 changes: 28 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
[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
6 changes: 4 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
43 changes: 31 additions & 12 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
slash_console (0.1.0)
slash_console (0.1.6)
rails (>= 7.0)
web-console (>= 4.0)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 10 additions & 7 deletions app/controllers/slash_console/console_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions app/views/slash_console/console/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
<head>
<title>Rails Console</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
<%# Asking Rails for the nonce here also makes it available to the
web-console middleware when it injects the console scripts below. %>
<style nonce="<%= content_security_policy_nonce %>">
body, html {
margin: 0;
padding: 0;
Expand All @@ -18,4 +20,4 @@
<body>
<!-- Console will be injected here by web-console middleware -->
</body>
</html>
</html>
8 changes: 0 additions & 8 deletions bin/rubocop

This file was deleted.

9 changes: 9 additions & 0 deletions lib/slash_console.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
require "slash_console/version"
require "slash_console/basic_auth_middleware"
require "slash_console/engine"
require "web-console"

module SlashConsole
# A fresh binding at the top level, so that console input sees the same
# +self+ and constant resolution as bin/rails console, rather than the
# engine's lexical namespace. Each call returns a new binding: local
# variables persist across evaluations within a console session but do
# not leak between sessions or into TOPLEVEL_BINDING itself.
def self.console_binding
TOPLEVEL_BINDING.eval("binding")
end
end
Loading
Loading