New Redmine Feature: Sudo Mode
Here’s a goodie from the upcoming Redmine 3.1 to make your Redmine installation a bit more robust against session hijacking attempts: sudo mode.
Github introduced this some time ago, and earlier this year we decided to implement something similar for Planio. As we were confident that this would be a nice addition to Redmine as well, we submitted a patch which now got merged to be part of the next Redmine release.
How Does It Work?
Just like Unix
sudo, Redmine’s sudo mode asks for your password if you want to
do something potentially dangerous. The goal is to reduce the risk that someone
who gained (physical or remote) access to your authenticated session can use
that to cause harm by abusing your privileges.
To avoid bothering you over and over again while you are doing admin work, your successful re-authentication is remembered for a certain time and will expire only if you stop doing things that require an active sudo mode. Length of that timeout can be changed in Redmine’s configuration file, where you also have to go to activate the feature in the first place.
The list of things in Redmine which we decided should be protected this way is quite long, and those things are spread all over the place. Starts with your account details (a change to your account email address that goes unnoticed can easily lead to your account being taken over at a later time), goes on with administrative functions like users, groups and permissions admin, and does not end with project memberships.
Two main challenges were quickly identified:
- allow use of the feature in various places, while avoiding duplication of code, and ideally making future use of the feature in other areas (and even plugins) very easy.
- do not annoy the user, i.e. only ask for the password before actually changing something, and do that with as little interryption to the user’s workflow as possible.
Avoiding duplication of code
I came up with a single class method to activate the feature in the relevant places:
class UsersController < ApplicationController require_sudo_mode :create, :update, :destroy end
If you are the author of a Redmine plugin and want to protect some sensitive parts of it, a line like that is the only thing you need to add to your controller (don’t forget to check the current Redmine version before unless you only target Redmine 3.1 and above).
Behind the scenes this method call installs a before filter for the given actions. This filter will check if sudo mode is already active, and render the password entry form otherwise, preventing execution of the original action. The filter also takes care of form redisplay or continuing the original action on success. Which directly leads to the next challenge:
Preserving Form Values
For us ‘do not annoy the user’ meant to intercept the user’s actual action (in most cases that is a form submit), then ask for the password if necessary, and continue with what the user originally wanted to do after the correct password was supplied without requiring her to repeat whatever she was about to do.
Sounds like a tricky thing to do, but actually was pretty straight forward. Key is to hook into the existing actions (instead of providing our own controller and action for processing submittal of our password form) and keeping all the user-submitted values from the original request, rendering them as hidden fields in our password form. To the controller, the submit of that form full of hidden fields looks like the original submit, but with an additional field carrying the user-supplied password.
Once that form is submitted, the before filter kicks in again, this time for
checking the password. On success it activates sudo mode and returns
true so the
original action continues processing the request.
As it turned out I didn’t even have to bother writing the serialization of the
params hash to hidden fields myself, thankfully somebody already did that
This hidden field approach will most certainly break when used with a file upload form, but since there appears to be no use case in Redmine that called for taking care of that, we didn’t try to tackle this one. I think it would be possible by saving the file before asking for the password, keeping a reference to it in a hidden field, then later restoring a ‘fake upload’ from that for the original action to process.
Things I Learned
You can install actual objects as before/after/around filters - they just have to respond to the corresponding method which then will be called with the controller instance as an argument. This is very handy in cases where you have recurring filter logic which only needs to be configured just a bit for every use case. That’s actually even described in the Rails Guide however until now I never made use of it.