diff --git a/keepercli-package/src/keepercli/commands/pam/pam_config.py b/keepercli-package/src/keepercli/commands/pam/pam_config.py index e52c353..82ef471 100644 --- a/keepercli-package/src/keepercli/commands/pam/pam_config.py +++ b/keepercli-package/src/keepercli/commands/pam/pam_config.py @@ -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() @@ -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 @@ -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 @@ -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): diff --git a/keepercli-package/src/keepercli/commands/pam/pam_connection.py b/keepercli-package/src/keepercli/commands/pam/pam_connection.py index 5a3dc9d..e16915c 100644 --- a/keepercli-package/src/keepercli/commands/pam/pam_connection.py +++ b/keepercli-package/src/keepercli/commands/pam/pam_connection.py @@ -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() @@ -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): @@ -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) @@ -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: @@ -183,8 +208,7 @@ 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: @@ -192,23 +216,33 @@ def execute(self, context: KeeperParams, **kwargs): 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, @@ -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) diff --git a/keepercli-package/src/keepercli/commands/pam/pam_rbi.py b/keepercli-package/src/keepercli/commands/pam/pam_rbi.py index 7bec570..2557a76 100644 --- a/keepercli-package/src/keepercli/commands/pam/pam_rbi.py +++ b/keepercli-package/src/keepercli/commands/pam/pam_rbi.py @@ -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() @@ -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: @@ -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 @@ -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 @@ -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 {} diff --git a/keepercli-package/src/keepercli/commands/pam/pam_rotation.py b/keepercli-package/src/keepercli/commands/pam/pam_rotation.py index c3302de..2b7c505 100644 --- a/keepercli-package/src/keepercli/commands/pam/pam_rotation.py +++ b/keepercli-package/src/keepercli/commands/pam/pam_rotation.py @@ -11,7 +11,6 @@ from keepersdk.helpers.tunnel.tunnel_graph import TunnelDAG from keepersdk.helpers.tunnel.tunnel_utils import get_keeper_tokens from keepersdk.vault import ( - nsf_management, record_management, vault_online, vault_record, @@ -27,6 +26,7 @@ from ...params import KeeperParams from ...helpers import gateway_utils, router_utils, report_utils, folder_utils, record_utils from keepersdk.helpers.keeper_dag.constants import PAM_CONFIGURATIONS +from . import pam_utils logger = api.get_logger() @@ -41,82 +41,6 @@ ) -def _resolve_nsf_record_uid(vault: vault_online.VaultOnline, identifier: str) -> Optional[str]: - """Resolve an NSF record UID from a UID or exact title.""" - if not vault.nsf_data or not identifier: - return None - if vault.nsf_data.get_record(identifier): - return identifier - try: - return nsf_management.resolve_nsf_record_uid(vault, identifier) - except nsf_management.NsfError: - return None - - -def _load_nsf_typed_record( - vault: vault_online.VaultOnline, record_uid: str) -> Optional[vault_record.TypedRecord]: - """Load an NSF record as TypedRecord (with record_key when available).""" - if not vault.nsf_data or not record_uid 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 [], - 'custom': meta.get('custom') or [], - }) - entry = vault.nsf_data.get_record(record_uid) - if entry and entry.record_key: - typed.record_key = entry.record_key - return typed - - -def _load_pam_typed_record( - vault: vault_online.VaultOnline, identifier: str) -> Optional[vault_record.TypedRecord]: - """Load a TypedRecord from classic vault or NSF by UID/title.""" - if not identifier: - return None - loaded = vault.vault_data.load_record(identifier) - if loaded and isinstance(loaded, vault_record.TypedRecord): - key = vault.vault_data.get_record_key(identifier) - if key: - loaded.record_key = key - return loaded - nsf_uid = _resolve_nsf_record_uid(vault, identifier) - if nsf_uid: - return _load_nsf_typed_record(vault, nsf_uid) - return None - - -def _iter_nsf_pam_configurations(vault: vault_online.VaultOnline): - """Yield NSF PAM configuration TypedRecords.""" - if not vault.nsf_data: - return - for entry in vault.nsf_data.records(): - typed = _load_nsf_typed_record(vault, entry.record_uid) - if typed and typed.record_type in PAM_CONFIGURATIONS: - yield typed - - -def _attach_record_key(vault: vault_online.VaultOnline, record: vault_record.TypedRecord) -> None: - """Ensure TypedRecord has record_key from classic or NSF storage when possible.""" - if getattr(record, 'record_key', None): - return - key = vault.vault_data.get_record_key(record.record_uid) - if not key and vault.nsf_data: - entry = vault.nsf_data.get_record(record.record_uid) - if entry: - key = entry.record_key - if key: - record.record_key = key - - class PAMListRecordRotationCommand(base.ArgparseCommand): def __init__(self): parser = argparse.ArgumentParser(prog='pam rotation list') @@ -152,7 +76,7 @@ def execute(self, context: KeeperParams, **kwargs): all_pam_config_records = list(record_utils.pam_configurations_get_all(vault)) seen_config_uids = {c.record_uid for c in all_pam_config_records} - for nsf_cfg in _iter_nsf_pam_configurations(vault): + for nsf_cfg in pam_utils.iter_nsf_pam_configurations(vault): if nsf_cfg.record_uid not in seen_config_uids: all_pam_config_records.append(nsf_cfg) seen_config_uids.add(nsf_cfg.record_uid) @@ -186,7 +110,7 @@ def execute(self, context: KeeperParams, **kwargs): if pam_config.record_uid == configuration_uid_str), None) if not pam_configuration and configuration_uid_str: - nsf_cfg = _load_nsf_typed_record(vault, configuration_uid_str) + nsf_cfg = pam_utils.load_nsf_typed_record(vault, configuration_uid_str) if nsf_cfg and nsf_cfg.record_type in PAM_CONFIGURATIONS: pam_configuration = nsf_cfg @@ -199,7 +123,7 @@ def execute(self, context: KeeperParams, **kwargs): record_title = rec.info.title record_type = rec.info.record_type else: - nsf_rec = _load_nsf_typed_record(vault, record_uid) + nsf_rec = pam_utils.load_nsf_typed_record(vault, record_uid) if nsf_rec: record_title = nsf_rec.title record_type = nsf_rec.record_type @@ -415,7 +339,7 @@ def config_resource(_dag, target_record, target_config_uid, silent=None): _dag.link_resource_to_config(target_record.record_uid) admin = kwargs.get('admin') - adm_rec = _load_pam_typed_record(vault, admin) if admin else None + adm_rec = pam_utils.load_pam_typed_record(vault, admin) if admin else None if adm_rec and isinstance(adm_rec, vault_record.TypedRecord): admin = adm_rec.record_uid @@ -533,7 +457,7 @@ def config_iam_aad_user(_dag, target_record, target_iam_aad_config_uid): if not record_config_uid: if current_record_rotation: record_config_uid = current_record_rotation.configuration_uid - pc = pam_configurations.get(record_config_uid) or _load_pam_typed_record( + pc = pam_configurations.get(record_config_uid) or pam_utils.load_pam_typed_record( vault, record_config_uid) if pc is None: skipped_records.append( @@ -808,7 +732,7 @@ def config_user(_dag, target_record, target_resource_uid, target_config_uid=None if not record_config_uid: if current_record_rotation: record_config_uid = current_record_rotation.configuration_uid - pc = pam_configurations.get(record_config_uid) or _load_pam_typed_record( + pc = pam_configurations.get(record_config_uid) or pam_utils.load_pam_typed_record( vault, record_config_uid) if pc is None: skipped_records.append( @@ -939,7 +863,7 @@ def config_user(_dag, target_record, target_resource_uid, target_config_uid=None elif vault.vault_data.load_record(record_name): record_uids.add(record_name) else: - nsf_uid = _resolve_nsf_record_uid(vault, record_name) + nsf_uid = pam_utils.resolve_nsf_record_uid(vault, record_name) if nsf_uid: record_uids.add(nsf_uid) else: @@ -1006,9 +930,9 @@ def add_folders(folder: vault_types.Folder): for record_uid in record_uids: record = vault.vault_data.load_record(record_uid) if not record: - record = _load_nsf_typed_record(vault, record_uid) + record = pam_utils.load_nsf_typed_record(vault, record_uid) if record and isinstance(record, vault_record.TypedRecord) and record.record_type in valid_record_types: - _attach_record_key(vault, record) + pam_utils.attach_record_key(vault, record) pam_records.append(record) if len(pam_records) == 0: @@ -1023,14 +947,14 @@ def add_folders(folder: vault_types.Folder): criteria=None, record_type=PAM_CONFIGURATIONS, record_version=6): loaded = vault.vault_data.load_record(x.record_uid) if loaded and isinstance(loaded, vault_record.TypedRecord): - _attach_record_key(vault, loaded) + pam_utils.attach_record_key(vault, loaded) pam_configurations[x.record_uid] = loaded - for nsf_cfg in _iter_nsf_pam_configurations(vault): + for nsf_cfg in pam_utils.iter_nsf_pam_configurations(vault): pam_configurations[nsf_cfg.record_uid] = nsf_cfg config_uid = kwargs.get('config') if config_uid: - cfg_rec = _load_pam_typed_record(vault, config_uid) + cfg_rec = pam_utils.load_pam_typed_record(vault, config_uid) if cfg_rec and cfg_rec.record_type in PAM_CONFIGURATIONS: pam_configurations[cfg_rec.record_uid] = cfg_rec config_uid = cfg_rec.record_uid @@ -1074,7 +998,7 @@ def add_folders(folder: vault_types.Folder): resource_uid = kwargs.get('resource') if resource_uid: - res_rec = _load_pam_typed_record(vault, resource_uid) + res_rec = pam_utils.load_pam_typed_record(vault, resource_uid) if res_rec and isinstance(res_rec, vault_record.TypedRecord): resource_uid = res_rec.record_uid elif not vault.vault_data.load_record(resource_uid): @@ -1205,7 +1129,7 @@ def execute(self, context: KeeperParams, **kwargs): record = vault.vault_data._records.get(record_uid) record_key = record.record_key if record else None if not record_key: - typed = _load_pam_typed_record(vault, record_uid) + typed = pam_utils.load_pam_typed_record(vault, record_uid) record_key = getattr(typed, 'record_key', None) if typed else None if record_key: complexity = crypto.decrypt_aes_v2(utils.base64_url_decode(rri.pwdComplexity), diff --git a/keepercli-package/src/keepercli/commands/pam/pam_utils.py b/keepercli-package/src/keepercli/commands/pam/pam_utils.py new file mode 100644 index 0000000..2e3504f --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/pam_utils.py @@ -0,0 +1,138 @@ +"""Shared PAM CLI helpers for classic vault + NSF typed-record access.""" + +from typing import Iterator, Optional + +from keepersdk.helpers.keeper_dag.constants import PAM_CONFIGURATIONS +from keepersdk.vault import ( + nsf_management, + record_management, + vault_extensions, + vault_online, + vault_record, +) + +from .. import base +from ...helpers import record_utils +from ...params import KeeperParams + + +def resolve_nsf_record_uid(vault: vault_online.VaultOnline, identifier: str) -> Optional[str]: + """Resolve an NSF record UID from a UID or exact title.""" + if not vault.nsf_data or not identifier: + return None + if vault.nsf_data.get_record(identifier): + return identifier + try: + return nsf_management.resolve_nsf_record_uid(vault, identifier) + except nsf_management.NsfError: + return None + + +def load_nsf_typed_record( + vault: vault_online.VaultOnline, record_uid: str) -> Optional[vault_record.TypedRecord]: + """Load an NSF record as TypedRecord (with record_key when available).""" + if not vault.nsf_data or not record_uid 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 [], + 'custom': meta.get('custom') or [], + }) + entry = vault.nsf_data.get_record(record_uid) + if entry and entry.record_key: + typed.record_key = entry.record_key + return typed + + +def is_nsf_record(vault: vault_online.VaultOnline, record_uid: str) -> bool: + return bool(record_uid and vault.nsf_data and vault.nsf_data.get_record(record_uid)) + + +def attach_record_key(vault: vault_online.VaultOnline, record: vault_record.TypedRecord) -> None: + """Ensure TypedRecord has record_key from classic or NSF storage when possible.""" + if getattr(record, 'record_key', None): + return + key = vault.vault_data.get_record_key(record.record_uid) + if not key and vault.nsf_data: + entry = vault.nsf_data.get_record(record.record_uid) + if entry: + key = entry.record_key + if key: + record.record_key = key + + +def load_typed_record( + context: KeeperParams, + identifier: str, +) -> Optional[vault_record.TypedRecord]: + """Load a TypedRecord from classic vault or NSF by UID/path/title.""" + vault = context.vault + if not vault or not identifier: + return None + loaded = vault.vault_data.load_record(identifier) + if isinstance(loaded, vault_record.TypedRecord): + return loaded + record_info = record_utils.try_resolve_single_record(identifier, context) + if record_info: + loaded = vault.vault_data.load_record(record_info.record_uid) + if isinstance(loaded, vault_record.TypedRecord): + return loaded + nsf_uid = resolve_nsf_record_uid(vault, identifier) + if nsf_uid: + return load_nsf_typed_record(vault, nsf_uid) + return None + + +def load_pam_typed_record( + vault: vault_online.VaultOnline, identifier: str) -> Optional[vault_record.TypedRecord]: + """Load a TypedRecord from classic vault or NSF by UID/title (vault-only).""" + if not identifier: + return None + loaded = vault.vault_data.load_record(identifier) + if loaded and isinstance(loaded, vault_record.TypedRecord): + attach_record_key(vault, loaded) + return loaded + nsf_uid = resolve_nsf_record_uid(vault, identifier) + if nsf_uid: + return load_nsf_typed_record(vault, nsf_uid) + return None + + +def save_typed_record(vault: vault_online.VaultOnline, record: vault_record.TypedRecord) -> None: + """Persist typed-record body changes via NSF or classic update.""" + if 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 + else: + record_management.update_record(vault, record) + vault.sync_down() + + +def iter_nsf_pam_configurations( + vault: vault_online.VaultOnline) -> Iterator[vault_record.TypedRecord]: + """Yield NSF PAM configuration TypedRecords.""" + if not vault.nsf_data: + return + for entry in vault.nsf_data.records(): + typed = load_nsf_typed_record(vault, entry.record_uid) + if typed and typed.record_type in PAM_CONFIGURATIONS: + yield typed