Skip to content
Open
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
60 changes: 5 additions & 55 deletions keepercli-package/src/keepercli/commands/pam/pam_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from keepersdk.helpers.keeper_dag import dag_utils
from keepersdk.helpers.keeper_dag.constants import PamConfigurationRecordType, PAM_CONFIGURATIONS
from .. import record_edit
from . import pam_utils


logger = api.get_logger()
Expand Down Expand Up @@ -216,37 +217,12 @@ def _resolve_nsf_folder(self, vault: vault_online.VaultOnline, record_uid: str,
except nsf_management.NsfError:
return None

def _load_nsf_typed_record(self, vault: vault_online.VaultOnline, record_uid: str):
"""Load an NSF record as a TypedRecord from decrypted cache metadata."""
if not vault.nsf_data or not vault.nsf_data.get_record(record_uid):
return None
try:
meta = nsf_management.load_nsf_record_metadata(vault, record_uid)
except nsf_management.NsfError:
return None
typed = vault_record.TypedRecord()
typed.record_uid = record_uid
typed.load_record_data({
'type': meta.get('type') or '',
'title': meta.get('title') or record_uid,
'notes': meta.get('notes') or '',
'fields': meta.get('fields') or [],
})
return typed

def _load_nsf_pam_configuration(self, vault: vault_online.VaultOnline, identifier: str):
"""Load a PAM configuration from NSF by UID or exact title."""
if not vault.nsf_data or not identifier:
return None
record_uid = identifier
if not vault.nsf_data.get_record(record_uid):
try:
record_uid = nsf_management.resolve_nsf_record_uid(vault, identifier)
except nsf_management.NsfError:
record_uid = None
record_uid = pam_utils.resolve_nsf_record_uid(vault, identifier)
if not record_uid:
return None
typed = self._load_nsf_typed_record(vault, record_uid)
typed = pam_utils.load_nsf_typed_record(vault, record_uid)
if typed and typed.record_type in PAM_CONFIGURATIONS:
return typed
return None
Expand All @@ -270,7 +246,7 @@ def _find_nsf_pam_configurations(self, vault: vault_online.VaultOnline):
'Following NSF configuration has unsupported type: UID: %s, Type: %s',
entry.record_uid, rec_type)
continue
typed = self._load_nsf_typed_record(vault, entry.record_uid)
typed = pam_utils.load_nsf_typed_record(vault, entry.record_uid)
if typed and typed.record_type in PAM_CONFIGURATIONS:
yield typed

Expand Down Expand Up @@ -1194,33 +1170,7 @@ def _find_configuration(self, vault: vault_online.VaultOnline, config_name: str)

def _load_nsf_pam_configuration_for_edit(self, vault: vault_online.VaultOnline, identifier: str):
"""Load a PAM configuration from NSF by UID or exact title."""
if not vault.nsf_data or not identifier:
return None
record_uid = identifier
if not vault.nsf_data.get_record(record_uid):
try:
record_uid = nsf_management.resolve_nsf_record_uid(vault, identifier)
except nsf_management.NsfError:
record_uid = None
if not record_uid:
return None
try:
meta = nsf_management.load_nsf_record_metadata(vault, record_uid)
except nsf_management.NsfError:
return None
rec_type = meta.get('type') or ''
if rec_type not in PAM_CONFIGURATIONS:
return None
typed = vault_record.TypedRecord()
typed.record_uid = record_uid
typed.load_record_data({
'type': rec_type,
'title': meta.get('title') or record_uid,
'notes': meta.get('notes') or '',
'fields': meta.get('fields') or [],
'custom': meta.get('custom') or [],
})
return typed
return self._load_nsf_pam_configuration(vault, identifier)

def _validate_configuration(self, vault: vault_online.VaultOnline, configuration, config_name: str,
*, is_nsf: bool = False):
Expand Down
103 changes: 69 additions & 34 deletions keepercli-package/src/keepercli/commands/pam/pam_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,22 @@

from keepersdk import utils
from keepersdk.helpers.keeper_dag import dag_utils
from keepersdk.helpers.keeper_dag.constants import (
PAM_CONFIGURATIONS,
PAM_DATABASE,
PAM_DIRECTORY,
PAM_MACHINE,
PAM_RESOURCES,
PAM_USER,
)
from keepersdk.helpers.tunnel.tunnel_graph import TunnelDAG
from keepersdk.helpers.tunnel.tunnel_utils import get_keeper_tokens, get_config_uid
from keepersdk.vault import record_management, vault_record
from keepersdk.vault import vault_record

from .. import base
from ... import api
from ...params import KeeperParams
from . import pam_utils


logger = api.get_logger()
Expand All @@ -18,6 +27,15 @@
protocols = ['', 'http', 'kubernetes', 'mysql', 'postgresql', 'rdp', 'sql-server', 'ssh', 'telnet', 'vnc']
choices = ['on', 'off', 'default']

# Resource + RBI + PAM configs (reuse keeper_dag constants where they exist).
_PAM_CONNECTION_RECORD_TYPES = (*PAM_RESOURCES, 'pamRemoteBrowser', *PAM_CONFIGURATIONS)
_PAM_SEED_RECORD_TYPES = (PAM_DATABASE, PAM_DIRECTORY, PAM_MACHINE, 'pamRemoteBrowser')
_PAM_RESOURCE_USER_LINK_TYPES = (PAM_DATABASE, PAM_DIRECTORY, PAM_MACHINE)


def _is_pam_config_record(record: vault_record.TypedRecord) -> bool:
return record.record_type in PAM_CONFIGURATIONS


class PAMConnectionEditCommand(base.ArgparseCommand):

Expand Down Expand Up @@ -75,29 +93,36 @@ def execute(self, context: KeeperParams, **kwargs):
record_name = kwargs.get('record')
if not record_name:
raise base.CommandError(f'Record parameter is required.')
record = vault.vault_data.load_record(record_name)
record = pam_utils.load_typed_record(context, record_name)
if not record:
raise base.CommandError(f'Record \"{record_name}\" not found.')
if not isinstance(record, vault_record.TypedRecord):
raise base.CommandError(f'Record \"{record_name}\" can not be edited.')

config_name = kwargs.get('config', None)
cfg_rec = vault.vault_data.load_record(config_name)
if not cfg_rec and record.version == 6:
cfg_rec = record
config_uid = cfg_rec.record_uid if cfg_rec else None

record_uid = record.record_uid
record_type = record.record_type
if record_type not in ("pamMachine pamDatabase pamDirectory pamNetworkConfiguration pamAwsConfiguration "
"pamRemoteBrowser pamAzureConfiguration").split():
if record_type not in _PAM_CONNECTION_RECORD_TYPES:
raise base.CommandError(f"This record's type is not supported for connections. "
f"Connections are only supported on pamMachine, pamDatabase, pamDirectory, "
f"pamRemoteBrowser, pamNetworkConfiguration pamAwsConfiguration, and "
f"pamAzureConfiguration records")

encrypted_session_token, encrypted_transmission_key, transmission_key = get_keeper_tokens(vault)
if record_type in "pamNetworkConfiguration pamAwsConfiguration pamAzureConfiguration".split():

config_name = kwargs.get('config', None)
cfg_rec = pam_utils.load_typed_record(context, config_name) if config_name else None
if not cfg_rec and _is_pam_config_record(record):
cfg_rec = record

# For resource records, fall back to the PAM config already linked in the DAG.
existing_config_uid = None
if not _is_pam_config_record(record):
existing_config_uid = get_config_uid(
vault, encrypted_session_token, encrypted_transmission_key, record_uid)
existing_config_uid = str(existing_config_uid) if existing_config_uid else ''
if not cfg_rec and existing_config_uid:
cfg_rec = pam_utils.load_typed_record(context, existing_config_uid)
config_uid = cfg_rec.record_uid if cfg_rec else None

if record_type in PAM_CONFIGURATIONS:
tdag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, record_uid,
is_config=True, transmission_key=transmission_key)
tdag.edit_tunneling_config(connections=_connections, session_recording=_recording, typescript_recording=_typescript_recording)
Expand All @@ -111,7 +136,7 @@ def execute(self, context: KeeperParams, **kwargs):
base64_seed = utils.base64_url_encode(seed)
record_seed = vault_record.TypedField.create_field('trafficEncryptionSeed', base64_seed, required=False)

record_types_with_seed = ("pamDatabase", "pamDirectory", "pamMachine", "pamRemoteBrowser")
record_types_with_seed = _PAM_SEED_RECORD_TYPES
if traffic_encryption_key:
traffic_encryption_key.value = [base64_seed]
elif record.record_type in record_types_with_seed:
Expand Down Expand Up @@ -183,32 +208,41 @@ def execute(self, context: KeeperParams, **kwargs):
logger.debug(f'Unexpected value for --key-events {key_events} (ignored)')

if dirty:
record_management.update_record(vault, record)
vault.sync_down()
pam_utils.save_typed_record(vault, record)

traffic_encryption_key = record.get_typed_field('trafficEncryptionSeed')
if not traffic_encryption_key:
raise base.CommandError(f"Unable to add Seed to record {record_uid}. "
f"Please make sure you have edit rights to record {record_uid}")
dirty = False

existing_config_uid = get_config_uid(vault, encrypted_session_token, encrypted_transmission_key, record_uid)
if not config_uid:
raise base.CommandError(
"No PAM Configuration UID set. "
"This must be set or supplied for connections to work. "
"Pass --config [ConfigUID] (see `pam config list`)."
)

tdag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, config_uid,
transmission_key=transmission_key)
old_dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, existing_config_uid,
transmission_key=transmission_key)
is_config=True, transmission_key=transmission_key)

if config_uid and existing_config_uid != config_uid:
if existing_config_uid and existing_config_uid != config_uid:
old_dag = TunnelDAG(
vault, encrypted_session_token, encrypted_transmission_key, existing_config_uid,
is_config=True, transmission_key=transmission_key,
)
old_dag.remove_from_dag(record_uid)
tdag.link_resource_to_config(record_uid)
elif not tdag.is_tunneling_config_set_up(record_uid):
tdag.link_resource_to_config(record_uid)

if tdag is None or not tdag.linking_dag.has_graph:
raise base.CommandError(f"No PAM Configuration UID set. "
f"This must be set or supplied for connections to work. This can be done by adding "
f"' --config [ConfigUID] "
f" The ConfigUID can be found by running "
f"'pam config list'")
if not tdag.linking_dag.has_graph:
raise base.CommandError(
f"No PAM Configuration DAG found for {config_uid}. "
"Initialize tunnel settings on the config first, e.g.\n"
f" pam connection edit {config_uid} --connections on "
"--connections-recording on"
)

if not tdag.check_tunneling_enabled_config(enable_connections=_connections,
enable_session_recording=_recording,
Expand Down Expand Up @@ -255,20 +289,21 @@ def execute(self, context: KeeperParams, **kwargs):
typescript_recording=kwargs.get('typescriptrecording', None))

admin_name = kwargs.get('admin')
adm_rec = vault.vault_data.load_record(admin_name)
admin_uid = adm_rec.record_uid if adm_rec else None
if admin_uid and record_type in ("pamDatabase", "pamDirectory", "pamMachine"):
tdag.link_user_to_resource(admin_uid, record_uid, is_admin=True, belongs_to=True)
if admin_name:
adm_rec = pam_utils.load_typed_record(context, admin_name)
admin_uid = adm_rec.record_uid if adm_rec else None
if admin_uid and record_type in _PAM_RESOURCE_USER_LINK_TYPES:
tdag.link_user_to_resource(admin_uid, record_uid, is_admin=True, belongs_to=True)

launch_user_name = kwargs.get('launch_user')
if launch_user_name:
launch_rec = vault.vault_data.load_record(launch_user_name)
launch_rec = pam_utils.load_typed_record(context, launch_user_name)
if not launch_rec:
raise base.CommandError(f'Launch user record "{launch_user_name}" not found.')
if not isinstance(launch_rec, vault_record.TypedRecord) or launch_rec.record_type != 'pamUser':
if launch_rec.record_type != PAM_USER:
raise base.CommandError(f'Launch user record must be a pamUser record type.')
launch_uid = launch_rec.record_uid
if record_type in ("pamDatabase", "pamDirectory", "pamMachine"):
if record_type in _PAM_RESOURCE_USER_LINK_TYPES:
tdag.clear_launch_credential_for_resource(record_uid, exclude_user_uid=launch_uid)
tdag.link_user_to_resource(launch_uid, record_uid, is_admin=True, belongs_to=True)
tdag.upgrade_resource_meta_to_v1(record_uid)
Expand Down
62 changes: 44 additions & 18 deletions keepercli-package/src/keepercli/commands/pam/pam_rbi.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,20 @@
from keepersdk.helpers.keeper_dag.constants import PAM_CONFIGURATIONS
from keepersdk.helpers.tunnel.tunnel_graph import TunnelDAG
from keepersdk.helpers.tunnel.tunnel_utils import get_keeper_tokens, get_config_uid
from keepersdk.vault import record_management, vault_online, vault_record
from keepersdk.vault import (
nsf_management,
record_management,
vault_extensions,
vault_online,
vault_record,
)

from .. import base
from ... import api
from ...helpers import record_utils
from ...params import KeeperParams
from . import pam_utils

choices = ['on', 'off', 'default']

logger = api.get_logger()
Expand Down Expand Up @@ -47,8 +55,24 @@ def _bootstrap_rbi_record(record: vault_record.TypedRecord) -> bool:


def _save_rbi_record(vault: vault_online.VaultOnline, record: vault_record.TypedRecord) -> None:
"""Persist RBI record body changes with a fresh revision (sync + retry on out-of-sync)."""
"""Persist RBI record body changes (classic or NSF) with sync + out-of-sync retry."""
vault.sync_down()
if pam_utils.is_nsf_record(vault, record.record_uid):
schema = vault.vault_data.get_record_type_by_name(record.record_type)
record_data = vault_extensions.extract_typed_record_data(record, schema)
try:
nsf_management.update_nsf_record(
vault,
record.record_uid,
title=record.title,
record_type=record.record_type,
record_data=record_data,
request_sync=True,
)
except nsf_management.NsfError as err:
raise base.CommandError(str(err)) from err
vault.sync_down()
return
try:
record_management.update_record(vault, record)
except KeeperApiError as err:
Expand All @@ -69,18 +93,23 @@ def _resolve_pam_config_record(
context: KeeperParams,
config_ref: str,
) -> Optional[vault_record.TypedRecord]:
"""Resolve a PAM configuration by UID or title (vault index version 6, not TypedRecord.version)."""
"""Resolve a PAM configuration by UID or title (classic v6 or NSF)."""
if not config_ref or context.vault is None:
return None
vault = context.vault
info = vault.vault_data.get_record(config_ref)
if not info:
info = record_utils.try_resolve_single_record(config_ref, context)
if not info or info.version != 6 or info.record_type not in PAM_CONFIGURATIONS:
return None
loaded = vault.vault_data.load_record(info.record_uid)
if isinstance(loaded, vault_record.TypedRecord):
return loaded
if info and info.version == 6 and info.record_type in PAM_CONFIGURATIONS:
loaded = vault.vault_data.load_record(info.record_uid)
if isinstance(loaded, vault_record.TypedRecord):
return loaded

nsf_uid = pam_utils.resolve_nsf_record_uid(vault, config_ref)
if nsf_uid:
typed = pam_utils.load_nsf_typed_record(vault, nsf_uid)
if typed and typed.record_type in PAM_CONFIGURATIONS:
return typed
return None


Expand Down Expand Up @@ -191,14 +220,9 @@ def execute(self, context: KeeperParams, **kwargs):

vault = context.vault

record_info = record_utils.try_resolve_single_record(record_name, context)
if not record_info:
raise base.CommandError(f'Record \"{record_name}\" not found.')
record = vault.vault_data.load_record(record_info.record_uid)
record = pam_utils.load_typed_record(context, record_name)
if not record:
raise base.CommandError(f'Record \"{record_name}\" not found.')
if not isinstance(record, vault_record.TypedRecord):
raise base.CommandError(f'Record \"{record_name}\" can not be edited.')

record_uid = record.record_uid
record_type = record.record_type
Expand All @@ -210,12 +234,14 @@ def execute(self, context: KeeperParams, **kwargs):
dirty = _bootstrap_rbi_record(record)

if autofill:
af_rec = vault.vault_data.load_record(autofill)
af_rec = pam_utils.load_typed_record(context, autofill)
if not af_rec:
raise base.CommandError(f'Record \"{autofill}\" not found.')
if not isinstance(af_rec, vault_record.TypedRecord) or af_rec.version != 3 or af_rec.record_type not in ("login", "pamUser"):
raise base.CommandError(f'Autofill credentials record \"{af_rec.record_uid}\" can not be linked. '
' RBI autofill credential records must be of type "login" or "pamUser"')
if af_rec.record_type not in ("login", "pamUser"):
raise base.CommandError(
f'Autofill credentials record \"{af_rec.record_uid}\" can not be linked. '
' RBI autofill credential records must be of type "login" or "pamUser"'
)

rbs_fld = record.get_typed_field('pamRemoteBrowserSettings')
val1 = rbs_fld.value[0] if isinstance(rbs_fld, vault_record.TypedField) and rbs_fld.value else {}
Expand Down
Loading