# frozen_string_literal: true require 'cased/cli/session' require 'tty/prompt' module Cased module CLI # InteractiveSession is responsible for initiating a Cased CLI session and # responding to all its possible states. # # InteractiveSession is intended to be used where a TTY is present to handle # the entire flow from authentication, reason required, waiting for # approval, canceled, or timed out. class InteractiveSession def self.start(reason: nil, command: nil, metadata: {}) return Cased::CLI::Session.current if Cased::CLI::Session.current&.approved? Cased::CLI::Log.log 'Running under Cased CLI.' new(reason: reason, command: command, metadata: metadata).create end attr_reader :session def initialize(reason: nil, command: nil, metadata: {}) @session = Cased::CLI::Session.new( reason: reason, command: command, metadata: metadata, ) @prompt = TTY::Prompt.new end def create signal_handler = Signal.trap('SIGINT') do if session.requested? Cased::CLI::Log.log 'Exiting and canceling request…' session.cancel exit 0 elsif signal_handler.respond_to?(:call) # We need to call the original handler if we exit this interactive # session successfully signal_handler.call else raise Interrupt end end if session.create handle_state(session.state) elsif session.reauthenticate? Cased::CLI::Log.log "You must re-authenticate with Cased due to recent changes to this application's settings." identity = Cased::CLI::Identity.new token, ip_address = identity.identify session.authentication.token = token session.forwarded_ip_address = ip_address create elsif session.unauthorized? if session.authentication.exists? Cased::CLI::Log.log "Existing credentials at #{session.authentication.credentials_path} are not valid." else Cased::CLI::Log.log "Could not find credentials at #{session.authentication.credentials_path}, looking up now…" end identity = Cased::CLI::Identity.new token, ip_address = identity.identify session.authentication.token = token session.forwarded_ip_address = ip_address create elsif session.reason_required? reason_prompt && create else Cased::CLI::Log.log 'Could not start CLI session.' exit 1 if Cased.config.guard_deny_if_unreachable? end session end private def reason_prompt reason = @prompt.multiline(Cased::CLI::Log.string('Please enter a reason for access:'), help: '(Press Ctrl+D or Ctrl+Z to submit)') session.reason = reason.join("\n") rescue TTY::Reader::InputInterrupt Cased::CLI::Log.log 'Exiting and canceling request…' exit 0 end def wait_for_approval sleep 1 session.refresh && handle_state(session.state) end def waiting_for_approval_message return if defined?(@waiting_for_approval_message_displayed) motd = session.guard_application.dig('settings', 'message_of_the_day') waiting_message = motd.blank? ? 'Approval request sent…' : motd Cased::CLI::Log.log "#{waiting_message} (id: #{session.id})" @waiting_for_approval_message_displayed = true end def handle_state(state) case state when 'approved' Cased::CLI::Log.log 'CLI session has been approved' session.record when 'requested' waiting_for_approval_message wait_for_approval when 'denied' Cased::CLI::Log.log 'CLI session has been denied' exit 1 when 'timed_out' Cased::CLI::Log.log 'CLI session has timed out' exit 1 when 'canceled' Cased::CLI::Log.log 'CLI session has been canceled' exit 0 end end end end end