You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

sudo_mode.rb 6.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. # frozen_string_literal: true
  2. require 'active_support/core_ext/object/to_query'
  3. require 'rack/utils'
  4. module Redmine
  5. module SudoMode
  6. class SudoRequired < StandardError
  7. end
  8. class Form
  9. include ActiveModel::Validations
  10. attr_accessor :password, :original_fields
  11. validate :check_password
  12. def initialize(password = nil)
  13. self.password = password
  14. end
  15. def check_password
  16. unless password.present? && User.current.check_password?(password)
  17. errors[:password] << :invalid
  18. end
  19. end
  20. end
  21. module Helper
  22. # Represents params data from hash as hidden fields
  23. #
  24. # taken from https://github.com/brianhempel/hash_to_hidden_fields
  25. def hash_to_hidden_fields(hash)
  26. cleaned_hash = hash.to_unsafe_h.reject { |k, v| v.nil? }
  27. pairs = cleaned_hash.to_query.split(Rack::Utils::DEFAULT_SEP)
  28. tags = pairs.map do |pair|
  29. key, value = pair.split('=', 2).map { |str| Rack::Utils.unescape(str) }
  30. hidden_field_tag(key, value)
  31. end
  32. tags.join("\n").html_safe
  33. end
  34. end
  35. module Controller
  36. extend ActiveSupport::Concern
  37. included do
  38. around_action :sudo_mode
  39. end
  40. # Sudo mode Around Filter
  41. #
  42. # Checks the 'last used' timestamp from session and sets the
  43. # SudoMode::active? flag accordingly.
  44. #
  45. # After the request refreshes the timestamp if sudo mode was used during
  46. # this request.
  47. def sudo_mode
  48. if sudo_timestamp_valid?
  49. SudoMode.active!
  50. end
  51. yield
  52. update_sudo_timestamp! if SudoMode.was_used?
  53. end
  54. # This renders the sudo mode form / handles sudo form submission.
  55. #
  56. # Call this method in controller actions if sudo permissions are required
  57. # for processing this request. This approach is good in cases where the
  58. # action needs to be protected in any case or where the check is simple.
  59. #
  60. # In cases where this decision depends on complex conditions in the model,
  61. # consider the declarative approach using the require_sudo_mode class
  62. # method and a corresponding declaration in the model that causes it to throw
  63. # a SudoRequired Error when necessary.
  64. #
  65. # All parameter names given are included as hidden fields to be resubmitted
  66. # along with the password.
  67. #
  68. # Returns true when processing the action should continue, false otherwise.
  69. # If false is returned, render has already been called for display of the
  70. # password form.
  71. #
  72. # if @user.mail_changed?
  73. # require_sudo_mode :user or return
  74. # end
  75. #
  76. def require_sudo_mode(*param_names)
  77. return true if SudoMode.active?
  78. if param_names.blank?
  79. param_names = params.keys - %w(id action controller sudo_password _method authenticity_token utf8)
  80. end
  81. process_sudo_form
  82. if SudoMode.active?
  83. true
  84. else
  85. render_sudo_form param_names
  86. false
  87. end
  88. end
  89. # display the sudo password form
  90. def render_sudo_form(param_names)
  91. @sudo_form ||= SudoMode::Form.new
  92. @sudo_form.original_fields = params.slice( *param_names )
  93. # a simple 'render "sudo_mode/new"' works when used directly inside an
  94. # action, but not when called from a before_action:
  95. respond_to do |format|
  96. format.html { render 'sudo_mode/new' }
  97. format.js { render 'sudo_mode/new' }
  98. end
  99. end
  100. # handle sudo password form submit
  101. def process_sudo_form
  102. if params[:sudo_password]
  103. @sudo_form = SudoMode::Form.new(params[:sudo_password])
  104. if @sudo_form.valid?
  105. SudoMode.active!
  106. else
  107. flash.now[:error] = l(:notice_account_wrong_password)
  108. end
  109. end
  110. end
  111. def sudo_timestamp_valid?
  112. session[:sudo_timestamp].to_i > SudoMode.timeout.ago.to_i
  113. end
  114. def update_sudo_timestamp!(new_value = Time.now.to_i)
  115. session[:sudo_timestamp] = new_value
  116. end
  117. # Before Filter which is used by the require_sudo_mode class method.
  118. class SudoRequestFilter < Struct.new(:parameters, :request_methods)
  119. def before(controller)
  120. method_matches = request_methods.blank? || request_methods.include?(controller.request.method_symbol)
  121. if controller.api_request?
  122. true
  123. elsif SudoMode.possible? && method_matches
  124. controller.require_sudo_mode( *parameters )
  125. else
  126. true
  127. end
  128. end
  129. end
  130. module ClassMethods
  131. # Handles sudo requirements for the given actions, preserving the named
  132. # parameters, or any parameters if you omit the :parameters option.
  133. #
  134. # Sudo enforcement by default is active for all requests to an action
  135. # but may be limited to a certain subset of request methods via the
  136. # :only option.
  137. #
  138. # Examples:
  139. #
  140. # require_sudo_mode :account, only: :post
  141. # require_sudo_mode :update, :create, parameters: %w(role)
  142. # require_sudo_mode :destroy
  143. #
  144. def require_sudo_mode(*args)
  145. actions = args.dup
  146. options = actions.extract_options!
  147. filter = SudoRequestFilter.new Array(options[:parameters]), Array(options[:only])
  148. before_action filter, only: actions
  149. end
  150. end
  151. end
  152. # true if the sudo mode state was queried during this request
  153. def self.was_used?
  154. !!RequestStore.store[:sudo_mode_was_used]
  155. end
  156. # true if sudo mode is currently active.
  157. #
  158. # Calling this method also turns was_used? to true, therefore
  159. # it is important to only call this when sudo is actually needed, as the last
  160. # condition to determine whether a change can be done or not.
  161. #
  162. # If you do it wrong, timeout of the sudo mode will happen too late or not at
  163. # all.
  164. def self.active?
  165. if !!RequestStore.store[:sudo_mode]
  166. RequestStore.store[:sudo_mode_was_used] = true
  167. end
  168. end
  169. def self.active!
  170. RequestStore.store[:sudo_mode] = true
  171. end
  172. def self.possible?
  173. enabled? && User.current.logged?
  174. end
  175. # Turn off sudo mode (never require password entry).
  176. def self.disable!
  177. RequestStore.store[:sudo_mode_disabled] = true
  178. end
  179. # Turn sudo mode back on
  180. def self.enable!
  181. RequestStore.store[:sudo_mode_disabled] = nil
  182. end
  183. def self.enabled?
  184. Redmine::Configuration['sudo_mode'] && !RequestStore.store[:sudo_mode_disabled]
  185. end
  186. # Timespan after which sudo mode expires when unused.
  187. def self.timeout
  188. m = Redmine::Configuration['sudo_mode_timeout'].to_i
  189. (m > 0 ? m : 15).minutes
  190. end
  191. end
  192. end