Skip to content
Merged
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
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: commander@keepersecurity.com
#

__version__ = '18.0.7'
__version__ = '18.0.8'
40 changes: 40 additions & 0 deletions keepercommander/auth/console_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,31 @@ def _stderr(msg=''):
print(msg, file=sys.stderr)


_HEADLESS_AUTH_MSG_SHOWN = False


def _is_interactive():
try:
return bool(sys.stdin) and sys.stdin.isatty()
except Exception:
return False


def _fail_headless_auth(step):
"""In headless/service mode, persistent login often needs a follow-up prompt
(password, SSO, 2FA, device approval) that cannot be answered. Log once and
cancel so the caller exits cleanly instead of looping or spamming getpass."""
global _HEADLESS_AUTH_MSG_SHOWN
if not _HEADLESS_AUTH_MSG_SHOWN:
_HEADLESS_AUTH_MSG_SHOWN = True
logging.error(
'Persistent login is not working in this non-interactive environment '
'(possibly due to an IP/location change). '
'Re-run Commander/Docker setup from this network, then restart the service.'
)
step.cancel()


class ConsoleLoginUi(login_steps.LoginUi):
def __init__(self):
self._show_device_approval_help = True
Expand All @@ -28,6 +53,9 @@ def __init__(self):
self._failed_password_attempt = 0

def on_device_approval(self, step):
if not _is_interactive():
_fail_headless_auth(step)
return
if self._show_device_approval_help:
_stderr(f"\n{Fore.YELLOW}Device Approval Required{Fore.RESET}\n")
_stderr(f"{Fore.CYAN}Select an approval method:{Fore.RESET}")
Expand Down Expand Up @@ -123,6 +151,9 @@ def two_factor_channel_to_desc(channel): # type: (login_steps.TwoFactorChannel
return 'Backup Codes'

def on_two_factor(self, step):
if not _is_interactive():
_fail_headless_auth(step)
return
channels = step.get_channels()

if self._show_two_factor_help:
Expand Down Expand Up @@ -273,6 +304,9 @@ def on_two_factor(self, step):
logging.warning(f'{Fore.YELLOW}Invalid 2FA code. Please try again.{Fore.RESET}')

def on_password(self, step):
if not _is_interactive():
_fail_headless_auth(step)
return
if self._show_password_help:
_stderr(f'{Fore.CYAN}Enter master password for {Fore.WHITE}{step.username}{Fore.RESET}')

Expand All @@ -293,6 +327,9 @@ def on_password(self, step):
step.cancel()

def on_sso_redirect(self, step):
if not _is_interactive():
_fail_headless_auth(step)
return
try:
wb = webbrowser.get()
wrappers = set('xdg-open|gvfs-open|gnome-open|x-www-browser|www-browser'.split('|'))
Expand Down Expand Up @@ -360,6 +397,9 @@ def on_sso_redirect(self, step):
break

def on_sso_data_key(self, step):
if not _is_interactive():
_fail_headless_auth(step)
return
if self._show_sso_data_key_help:
_stderr(f'\n{Fore.YELLOW}Device Approval Required for SSO{Fore.RESET}\n')
_stderr(f'{Fore.CYAN}Select an approval method:{Fore.RESET}')
Expand Down
2 changes: 1 addition & 1 deletion keepercommander/commands/credential_provision.py
Original file line number Diff line number Diff line change
Expand Up @@ -1773,7 +1773,7 @@ def _create_dag_link(
pam_config_record = vault.KeeperRecord.load(params, pam_config_uid)

# Create RecordLink instance
record_link = RecordLink(record=pam_config_record, params=params, fail_on_corrupt=False, use_per_graph_endpoints=True)
record_link = RecordLink(record=pam_config_record, params=params, fail_on_corrupt=False)

# Create belongs_to relationship: PAM User belongs_to PAM Configuration
record_link.belongs_to(
Expand Down
2 changes: 1 addition & 1 deletion keepercommander/commands/discover/job_remove.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def execute(self, params, **kwargs):
all_gateways = GatewayContext.all_gateways(params)

def _find_job(configuration_record) -> Optional[Dict]:
jobs_obj = Jobs(record=configuration_record, params=params, use_per_graph_endpoints=True)
jobs_obj = Jobs(record=configuration_record, params=params)
job_item = jobs_obj.get_job(job_id)
if job_item is not None:
return {
Expand Down
2 changes: 1 addition & 1 deletion keepercommander/commands/discover/job_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def execute(self, params, **kwargs):
multi_conf_msg(gateway, err)
return

jobs = Jobs(record=gateway_context.configuration, params=params, use_per_graph_endpoints=True)
jobs = Jobs(record=gateway_context.configuration, params=params)
current_job_item = jobs.current_job
removed_prior_job = None
if current_job_item is not None:
Expand Down
11 changes: 5 additions & 6 deletions keepercommander/commands/discover/job_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from ...display import bcolors
from ...discovery_common.jobs import Jobs
from ...discovery_common.infrastructure import Infrastructure
from ...keeper_dag.types import PamEndpoints
from ...keeper_dag.types import PamGraphId
from ...discovery_common.types import DiscoveryDelta, DiscoveryObject
from ...keeper_dag.dag import DAG
from typing import Optional, Dict, List, TYPE_CHECKING
Expand Down Expand Up @@ -160,7 +160,7 @@ def print_job_detail(params: KeeperParams,
job_id: str):

def _find_job(configuration_record) -> Optional[Dict]:
jobs_obj = Jobs(record=configuration_record, params=params, use_per_graph_endpoints=True)
jobs_obj = Jobs(record=configuration_record, params=params)
job_item = jobs_obj.get_job(job_id)
if job_item is not None:
return {
Expand All @@ -175,7 +175,7 @@ def _find_job(configuration_record) -> Optional[Dict]:
if gateway_context is not None:
jobs = payload["jobs"]
job = jobs.get_job(job_id) # type: JobItem
infra = Infrastructure(record=gateway_context.configuration, params=params, use_per_graph_endpoints=True)
infra = Infrastructure(record=gateway_context.configuration, params=params)

color = bcolors.OKBLUE
status = "RUNNING"
Expand Down Expand Up @@ -257,8 +257,7 @@ def _find_job(configuration_record) -> Optional[Dict]:
print("Fall back to raw graph.")
print("")
dag = DAG(conn=infra.conn, record=infra.record,
read_endpoint=PamEndpoints.INFRASTRUCTURE,
write_endpoint=PamEndpoints.INFRASTRUCTURE)
graph_id=PamGraphId.INFRASTRUCTURE)
print(dag.to_dot_raw(sync_point=job.sync_point, rank_dir="RL"))

else:
Expand Down Expand Up @@ -325,7 +324,7 @@ def execute(self, params, **kwargs):
if len(gateway_context.gateway_name) > max_gateway_name:
max_gateway_name = len(gateway_context.gateway_name)

jobs = Jobs(record=configuration_record, params=params, use_per_graph_endpoints=True)
jobs = Jobs(record=configuration_record, params=params)
if show_history is True:
job_list = reversed(jobs.history)
else:
Expand Down
7 changes: 3 additions & 4 deletions keepercommander/commands/discover/result_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -1334,7 +1334,7 @@ def _get_directory_info(domain: str,
def remove_job(params: KeeperParams, configuration_record: KeeperRecord, job_id: str):

try:
jobs = Jobs(record=configuration_record, params=params, use_per_graph_endpoints=True)
jobs = Jobs(record=configuration_record, params=params)
jobs.cancel(job_id)
print(f"{bcolors.OKGREEN}No items left to process. Removing completed discovery job.{bcolors.ENDC}")
except Exception as err:
Expand All @@ -1352,8 +1352,7 @@ def preview(self, job_item: JobItem, params: KeeperParams, gateway_context: Gate
infra = Infrastructure(record=gateway_context.configuration,
params=params,
logger=logging,
debug_level=debug_level,
use_per_graph_endpoints=True)
debug_level=debug_level)
infra.load(sync_point)

configuration = None
Expand Down Expand Up @@ -1512,7 +1511,7 @@ def execute(self, params: KeeperParams, **kwargs):
# Get the current job.
# There can only be one active job.
# This will give us the sync point for the delta
jobs = Jobs(record=configuration_record, params=params, logger=logging, debug_level=debug_level, use_per_graph_endpoints=True)
jobs = Jobs(record=configuration_record, params=params, logger=logging, debug_level=debug_level)
job_item = jobs.current_job
if job_item is None:
continue
Expand Down
2 changes: 1 addition & 1 deletion keepercommander/commands/discover/rule_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def execute(self, params, **kwargs):
return

# If the rule passes its validation, then add control DAG
rules = Rules(record=gateway_context.configuration, params=params, use_per_graph_endpoints=True)
rules = Rules(record=gateway_context.configuration, params=params)
new_rule = ActionRuleItem(
name=kwargs.get("name"),
action=kwargs.get("rule_action"),
Expand Down
2 changes: 1 addition & 1 deletion keepercommander/commands/discover/rule_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def execute(self, params, **kwargs):
multi_conf_msg(gateway, err)
return

rules = Rules(record=gateway_context.configuration, params=params, use_per_graph_endpoints=True)
rules = Rules(record=gateway_context.configuration, params=params)
rule_list = rules.rule_list(rule_type=RuleTypeEnum.ACTION,
search=kwargs.get("search")) # type: List[RuleItem]
if len(rule_list) == 0:
Expand Down
2 changes: 1 addition & 1 deletion keepercommander/commands/discover/rule_remove.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def execute(self, params, **kwargs):
return

try:
rules = Rules(record=gateway_context.configuration, params=params, use_per_graph_endpoints=True)
rules = Rules(record=gateway_context.configuration, params=params)
if remove_all:
rules.remove_all(RuleTypeEnum.ACTION)
print(f"{bcolors.OKGREEN}All rules removed.{bcolors.ENDC}")
Expand Down
2 changes: 1 addition & 1 deletion keepercommander/commands/discover/rule_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def execute(self, params, **kwargs):

try:
rule_id = kwargs.get("rule_id")
rules = Rules(record=gateway_context.configuration, params=params, use_per_graph_endpoints=True)
rules = Rules(record=gateway_context.configuration, params=params)
rule_item = rules.get_rule_item(rule_type=RuleTypeEnum.ACTION, rule_id=rule_id)
if rule_item is None:
raise ValueError("Rule Id does not exist.")
Expand Down
2 changes: 1 addition & 1 deletion keepercommander/commands/discoveryrotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3372,7 +3372,7 @@ def record_rotate(self, params, record_uid, slient: bool = False):
# Check the graph for the noop setting.
record_link = RecordLink(record=pam_config,
params=params,
fail_on_corrupt=False, use_per_graph_endpoints=True)
fail_on_corrupt=False)
acl = record_link.get_acl(record_uid, pam_config.record_uid)
if acl is not None and acl.rotation_settings is not None:
is_noop = acl.rotation_settings.noop
Expand Down
17 changes: 17 additions & 0 deletions keepercommander/commands/helpers/record.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
import re
from typing import Set, Optional

from ... import api
from ...error import CommandError
from ...params import KeeperParams
from ...subfolder import try_resolve_path

# Block shell chaining markers in `get` lookup tokens.
_GET_LOOKUP_CONTROL_CHARS_RE = re.compile(r'[\r\n\x00]')
_GET_LOOKUP_SHELL_METACHAR_RE = re.compile(r'[;|]')
_GET_LOOKUP_CHAIN_RE = re.compile(r'&&')


def raise_if_unsafe_get_lookup_token(token, command='get'):
# type: (str, str) -> None
if not token:
raise CommandError(command, 'Invalid record identifier: forbidden characters')
if (_GET_LOOKUP_CONTROL_CHARS_RE.search(token)
or _GET_LOOKUP_SHELL_METACHAR_RE.search(token)
or _GET_LOOKUP_CHAIN_RE.search(token)):
raise CommandError(command, 'Invalid record identifier: forbidden characters')


# Get record UID(s) given one of its identifiers: name (if current folder contains the record), path, or UID
def get_record_uids(params, name): # type: (KeeperParams, str) -> Set[Optional[str]]
Expand Down
41 changes: 40 additions & 1 deletion keepercommander/commands/ksm.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#

import argparse
import base64
import datetime
import hmac
import json
Expand Down Expand Up @@ -1594,6 +1595,8 @@ def init_ksm_config(params, one_time_token, config_init, include_config_dict=Fal
if 'KEY_OWNER_PUBLIC_KEY' in ConfigKeys.__members__ and ksm_conf_storage.config.get(ConfigKeys.KEY_OWNER_PUBLIC_KEY):
config_dict[ConfigKeys.KEY_OWNER_PUBLIC_KEY.value] = ksm_conf_storage.config.get(ConfigKeys.KEY_OWNER_PUBLIC_KEY)

KSMCommand.validate_ksm_config_dict(config_dict)

converted_config = KSMCommand.convert_config_dict(config_dict, config_init)

if include_config_dict:
Expand All @@ -1604,13 +1607,49 @@ def init_ksm_config(params, one_time_token, config_init, include_config_dict=Fal
else:
return converted_config

@staticmethod
def validate_ksm_config_dict(config_dict):
"""Verify a freshly generated KSM device config is intact.

The config is handed out as an opaque base64 blob (gateway install,
k8s secret) and a corrupted clientId/privateKey only surfaces much
later as an unusable device, so fail loudly at the source instead.

Note: if this validation passes but the consumer still receives a
malformed token, the base64 was most likely mangled by the console -
lines overwritten/lost during print (wrapped rows, redraws) or a bad
copy/paste. For comparison capture it losslessly with a redirect:
pam project import ... > out.json
"""
required_keys = ('hostname', 'clientId', 'privateKey', 'serverPublicKeyId', 'appKey')
for key in required_keys:
value = config_dict.get(key)
if not value or not isinstance(value, str):
raise Exception(f'Generated KSM config is invalid: "{key}" is missing or empty. '
'Please remove the client device and try again.')
for key in ('clientId', 'privateKey', 'appKey'):
try:
decoded = base64.b64decode(config_dict[key], validate=True)
except Exception:
raise Exception(f'Generated KSM config is invalid: "{key}" is not valid base64. '
'Please remove the client device and try again.')
if key == 'clientId' and len(decoded) != 64: # HMAC-SHA512 digest
raise Exception(f'Generated KSM config is invalid: "clientId" decodes to '
f'{len(decoded)} bytes, expected 64. '
'Please remove the client device and try again.')

@staticmethod
def convert_config_dict(config_dict, conversion_type='json'):

config = json.dumps(config_dict)

if conversion_type in ['b64', 'k8s']:
config = json_to_base64(config)
encoded = json_to_base64(config)
# the encoded blob must round-trip to the exact JSON it was built
# from - catches any corruption before the config is handed out
if base64.b64decode(encoded).decode('utf-8') != config:
raise Exception('KSM config base64 encoding failed the integrity check')
config = encoded

if conversion_type == 'k8s':
config = "\n" \
Expand Down
2 changes: 1 addition & 1 deletion keepercommander/commands/pam_debug/acl.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def execute(self, params: KeeperParams, **kwargs):
record_link = RecordLink(record=gateway_context.configuration,
params=params,
logger=logging,
debug_level=debug_level, use_per_graph_endpoints=True)
debug_level=debug_level)

user_record = vault.KeeperRecord.load(params, user_uid) # type: Optional[TypedRecord]
if user_record is None:
Expand Down
6 changes: 3 additions & 3 deletions keepercommander/commands/pam_debug/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ def execute(self, params: KeeperParams, **kwargs):
multi_conf_msg(gateway, err)
return

infra = Infrastructure(record=gateway_context.configuration, params=params, fail_on_corrupt=False, use_per_graph_endpoints=True)
infra = Infrastructure(record=gateway_context.configuration, params=params, fail_on_corrupt=False)
infra.load()

record_link = RecordLink(record=gateway_context.configuration, params=params, fail_on_corrupt=False, use_per_graph_endpoints=True)
user_service = UserService(record=gateway_context.configuration, params=params, fail_on_corrupt=False, use_per_graph_endpoints=True)
record_link = RecordLink(record=gateway_context.configuration, params=params, fail_on_corrupt=False)
user_service = UserService(record=gateway_context.configuration, params=params, fail_on_corrupt=False)

if gateway_context is None:
print(f" {self._f('Cannot get gateway information. Gateway may not be up.')}")
Expand Down
Loading
Loading