From e853c2702d4f8263e805e68a7bd4dde2948b5231 Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Wed, 7 Feb 2024 12:32:20 +0100 Subject: [PATCH 1/3] reproduce issue 102 and fix using extra_key_dirs --- src/tufup/repo/__init__.py | 6 ++++-- tests/test_repo.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/tufup/repo/__init__.py b/src/tufup/repo/__init__.py index 84f01ec..62b4e8c 100644 --- a/src/tufup/repo/__init__.py +++ b/src/tufup/repo/__init__.py @@ -614,7 +614,7 @@ def from_config(cls): instance._load_keys_and_roles(create_keys=False) return instance - def initialize(self): + def initialize(self, extra_key_dirs: Optional[list[pathlib.Path]] = None): """ Initialize (or update) the local repository. @@ -627,6 +627,8 @@ def initialize(self): Safe to call for existing keys and roles. """ + extra_key_dirs = extra_key_dirs or [] + # Ensure dirs exist for path in [self.keys_dir, self.metadata_dir, self.targets_dir]: path.mkdir(parents=True, exist_ok=True) @@ -636,7 +638,7 @@ def initialize(self): # Publish root metadata (save 1.root.json and copy to root.json) if not self.roles.file_path('root').exists(): - self.publish_changes(private_key_dirs=[self.keys_dir]) + self.publish_changes(private_key_dirs=[self.keys_dir] + extra_key_dirs) def refresh_expiration_date(self, role_name: str, days: Optional[int] = None): if days is None: diff --git a/tests/test_repo.py b/tests/test_repo.py index 5c43453..5e92390 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -611,6 +611,35 @@ def test_initialize(self): repo.roles.root.signed.expires.date(), ) + def test_initialize_extra_key_dirs(self): + # prepare + repo = Repository( + app_name='test', + keys_dir=self.temp_dir_path / 'keystore', + repo_dir=self.temp_dir_path / 'repo', + expiration_days=DUMMY_EXPIRATION_DAYS, + ) + repo.initialize() + # move private keys to separate dir + private_key_dir = self.temp_dir_path / 'private_keys' + private_key_dir.mkdir() + for path in repo.keys_dir.iterdir(): + if path.is_file() and not path.suffix: + path.rename(target=private_key_dir / path.name) + # remove metadata files + for path in repo.metadata_dir.iterdir(): + if path.suffix == '.json': + path.unlink() + # reproduce issue #102 + with self.assertRaises(Exception) as context: + repo.initialize() + self.assertIn('no private keys found', str(context.exception).lower()) + # test fix + try: + repo.initialize(extra_key_dirs=[private_key_dir]) + except Exception as e: + self.fail(msg=f'unexpected exception: {e}') + def test_refresh_expiration_date(self): repo = Repository( app_name='test', From 2aacb3b7203a21624c14e5ff1e5267644e941fe4 Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Wed, 7 Feb 2024 13:19:46 +0100 Subject: [PATCH 2/3] make Keys.find_private_key() recursive --- src/tufup/repo/__init__.py | 22 +++++++++++++++++----- tests/test_repo.py | 9 +++++---- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/tufup/repo/__init__.py b/src/tufup/repo/__init__.py index 62b4e8c..c6409ff 100644 --- a/src/tufup/repo/__init__.py +++ b/src/tufup/repo/__init__.py @@ -260,16 +260,28 @@ def roles(self): return roles_map @classmethod - def find_private_key(cls, key_name: str, key_dirs: List[Union[pathlib.Path, str]]): - private_key_path = None + def find_private_key( + cls, key_name: str, key_dirs: List[Union[pathlib.Path, str]] + ) -> Optional[pathlib.Path]: + """ + recursively search key_dirs for a private key with specified key_name + + returns path to first matching file (or None) + """ private_key_filename = cls.filename_pattern.format(key_name=key_name) for key_dir in key_dirs: key_dir = pathlib.Path(key_dir) # ensure Path for path in key_dir.iterdir(): if path.is_file() and path.name == private_key_filename: - private_key_path = path - break - return private_key_path + # base case + return path + elif path.is_dir(): + # recursive case + private_key_path = cls.find_private_key( + key_name=key_name, key_dirs=[path] + ) + if private_key_path: + return private_key_path class Roles(Base): diff --git a/tests/test_repo.py b/tests/test_repo.py index 5e92390..271dfde 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -257,21 +257,22 @@ def test_find_private_key(self): # create dummy private key files in separate folders key_names = [ ('online', [Snapshot.type, Timestamp.type]), - ('offline', [Root.type, Targets.type]), + ('offline/subdir', [Root.type, Targets.type]), # subdir tests recursion ] - key_dirs = [] for dir_name, role_names in key_names: dir_path = self.temp_dir_path / dir_name - dir_path.mkdir() - key_dirs.append(dir_path) + dir_path.mkdir(parents=True) for role_name in role_names: filename = Keys.filename_pattern.format(key_name=role_name) (dir_path / filename).touch() # test + key_dirs = list(self.temp_dir_path.iterdir()) # ['online', 'offline'] for role_name in TOP_LEVEL_ROLE_NAMES: key_path = Keys.find_private_key(key_name=role_name, key_dirs=key_dirs) + self.assertTrue(key_path) self.assertIn(role_name, str(key_path)) self.assertTrue(key_path.exists()) + self.assertIsNone(Keys.find_private_key(key_name='missing', key_dirs=key_dirs)) class RolesTests(TempDirTestCase): From e5bab28a802b6b7a12b0fcce22097c166d980e5f Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Wed, 7 Feb 2024 13:31:14 +0100 Subject: [PATCH 3/3] use old-style typing for python 3.8 support --- src/tufup/repo/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tufup/repo/__init__.py b/src/tufup/repo/__init__.py index c6409ff..fc90eed 100644 --- a/src/tufup/repo/__init__.py +++ b/src/tufup/repo/__init__.py @@ -626,7 +626,7 @@ def from_config(cls): instance._load_keys_and_roles(create_keys=False) return instance - def initialize(self, extra_key_dirs: Optional[list[pathlib.Path]] = None): + def initialize(self, extra_key_dirs: Optional[List[pathlib.Path]] = None): """ Initialize (or update) the local repository.