From 21a85e558299ddd54f36e1972f76052f6900a006 Mon Sep 17 00:00:00 2001 From: lthievenaz-keeper Date: Thu, 10 Sep 2026 10:42:48 +0100 Subject: [PATCH 1/3] Import Command - Match Shared Folders by UID In import (e.g. JSON), shared folders are matched by path, not UID. If a vault has a shared folder with the same UID, but different path (e.g. the shared folder is nested in a personal folder), the import will create a new shared folder. This changes the import process so that if the import includes a UID which matches that of a shared folder in the vault, the folder will not be duplicated, records/sub folders will be added to the existing folder in it's current location. The path set in the import folder will be ignored. Currently only wired up for Classic folders --- keepercommander/importer/imp_exp.py | 54 ++++++++++++++++++----------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/keepercommander/importer/imp_exp.py b/keepercommander/importer/imp_exp.py index 75914c0df..9f85f1b01 100644 --- a/keepercommander/importer/imp_exp.py +++ b/keepercommander/importer/imp_exp.py @@ -1889,32 +1889,46 @@ def prepare_folder_add(params, folders, records, manage_users, manage_records, c is_last = True if digest not in folder_hash: - folder_uid = api.generate_record_uid() - folder_type = 'shared_folder' if is_last else 'user_folder' + existing_by_uid = None + if is_last: + sf_uid = getattr(fol, 'uid', None) + if sf_uid and sf_uid in params.shared_folder_cache and sf_uid in params.folder_cache: + # The import file names an existing shared folder by uid. + # Reuse it in place (no new folder, no move) so any + # records that reference this path resolve to it. + shared_folder_key = params.shared_folder_cache[sf_uid]['shared_folder_key_unencrypted'] + existing_by_uid = sf_uid, 'shared_folder', shared_folder_key + + if existing_by_uid is not None: + folder_uid, folder_type, folder_key = existing_by_uid + folder_hash[digest] = existing_by_uid + else: + folder_uid = api.generate_record_uid() + folder_type = 'shared_folder' if is_last else 'user_folder' - fol_req = folder_pb2.FolderRequest() - fol_req.folderUid = base64.urlsafe_b64decode(folder_uid + '==') - fol_req.folderType = 2 if folder_type == 'shared_folder' else 1 + fol_req = folder_pb2.FolderRequest() + fol_req.folderUid = base64.urlsafe_b64decode(folder_uid + '==') + fol_req.folderType = 2 if folder_type == 'shared_folder' else 1 - if parent_uid: - fol_req.parentFolderUid = base64.urlsafe_b64decode(parent_uid + '==') + if parent_uid: + fol_req.parentFolderUid = base64.urlsafe_b64decode(parent_uid + '==') - folder_key = utils.generate_aes_key() - fol_req.encryptedFolderKey = crypto.encrypt_aes_v1(folder_key, params.data_key) + folder_key = utils.generate_aes_key() + fol_req.encryptedFolderKey = crypto.encrypt_aes_v1(folder_key, params.data_key) - data = {'name': comp} - fol_req.folderData = crypto.encrypt_aes_v1(json.dumps(data).encode('utf-8'), folder_key) + data = {'name': comp} + fol_req.folderData = crypto.encrypt_aes_v1(json.dumps(data).encode('utf-8'), folder_key) - if folder_type == 'shared_folder': - fol_req.sharedFolderFields.encryptedFolderName = \ - crypto.encrypt_aes_v1(comp.encode('utf-8'), folder_key) - fol_req.sharedFolderFields.manageUsers = fol.manage_users or manage_users - fol_req.sharedFolderFields.manageRecords = fol.manage_records or manage_records - fol_req.sharedFolderFields.canEdit = fol.can_edit or can_edit - fol_req.sharedFolderFields.canShare = fol.can_share or can_share + if folder_type == 'shared_folder': + fol_req.sharedFolderFields.encryptedFolderName = \ + crypto.encrypt_aes_v1(comp.encode('utf-8'), folder_key) + fol_req.sharedFolderFields.manageUsers = fol.manage_users or manage_users + fol_req.sharedFolderFields.manageRecords = fol.manage_records or manage_records + fol_req.sharedFolderFields.canEdit = fol.can_edit or can_edit + fol_req.sharedFolderFields.canShare = fol.can_share or can_share - folder_add.append(fol_req) - folder_hash[digest] = folder_uid, folder_type, folder_key if folder_type == 'shared_folder' else None + folder_add.append(fol_req) + folder_hash[digest] = folder_uid, folder_type, folder_key if folder_type == 'shared_folder' else None else: folder_uid, folder_type, folder_key = folder_hash[digest] if is_last: From 2832273a56addc722b20d1458a3a0bc375174ed4 Mon Sep 17 00:00:00 2001 From: lthievenaz-keeper Date: Thu, 10 Sep 2026 10:44:32 +0100 Subject: [PATCH 2/3] Support unsafe flag for ApplyMembership Currently, apply-membership --full-sync will remove folder permissions so that it matches the JSON file, however it won't remove yourself. Added --unsafe flag, which will allow removing yourself if you're not set on the folder permissions. This can be useful: - If the folder is only meant to be shared to your team and not you. - To reproduce enterprise-push behavior with shared folders at scale --- keepercommander/importer/imp_exp.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/keepercommander/importer/imp_exp.py b/keepercommander/importer/imp_exp.py index 9f85f1b01..66e1d3075 100644 --- a/keepercommander/importer/imp_exp.py +++ b/keepercommander/importer/imp_exp.py @@ -617,7 +617,7 @@ def import_teams(params, teams, full_sync=False): # type: (KeeperParams, List[ def import_user_permissions(params, shared_folders, - full_sync=False): # type: (KeeperParams, List[ImportSharedFolder], bool) -> None + full_sync=False, unsafe=False): # type: (KeeperParams, List[ImportSharedFolder], bool) -> None if not shared_folders: return @@ -654,7 +654,7 @@ def import_user_permissions(params, folders = [x for x in folders if x.uid in params.shared_folder_cache] if folders: - permissions = prepare_folder_permission(params, folders, full_sync) + permissions = prepare_folder_permission(params, folders, full_sync, unsafe) if permissions: teams_added = 0 users_added = 0 @@ -2586,7 +2586,7 @@ def prepare_record_link(params, records): return record_links -def prepare_folder_permission(params, folders, full_sync): +def prepare_folder_permission(params, folders, full_sync, unsafe=False): # type: (KeeperParams, List[ImportSharedFolder], bool) -> list """Prepare a list of API interactions for changes to folder permissions.""" shared_folder_lookup = {} @@ -2682,6 +2682,9 @@ def prepare_folder_permission(params, folders, full_sync): existing_users.update((x['username'] for x in shared_folder['users'])) if params.user in existing_users: existing_users.remove(params.user) + if unsafe: + # Set user to end of array to be removed last + existing_users.add(params.user) keep_teams = set() keep_users = set() From 738619301fa4737a77374b19746afeefb94451da Mon Sep 17 00:00:00 2001 From: lthievenaz-keeper Date: Thu, 10 Sep 2026 10:46:08 +0100 Subject: [PATCH 3/3] Add unsafe flag to ApplyMembership Add unsafe flag and manual documentation. Unsafe flag allows the apply-membership to remove yourself from the folders if set. --- keepercommander/importer/commands.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/keepercommander/importer/commands.py b/keepercommander/importer/commands.py index 223f5525b..83f2d75ed 100644 --- a/keepercommander/importer/commands.py +++ b/keepercommander/importer/commands.py @@ -155,6 +155,7 @@ def _cyberark_skip_arg(value): apply_membership_parser = argparse.ArgumentParser(prog='apply-membership', description='Loads shared folder membership from JSON file') apply_membership_parser.add_argument('--full-sync', dest='full_sync', action='store_true', help='Update and remove membership also.') +apply_membership_parser.add_argument('--unsafe', dest='unsafe', action='store_true', help='Combined with --full-sync. Will remove yourself from folders where you have no permission.') apply_membership_parser.add_argument('name', type=str, nargs='?', help='Input file name. "shared_folder_membership.json" if omitted.') apply_membership_parser.error = raise_parse_exception apply_membership_parser.exit = suppress_exit @@ -547,8 +548,9 @@ def execute(self, params, **kwargs): teams.append(obj) full_sync = kwargs.get('full_sync') is True + unsafe = kwargs.get('unsafe') is True if len(shared_folders) > 0: - imp_exp.import_user_permissions(params, shared_folders, full_sync) + imp_exp.import_user_permissions(params, shared_folders, full_sync, unsafe) if len(teams) > 0: imp_exp.import_teams(params, teams, full_sync)