diff --git a/keepercommander/commands/tunnel/port_forward/TunnelGraph.py b/keepercommander/commands/tunnel/port_forward/TunnelGraph.py index 98aaaf979..027a4d822 100644 --- a/keepercommander/commands/tunnel/port_forward/TunnelGraph.py +++ b/keepercommander/commands/tunnel/port_forward/TunnelGraph.py @@ -522,7 +522,8 @@ def set_resource_allowed(self, resource_uid, tunneling=None, connections=None, r session_recording=None, typescript_recording=None, remote_browser_isolation=None, ai_enabled=None, ai_session_terminate=None, allowed_settings_name='allowedSettings', is_config=False, - v_type: RefType=str(RefType.PAM_MACHINE), meta_version=None): + v_type: RefType=str(RefType.PAM_MACHINE), meta_version=None, + rotate_on_termination=None): v_type = RefType(v_type) allowed_ref_types = [RefType.PAM_MACHINE, RefType.PAM_DATABASE, RefType.PAM_DIRECTORY, RefType.PAM_BROWSER] if v_type not in allowed_ref_types: @@ -625,13 +626,23 @@ def set_resource_allowed(self, resource_uid, tunneling=None, connections=None, r else: settings["aiSessionTerminate"] = ai_session_terminate + if rotate_on_termination is not None: + if content is None: + content = {allowed_settings_name: {}} + dirty = True + current_rot = bool(content.get("rotateOnTermination", False)) + if rotate_on_termination != current_rot: + dirty = True + content = ensure_resource_meta_v1(dict(content)) + content["rotateOnTermination"] = bool(rotate_on_termination) + if dirty: # Legacy: missing or meta_version=0 -> write content as-is (no version in meta) if meta_version is not None and meta_version != 0: meta_payload = build_resource_meta( meta_version, content.get(allowed_settings_name, {}), - rotate_on_termination=False, + rotate_on_termination=bool(content.get("rotateOnTermination", False)), ) resource_vertex.add_data(content=meta_payload, path='meta', needs_encryption=False) else: diff --git a/keepercommander/commands/tunnel_and_connections.py b/keepercommander/commands/tunnel_and_connections.py index 0117a4382..5b74bd544 100644 --- a/keepercommander/commands/tunnel_and_connections.py +++ b/keepercommander/commands/tunnel_and_connections.py @@ -1862,7 +1862,9 @@ class PAMConnectionEditCommand(Command): 'credential on the PAM Resource') parser.add_argument('--launch-user', '-lu', required=False, dest='launch_user', action='store', help='The record path or UID of the PAM User record to configure as the launch ' - 'credential on the PAM Resource') + 'credential on the PAM Resource.') + parser.add_argument('--clear-launch-user', required=False, dest='clear_launch_user', action='store_true', + help='Remove the launch credential from the resource (clears is_launch_credential in the DAG)') parser.add_argument('--protocol', '-p', dest='protocol', choices=protocols, help='Set connection protocol') parser.add_argument('--connections', '-cn', dest='connections', choices=choices, @@ -1876,6 +1878,9 @@ class PAMConnectionEditCommand(Command): 'the port from the record will be used.') parser.add_argument('--key-events', '-k', dest='key_events', choices=choices, help='Toggle Key Events settings') + parser.add_argument('--rotate-on-termination', required=False, dest='rotate_on_termination', + choices=['on', 'off'], + help='Rotate launch credentials when the PAM session ends (DAG resource meta)') parser.add_argument('--silent', '-s', required=False, dest='silent', action='store_true', help='Silent mode - don\'t print PAM User, PAM Config etc.') @@ -2072,24 +2077,47 @@ def execute(self, params, **kwargs): if _typescript_recording is not None and tdag.check_if_resource_allowed(record_uid, "typescriptRecording") != _typescript_recording: dirty = True - if dirty: + launch_credential_record_types = ("pamDatabase", "pamDirectory", "pamMachine") + rot_kw = kwargs.get('rotate_on_termination') + rot_bool = True if rot_kw == 'on' else False if rot_kw == 'off' else None + if rot_bool is not None and record_type not in launch_credential_record_types: + raise CommandError('pam connection edit', + f'{bcolors.FAIL}--rotate-on-termination is only supported for pamMachine, pamDatabase, and ' + f'pamDirectory records. Record "{record_uid}" is of type "{record_type}" and does not support ' + f'launch credentials.{bcolors.ENDC}') + + if dirty or rot_bool is not None: tdag.set_resource_allowed(resource_uid=record_uid, allowed_settings_name=allowed_settings_name, connections=kwargs.get('connections', None), session_recording=kwargs.get('recording', None), - typescript_recording=kwargs.get('typescriptrecording', None)) + typescript_recording=kwargs.get('typescriptrecording', None), + rotate_on_termination=rot_bool) # admin parameter is optional yet if not set connections may fail admin_name = kwargs.get('admin') adm_rec = RecordMixin.resolve_single_record(params, admin_name) admin_uid = adm_rec.record_uid if adm_rec else None - if admin_uid and record_type in ("pamDatabase", "pamDirectory", "pamMachine"): + if admin_uid and record_type in launch_credential_record_types: tdag.link_user_to_resource(admin_uid, record_uid, is_admin=True, belongs_to=True) # tdag.link_user_to_config(admin_uid) # is_iam_user=True - # launch-user parameter sets the launch credential on the resource + # launch-user parameter sets the launch credential; --clear-launch-user removes it + clear_launch_user = bool(kwargs.get('clear_launch_user')) launch_user_name = kwargs.get('launch_user') - if launch_user_name: + + if clear_launch_user and launch_user_name: + raise CommandError('pam connection edit', + f'{bcolors.FAIL}Use either --clear-launch-user or --launch-user, not both.{bcolors.ENDC}') + if clear_launch_user: + if record_type not in launch_credential_record_types: + raise CommandError('pam connection edit', + f'{bcolors.FAIL}--clear-launch-user is only supported for pamMachine, pamDatabase, and ' + f'pamDirectory records. Record "{record_uid}" is of type "{record_type}" and does not ' + f'support launch credentials.{bcolors.ENDC}') + tdag.clear_launch_credential_for_resource(record_uid) + tdag.upgrade_resource_meta_to_v1(record_uid) + elif launch_user_name: launch_rec = RecordMixin.resolve_single_record(params, launch_user_name) if not launch_rec: raise CommandError('', @@ -2098,7 +2126,7 @@ def execute(self, params, **kwargs): raise CommandError('', f'{bcolors.FAIL}Launch user record must be a pamUser record type.{bcolors.ENDC}') launch_uid = launch_rec.record_uid - if record_type in ("pamDatabase", "pamDirectory", "pamMachine"): + if record_type in launch_credential_record_types: tdag.clear_launch_credential_for_resource(record_uid, exclude_user_uid=launch_uid) tdag.link_user_to_resource(launch_uid, record_uid, is_launch_credential=True, belongs_to=True) tdag.upgrade_resource_meta_to_v1(record_uid)