Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add password protection in console sessions #103

Open
ashhadsheikh opened this issue Feb 26, 2024 · 9 comments · May be fixed by #116
Open

Add password protection in console sessions #103

ashhadsheikh opened this issue Feb 26, 2024 · 9 comments · May be fixed by #116

Comments

@ashhadsheikh
Copy link

It currently asks for a name to log info, but with multiple folks having access any one can use anyone's name. Would be great to have a password issued to each user so they can use to only login using their password.

@olivier-thatch
Copy link
Contributor

I was just thinking about this. I don't think console1984 should provide its own authentication mechanism, but it would be nice to be able to plug an existing authentication mechanism. Maybe something like this?

config.console1984.user_authentication = proc do
  username = Console1984.supervisor.current_username # this will use the configured username resolver or ask for a username as needed
  user = User.find_by(username: username)
  raise "Invalid username" if user.nil?
  password = $stdin.getpass("Password: ")
  raise "Invalid password" unless user.valid_password?(password)
  raise "Not an admin" unless user.is_admin?
end

Then console1984 would call the user_authentication callable if it's set, or keep the current behavior if not.

This is a just a rough example, but I'd be happy to submit a PR if the maintainers would be open to such a feature. @jorgemanrubia wdyt?

@jorgemanrubia
Copy link
Member

I like the idea @olivier-thatch 👍

@mark-kraemer
Copy link

@olivier-thatch I'm also interested in this functionality. Has any progress been made yet?

@olivier-thatch
Copy link
Contributor

No, sorry, I've been very busy with an ongoing project and probably won't get a chance to work on this in the next few weeks.

@mark-kraemer
Copy link

No worries! Completely understand. I have a workaround by configuring a custom session_logger that handles the password check

@marcelino056
Copy link

marcelino056 commented Jul 24, 2024

I temporarily override the gem to handle it with Devise users

module Console1984
  class Supervisor
    def current_username
      if Console1984.config.ask_for_user_if_empty
        unless @current_username.present?
          email = ask_for_value "Please, enter your email"
          user = ::User.find_by(email: email)
          return raise Console1984::Errors::MissingUsername unless user.present?
          password = ask_for_value "Password: "
          return raise Console1984::Errors::MissingUsername unless user.valid_password? password
        end
        @current_username ||= user.email
      else
        @current_username ||= username_resolver.current.presence || handle_empty_username
      end
    end

    private
    def handle_empty_username
      if Console1984.config.ask_for_username_if_empty
        ask_for_value "Please, enter your name:"
      else
        raise Console1984::Errors::MissingUsername
      end
    end
  end

  class Config
    remove_const(:PROPERTIES) if (defined?(PROPERTIES))

    PROPERTIES = %i[
      session_logger username_resolver ask_for_username_if_empty shield command_executor
      protected_environments protected_urls
      production_data_warning enter_unprotected_encryption_mode_warning enter_protected_mode_warning
      incinerate incinerate_after incineration_queue
      protections_config
      base_record_class
      debug test_mode
      ask_for_user_if_empty
    ]
  
    attr_accessor(*PROPERTIES)

    private
    def set_defaults
      super
      self.ask_for_user_if_empty = false
    end
  end

  class Shield
    private
    def prevent_invoking_protected_methods
      Console1984::ProtectionsConfig.new(YAML.safe_load(File.read(Console1984::Engine.root.join("config/protections.yml"))).symbolize_keys)
    end
  end
end

Console1984.config.ask_for_username_if_empty = false
Console1984.config.ask_for_user_if_empty = true

@grk
Copy link

grk commented Oct 22, 2024

A custom resolver worked for us:

config.console1984.username_resolver = CustomResolver

class CustomResolver
  include Console1984::Freezable

  def current
    # user/pass/totp auth here
  end
end

@chaadow
Copy link

chaadow commented Nov 10, 2024

I did a similar approach to @grk, ( at first I forked #116 but the more I read the source code, the more I figured out a built-in approach )

Here is a full example implementation for email/password with devise with comments explaining my "technical" decisions.

# frozen_string_literal: true

# Custom username resolver for console1984 to authenticate
# administrators to production console
module CustomConsole1984
  class UsernameResolver
    include ::Console1984::Freezeable # Inspired by the default resolver provided by the gem.
    include ::Console1984::InputOutput # to access `ask_for_value` built-in helper

    def current
      email = ask_for_value("Please, enter your email:")
      password = ask_for_password
      user = User.find_for_authentication(email: email)

      raise "User with email: `#{email}` not found" unless administrator

      is_valid = user.valid_for_authentication? { user.valid_password?(password) }

     # it's important to return a string representing a username for console1984
     # i recommend prepending with the `id` because the username value will be stored in `Console1984::User` table
     # This will allow to properly distinguish user, and is a safe approach IMO
      is_valid ? "#{user.id}_#{user.full_name.sub(' ', '_')}" : raise("Authentication failed for user `#{email}`")
    end

    private

    # helper to hide password typing, similar to any `sudo ...` command in UNIX.
    def ask_for_password
      puts Rainbow("Please, enter your password:").green
 
      password = $stdin.noecho(&:gets).chomp until password.present?
      password
    end
  end
end

then in config/application.rb do the following.

 # important, this require_relative must be called AFTER `Bundler.require(*Rails.groups)`
 require_relative '../lib/my_path_to_resolver'

config.console1984.username_resolver = CustomConsole1984::UsernameResolver.new # Important to instantiate an instance.

@chaadow
Copy link

chaadow commented Nov 10, 2024

I also wanted to send a slack notification after each successful rails console connection.
Initially I tweaked it inside the username resolver, but it felt dirty, and I also wanted to send the reason

After reading the source code, I found there is a session_logger config ( undocumented option though :/ ) So instead of reinventing the wheel, knowing that I just wanted to augment/enhance the default session logger, I did the following

# frozen_string_literal: true

module CustomConsole1984
  class SessionLogger < ::Console1984::SessionsLogger::Database
  
    # reopen `start_session` method
    def start_session(username, reason)
      super

      SlackMonitoring::AlertWhenAdministratorConnectsToProdConsole.call(username:, reason:)
    end
  end
end

You can also create your own logger but the interface/contract is a bit heavy, (as well as undocumented :/ ) So I personally recommend my approach 🤷🏼‍♂️

Finally add the following in config/application.rb

# important, this require_relative must be called AFTER `Bundler.require(*Rails.groups)`
require_relative '../lib/my_path_to_resolver' 

# ...

config.console1984.session_logger = CustomConsole1984::SessionLogger.new # important to create an instance.

You can probably create these custom resolvers/loggers with the once autoloader, although not sure because you need to access them right away, but you can try 🤷🏼‍♂️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants