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
15 changes: 11 additions & 4 deletions keepercommander/commands/pam_launch/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
ALL_TERMINAL,
CONNECT_AS_MIN_VERSION,
_version_at_least,
_pam_settings_connection_port,
)
from .terminal_size import get_terminal_size_pixels, is_interactive_tty
from .guac_cli.stdin_handler import StdinHandler
Expand Down Expand Up @@ -122,15 +123,19 @@ def _get_host_port_from_record(record: Any) -> Tuple[Optional[str], Optional[int
"""
Extract (hostName, port) from a record's pamHostname or host typed fields.

Enforces exactly one non-empty host field (hostName AND port both non-empty/valid).
Raises CommandError if more than one such field is found (ambiguous configuration).
Requires a non-empty hostName on exactly one such field. Port comes from
pamSettings.connection.port when the record is pamMachine/pamDirectory/pamDatabase
and that port is set (overrides the field's port); otherwise from the field's port.

Raises CommandError if more than one qualifying host field is found (ambiguous).

Returns:
Tuple of (host, port) where either may be None if none found.
"""
if not record:
return None, None

pam_override_port = _pam_settings_connection_port(record)
candidates: list = []
for field in _iter_record_fields(record):
if getattr(field, 'type', None) not in ('pamHostname', 'host'):
Expand All @@ -139,8 +144,10 @@ def _get_host_port_from_record(record: Any) -> Tuple[Optional[str], Optional[int
if not isinstance(value, dict):
continue
host = (value.get('hostName') or '').strip()
port_raw = value.get('port')
if not host or not port_raw:
if not host:
continue
port_raw = pam_override_port if pam_override_port is not None else value.get('port')
if not port_raw:
continue
try:
p = int(port_raw)
Expand Down
64 changes: 52 additions & 12 deletions keepercommander/commands/pam_launch/terminal_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,38 @@ def detect_protocol(params: KeeperParams, record_uid: str) -> Optional[str]:
return None


_PAM_TYPES_WITH_CONNECTION_PORT = ['pamMachine', 'pamDatabase', 'pamDirectory']


def _pam_settings_connection_port(record: Any) -> Optional[int]:
"""
For PAM machine record types only, return a valid pamSettings.connection.port if set.
"""
if getattr(record, 'record_type', None) not in _PAM_TYPES_WITH_CONNECTION_PORT:
return None
if not hasattr(record, 'get_typed_field'):
return None
psf = record.get_typed_field('pamSettings')
if not psf or not hasattr(psf, 'get_default_value'):
return None
pam_val = psf.get_default_value(dict)
if not isinstance(pam_val, dict):
return None
connection = pam_val.get('connection')
if not isinstance(connection, dict):
return None
conn_port = connection.get('port')
if conn_port is None or conn_port == '':
return None
try:
p = int(conn_port)
except (ValueError, TypeError):
return None
if 1 <= p <= 65535:
return p
return None


def extract_terminal_settings(
params: KeeperParams,
record_uid: str,
Expand Down Expand Up @@ -370,20 +402,27 @@ def extract_terminal_settings(
}

# Extract hostname and port from record - enforce single non-empty host/pamHostname field.
# Collect all pamHostname/host fields with both hostName and port non-empty.
# Host requires non-empty hostName; port is pamSettings.connection.port (PAM types only)
# when set, else the field's port — same precedence as launch._get_host_port_from_record.
_pam_override_port = _pam_settings_connection_port(record)
_host_candidates = []
for _f in list(getattr(record, 'fields', None) or []) + list(getattr(record, 'custom', None) or []):
if getattr(_f, 'type', None) in ('pamHostname', 'host'):
_hv = _f.get_default_value(dict) if hasattr(_f, 'get_default_value') else {}
_hn = ((_hv.get('hostName') or '').strip()) if isinstance(_hv, dict) else ''
_pr = _hv.get('port') if isinstance(_hv, dict) else None
if _hn and _pr:
try:
_pp = int(_pr)
if 1 <= _pp <= 65535:
_host_candidates.append((_hn, _pp, _hv))
except (ValueError, TypeError):
pass
if not _hn:
continue
_pr = _pam_override_port if _pam_override_port is not None else (
_hv.get('port') if isinstance(_hv, dict) else None
)
if not _pr:
continue
try:
_pp = int(_pr)
if 1 <= _pp <= 65535:
_host_candidates.append((_hn, _pp, _hv))
except (ValueError, TypeError):
pass
if len(_host_candidates) > 1:
raise CommandError('pam launch',
f'Record {record_uid} has {len(_host_candidates)} non-empty host/pamHostname fields '
Expand All @@ -397,8 +436,9 @@ def extract_terminal_settings(
settings['hostname'] = custom_host
logging.debug(f"Using custom host override: {custom_host}")

# Port precedence: CLI (custom_port) > record port > pamSettings.connection.port > DEFAULT
# pamSettings fallback is applied after the pamSettings block below.
# Port precedence: CLI (custom_port) > record (pamSettings.connection.port overrides host field
# on PAM types, else field port) > pamSettings.connection.port when record port still unset >
# protocol DEFAULT. pamSettings fallback runs in the pamSettings block below.
if custom_port is not None:
settings['port'] = custom_port
elif _record_port_val is not None:
Expand Down Expand Up @@ -458,7 +498,7 @@ def extract_terminal_settings(
else:
logging.debug(f"Using userRecordUid from pamSettings: {fallback_uid}")

# pamSettings.connection.port fallback (applied when CLI and record port are absent)
# pamSettings.connection.port when CLI and host-derived port are still absent
if settings['port'] is None:
conn_port = connection.get('port')
if conn_port:
Expand Down
Loading