|
|
require 'active_support/core_ext/object/to_query'
|
|
|
require 'rack/utils'
|
|
|
|
|
|
module Redmine
|
|
|
module SudoMode
|
|
|
|
|
|
class SudoRequired < StandardError
|
|
|
end
|
|
|
|
|
|
|
|
|
class Form
|
|
|
include ActiveModel::Validations
|
|
|
|
|
|
attr_accessor :password, :original_fields
|
|
|
validate :check_password
|
|
|
|
|
|
def initialize(password = nil)
|
|
|
self.password = password
|
|
|
end
|
|
|
|
|
|
def check_password
|
|
|
unless password.present? && User.current.check_password?(password)
|
|
|
errors[:password] << :invalid
|
|
|
end
|
|
|
end
|
|
|
end
|
|
|
|
|
|
|
|
|
module Helper
|
|
|
# Represents params data from hash as hidden fields
|
|
|
#
|
|
|
# taken from https://github.com/brianhempel/hash_to_hidden_fields
|
|
|
def hash_to_hidden_fields(hash)
|
|
|
cleaned_hash = hash.reject { |k, v| v.nil? }
|
|
|
pairs = cleaned_hash.to_query.split(Rack::Utils::DEFAULT_SEP)
|
|
|
tags = pairs.map do |pair|
|
|
|
key, value = pair.split('=', 2).map { |str| Rack::Utils.unescape(str) }
|
|
|
hidden_field_tag(key, value)
|
|
|
end
|
|
|
tags.join("\n").html_safe
|
|
|
end
|
|
|
end
|
|
|
|
|
|
|
|
|
module Controller
|
|
|
extend ActiveSupport::Concern
|
|
|
|
|
|
included do
|
|
|
around_filter :sudo_mode
|
|
|
end
|
|
|
|
|
|
# Sudo mode Around Filter
|
|
|
#
|
|
|
# Checks the 'last used' timestamp from session and sets the
|
|
|
# SudoMode::active? flag accordingly.
|
|
|
#
|
|
|
# After the request refreshes the timestamp if sudo mode was used during
|
|
|
# this request.
|
|
|
def sudo_mode
|
|
|
if sudo_timestamp_valid?
|
|
|
SudoMode.active!
|
|
|
end
|
|
|
yield
|
|
|
update_sudo_timestamp! if SudoMode.was_used?
|
|
|
end
|
|
|
|
|
|
# This renders the sudo mode form / handles sudo form submission.
|
|
|
#
|
|
|
# Call this method in controller actions if sudo permissions are required
|
|
|
# for processing this request. This approach is good in cases where the
|
|
|
# action needs to be protected in any case or where the check is simple.
|
|
|
#
|
|
|
# In cases where this decision depends on complex conditions in the model,
|
|
|
# consider the declarative approach using the require_sudo_mode class
|
|
|
# method and a corresponding declaration in the model that causes it to throw
|
|
|
# a SudoRequired Error when necessary.
|
|
|
#
|
|
|
# All parameter names given are included as hidden fields to be resubmitted
|
|
|
# along with the password.
|
|
|
#
|
|
|
# Returns true when processing the action should continue, false otherwise.
|
|
|
# If false is returned, render has already been called for display of the
|
|
|
# password form.
|
|
|
#
|
|
|
# if @user.mail_changed?
|
|
|
# require_sudo_mode :user or return
|
|
|
# end
|
|
|
#
|
|
|
def require_sudo_mode(*param_names)
|
|
|
return true if SudoMode.active?
|
|
|
|
|
|
if param_names.blank?
|
|
|
param_names = params.keys - %w(id action controller sudo_password)
|
|
|
end
|
|
|
|
|
|
process_sudo_form
|
|
|
|
|
|
if SudoMode.active?
|
|
|
true
|
|
|
else
|
|
|
render_sudo_form param_names
|
|
|
false
|
|
|
end
|
|
|
end
|
|
|
|
|
|
# display the sudo password form
|
|
|
def render_sudo_form(param_names)
|
|
|
@sudo_form ||= SudoMode::Form.new
|
|
|
@sudo_form.original_fields = params.slice( *param_names )
|
|
|
# a simple 'render "sudo_mode/new"' works when used directly inside an
|
|
|
# action, but not when called from a before_filter:
|
|
|
respond_to do |format|
|
|
|
format.html { render 'sudo_mode/new' }
|
|
|
format.js { render 'sudo_mode/new' }
|
|
|
end
|
|
|
end
|
|
|
|
|
|
# handle sudo password form submit
|
|
|
def process_sudo_form
|
|
|
if params[:sudo_password]
|
|
|
@sudo_form = SudoMode::Form.new(params[:sudo_password])
|
|
|
if @sudo_form.valid?
|
|
|
SudoMode.active!
|
|
|
else
|
|
|
flash.now[:error] = l(:notice_account_wrong_password)
|
|
|
end
|
|
|
end
|
|
|
end
|
|
|
|
|
|
def sudo_timestamp_valid?
|
|
|
session[:sudo_timestamp].to_i > SudoMode.timeout.ago.to_i
|
|
|
end
|
|
|
|
|
|
def update_sudo_timestamp!(new_value = Time.now.to_i)
|
|
|
session[:sudo_timestamp] = new_value
|
|
|
end
|
|
|
|
|
|
# Before Filter which is used by the require_sudo_mode class method.
|
|
|
class SudoRequestFilter < Struct.new(:parameters, :request_methods)
|
|
|
def before(controller)
|
|
|
method_matches = request_methods.blank? || request_methods.include?(controller.request.method_symbol)
|
|
|
if controller.api_request?
|
|
|
true
|
|
|
elsif SudoMode.possible? && method_matches
|
|
|
controller.require_sudo_mode( *parameters )
|
|
|
else
|
|
|
true
|
|
|
end
|
|
|
end
|
|
|
end
|
|
|
|
|
|
module ClassMethods
|
|
|
|
|
|
# Handles sudo requirements for the given actions, preserving the named
|
|
|
# parameters, or any parameters if you omit the :parameters option.
|
|
|
#
|
|
|
# Sudo enforcement by default is active for all requests to an action
|
|
|
# but may be limited to a certain subset of request methods via the
|
|
|
# :only option.
|
|
|
#
|
|
|
# Examples:
|
|
|
#
|
|
|
# require_sudo_mode :account, only: :post
|
|
|
# require_sudo_mode :update, :create, parameters: %w(role)
|
|
|
# require_sudo_mode :destroy
|
|
|
#
|
|
|
def require_sudo_mode(*args)
|
|
|
actions = args.dup
|
|
|
options = actions.extract_options!
|
|
|
filter = SudoRequestFilter.new Array(options[:parameters]), Array(options[:only])
|
|
|
before_filter filter, only: actions
|
|
|
end
|
|
|
end
|
|
|
end
|
|
|
|
|
|
|
|
|
# true if the sudo mode state was queried during this request
|
|
|
def self.was_used?
|
|
|
!!RequestStore.store[:sudo_mode_was_used]
|
|
|
end
|
|
|
|
|
|
# true if sudo mode is currently active.
|
|
|
#
|
|
|
# Calling this method also turns was_used? to true, therefore
|
|
|
# it is important to only call this when sudo is actually needed, as the last
|
|
|
# condition to determine wether a change can be done or not.
|
|
|
#
|
|
|
# If you do it wrong, timeout of the sudo mode will happen too late or not at
|
|
|
# all.
|
|
|
def self.active?
|
|
|
if !!RequestStore.store[:sudo_mode]
|
|
|
RequestStore.store[:sudo_mode_was_used] = true
|
|
|
end
|
|
|
end
|
|
|
|
|
|
def self.active!
|
|
|
RequestStore.store[:sudo_mode] = true
|
|
|
end
|
|
|
|
|
|
def self.possible?
|
|
|
enabled? && User.current.logged?
|
|
|
end
|
|
|
|
|
|
# Turn off sudo mode (never require password entry).
|
|
|
def self.disable!
|
|
|
RequestStore.store[:sudo_mode_disabled] = true
|
|
|
end
|
|
|
|
|
|
# Turn sudo mode back on
|
|
|
def self.enable!
|
|
|
RequestStore.store[:sudo_mode_disabled] = nil
|
|
|
end
|
|
|
|
|
|
def self.enabled?
|
|
|
Redmine::Configuration['sudo_mode'] && !RequestStore.store[:sudo_mode_disabled]
|
|
|
end
|
|
|
|
|
|
# Timespan after which sudo mode expires when unused.
|
|
|
def self.timeout
|
|
|
m = Redmine::Configuration['sudo_mode_timeout'].to_i
|
|
|
(m > 0 ? m : 15).minutes
|
|
|
end
|
|
|
end
|
|
|
end
|
|
|
|