diff --git a/keepercommander/nested_share_folder/folder_api.py b/keepercommander/nested_share_folder/folder_api.py index c2555bb21..b307c543d 100644 --- a/keepercommander/nested_share_folder/folder_api.py +++ b/keepercommander/nested_share_folder/folder_api.py @@ -732,6 +732,40 @@ def _resolve_uid_to_username(params, uid_b64: str) -> Optional[str]: return None +def _resolve_uid_to_team_name(params, uid_b64: str) -> Optional[str]: + """Try to resolve a base64-url team UID to a human-readable team name.""" + team_cache = getattr(params, 'team_cache', None) or {} + team = team_cache.get(uid_b64) + if isinstance(team, dict): + name = team.get('name') + if name: + return name + + enterprise = getattr(params, 'enterprise', None) + if enterprise: + for t in enterprise.get('teams', []): + if t.get('team_uid') == uid_b64 and t.get('name'): + return t['name'] + + try: + teams = api.get_share_objects(params).get('teams', {}) or {} + team_info = teams.get(uid_b64) + if isinstance(team_info, dict): + name = team_info.get('name') + if name: + return name + except Exception: + pass + + for t in (getattr(params, 'available_team_cache', None) or []): + if t.get('team_uid') == uid_b64: + name = t.get('team_name') + if name: + return name + + return None + + # ══════════════════════════════════════════════════════════════════════════ # High-level: get_folder_access_v3 # ══════════════════════════════════════════════════════════════════════════ @@ -776,19 +810,22 @@ def get_folder_access_v3(params, folder_uids, continuation_token=None, at = folder_pb2.AccessType.Name(a.accessType) rt = folder_pb2.AccessRoleType.Name(a.accessRoleType) username = None - if resolve_usernames and at == 'AT_USER': - username = getattr(params, 'user_cache', {}).get(auid) - if not username and hasattr(params, 'enterprise') and params.enterprise: - for u in params.enterprise.get('users', []): - if u.get('user_account_uid') == auid: - username = u.get('username') - break - if not username: - username = _resolve_uid_to_username(params, auid) - if username: - if not hasattr(params, 'user_cache'): - params.user_cache = {} - params.user_cache[auid] = username + if resolve_usernames: + if at == 'AT_USER': + username = getattr(params, 'user_cache', {}).get(auid) + if not username and hasattr(params, 'enterprise') and params.enterprise: + for u in params.enterprise.get('users', []): + if u.get('user_account_uid') == auid: + username = u.get('username') + break + if not username: + username = _resolve_uid_to_username(params, auid) + if username: + if not hasattr(params, 'user_cache'): + params.user_cache = {} + params.user_cache[auid] = username + elif at == 'AT_TEAM': + username = _resolve_uid_to_team_name(params, auid) ai = { 'accessor_uid': auid, 'access_type': at, 'role': rt, 'inherited': bool(a.inherited), 'hidden': bool(a.hidden), diff --git a/unit-tests/test_nested_share_folder.py b/unit-tests/test_nested_share_folder.py index f946d8b07..d9908ffdb 100644 --- a/unit-tests/test_nested_share_folder.py +++ b/unit-tests/test_nested_share_folder.py @@ -1151,6 +1151,59 @@ def test_parse_folder_access_result_treats_success_message_as_success(self): self.assertTrue(parsed['success']) self.assertEqual(parsed['status'], 'SUCCESS') + def test_resolve_uid_to_team_name_uses_team_cache(self): + from keepercommander.nested_share_folder.folder_api import _resolve_uid_to_team_name + + team_uid = utils.generate_uid() + params = _make_params(team_cache={ + team_uid: {'name': 'Engineering Team', 'team_uid': team_uid}, + }) + self.assertEqual( + _resolve_uid_to_team_name(params, team_uid), 'Engineering Team') + + @patch('keepercommander.nested_share_folder.folder_api.api.get_share_objects') + def test_resolve_uid_to_team_name_falls_back_to_share_objects(self, mock_share_objects): + from keepercommander.nested_share_folder.folder_api import _resolve_uid_to_team_name + + team_uid = utils.generate_uid() + mock_share_objects.return_value = { + 'teams': {team_uid: {'name': 'Ops Team'}}, + } + params = _make_params() + self.assertEqual(_resolve_uid_to_team_name(params, team_uid), 'Ops Team') + + @patch('keepercommander.nested_share_folder.folder_api.api.communicate_rest') + @patch('keepercommander.nested_share_folder.folder_api.resolve_folder_identifier') + def test_get_folder_access_v3_resolves_team_accessor_name( + self, mock_resolve_folder, mock_communicate): + from keepercommander.nested_share_folder.folder_api import get_folder_access_v3 + from keepercommander.proto import folder_access_pb2 + + folder_uid = utils.generate_uid() + team_uid = utils.generate_uid() + mock_resolve_folder.return_value = folder_uid + + access_result = folder_access_pb2.GetFolderAccessResult() + access_result.folderUid = utils.base64_url_decode(folder_uid) + accessor = access_result.accessors.add() + accessor.accessTypeUid = utils.base64_url_decode(team_uid) + accessor.accessType = folder_pb2.AT_TEAM + accessor.accessRoleType = folder_pb2.VIEWER + + response = folder_access_pb2.GetFolderAccessResponse() + response.folderAccessResults.append(access_result) + mock_communicate.return_value = response + + params = _make_params(team_cache={ + team_uid: {'name': 'Engineering Team', 'team_uid': team_uid}, + }) + result = get_folder_access_v3(params, [folder_uid]) + team_accessor = result['results'][0]['accessors'][0] + + self.assertEqual(team_accessor['username'], 'Engineering Team') + self.assertEqual(team_accessor['accessor_uid'], team_uid) + self.assertEqual(team_accessor['access_type'], 'AT_TEAM') + class TestNestedShareFolderRecordApi(TestCase): @@ -1310,6 +1363,38 @@ def test_get_record_details(self, mock_details): def test_get_record_access(self, mock_accesses): pass + @patch('keepercommander.nested_share_folder.get_folder_access_v3') + def test_folder_json_uses_team_name_in_team_permissions(self, mock_access): + """nsf-get JSON should show team names, not team UIDs, in team_permissions.""" + from keepercommander.commands.nested_share_folder.display_commands import NestedShareGetCommand + + folder_uid, folder_obj = _make_folder(name='Shared Folder') + team_uid = utils.generate_uid() + mock_access.return_value = { + 'results': [{ + 'success': True, + 'accessors': [{ + 'accessor_uid': team_uid, + 'username': 'Engineering Team', + 'access_type': 'AT_TEAM', + 'role': 'VIEWER', + 'inherited': False, + 'permissions': {}, + }], + }], + } + + captured = [] + with mock.patch('builtins.print', side_effect=captured.append): + NestedShareGetCommand._folder_json( + _make_params(nested_share_folders={folder_uid: folder_obj}), + folder_uid, + verbose=False, + ) + + payload = json.loads(captured[-1]) + self.assertEqual(payload['team_permissions'][0]['accessor'], 'Engineering Team') + class TestCommandRegistration(TestCase):