mirror of
https://github.com/discourse/discourse.git
synced 2026-07-31 00:28:19 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user