DEV: Add /dev-mode endpoint for MiniProfiler auth cookie (#37027)

What is the problem?

Developers need to profile pages while browsing as anonymous users or
while impersonating other users. Currently, MiniProfiler authorization
is tied to the session, so it's lost when the session changes.

What is the solution?

Add a `/dev-mode` endpoint that sets an encrypted cookie to persist
MiniProfiler authorization for 1 hour, independent of the session:
- New `DevModeController` with CSRF-protected POST form
- Only accessible to users in the Developer group
- Cookie validated on each request by checking timestamp, user
  existence, and developer status
This commit is contained in:
Alan Guo Xiang Tan
2026-01-12 10:01:20 +08:00
committed by GitHub
parent d2252b5cd3
commit 9d544dc2dd
7 changed files with 220 additions and 3 deletions
+18 -3
View File
@@ -63,6 +63,8 @@ class ApplicationController < ActionController::Base
HONEYPOT_KEY = "HONEYPOT_KEY"
CHALLENGE_KEY = "CHALLENGE_KEY"
MINI_PROFILER_AUTH_COOKIE_EXPIRES_IN = 1.hour
MINI_PROFILER_CLASS = defined?(Rack::MiniProfiler) ? Rack::MiniProfiler : nil
layout :set_layout
@@ -684,12 +686,25 @@ class ApplicationController < ActionController::Base
end
def mini_profiler_enabled?
defined?(Rack::MiniProfiler) && (guardian.is_developer? || Rails.env.development?)
return false unless MINI_PROFILER_CLASS
return true if Rails.env.development?
return true if guardian.is_developer?
if auth = cookies.encrypted[:_mp_auth]
user_id = auth[:user_id]
issued_at = auth[:issued_at]
if issued_at && issued_at > MINI_PROFILER_AUTH_COOKIE_EXPIRES_IN.ago.to_i
user = User.find_by(id: user_id)
return true if user && Guardian.new(user).is_developer?
end
end
false
end
def authorize_mini_profiler
return unless mini_profiler_enabled?
Rack::MiniProfiler.authorize_request
MINI_PROFILER_CLASS.authorize_request if mini_profiler_enabled?
end
def check_xhr
+34
View File
@@ -0,0 +1,34 @@
# frozen_string_literal: true
class DevModeController < ApplicationController
layout "no_ember"
skip_before_action :preload_json, :check_xhr
before_action :ensure_developer
def index
response.headers["X-Robots-Tag"] = "noindex, nofollow"
end
def enter
if params["enable_rack_mini_profiler"] == "true"
cookies.encrypted[:_mp_auth] = {
value: {
user_id: current_user.id,
issued_at: Time.now.to_i,
},
expires: MINI_PROFILER_AUTH_COOKIE_EXPIRES_IN.from_now,
httponly: true,
secure: SiteSetting.force_https,
same_site: :strict,
}
end
redirect_to path("/")
end
private
def ensure_developer
raise Discourse::NotFound unless guardian.is_developer?
end
end
+14
View File
@@ -0,0 +1,14 @@
<div class='container dev-mode'>
<h2><%= t 'dev_mode.title' %></h2>
<%= form_tag(dev_mode_enter_path) do %>
<p>
<label>
<%= check_box_tag 'enable_rack_mini_profiler', true, true %>
<%= t 'dev_mode.enable_rack_mini_profiler' %>
</label>
</p>
<p>
<%= submit_tag t('dev_mode.enter'), class: 'btn btn-primary' %>
</p>
<% end %>
</div>
+4
View File
@@ -5539,6 +5539,10 @@ en:
deprecation_error_label: Make Javascript deprecations raise an error
enter: "Enter Safe Mode"
must_select: "You must select at least one option to enter safe mode."
dev_mode:
title: "Developer Mode"
enable_rack_mini_profiler: "Enable Rack Mini Profiler to persist across session changes for 1 hour"
enter: "Enter"
wizard:
title: "Discourse Setup"
step:
+3
View File
@@ -1775,6 +1775,9 @@ Discourse::Application.routes.draw do
get "/safe-mode" => "safe_mode#index"
post "/safe-mode" => "safe_mode#enter", :as => "safe_mode_enter"
get "/dev-mode" => "dev_mode#index"
post "/dev-mode" => "dev_mode#enter", :as => "dev_mode_enter"
get "/theme-qunit" => "qunit#theme"
get "/theme-tests", to: redirect("/theme-qunit")
@@ -1997,4 +1997,82 @@ RSpec.describe ApplicationController do
)
end
end
describe "when authorizing mini_profiler" do
mini_profiler_stub = Class.new { def self.authorize_request = nil }
around do |example|
stub_const(ApplicationController, :MINI_PROFILER_CLASS, mini_profiler_stub) { example.run }
end
fab!(:developer) { Fabricate(:admin).tap { |u| Developer.create!(user_id: u.id) } }
fab!(:user)
fab!(:admin)
before { allow(mini_profiler_stub).to receive(:authorize_request) }
after { Developer.rebuild_cache }
it "authorizes mini_profiler for developer user" do
sign_in(developer)
get "/latest"
expect(mini_profiler_stub).to have_received(:authorize_request)
end
it "does not authorize mini_profiler for non-developer user" do
sign_in(admin)
get "/latest"
expect(mini_profiler_stub).not_to have_received(:authorize_request)
end
describe "using the mini_profiler auth cookie" do
def set_mini_profiler_auth_cookie(user, issued_at: Time.now.to_i)
data = { user_id: user.id, issued_at: issued_at }
jar = ActionDispatch::Cookies::CookieJar.build(ActionDispatch::TestRequest.create, {})
jar.encrypted[:_mp_auth] = { value: data }
cookies[:_mp_auth] = jar[:_mp_auth]
end
it "authorizes mini_profiler for anon user with valid cookie" do
set_mini_profiler_auth_cookie(developer)
get "/latest"
expect(mini_profiler_stub).to have_received(:authorize_request)
end
it "does not authorize with expired cookie" do
set_mini_profiler_auth_cookie(
developer,
issued_at:
(ApplicationController::MINI_PROFILER_AUTH_COOKIE_EXPIRES_IN + 1.hour).ago.to_i,
)
get "/latest"
expect(mini_profiler_stub).not_to have_received(:authorize_request)
end
it "does not authorize if user no longer exists" do
set_mini_profiler_auth_cookie(developer)
developer.destroy!
get "/latest"
expect(mini_profiler_stub).not_to have_received(:authorize_request)
end
it "does not authorize if user is no longer a developer" do
set_mini_profiler_auth_cookie(developer)
Developer.find_by(user_id: developer.id).destroy!
get "/latest"
expect(mini_profiler_stub).not_to have_received(:authorize_request)
end
end
end
end
+69
View File
@@ -0,0 +1,69 @@
# frozen_string_literal: true
RSpec.describe DevModeController do
fab!(:developer) { Fabricate(:admin).tap { |u| Developer.create!(user_id: u.id) } }
fab!(:user)
fab!(:admin)
after { Developer.rebuild_cache }
describe "#index" do
it "returns 200 for developers" do
sign_in(developer)
get "/dev-mode"
expect(response.status).to eq(200)
end
it "returns 404 for anonymous users" do
get "/dev-mode"
expect(response.status).to eq(404)
end
it "returns 404 for normal users" do
sign_in(user)
get "/dev-mode"
expect(response.status).to eq(404)
end
it "returns 404 for admins who are not developers" do
sign_in(admin)
get "/dev-mode"
expect(response.status).to eq(404)
end
end
describe "#enter" do
it "sets _mp_auth cookie when enable_rack_mini_profiler param is present" do
sign_in(developer)
post "/dev-mode", params: { enable_rack_mini_profiler: "true" }
expect(response.cookies["_mp_auth"]).to be_present
expect(response).to redirect_to("/")
end
it "does not set cookie when enable_rack_mini_profiler param is absent" do
sign_in(developer)
post "/dev-mode"
expect(response.cookies["_mp_auth"]).to be_nil
expect(response).to redirect_to("/")
end
it "returns 404 for anonymous users" do
post "/dev-mode", params: { enable_rack_mini_profiler: "true" }
expect(response.status).to eq(404)
end
it "returns 404 for normal users" do
sign_in(user)
post "/dev-mode", params: { enable_rack_mini_profiler: "true" }
expect(response.status).to eq(404)
end
it "returns 404 for admins who are not developers" do
sign_in(admin)
post "/dev-mode", params: { enable_rack_mini_profiler: "true" }
expect(response.status).to eq(404)
end
end
end