From 785b5374a868ea54c538e1cd7c3bbdb8c7930b1c Mon Sep 17 00:00:00 2001 From: tbjones-ks Date: Wed, 11 Feb 2026 16:38:25 -0800 Subject: [PATCH] Commander | 45-add-keeper-db-proxy-option Change List: - Add --keeper-db-proxy (-kdbp) to pam tunnel edit for pamDatabase records - Add launch credential check when enabling DB proxy (--keeper-db-proxy=on) - Add --launch-user (-lu) flag to pam connection edit for setting launch credentials - Add check_if_resource_has_launch_credential() to TunnelDAG - Add is_launch_credential support to link_user/link_user_to_resource in TunnelDAG - Validate --launch-user record is pamUser type --- .../tunnel/port_forward/TunnelGraph.py | 21 ++++++-- .../commands/tunnel_and_connections.py | 54 +++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/keepercommander/commands/tunnel/port_forward/TunnelGraph.py b/keepercommander/commands/tunnel/port_forward/TunnelGraph.py index 44242b782..ca95b9f0e 100644 --- a/keepercommander/commands/tunnel/port_forward/TunnelGraph.py +++ b/keepercommander/commands/tunnel/port_forward/TunnelGraph.py @@ -325,15 +325,16 @@ def unlink_user_from_resource(self, user_uid, resource_uid) -> bool: return False - def link_user_to_resource(self, user_uid, resource_uid, is_admin=None, belongs_to=None): + def link_user_to_resource(self, user_uid, resource_uid, is_admin=None, belongs_to=None, is_launch_credential=None): resource_vertex = self.linking_dag.get_vertex(resource_uid) if resource_vertex is None or not self.resource_belongs_to_config(resource_uid): print(f"{bcolors.FAIL}Resource {resource_uid} does not belong to the configuration{bcolors.ENDC}") return False - self.link_user(user_uid, resource_vertex, is_admin, belongs_to) + self.link_user(user_uid, resource_vertex, is_admin, belongs_to, is_launch_credential=is_launch_credential) return None - def link_user(self, user_uid, source_vertex: DAGVertex, is_admin=None, belongs_to=None, is_iam_user=None): + def link_user(self, user_uid, source_vertex: DAGVertex, is_admin=None, belongs_to=None, is_iam_user=None, + is_launch_credential=None): user_vertex = self.linking_dag.get_vertex(user_uid) if user_vertex is None: @@ -347,6 +348,8 @@ def link_user(self, user_uid, source_vertex: DAGVertex, is_admin=None, belongs_t content["is_admin"] = bool(is_admin) if is_iam_user is not None: content["is_iam_user"] = bool(is_iam_user) + if is_launch_credential is not None: + content["is_launch_credential"] = bool(is_launch_credential) if user_vertex.vertex_type != RefType.PAM_USER: user_vertex.vertex_type = RefType.PAM_USER @@ -395,6 +398,18 @@ def check_if_resource_has_admin(self, resource_uid): return user_vertex.uid return False + def check_if_resource_has_launch_credential(self, resource_uid): + resource_vertex = self.linking_dag.get_vertex(resource_uid) + if resource_vertex is None: + return False + for user_vertex in resource_vertex.has_vertices(EdgeType.ACL): + acl_edge = user_vertex.get_edge(resource_vertex, EdgeType.ACL) + if acl_edge: + content = acl_edge.content_as_dict + if content.get('is_launch_credential'): + return user_vertex.uid + return False + def check_if_resource_allowed(self, resource_uid, setting): resource_vertex = self.linking_dag.get_vertex(resource_uid) content = get_vertex_content(resource_vertex) diff --git a/keepercommander/commands/tunnel_and_connections.py b/keepercommander/commands/tunnel_and_connections.py index 0f07017fd..48cd8b2dd 100644 --- a/keepercommander/commands/tunnel_and_connections.py +++ b/keepercommander/commands/tunnel_and_connections.py @@ -274,6 +274,9 @@ class PAMTunnelEditCommand(Command): pam_cmd_parser.add_argument('--remove-tunneling-override-port', '-rtop', required=False, dest='remove_tunneling_override_port', action='store_true', help='Remove tunneling override port') + pam_cmd_parser.add_argument('--keeper-db-proxy', '-kdbp', required=False, dest='keeper_db_proxy', + choices=['on', 'off', 'default'], + help='Enable/disable Keeper Database Proxy for pamDatabase records (on/off/default)') def get_parser(self): return PAMTunnelEditCommand.pam_cmd_parser @@ -422,8 +425,42 @@ def execute(self, params, **kwargs): if _tunneling is not None and tmp_dag.check_if_resource_allowed(record_uid, "portForwards") != _tunneling: dirty = True + # Handle --keeper-db-proxy option for database proxy routing (pamDatabase records only) + keeper_db_proxy = kwargs.get('keeper_db_proxy') + if keeper_db_proxy: + if record_type != 'pamDatabase': + raise CommandError('pam tunnel edit', + f'{bcolors.FAIL}--keeper-db-proxy is only supported for pamDatabase records. ' + f'Record "{record_name}" is of type "{record_type}".{bcolors.ENDC}') + if keeper_db_proxy == 'on' and not tmp_dag.check_if_resource_has_launch_credential(record_uid): + raise CommandError('', + f'{bcolors.FAIL}No Launch Credentials assigned to record "{record_uid}". ' + f'Please assign launch credentials to the record before enabling ' + f'the database proxy.\n' + f'Use: {bcolors.OKBLUE}pam connection edit ' + f'--launch-user (-lu) {bcolors.ENDC}') + if not pam_settings: + pam_settings = vault.TypedField.new_field('pamSettings', {"connection": {}, "portForward": {}}, "") + record.custom.append(pam_settings) + if not pam_settings.value: + pam_settings.value.append({"connection": {}, "portForward": {}}) + if "connection" not in pam_settings.value[0]: + pam_settings.value[0]["connection"] = {} + current_value = pam_settings.value[0]["connection"].get('allowKeeperDBProxy') + if keeper_db_proxy == 'on' and current_value is not True: + pam_settings.value[0]["connection"]["allowKeeperDBProxy"] = True + dirty = True + elif keeper_db_proxy == 'off' and current_value is not False: + pam_settings.value[0]["connection"]["allowKeeperDBProxy"] = False + dirty = True + elif keeper_db_proxy == 'default' and current_value is not None: + pam_settings.value[0]["connection"].pop('allowKeeperDBProxy', None) + dirty = True + if dirty: tmp_dag.set_resource_allowed(resource_uid=record_uid, tunneling=_tunneling, allowed_settings_name=allowed_settings_name) + record_management.update_record(params, record) + api.sync_down(params) # Print out the tunnel settings if not kwargs.get('silent'): @@ -770,6 +807,9 @@ class PAMConnectionEditCommand(Command): parser.add_argument('--admin-user', '-a', required=False, dest='admin', action='store', help='The record path or UID of the PAM User record to configure the admin ' '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') parser.add_argument('--protocol', '-p', dest='protocol', choices=protocols, help='Set connection protocol') parser.add_argument('--connections', '-cn', dest='connections', choices=choices, @@ -994,6 +1034,20 @@ def execute(self, params, **kwargs): 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_name = kwargs.get('launch_user') + if launch_user_name: + launch_rec = RecordMixin.resolve_single_record(params, launch_user_name) + if not launch_rec: + raise CommandError('', + f'{bcolors.FAIL}Launch user record "{launch_user_name}" not found.{bcolors.ENDC}') + if not isinstance(launch_rec, vault.TypedRecord) or launch_rec.record_type != 'pamUser': + 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"): + tdag.link_user_to_resource(launch_uid, record_uid, is_launch_credential=True, belongs_to=True) + # Print out PAM Settings if not kwargs.get("silent", False): tdag.print_tunneling_config(record_uid, record.get_typed_field('pamSettings'), config_uid)