summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorJean-Philippe Lang <jp_lang@yahoo.fr>2015-06-19 18:41:10 +0000
committerJean-Philippe Lang <jp_lang@yahoo.fr>2015-06-19 18:41:10 +0000
commitd6f389658b9e83d7a5d74c57fc46a203a5a88591 (patch)
tree534fd5f3520833e1c1c2bb2105971ce86008b991 /lib
parent3811ff5d95bd848f457c9d29a162ce83f12fe3ac (diff)
downloadredmine-d6f389658b9e83d7a5d74c57fc46a203a5a88591.tar.gz
redmine-d6f389658b9e83d7a5d74c57fc46a203a5a88591.zip
Require password re-entry for sensitive actions (#19851).
Patch by Jens Krämer. git-svn-id: http://svn.redmine.org/redmine/trunk@14333 e93f8b46-1217-0410-a6f0-8f06a7374b81
Diffstat (limited to 'lib')
-rw-r--r--lib/redmine/sudo_mode.rb224
1 files changed, 224 insertions, 0 deletions
diff --git a/lib/redmine/sudo_mode.rb b/lib/redmine/sudo_mode.rb
new file mode 100644
index 000000000..3197fe11b
--- /dev/null
+++ b/lib/redmine/sudo_mode.rb
@@ -0,0 +1,224 @@
+require 'active_support/core_ext/object/to_query'
+require 'rack/utils'
+
+module Redmine
+ module SudoMode
+
+ # timespan after which sudo mode expires when unused.
+ MAX_INACTIVITY = 15.minutes
+
+
+ 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 api_request?
+ SudoMode.disable!
+ elsif 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 > MAX_INACTIVITY.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 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?
+ !disabled? && 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.disabled?
+ !!RequestStore.store[:sudo_mode_disabled]
+ end
+
+ end
+end
+