##// END OF EJS Templates
Don't use SudoMode.disable! to skip API requests (#19851)....
Jean-Philippe Lang -
r13956:c2fca3799927
parent child
Show More
@@ -1,223 +1,223
1 1 require 'active_support/core_ext/object/to_query'
2 2 require 'rack/utils'
3 3
4 4 module Redmine
5 5 module SudoMode
6 6
7 7 # timespan after which sudo mode expires when unused.
8 8 MAX_INACTIVITY = 15.minutes
9 9
10 10
11 11 class SudoRequired < StandardError
12 12 end
13 13
14 14
15 15 class Form
16 16 include ActiveModel::Validations
17 17
18 18 attr_accessor :password, :original_fields
19 19 validate :check_password
20 20
21 21 def initialize(password = nil)
22 22 self.password = password
23 23 end
24 24
25 25 def check_password
26 26 unless password.present? && User.current.check_password?(password)
27 27 errors[:password] << :invalid
28 28 end
29 29 end
30 30 end
31 31
32 32
33 33 module Helper
34 34 # Represents params data from hash as hidden fields
35 35 #
36 36 # taken from https://github.com/brianhempel/hash_to_hidden_fields
37 37 def hash_to_hidden_fields(hash)
38 38 cleaned_hash = hash.reject { |k, v| v.nil? }
39 39 pairs = cleaned_hash.to_query.split(Rack::Utils::DEFAULT_SEP)
40 40 tags = pairs.map do |pair|
41 41 key, value = pair.split('=', 2).map { |str| Rack::Utils.unescape(str) }
42 42 hidden_field_tag(key, value)
43 43 end
44 44 tags.join("\n").html_safe
45 45 end
46 46 end
47 47
48 48
49 49 module Controller
50 50 extend ActiveSupport::Concern
51 51
52 52 included do
53 53 around_filter :sudo_mode
54 54 end
55 55
56 56 # Sudo mode Around Filter
57 57 #
58 58 # Checks the 'last used' timestamp from session and sets the
59 59 # SudoMode::active? flag accordingly.
60 60 #
61 61 # After the request refreshes the timestamp if sudo mode was used during
62 62 # this request.
63 63 def sudo_mode
64 if api_request?
65 SudoMode.disable!
66 elsif sudo_timestamp_valid?
64 if sudo_timestamp_valid?
67 65 SudoMode.active!
68 66 end
69 67 yield
70 68 update_sudo_timestamp! if SudoMode.was_used?
71 69 end
72 70
73 71 # This renders the sudo mode form / handles sudo form submission.
74 72 #
75 73 # Call this method in controller actions if sudo permissions are required
76 74 # for processing this request. This approach is good in cases where the
77 75 # action needs to be protected in any case or where the check is simple.
78 76 #
79 77 # In cases where this decision depends on complex conditions in the model,
80 78 # consider the declarative approach using the require_sudo_mode class
81 79 # method and a corresponding declaration in the model that causes it to throw
82 80 # a SudoRequired Error when necessary.
83 81 #
84 82 # All parameter names given are included as hidden fields to be resubmitted
85 83 # along with the password.
86 84 #
87 85 # Returns true when processing the action should continue, false otherwise.
88 86 # If false is returned, render has already been called for display of the
89 87 # password form.
90 88 #
91 89 # if @user.mail_changed?
92 90 # require_sudo_mode :user or return
93 91 # end
94 92 #
95 93 def require_sudo_mode(*param_names)
96 94 return true if SudoMode.active?
97 95
98 96 if param_names.blank?
99 97 param_names = params.keys - %w(id action controller sudo_password)
100 98 end
101 99
102 100 process_sudo_form
103 101
104 102 if SudoMode.active?
105 103 true
106 104 else
107 105 render_sudo_form param_names
108 106 false
109 107 end
110 108 end
111 109
112 110 # display the sudo password form
113 111 def render_sudo_form(param_names)
114 112 @sudo_form ||= SudoMode::Form.new
115 113 @sudo_form.original_fields = params.slice( *param_names )
116 114 # a simple 'render "sudo_mode/new"' works when used directly inside an
117 115 # action, but not when called from a before_filter:
118 116 respond_to do |format|
119 117 format.html { render 'sudo_mode/new' }
120 118 format.js { render 'sudo_mode/new' }
121 119 end
122 120 end
123 121
124 122 # handle sudo password form submit
125 123 def process_sudo_form
126 124 if params[:sudo_password]
127 125 @sudo_form = SudoMode::Form.new(params[:sudo_password])
128 126 if @sudo_form.valid?
129 127 SudoMode.active!
130 128 else
131 129 flash.now[:error] = l(:notice_account_wrong_password)
132 130 end
133 131 end
134 132 end
135 133
136 134 def sudo_timestamp_valid?
137 135 session[:sudo_timestamp].to_i > MAX_INACTIVITY.ago.to_i
138 136 end
139 137
140 138 def update_sudo_timestamp!(new_value = Time.now.to_i)
141 139 session[:sudo_timestamp] = new_value
142 140 end
143 141
144 142 # Before Filter which is used by the require_sudo_mode class method.
145 143 class SudoRequestFilter < Struct.new(:parameters, :request_methods)
146 144 def before(controller)
147 145 method_matches = request_methods.blank? || request_methods.include?(controller.request.method_symbol)
148 if SudoMode.possible? && method_matches
146 if controller.api_request?
147 true
148 elsif SudoMode.possible? && method_matches
149 149 controller.require_sudo_mode( *parameters )
150 150 else
151 151 true
152 152 end
153 153 end
154 154 end
155 155
156 156 module ClassMethods
157 157
158 158 # Handles sudo requirements for the given actions, preserving the named
159 159 # parameters, or any parameters if you omit the :parameters option.
160 160 #
161 161 # Sudo enforcement by default is active for all requests to an action
162 162 # but may be limited to a certain subset of request methods via the
163 163 # :only option.
164 164 #
165 165 # Examples:
166 166 #
167 167 # require_sudo_mode :account, only: :post
168 168 # require_sudo_mode :update, :create, parameters: %w(role)
169 169 # require_sudo_mode :destroy
170 170 #
171 171 def require_sudo_mode(*args)
172 172 actions = args.dup
173 173 options = actions.extract_options!
174 174 filter = SudoRequestFilter.new Array(options[:parameters]), Array(options[:only])
175 175 before_filter filter, only: actions
176 176 end
177 177 end
178 178 end
179 179
180 180
181 181 # true if the sudo mode state was queried during this request
182 182 def self.was_used?
183 183 !!RequestStore.store[:sudo_mode_was_used]
184 184 end
185 185
186 186 # true if sudo mode is currently active.
187 187 #
188 188 # Calling this method also turns was_used? to true, therefore
189 189 # it is important to only call this when sudo is actually needed, as the last
190 190 # condition to determine wether a change can be done or not.
191 191 #
192 192 # If you do it wrong, timeout of the sudo mode will happen too late or not at
193 193 # all.
194 194 def self.active?
195 195 if !!RequestStore.store[:sudo_mode]
196 196 RequestStore.store[:sudo_mode_was_used] = true
197 197 end
198 198 end
199 199
200 200 def self.active!
201 201 RequestStore.store[:sudo_mode] = true
202 202 end
203 203
204 204 def self.possible?
205 205 enabled? && User.current.logged?
206 206 end
207 207
208 208 # Turn off sudo mode (never require password entry).
209 209 def self.disable!
210 210 RequestStore.store[:sudo_mode_disabled] = true
211 211 end
212 212
213 213 # Turn sudo mode back on
214 214 def self.enable!
215 215 RequestStore.store[:sudo_mode_disabled] = nil
216 216 end
217 217
218 218 def self.enabled?
219 219 Redmine::Configuration['sudo_mode'] && !RequestStore.store[:sudo_mode_disabled]
220 220 end
221 221 end
222 222 end
223 223
@@ -1,146 +1,161
1 1 require File.expand_path('../../test_helper', __FILE__)
2 2
3 3 class SudoTest < Redmine::IntegrationTest
4 4 fixtures :projects, :members, :member_roles, :roles, :users
5 5
6 6 def setup
7 7 Redmine::SudoMode.stubs(:enabled?).returns(true)
8 8 end
9 9
10 10 def test_add_user
11 11 log_user("admin", "admin")
12 12 get "/users/new"
13 13 assert_response :success
14 14 post "/users",
15 15 :user => { :login => "psmith", :firstname => "Paul",
16 16 :lastname => "Smith", :mail => "psmith@somenet.foo",
17 17 :language => "en", :password => "psmith09",
18 18 :password_confirmation => "psmith09" }
19 19 assert_response :success
20 20 assert_nil User.find_by_login("psmith")
21 21
22 22 post "/users",
23 23 :user => { :login => "psmith", :firstname => "Paul",
24 24 :lastname => "Smith", :mail => "psmith@somenet.foo",
25 25 :language => "en", :password => "psmith09",
26 26 :password_confirmation => "psmith09" },
27 27 :sudo_password => 'admin'
28 28 assert_response 302
29 29
30 30 user = User.find_by_login("psmith")
31 31 assert_kind_of User, user
32 32 end
33 33
34 34 def test_create_member_xhr
35 35 log_user 'admin', 'admin'
36 36 get '/projects/ecookbook/settings/members'
37 37 assert_response :success
38 38
39 39 assert_no_difference 'Member.count' do
40 40 xhr :post, '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}
41 41 end
42 42
43 43 assert_no_difference 'Member.count' do
44 44 xhr :post, '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: ''
45 45 end
46 46
47 47 assert_no_difference 'Member.count' do
48 48 xhr :post, '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: 'wrong'
49 49 end
50 50
51 51 assert_difference 'Member.count' do
52 52 xhr :post, '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: 'admin'
53 53 end
54 54 assert User.find(7).member_of?(Project.find(1))
55 55 end
56 56
57 57 def test_create_member
58 58 log_user 'admin', 'admin'
59 59 get '/projects/ecookbook/settings/members'
60 60 assert_response :success
61 61
62 62 assert_no_difference 'Member.count' do
63 63 post '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}
64 64 end
65 65
66 66 assert_no_difference 'Member.count' do
67 67 post '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: ''
68 68 end
69 69
70 70 assert_no_difference 'Member.count' do
71 71 post '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: 'wrong'
72 72 end
73 73
74 74 assert_difference 'Member.count' do
75 75 post '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: 'admin'
76 76 end
77 77
78 78 assert_redirected_to '/projects/ecookbook/settings/members'
79 79 assert User.find(7).member_of?(Project.find(1))
80 80 end
81 81
82 82 def test_create_role
83 83 log_user 'admin', 'admin'
84 84 get '/roles'
85 85 assert_response :success
86 86
87 87 get '/roles/new'
88 88 assert_response :success
89 89
90 90 post '/roles', role: { }
91 91 assert_response :success
92 92 assert_select 'h2', 'Confirm your password to continue'
93 93 assert_select 'form[action="/roles"]'
94 94 assert assigns(:sudo_form).errors.blank?
95 95
96 96 post '/roles', role: { name: 'new role', issues_visibility: 'all' }
97 97 assert_response :success
98 98 assert_select 'h2', 'Confirm your password to continue'
99 99 assert_select 'form[action="/roles"]'
100 100 assert_match /"new role"/, response.body
101 101 assert assigns(:sudo_form).errors.blank?
102 102
103 103 post '/roles', role: { name: 'new role', issues_visibility: 'all' }, sudo_password: 'wrong'
104 104 assert_response :success
105 105 assert_select 'h2', 'Confirm your password to continue'
106 106 assert_select 'form[action="/roles"]'
107 107 assert_match /"new role"/, response.body
108 108 assert assigns(:sudo_form).errors[:password].present?
109 109
110 110 assert_difference 'Role.count' do
111 111 post '/roles', role: { name: 'new role', issues_visibility: 'all', assignable: '1', permissions: %w(view_calendar) }, sudo_password: 'admin'
112 112 end
113 113 assert_redirected_to '/roles'
114 114 end
115 115
116 116 def test_update_email_address
117 117 log_user 'jsmith', 'jsmith'
118 118 get '/my/account'
119 119 assert_response :success
120 120 post '/my/account', user: { mail: 'newmail@test.com' }
121 121 assert_response :success
122 122 assert_select 'h2', 'Confirm your password to continue'
123 123 assert_select 'form[action="/my/account"]'
124 124 assert_match /"newmail@test\.com"/, response.body
125 125 assert assigns(:sudo_form).errors.blank?
126 126
127 127 # wrong password
128 128 post '/my/account', user: { mail: 'newmail@test.com' }, sudo_password: 'wrong'
129 129 assert_response :success
130 130 assert_select 'h2', 'Confirm your password to continue'
131 131 assert_select 'form[action="/my/account"]'
132 132 assert_match /"newmail@test\.com"/, response.body
133 133 assert assigns(:sudo_form).errors[:password].present?
134 134
135 135 # correct password
136 136 post '/my/account', user: { mail: 'newmail@test.com' }, sudo_password: 'jsmith'
137 137 assert_redirected_to '/my/account'
138 138 assert_equal 'newmail@test.com', User.find_by_login('jsmith').mail
139 139
140 140 # sudo mode should now be active and not require password again
141 141 post '/my/account', user: { mail: 'even.newer.mail@test.com' }
142 142 assert_redirected_to '/my/account'
143 143 assert_equal 'even.newer.mail@test.com', User.find_by_login('jsmith').mail
144 144 end
145 145
146 def test_sudo_mode_should_skip_api_requests
147 with_settings :rest_api_enabled => '1' do
148 assert_difference('User.count') do
149 post '/users.json', {
150 :user => {
151 :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname',
152 :mail => 'foo@example.net', :password => 'secret123',
153 :mail_notification => 'only_assigned'}
154 },
155 credentials('admin')
156
157 assert_response :created
158 end
159 end
160 end
146 161 end
General Comments 0
You need to be logged in to leave comments. Login now