/sudo_rails

🔒 Sudo mode for your Rails controllers

Primary LanguageRubyMIT LicenseMIT

Sudo Rails

Gem Build Status Maintainability

Sudo mode for your Rails controllers

🔒 Protect any Rails action with a customizable password confirmation strategy.

class SecretController < ApplicationController
  sudo
end

Inspired by Unix sudo command and GitHub Sudo mode.

Installation

Add this line to your Gemfile and then execute bundle install:

gem 'sudo_rails'

Usage

From now on, you have the sudo method available in your controllers, you can protect the whole controller or only some actions:

class SettingsController < ApplicationController
  sudo only: :sensible_settings
end

Under the hood, the sudo method delegates to a before_action callback, so you're able to pass the following options: :only, :except, :if and :unless.

The gem also provides a couple of controller helpers, useful to manually manage the sudo session status:

  • reset_sudo_session!: resets the current sudo session, if any.
  • extend_sudo_session!: marks the current session as a valid sudo session.

Configuration

You can use the setup method to configure and customize different things:

# config/initializers/sudo_rails.rb
SudoRails.setup do |config|
  # On/off engine
  config.enabled = true

  # Sudo mode sessions duration, default is 30 minutes
  config.sudo_session_duration = 10.minutes

  # Confirmation page styling
  config.custom_logo = '/images/logo_medium.png'
  config.primary_color = '#1a7191'
  config.background_color = '#1a1a1a'
  config.layout = 'admin'

  # Confirmation strategy implementation
  config.confirm_strategy = -> (context, password) {
    user = context.current_user
    user.valid_password?(password)
  }

  # Reset password link
  config.reset_pass_link = '/users/password/new'

  # Subscribe to different events
  config.callbacks = {
    invalid_sudo_session: -> (context) {
      user = context.current_user
      AuthService.send_code(user)
    },
    invalid_confirmation: -> (context) {
      user = context.current_user
      Rails.logger.warn("[SUDO_RAILS] invalid password for #{user.email}")
    }
  }
end

Use the provided sudo_rails:config generator to create a default config file under your initializers folder.

Sudo sessions

Using the sudo_session_duration option you are able to configure the sudo session duration (30 minutes by default).

If you set it to nil, your sudo session won't expire automatically and you will have to do it manually by using the reset_sudo_session! helper.

Styling

Using the custom_logo, primary_color and background_color options, you can customize the confirmation page. In case you want full control of the styles, you can use your own layout (and consequently your own styles too) using the layout option.

See some 📷 examples here.

ℹ️ If you are using your own layout, don't forget to render the flash messages in that layout. You can do something like this.

You can also override the view by calling the sudo_rails:view generator. This will create a copy of the view file at app/views/sudo_rails/confirm_form.html.erb which can be later modified as per your requirements.

Confirmation strategy

You should define how to validate the password using the confirm_strategy option. It must be a lambda, which will receive 2 arguments: the controller instance (context) and the password from the user.

By default, the gem ships with Devise and Clearance integration. Check it here.

ℹ️ In order to autoload Devise or Clearance strategy properly, you should place the sudo_rails gem after them in the Gemfile.

Implementation examples:

# Devise implementation
config.confirm_strategy = -> (context, password) {
  user = context.current_user
  user.valid_password?(password)
}

# has_secure_password implementation
config.confirm_strategy = -> (context, password) {
  user = context.current_user
  user.authenticate(password)
}

# Another example, using ENV vars
config.confirm_strategy = -> (context, password) {
  user = context.current_user
  user.admin? && password == ENV['SUPER_SECRET_PASSWORD']
}

Callbacks

You can subscribe to different lifecycle events via the callbacks option. Each callback must be a lambda, which will receive 1 argument, the controller instance (context).

You can subscribe to the following events:

  • :invalid_sudo_session: fired when the confirmation page is rendered, because there is no valid sudo session. Be careful! If the page is re-submitted or the password is invalid, the confirmation page will be rendered again and this event will be fired again too.
  • :new_sudo_session: fired when a new sudo session is started.
  • :invalid_confirmation: fired when an invalid password is submitted.

This can be really useful for example for instrumentation or logging:

config.callbacks = {
  invalid_confirmation: -> (context) {
    user = context.current_user
    request = context.request

    Rails.logger.warn("[SUDO_RAILS] Invalid verification: #{user.email} - #{request.remote_ip}")
  }
}

Or you can even implement custom workflows along with the confirm_strategy option. Like for example, using your 2FA system instead of the session password:

config.callbacks = {
  invalid_sudo_session: -> (context) {
    user = context.current_user
    AuthService.send_code(user)
  }
}

config.confirm_strategy = -> (context, code) {
  user = context.current_user
  AuthService.validate_code(user, code)
}

I18n

sudo_rails uses I18n by default. Take a look at our locale file to check all available messages.

Development

Any kind of feedback, bug report, idea or enhancement are really appreciated.

To contribute, just fork the repo, hack on it and send a pull request. Don't forget to add tests for behaviour changes and run the test suite:

> bundle exec rspec

License

Copyright (c) Marc Anguera. SudoRails is released under the MIT License.