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