Skip to content
Merged

Release #1747

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ dr-logs
*.swp
CLAUDE.md
AGENTS.md
keeper_db.sqlite
2 changes: 1 addition & 1 deletion keepercommander/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
# Contact: ops@keepersecurity.com
#

__version__ = '17.2.3'
__version__ = '17.2.4'
4 changes: 1 addition & 3 deletions keepercommander/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ def main(from_package=False):
print('Keeper Commander - CLI-based vault and admin interface to the Keeper platform')
print('')
print('To get started:')
print(' keeper login Authenticate to Keeper')
print(' keeper shell Open interactive command shell')
print(' keeper supershell Open full-screen vault browser (TUI)')
print(' keeper -h Show help and available options')
Expand All @@ -417,9 +418,6 @@ def main(from_package=False):
# Special handling for shell/- when NOT asking for help
if opts.command == '-':
params.batch_mode = True
elif opts.command == 'login' and not original_args_after_command and not command_wants_help:
# 'keeper login' with no args - just open shell and let it handle login
pass
elif opts.command and os.path.isfile(opts.command):
with open(opts.command, 'r') as f:
lines = f.readlines()
Expand Down
7 changes: 5 additions & 2 deletions keepercommander/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ def login(params, new_login=False, login_ui=None):
try:
flow.login(params, new_login=new_login)
except loginv3.InvalidDeviceToken:
logging.warning('Registering new device')
from colorama import Fore
print(f'{Fore.CYAN}Registering new device...{Fore.RESET}')
flow.login(params, new_device=True)


Expand Down Expand Up @@ -706,8 +707,10 @@ def execute_router_rest(params: KeeperParams, endpoint: str, payload: Optional[b
up = urlparse(os.environ['ROUTER_URL'])
url_comp = (up.scheme, up.netloc, f'api/user/{endpoint}', None, None, None)
else:
from .constants import get_router_host
up = urlparse(params.rest_context.server_base)
url_comp = ('https', f'connect.{up.hostname}', f'api/user/{endpoint}', None, None, None)
router_host = get_router_host(up.hostname)
url_comp = ('https', router_host, f'api/user/{endpoint}', None, None, None)
url = urlunparse(url_comp)

if payload is not None:
Expand Down
80 changes: 41 additions & 39 deletions keepercommander/auth/console_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from colorama import Fore, Style
from . import login_steps
from .. import utils
from ..display import bcolors
from ..error import KeeperApiError


Expand All @@ -24,8 +23,8 @@ def __init__(self):

def on_device_approval(self, step):
if self._show_device_approval_help:
print(f"\n{Style.BRIGHT}Device Approval Required{Style.RESET_ALL}\n")
print("Select an approval method:")
print(f"\n{Fore.YELLOW}Device Approval Required{Fore.RESET}\n")
print(f"{Fore.CYAN}Select an approval method:{Fore.RESET}")
print(f" {Fore.GREEN}1{Fore.RESET}. Email - Send approval link to your email")
print(f" {Fore.GREEN}2{Fore.RESET}. Keeper Push - Send notification to an approved device")
print(f" {Fore.GREEN}3{Fore.RESET}. 2FA Push - Send code via your 2FA method")
Expand All @@ -35,12 +34,12 @@ def on_device_approval(self, step):
print()
self._show_device_approval_help = False
else:
print(f"\n{Style.BRIGHT}Waiting for device approval.{Style.RESET_ALL}")
print(f"Check email, SMS, or push notification on the approved device.")
print(f"\n{Fore.YELLOW}Waiting for device approval.{Fore.RESET}")
print(f"{Fore.CYAN}Check email, SMS, or push notification on the approved device.{Fore.RESET}")
print(f"Enter {Fore.GREEN}c <code>{Fore.RESET} to submit a verification code.\n")

try:
selection = input('Selection (or Enter to check status): ').strip().lower()
selection = input(f'{Fore.GREEN}Selection{Fore.RESET} (or Enter to check status): ').strip().lower()

if selection == '1' or selection == 'email_send' or selection == 'es':
step.send_push(login_steps.DeviceApprovalChannel.Email)
Expand All @@ -60,7 +59,7 @@ def on_device_approval(self, step):
elif selection == 'c' or selection.startswith('c '):
# Support both "c" (prompts for code) and "c <code>" (code inline)
if selection == 'c':
code_input = input('Enter verification code: ').strip()
code_input = input(f'{Fore.GREEN}Enter verification code: {Fore.RESET}').strip()
else:
code_input = selection[2:].strip() # Extract code after "c "

Expand Down Expand Up @@ -99,7 +98,7 @@ def on_device_approval(self, step):
step.cancel()
except KeeperApiError as kae:
print()
print(bcolors.WARNING + kae.message + bcolors.ENDC)
print(f'{Fore.YELLOW}{kae.message}{Fore.RESET}')
pass

@staticmethod
Expand All @@ -123,16 +122,18 @@ def on_two_factor(self, step):
channels = step.get_channels()

if self._show_two_factor_help:
print("\nThis account requires 2FA Authentication\n")
print(f"\n{Fore.YELLOW}Two-Factor Authentication Required{Fore.RESET}\n")
print(f"{Fore.CYAN}Select your 2FA method:{Fore.RESET}")
for i in range(len(channels)):
channel = channels[i]
print(f"{i+1:>3}. {ConsoleLoginUi.two_factor_channel_to_desc(channel.channel_type)} {channel.channel_name} {channel.phone}")
print(f"{'q':>3}. Quit login attempt and return to Commander prompt")
print(f" {Fore.GREEN}{i+1}{Fore.RESET}. {ConsoleLoginUi.two_factor_channel_to_desc(channel.channel_type)} {channel.channel_name} {channel.phone}")
print(f" {Fore.GREEN}q{Fore.RESET}. Cancel login")
print()
self._show_device_approval_help = False

channel = None # type: Optional[login_steps.TwoFactorChannelInfo]
while channel is None:
selection = input('Selection: ')
selection = input(f'{Fore.GREEN}Selection: {Fore.RESET}')
if selection == 'q':
raise KeyboardInterrupt()

Expand All @@ -154,7 +155,7 @@ def on_two_factor(self, step):
mfa_prompt = True
try:
step.send_push(channel.channel_uid, login_steps.TwoFactorPushAction.TextMessage)
print(bcolors.OKGREEN + "\nSuccessfully sent SMS.\n" + bcolors.ENDC)
print(f'\n{Fore.GREEN}SMS sent successfully.{Fore.RESET}\n')
except KeeperApiError:
print("Was unable to send SMS.")
elif channel.channel_type == login_steps.TwoFactorChannel.SecurityKey:
Expand All @@ -177,14 +178,14 @@ def on_two_factor(self, step):
}
step.duration = login_steps.TwoFactorDuration.EveryLogin
step.send_code(channel.channel_uid, json.dumps(signature))
print(bcolors.OKGREEN + "Verified Security Key." + bcolors.ENDC)
print(f'{Fore.GREEN}Security key verified.{Fore.RESET}')

except ImportError as e:
from ..yubikey import display_fido2_warning
display_fido2_warning()
logging.warning(e)
except KeeperApiError:
print(bcolors.FAIL + "Unable to verify code generated by security key" + bcolors.ENDC)
print(f'{Fore.RED}Unable to verify security key.{Fore.RESET}')
except Exception as e:
logging.error(e)

Expand Down Expand Up @@ -230,7 +231,7 @@ def on_two_factor(self, step):
print(prompt_exp)

try:
answer = input('\nEnter 2FA Code or Duration: ')
answer = input(f'\n{Fore.GREEN}Enter 2FA Code: {Fore.RESET}')
except KeyboardInterrupt:
step.cancel()
return
Expand Down Expand Up @@ -263,19 +264,18 @@ def on_two_factor(self, step):
step.duration = mfa_expiration
try:
step.send_code(channel.channel_uid, otp_code)
print(bcolors.OKGREEN + "Successfully verified 2FA Code." + bcolors.ENDC)
print(f'{Fore.GREEN}2FA code verified.{Fore.RESET}')
except KeeperApiError:
warning_msg = bcolors.WARNING + f"Unable to verify 2FA code. Regenerate the code and try again." + bcolors.ENDC
print(warning_msg)
print(f'{Fore.YELLOW}Invalid 2FA code. Please try again.{Fore.RESET}')

def on_password(self, step):
if self._show_password_help:
print(f'Enter master password for {step.username}')
logging.info(f'{Fore.CYAN}Enter master password for {Fore.WHITE}{step.username}{Fore.RESET}')

if self._failed_password_attempt > 0:
print('Forgot password? Type "recover"<Enter>')
print(f'{Fore.YELLOW}Forgot password? Type "recover"<Enter>{Fore.RESET}')

password = getpass.getpass(prompt='Password: ', stream=None)
password = getpass.getpass(prompt=f'{Fore.GREEN}Password: {Fore.RESET}', stream=None)
if not password:
step.cancel()
elif password == 'recover':
Expand All @@ -300,24 +300,24 @@ def on_sso_redirect(self, step):
wb = None

sp_url = step.sso_login_url
print(f'\nSSO Login URL:\n{sp_url}\n')
print(f'\n{Fore.CYAN}SSO Login URL:{Fore.RESET}\n{sp_url}\n')
if self._show_sso_redirect_help:
print('Navigate to SSO Login URL with your browser and complete login.')
print('Copy a returned SSO Token into clipboard.')
print('Paste that token into Commander')
print('NOTE: To copy SSO Token please click "Copy login token" button on "SSO Connect" page.')
print(f'{Fore.CYAN}Navigate to SSO Login URL with your browser and complete login.{Fore.RESET}')
print(f'{Fore.CYAN}Copy the returned SSO Token and paste it here.{Fore.RESET}')
print(f'{Fore.YELLOW}TIP: Click "Copy login token" button on the SSO Connect page.{Fore.RESET}')
print('')
print(' a. SSO User with a Master Password')
print(' c. Copy SSO Login URL to clipboard')
print(f' {Fore.GREEN}a{Fore.RESET}. SSO User with a Master Password')
print(f' {Fore.GREEN}c{Fore.RESET}. Copy SSO Login URL to clipboard')
if wb:
print(' o. Navigate to SSO Login URL with the default web browser')
print(' p. Paste SSO Token from clipboard')
print(' q. Quit SSO login attempt and return to Commander prompt')
print(f' {Fore.GREEN}o{Fore.RESET}. Open SSO Login URL in web browser')
print(f' {Fore.GREEN}p{Fore.RESET}. Paste SSO Token from clipboard')
print(f' {Fore.GREEN}q{Fore.RESET}. Cancel SSO login')
print()
self._show_sso_redirect_help = False

while True:
try:
token = input('Selection: ')
token = input(f'{Fore.GREEN}Selection: {Fore.RESET}')
except KeyboardInterrupt:
step.cancel()
return
Expand Down Expand Up @@ -357,17 +357,19 @@ def on_sso_redirect(self, step):

def on_sso_data_key(self, step):
if self._show_sso_data_key_help:
print('\nApprove this device by selecting a method below:')
print(' 1. Keeper Push. Send a push notification to your device.')
print(' 2. Admin Approval. Request your admin to approve this device.')
print(f'\n{Fore.YELLOW}Device Approval Required for SSO{Fore.RESET}\n')
print(f'{Fore.CYAN}Select an approval method:{Fore.RESET}')
print(f' {Fore.GREEN}1{Fore.RESET}. Keeper Push - Send a push notification to your device')
print(f' {Fore.GREEN}2{Fore.RESET}. Admin Approval - Request your admin to approve this device')
print('')
print(' r. Resume SSO login after device is approved.')
print(' q. Quit SSO login attempt and return to Commander prompt.')
print(f' {Fore.GREEN}r{Fore.RESET}. Resume SSO login after device is approved')
print(f' {Fore.GREEN}q{Fore.RESET}. Cancel SSO login')
print()
self._show_sso_data_key_help = False

while True:
try:
answer = input('Selection: ')
answer = input(f'{Fore.GREEN}Selection: {Fore.RESET}')
except KeyboardInterrupt:
answer = 'q'

Expand Down
2 changes: 1 addition & 1 deletion keepercommander/breachwatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def scan_passwords(self, params, passwords):
status.breachDetected = True
results[password] = status
if len(bw_hashes) > 0:
logging.info('Breachwatch: %d passwords to scan', len(bw_hashes))
logging.info('Breachwatch: %d %s to scan', len(bw_hashes), 'password' if len(bw_hashes) == 1 else 'passwords')
hashes = [] # type: List[breachwatch_pb2.HashCheck]
for bw_hash in bw_hashes:
check = breachwatch_pb2.HashCheck()
Expand Down
18 changes: 12 additions & 6 deletions keepercommander/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from prompt_toolkit import PromptSession
from prompt_toolkit.enums import EditingMode
from prompt_toolkit.shortcuts import CompleteStyle
from prompt_toolkit.key_binding import KeyBindings

from . import api, display, ttk, utils
from . import versioning
Expand Down Expand Up @@ -380,7 +381,8 @@ def is_msp(params_local):
try:
# Some commands (like logout) need auth but not sync
skip_sync = getattr(command, 'skip_sync_on_auth', False)
LoginCommand().execute(params, email=params.user, password=params.password, new_login=False, skip_sync=skip_sync)
# Auto-login for commands - don't show help text (show_help=False)
LoginCommand().execute(params, email=params.user, password=params.password, new_login=False, skip_sync=skip_sync, show_help=False)
except KeyboardInterrupt:
logging.info('Canceled')
if not params.session_token:
Expand Down Expand Up @@ -691,7 +693,7 @@ def read_command_with_continuation(prompt_session, params):
return result


def loop(params, skip_init=False, suppress_goodbye=False, new_login=False): # type: (KeeperParams, bool, bool, bool) -> int
def loop(params, skip_init=False, suppress_goodbye=False, new_login=False): # type: (KeeperParams, bool, bool, bool) -> int # suppress_goodbye kept for API compat
global prompt_session
error_no = 0
suppress_errno = False
Expand All @@ -702,11 +704,18 @@ def loop(params, skip_init=False, suppress_goodbye=False, new_login=False): # t
if not params.batch_mode:
if os.isatty(0) and os.isatty(1):
completer = CommandCompleter(params, aliases)
# Create key bindings with Ctrl+Q to exit (consistent with supershell)
bindings = KeyBindings()
@bindings.add('c-q')
def _(event):
"""Exit shell on Ctrl+Q"""
event.app.exit(exception=EOFError)
prompt_session = PromptSession(multiline=False,
editing_mode=EditingMode.VI,
completer=completer,
complete_style=CompleteStyle.MULTI_COLUMN,
complete_while_typing=False)
complete_while_typing=False,
key_bindings=bindings)

if not skip_init:
display.welcome()
Expand Down Expand Up @@ -810,9 +819,6 @@ def loop(params, skip_init=False, suppress_goodbye=False, new_login=False): # t
# Clear the shell loop flag
params._in_shell_loop = False

if not params.batch_mode and not suppress_goodbye:
logging.info('\nGoodbye.\n')

return error_no


Expand Down
Loading
Loading