From 6370a491f52b5d7e4c2a42e7ba29c695ac3e9054 Mon Sep 17 00:00:00 2001 From: Ilia Vorontcov Date: Wed, 16 Sep 2026 15:35:48 -0400 Subject: [PATCH 1/2] allow renaming root node --- keepercommander/commands/enterprise.py | 4 ++- unit-tests/test_command_enterprise.py | 46 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/keepercommander/commands/enterprise.py b/keepercommander/commands/enterprise.py index 57fffe7a6..f2bb17013 100644 --- a/keepercommander/commands/enterprise.py +++ b/keepercommander/commands/enterprise.py @@ -1554,7 +1554,9 @@ def is_in_chain(node_id, parent_id): 'node_id': node['node_id'], 'encrypted_data': encrypted_data } - rq['parent_id'] = parent_id if parent_id else node.get('parent_id') + effective_parent_id = parent_id if parent_id else node.get('parent_id') + if effective_parent_id: + rq['parent_id'] = effective_parent_id request_batch.append(rq) if request_batch: diff --git a/unit-tests/test_command_enterprise.py b/unit-tests/test_command_enterprise.py index 9e56f87a4..d159cbd9f 100644 --- a/unit-tests/test_command_enterprise.py +++ b/unit-tests/test_command_enterprise.py @@ -87,6 +87,52 @@ def test_enterprise_info_users_verbose_returns_ids(self): 'role_name': ent_env.role1_name, }]) + def test_enterprise_node_rename_root_omits_parent_id(self): + params = get_connected_params() + api.query_enterprise(params) + cmd = enterprise.EnterpriseNodeCommand() + + with mock.patch('keepercommander.commands.enterprise.api.execute_batch', + return_value=[{'result': 'success'}]) as execute_batch: + cmd.execute(params, node=[str(ent_env.node1_id)], displayname='Renamed Enterprise') + + request = execute_batch.call_args.args[1][0] + self.assertNotIn('parent_id', request) + data = crypto.decrypt_aes_v1( + utils.base64_url_decode(request['encrypted_data']), + params.enterprise['unencrypted_tree_key'], + ) + self.assertEqual(json.loads(data.decode('utf-8'))['displayname'], 'Renamed Enterprise') + + def test_enterprise_node_rename_child_preserves_parent_id(self): + params = get_connected_params() + api.query_enterprise(params) + cmd = enterprise.EnterpriseNodeCommand() + + with mock.patch('keepercommander.commands.enterprise.api.execute_batch', + return_value=[{'result': 'success'}]) as execute_batch: + cmd.execute(params, node=[str(ent_env.node2_id)], displayname='Renamed Child') + + request = execute_batch.call_args.args[1][0] + self.assertEqual(request['parent_id'], ent_env.node1_id) + data = crypto.decrypt_aes_v1( + utils.base64_url_decode(request['encrypted_data']), + params.enterprise['unencrypted_tree_key'], + ) + self.assertEqual(json.loads(data.decode('utf-8'))['displayname'], 'Renamed Child') + + def test_enterprise_node_move_sets_selected_parent_id(self): + params = get_connected_params() + api.query_enterprise(params) + cmd = enterprise.EnterpriseNodeCommand() + + with mock.patch('keepercommander.commands.enterprise.api.execute_batch', + return_value=[{'result': 'success'}]) as execute_batch: + cmd.execute(params, node=[str(ent_env.node2_id)], parent=str(ent_env.node1_id)) + + request = execute_batch.call_args.args[1][0] + self.assertEqual(request['parent_id'], ent_env.node1_id) + def test_enterprise_add_user(self): params = get_connected_params() api.query_enterprise(params) From 3e03083ce4edaba2d70fd0eb44b4deb1ea5f1c96 Mon Sep 17 00:00:00 2001 From: Ilia Vorontcov Date: Wed, 16 Sep 2026 16:37:21 -0400 Subject: [PATCH 2/2] allow to use updated root node name as displayName --- keepercommander/commands/enterprise.py | 3 +- keepercommander/commands/enterprise_common.py | 29 +++++++++++------ unit-tests/test_command_enterprise.py | 31 +++++++++++++++++++ 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/keepercommander/commands/enterprise.py b/keepercommander/commands/enterprise.py index f2bb17013..44547c5a6 100644 --- a/keepercommander/commands/enterprise.py +++ b/keepercommander/commands/enterprise.py @@ -397,7 +397,8 @@ def execute(self, params, **kwargs): nodes[node_id] = { 'node_id': node_id, 'parent_id': node.get('parent_id') or 0, - 'name': (node['data'].get('displayname') or '') if node.get('parent_id') else params.enterprise['enterprise_name'], + 'name': node['data'].get('displayname') or + (params.enterprise['enterprise_name'] if not node.get('parent_id') else ''), 'isolated': node.get('restrict_visibility') or False, 'users': [], 'teams': [], diff --git a/keepercommander/commands/enterprise_common.py b/keepercommander/commands/enterprise_common.py index eb0bba7e5..f967c93b6 100644 --- a/keepercommander/commands/enterprise_common.py +++ b/keepercommander/commands/enterprise_common.py @@ -27,7 +27,7 @@ def __init__(self): self.public_keys = {} self.team_keys = {} self._node_map = None - self._node_map_enterprise_name = None + self._node_map_signature = None def execute_args(self, params, args, **kwargs): if params.enterprise: @@ -296,11 +296,21 @@ def get_enterprise_ids(params, num_ids=1): return enterprise_ids def get_node_path(self, params, node_id, omit_root=False): - current_enterprise_name = params.enterprise.get('enterprise_name') - if self._node_map is None or self._node_map_enterprise_name != current_enterprise_name: - self._node_map_enterprise_name = current_enterprise_name + node_map_signature = ( + params.enterprise.get('enterprise_name'), + tuple( + (x['node_id'], x.get('parent_id', 0), x['data'].get('displayname'), x.get('name')) + for x in params.enterprise['nodes'] + ), + ) + if self._node_map is None or self._node_map_signature != node_map_signature: + self._node_map_signature = node_map_signature self._node_map = { - x['node_id']: (x['data'].get('displayname') or x.get('name') or str(x['node_id']) if x.get('parent_id', 0) > 0 else params.enterprise['enterprise_name'], x.get('parent_id', 0)) + x['node_id']: ( + x['data'].get('displayname') or x.get('name') or + (params.enterprise['enterprise_name'] if not x.get('parent_id') else str(x['node_id'])), + x.get('parent_id', 0), + ) for x in params.enterprise['nodes']} path = '' node = self._node_map.get(node_id) @@ -328,11 +338,10 @@ def resolve_nodes(params, name): # type: (KeeperParams, str) -> collections.It yield node continue if node_name: - if 'parent_id' in node: - display_name = node['data'].get('displayname') or '' - else: - display_name = params.enterprise['enterprise_name'] or '' - if display_name and display_name.lower() == node_name: + display_names = [node['data'].get('displayname') or ''] + if not node.get('parent_id'): + display_names.append(params.enterprise['enterprise_name'] or '') + if any(x and x.lower() == node_name for x in display_names): yield node else: if 'parent_id' not in node: diff --git a/unit-tests/test_command_enterprise.py b/unit-tests/test_command_enterprise.py index d159cbd9f..4ab803fdc 100644 --- a/unit-tests/test_command_enterprise.py +++ b/unit-tests/test_command_enterprise.py @@ -87,6 +87,37 @@ def test_enterprise_info_users_verbose_returns_ids(self): 'role_name': ent_env.role1_name, }]) + def test_enterprise_info_uses_root_displayname(self): + params = get_connected_params() + api.query_enterprise(params) + params.enterprise['nodes'][0]['data']['displayname'] = 'ECC-cmdr' + cmd = enterprise.EnterpriseInfoCommand() + + report = json.loads(cmd.execute(params, nodes=True, format='json', quiet=True)) + root = next(x for x in report if x['node_id'] == ent_env.node1_id) + self.assertEqual(root['name'], 'ECC-cmdr') + self.assertIn('ECC-cmdr', cmd.execute(params, quiet=True)) + self.assertIn('ECC-cmdr', cmd.execute(params, nodes=True, format='csv', quiet=True)) + + self.assertEqual( + [x['node_id'] for x in cmd.resolve_nodes(params, 'ECC-cmdr')], + [ent_env.node1_id], + ) + self.assertEqual( + [x['node_id'] for x in cmd.resolve_nodes(params, 'Enterprise 1')], + [ent_env.node1_id], + ) + self.assertEqual(cmd.get_node_path(params, ent_env.node2_id), 'ECC-cmdr\\Sub node 1') + + def test_enterprise_node_path_cache_refreshes_displayname(self): + params = get_connected_params() + api.query_enterprise(params) + cmd = enterprise.EnterpriseInfoCommand() + + self.assertEqual(cmd.get_node_path(params, ent_env.node2_id), 'Enterprise 1\\Sub node 1') + params.enterprise['nodes'][0]['data']['displayname'] = 'ECC-cmdr' + self.assertEqual(cmd.get_node_path(params, ent_env.node2_id), 'ECC-cmdr\\Sub node 1') + def test_enterprise_node_rename_root_omits_parent_id(self): params = get_connected_params() api.query_enterprise(params)