mirror of
				https://github.com/redmine/redmine.git
				synced 2025-11-03 11:56:18 +01:00 
			
		
		
		
	git-svn-id: http://svn.redmine.org/redmine/trunk@17977 e93f8b46-1217-0410-a6f0-8f06a7374b81
		
			
				
	
	
		
			227 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			227 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
# 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[: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
 |