123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- # frozen_string_literal: true
-
- 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.add(: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.to_unsafe_h.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_action :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 _method authenticity_token utf8)
- 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_action:
- 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_action 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 whether 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
|