diff options
author | Jean-Philippe Lang <jp_lang@yahoo.fr> | 2015-06-19 18:41:10 +0000 |
---|---|---|
committer | Jean-Philippe Lang <jp_lang@yahoo.fr> | 2015-06-19 18:41:10 +0000 |
commit | d6f389658b9e83d7a5d74c57fc46a203a5a88591 (patch) | |
tree | 534fd5f3520833e1c1c2bb2105971ce86008b991 /lib | |
parent | 3811ff5d95bd848f457c9d29a162ce83f12fe3ac (diff) | |
download | redmine-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.rb | 224 |
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 + |