diff --git a/CHANGELOG.md b/CHANGELOG.md index c7f6f8d6..1fee5738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning][semver]. ### Changed +- Improve CLI error handling ([346]) +- Update signing keys loading. Add a flag for specifying if the user will be asked to manually enter a key ([346]) - Remove default branch specification from updater ([343]) - Updater: only load repositories defined in the newest version of repositories.json ([341]) - Updater: automatically determine url if local repository exists ([340]) @@ -24,6 +26,7 @@ and this project adheres to [Semantic Versioning][semver]. - Fix commits per repositories function when same target commits are on different branches ([337]) - Add missing `write` flag to `taf targets sign` ([329]) +[346]: https://github.com/openlawlibrary/taf/pull/346 [343]: https://github.com/openlawlibrary/taf/pull/343 [342]: https://github.com/openlawlibrary/taf/pull/342 [341]: https://github.com/openlawlibrary/taf/pull/341 diff --git a/taf/api/metadata.py b/taf/api/metadata.py index f9a439d2..5720af05 100644 --- a/taf/api/metadata.py +++ b/taf/api/metadata.py @@ -3,7 +3,7 @@ from pathlib import Path from logdecorator import log_on_end, log_on_error from taf.api.utils import check_if_clean -from taf.exceptions import TargetsMetadataUpdateError +from taf.exceptions import TAFError, TargetsMetadataUpdateError from taf.git import GitRepository from taf.keys import load_signing_keys from taf.constants import DEFAULT_RSA_SIGNATURE_SCHEME @@ -15,6 +15,7 @@ ERROR, "An error occurred while checking expiration dates: {e!r}", logger=taf_logger, + on_exceptions=TAFError, reraise=False, ) def check_expiration_dates(path, interval=None, start_date=None, excluded_roles=None): @@ -69,6 +70,7 @@ def update_metadata_expiration_date( scheme=None, start_date=None, no_commit=False, + prompt_for_keys=False, ): """ Update expiration dates of the specified roles and all other roles that need @@ -113,12 +115,16 @@ def update_metadata_expiration_date( roles_to_update.append("timestamp") for role in roles_to_update: - try: - _update_expiration_date_of_role( - taf_repo, role, loaded_yubikeys, keystore, start_date, interval, scheme - ) - except Exception: - return + _update_expiration_date_of_role( + taf_repo, + role, + loaded_yubikeys, + keystore, + start_date, + interval, + scheme, + prompt_for_keys, + ) if no_commit: print("\nNo commit was set. Please commit manually. \n") @@ -131,12 +137,20 @@ def update_metadata_expiration_date( @log_on_end(INFO, "Updated expiration date of {role:s}", logger=taf_logger) @log_on_error( ERROR, - "Could not update expiration date of {role:s}: {e!r}", + "Error: could not update expiration date: {e}", logger=taf_logger, + on_exceptions=TAFError, reraise=True, ) def _update_expiration_date_of_role( - taf_repo, role, loaded_yubikeys, keystore, start_date, interval, scheme + taf_repo, + role, + loaded_yubikeys, + keystore, + start_date, + interval, + scheme, + prompt_for_keys, ): keys, yubikeys = load_signing_keys( taf_repo, @@ -144,6 +158,7 @@ def _update_expiration_date_of_role( loaded_yubikeys=loaded_yubikeys, keystore=keystore, scheme=scheme, + prompt_for_keys=prompt_for_keys, ) # sign with keystore if len(keys): @@ -159,12 +174,17 @@ def _update_expiration_date_of_role( @log_on_end(INFO, "Updated snapshot and timestamp", logger=taf_logger) @log_on_error( ERROR, - "Could not update snapshot and timestamp: {e!r}", + "Could not update snapshot and timestamp: {e}", logger=taf_logger, + on_exceptions=TAFError, reraise=True, ) def update_snapshot_and_timestamp( - taf_repo, keystore, scheme=DEFAULT_RSA_SIGNATURE_SCHEME, write_all=True + taf_repo, + keystore, + scheme=DEFAULT_RSA_SIGNATURE_SCHEME, + write_all=True, + prompt_for_keys=False, ): """ Sign snapshot and timestamp metadata files. @@ -186,7 +206,12 @@ def update_snapshot_and_timestamp( for role in ("snapshot", "timestamp"): keystore_keys, yubikeys = load_signing_keys( - taf_repo, role, keystore, loaded_yubikeys, scheme=scheme + taf_repo, + role, + keystore, + loaded_yubikeys, + scheme=scheme, + prompt_for_keys=prompt_for_keys, ) if len(yubikeys): update_method = taf_repo.roles_yubikeys_update_method(role) @@ -202,8 +227,9 @@ def update_snapshot_and_timestamp( @log_on_end(INFO, "Updated target metadata", logger=taf_logger) @log_on_error( ERROR, - "Could not update target metadata: {e!r}", + "Could not update target metadata: {e}", logger=taf_logger, + on_exceptions=TAFError, reraise=True, ) def update_target_metadata( @@ -213,6 +239,7 @@ def update_target_metadata( keystore, write=False, scheme=DEFAULT_RSA_SIGNATURE_SCHEME, + prompt_for_keys=False, ): """Given dictionaries containing targets that should be added and targets that should be removed, update and sign target metadata files and, if write is True, also @@ -251,7 +278,12 @@ def update_target_metadata( loaded_yubikeys = {} for role, target_paths in roles_targets.items(): keystore_keys, yubikeys = load_signing_keys( - taf_repo, role, keystore, loaded_yubikeys, scheme=scheme + taf_repo, + role, + keystore, + loaded_yubikeys, + scheme=scheme, + prompt_for_keys=prompt_for_keys, ) targets_data = dict( added_targets_data={ @@ -274,4 +306,6 @@ def update_target_metadata( ) if write: - update_snapshot_and_timestamp(taf_repo, keystore, scheme=scheme) + update_snapshot_and_timestamp( + taf_repo, keystore, scheme=scheme, prompt_for_keys=prompt_for_keys + ) diff --git a/taf/api/repository.py b/taf/api/repository.py index e059cfd4..99135271 100644 --- a/taf/api/repository.py +++ b/taf/api/repository.py @@ -28,8 +28,9 @@ @log_on_end(DEBUG, "Finished adding or updating dependency", logger=taf_logger) @log_on_error( ERROR, - "An error occurred while adding a new dependency: {e!r}", + "An error occurred while adding a new dependency: {e}", logger=taf_logger, + on_exceptions=TAFError, reraise=True, ) @check_if_clean @@ -43,6 +44,7 @@ def add_dependency( library_dir: str = None, scheme: str = DEFAULT_RSA_SIGNATURE_SCHEME, custom=None, + prompt_for_keys=False, ): """ Add a dependency (an authentication repository) to dependencies.json or update it if it was already added to this file. @@ -132,22 +134,26 @@ def add_dependency( keystore, write=False, scheme=scheme, + prompt_for_keys=prompt_for_keys, ) # update snapshot and timestamp calls write_all, so targets updates will be saved too - update_snapshot_and_timestamp(auth_repo, keystore, scheme=scheme) + update_snapshot_and_timestamp( + auth_repo, keystore, scheme=scheme, prompt_for_keys=prompt_for_keys + ) commit_message = input("\nEnter commit message and press ENTER\n\n") auth_repo.commit(commit_message) @log_on_start( - INFO, "Creating a new authentication repository {repo_path:s}", logger=taf_logger + INFO, "Creating a new authentication repository {path:s}", logger=taf_logger ) @log_on_end(INFO, "Finished creating a new repository", logger=taf_logger) @log_on_error( ERROR, - "An error occurred while creating a new repository: {e!r}", + "An error occurred while creating a new repository: {e}", logger=taf_logger, + on_exceptions=TAFError, reraise=True, ) def create_repository( @@ -306,8 +312,9 @@ def _determine_out_of_band_data(dependency, branch_name, out_of_band_commit): @log_on_end(DEBUG, "Finished removing dependency", logger=taf_logger) @log_on_error( ERROR, - "An error occurred while removing a dependency: {e!r}", + "An error occurred while removing a dependency: {e}", logger=taf_logger, + on_exceptions=TAFError, reraise=True, ) @check_if_clean @@ -316,6 +323,7 @@ def remove_dependency( dependency_name: str, keystore: str, scheme: str = DEFAULT_RSA_SIGNATURE_SCHEME, + prompt_for_keys: bool = False, ): """ Remove a dependency (an authentication repository) from dependencies.json @@ -370,10 +378,13 @@ def remove_dependency( keystore, write=False, scheme=scheme, + prompt_for_keys=prompt_for_keys, ) # update snapshot and timestamp calls write_all, so targets updates will be saved too - update_snapshot_and_timestamp(auth_repo, keystore, scheme=scheme) + update_snapshot_and_timestamp( + auth_repo, keystore, scheme=scheme, prompt_for_keys=prompt_for_keys + ) commit_message = input("\nEnter commit message and press ENTER\n\n") auth_repo.commit(commit_message) diff --git a/taf/api/roles.py b/taf/api/roles.py index c0441d0d..fec9b1e3 100644 --- a/taf/api/roles.py +++ b/taf/api/roles.py @@ -7,6 +7,7 @@ from pathlib import Path from logdecorator import log_on_end, log_on_error, log_on_start from taf.api.utils import check_if_clean +from taf.exceptions import TAFError from taf.repositoriesdb import REPOSITORIES_JSON_PATH from tuf.repository_tool import TARGETS_DIRECTORY_NAME import tuf.roledb @@ -41,8 +42,9 @@ @log_on_end(DEBUG, "Finished adding a new role", logger=taf_logger) @log_on_error( ERROR, - "An error occurred while adding a new role {role:s}: {e!r}", + "An error occurred while adding a new role {role:s}: {e}", logger=taf_logger, + on_exceptions=TAFError, reraise=True, ) @check_if_clean @@ -58,6 +60,7 @@ def add_role( scheme: str = DEFAULT_RSA_SIGNATURE_SCHEME, auth_repo: AuthenticationRepository = None, commit=True, + prompt_for_keys=False, ): """ Add a new delegated target role and update and sign metadata files. @@ -113,9 +116,13 @@ def add_role( _create_delegations( roles_infos, auth_repo, verification_keys, signing_keys, existing_roles ) - _update_role(auth_repo, parent_role, keystore, scheme=scheme) + _update_role( + auth_repo, parent_role, keystore, scheme=scheme, prompt_for_keys=prompt_for_keys + ) if commit: - update_snapshot_and_timestamp(auth_repo, keystore, scheme=scheme) + update_snapshot_and_timestamp( + auth_repo, keystore, scheme=scheme, prompt_for_keys=prompt_for_keys + ) commit_message = input("\nEnter commit message and press ENTER\n\n") auth_repo.commit(commit_message) @@ -124,12 +131,19 @@ def add_role( @log_on_end(DEBUG, "Finished adding new paths to role", logger=taf_logger) @log_on_error( ERROR, - "An error occurred while adding new paths to role {role:s}: {e!r}", + "An error occurred while adding new paths to role {role:s}: {e}", logger=taf_logger, + on_exceptions=TAFError, reraise=True, ) def add_role_paths( - paths, delegated_role, keystore, commit=True, auth_repo=None, auth_path=None + paths, + delegated_role, + keystore, + commit=True, + auth_repo=None, + auth_path=None, + prompt_for_keys=False, ): """ Adds additional delegated target paths to the specified role. That means that @@ -155,9 +169,11 @@ def add_role_paths( parent_role = auth_repo.find_delegated_roles_parent(delegated_role) parent_role_obj = _role_obj(parent_role, auth_repo) parent_role_obj.add_paths(paths, delegated_role) - _update_role(auth_repo, parent_role, keystore) + _update_role(auth_repo, parent_role, keystore, prompt_for_keys=prompt_for_keys) if commit: - update_snapshot_and_timestamp(auth_repo, keystore) + update_snapshot_and_timestamp( + auth_repo, keystore, prompt_for_keys=prompt_for_keys + ) commit_message = input("\nEnter commit message and press ENTER\n\n") auth_repo.commit(commit_message) @@ -166,8 +182,9 @@ def add_role_paths( @log_on_end(DEBUG, "Finished adding new roles", logger=taf_logger) @log_on_error( ERROR, - "An error occurred while adding new roles: {e!r}", + "An error occurred while adding new roles: {e}", logger=taf_logger, + on_exceptions=TAFError, reraise=True, ) @check_if_clean @@ -176,6 +193,7 @@ def add_roles( keystore=None, roles_key_infos=None, scheme=DEFAULT_RSA_SIGNATURE_SCHEME, + prompt_for_keys=False, ): """ Add new target roles and sign all metadata files given information stored in roles_key_infos @@ -257,16 +275,25 @@ def add_roles( roles_infos, repository, verification_keys, signing_keys, existing_roles ) for parent_role in parent_roles: - _update_role(auth_repo, parent_role, keystore, scheme=scheme) - update_snapshot_and_timestamp(auth_repo, keystore, scheme=scheme) + _update_role( + auth_repo, + parent_role, + keystore, + scheme=scheme, + prompt_for_keys=prompt_for_keys, + ) + update_snapshot_and_timestamp( + auth_repo, keystore, scheme=scheme, prompt_for_keys=prompt_for_keys + ) @log_on_start(DEBUG, "Adding new signing key to roles", logger=taf_logger) @log_on_end(DEBUG, "Finished adding new signing key to roles", logger=taf_logger) @log_on_error( ERROR, - "An error occurred while adding new signing key to roles: {e!r}", + "An error occurred while adding new signing key to roles: {e}", logger=taf_logger, + on_exceptions=TAFError, reraise=True, ) @check_if_clean @@ -277,6 +304,7 @@ def add_signing_key( keystore=None, roles_key_infos=None, scheme=DEFAULT_RSA_SIGNATURE_SCHEME, + prompt_for_keys=False, ): """ Add a new signing key to the listed roles. Update root metadata if one or more roles is one of the main TUF roles, @@ -338,9 +366,17 @@ def add_signing_key( taf_repo.unmark_dirty_roles(list(set(roles) - parent_roles)) for parent_role in parent_roles: - _update_role(taf_repo, parent_role, keystore, scheme=scheme) + _update_role( + taf_repo, + parent_role, + keystore, + scheme=scheme, + prompt_for_keys=prompt_for_keys, + ) - update_snapshot_and_timestamp(taf_repo, keystore, scheme=scheme) + update_snapshot_and_timestamp( + taf_repo, keystore, scheme=scheme, prompt_for_keys=prompt_for_keys + ) def _enter_roles_infos(keystore, roles_key_infos): @@ -515,7 +551,9 @@ def _initialize_roles_and_keystore(roles_key_infos, keystore, enter_info=True): if roles_key_infos is not None and type(roles_key_infos) == str: roles_key_infos_path = Path(roles_key_infos) if roles_key_infos_path.is_file() and "keystore" in roles_key_infos_dict: - keystore_path = Path(roles_key_infos_dict["keystore"]) + keystore_path = ( + Path(roles_key_infos_dict["keystore"]).expanduser().resolve() + ) if not keystore_path.is_absolute(): keystore_path = ( roles_key_infos_path.parent / keystore_path @@ -558,6 +596,9 @@ def _create_delegations( existing_roles = [] for role_name, role_info in roles_infos.items(): if "delegations" in role_info: + delegations = role_info["delegations"] + if "roles" not in delegations: + continue parent_role_obj = _role_obj(role_name, repository) delegations_info = role_info["delegations"]["roles"] for delegated_role_name, delegated_role_info in delegations_info.items(): @@ -624,8 +665,9 @@ def _role_obj(role, repository, parent=None): @log_on_end(DEBUG, "Finished removing the role", logger=taf_logger) @log_on_error( ERROR, - "An error occurred while removing role {role:s}: {e!r}", + "An error occurred while removing role {role:s}: {e}", logger=taf_logger, + on_exceptions=TAFError, reraise=True, ) @check_if_clean @@ -637,6 +679,7 @@ def remove_role( commit: bool = True, remove_targets: bool = False, auth_repo: AuthenticationRepository = None, + prompt_for_keys: bool = False, ): """ Remove a delegated target role and update and sign metadata files. @@ -692,7 +735,7 @@ def remove_role( parent_role_obj = _role_obj(parent_role, auth_repo) parent_role_obj.revoke(role) - _update_role(auth_repo, parent_role, keystore) + _update_role(auth_repo, parent_role, keystore, prompt_for_keys) if len(added_targets_data): removed_targets_data = {} update_target_metadata( @@ -702,6 +745,8 @@ def remove_role( keystore, write=False, scheme=DEFAULT_RSA_SIGNATURE_SCHEME, + update_target_metadata=update_target_metadata, + prompt_for_keys=prompt_for_keys, ) # if targets should be deleted, also removed them from repositories.json @@ -717,7 +762,9 @@ def remove_role( json.dumps(repositories_json, indent=4) ) - update_snapshot_and_timestamp(auth_repo, keystore, scheme=scheme) + update_snapshot_and_timestamp( + auth_repo, keystore, scheme=scheme, prompt_for_keys=prompt_for_keys + ) if commit: commit_message = input("\nEnter commit message and press ENTER\n\n") auth_repo.commit(commit_message) @@ -727,11 +774,12 @@ def remove_role( @log_on_end(DEBUG, "Finished removing delegated paths", logger=taf_logger) @log_on_error( ERROR, - "An error occurred while removing roles: {e!r}", + "An error occurred while removing roles: {e}", logger=taf_logger, + on_exceptions=TAFError, reraise=True, ) -def remove_paths(path, paths, keystore, commit=True): +def remove_paths(path, paths, keystore, commit=True, prompt_for_keys=False): """ Remove delegated paths. Update parent roles of the roles associated with the removed paths, as well as snapshot and timestamp. Optionally commit the changes. @@ -757,9 +805,13 @@ def remove_paths(path, paths, keystore, commit=True): _remove_path_from_role_info( path_to_remove, parent_role, delegated_role, auth_repo ) - _update_role(auth_repo, parent_role, keystore) + _update_role( + auth_repo, parent_role, keystore, prompt_for_keys=prompt_for_keys + ) if commit: - update_snapshot_and_timestamp(auth_repo, keystore) + update_snapshot_and_timestamp( + auth_repo, keystore, prompt_for_keys=prompt_for_keys + ) commit_message = input("\nEnter commit message and press ENTER\n\n") auth_repo.commit(commit_message) @@ -806,13 +858,17 @@ def _setup_role( ) -def _update_role(taf_repo, role, keystore, scheme=DEFAULT_RSA_SIGNATURE_SCHEME): +def _update_role( + taf_repo, role, keystore, scheme=DEFAULT_RSA_SIGNATURE_SCHEME, prompt_for_keys=False +): """ Update the specified role's metadata's expiration date, load the signing keys from either a keystore file or yubikey and sign the file without updating snapshot and timestamp and writing changes to disk """ - keystore_keys, yubikeys = load_signing_keys(taf_repo, role, keystore, scheme=scheme) + keystore_keys, yubikeys = load_signing_keys( + taf_repo, role, keystore, scheme=scheme, prompt_for_keys=prompt_for_keys + ) if len(keystore_keys): taf_repo.update_role_keystores(role, keystore_keys, write=False) if len(yubikeys): diff --git a/taf/api/targets.py b/taf/api/targets.py index 178cef65..52e3e692 100644 --- a/taf/api/targets.py +++ b/taf/api/targets.py @@ -28,8 +28,9 @@ @log_on_end(DEBUG, "Finished adding target repository", logger=taf_logger) @log_on_error( ERROR, - "An error occurred while adding a new target repository {target_name:s}: {e!r}", + "An error occurred while adding a new target repository {target_name:s}: {e}", logger=taf_logger, + on_exceptions=TAFError, reraise=True, ) @check_if_clean @@ -42,6 +43,7 @@ def add_target_repo( keystore: str, scheme: str = DEFAULT_RSA_SIGNATURE_SCHEME, custom=None, + prompt_for_keys=False, ): """ Add a new target repository by adding it to repositories.json, creating a delegation (if targets is not @@ -68,7 +70,6 @@ def add_target_repo( Returns: None """ - auth_repo = AuthenticationRepository(path=path) if not auth_repo.is_git_repository_root: print(f"{path} is not a git repository!") @@ -101,21 +102,29 @@ def add_target_repo( paths.append(target_name) add_role( - path, - role, - parent_role or "targets", - paths, - keys_number, - threshold, - yubikey, - keystore, - DEFAULT_RSA_SIGNATURE_SCHEME, + path=path, + role=role, + parent_role=parent_role or "targets", + paths=paths, + keys_number=keys_number, + threshold=threshold, + yubikey=yubikey, + keystore=keystore, + scheme=DEFAULT_RSA_SIGNATURE_SCHEME, commit=False, auth_repo=auth_repo, + prompt_for_keys=prompt_for_keys, ) else: print("Role already exists") - add_role_paths([target_name], role, keystore, commit=False, auth_repo=auth_repo) + add_role_paths( + paths=[target_name], + delegated_role=role, + keystore=keystore, + commit=False, + auth_repo=auth_repo, + prompt_for_keys=prompt_for_keys, + ) # target repo should be added to repositories.json # delegation paths should be extended if role != targets @@ -149,10 +158,13 @@ def add_target_repo( keystore, write=False, scheme=scheme, + prompt_for_keys=prompt_for_keys, ) # update snapshot and timestamp calls write_all, so targets updates will be saved too - update_snapshot_and_timestamp(auth_repo, keystore, scheme=scheme) + update_snapshot_and_timestamp( + auth_repo, keystore, scheme=scheme, prompt_for_keys=prompt_for_keys + ) commit_message = input("\nEnter commit message and press ENTER\n\n") auth_repo.commit(commit_message) @@ -276,8 +288,9 @@ def list_targets( @log_on_start(INFO, "Signing target files", logger=taf_logger) @log_on_error( ERROR, - "An error occurred while signing target files: {e!r}", + "An error occurred while signing target files: {e}", logger=taf_logger, + on_exceptions=TAFError, reraise=True, ) def register_target_files( @@ -288,6 +301,7 @@ def register_target_files( scheme=DEFAULT_RSA_SIGNATURE_SCHEME, taf_repo=None, write=False, + prompt_for_keys=False, ): """ Register all files found in the target directory as targets - update the targets @@ -324,6 +338,7 @@ def register_target_files( keystore, scheme=scheme, write=write, + prompt_for_keys=prompt_for_keys, ) if write: @@ -338,15 +353,14 @@ def register_target_files( @log_on_end(DEBUG, "Finished removing target repository", logger=taf_logger) @log_on_error( ERROR, - "An error occurred while removing target repository {target_name:s}: {e!r}", + "An error occurred while removing target repository {target_name:s}: {e}", logger=taf_logger, + on_exceptions=TAFError, reraise=True, ) @check_if_clean def remove_target_repo( - path: str, - target_name: str, - keystore: str, + path: str, target_name: str, keystore: str, prompt_for_keys: bool = False ): """ Remove target repository from repositories.json, remove delegation, and target files and @@ -396,17 +410,26 @@ def remove_target_repo( removed_targets_data, keystore, write=False, + prompt_for_keys=prompt_for_keys, ) update_snapshot_and_timestamp( - auth_repo, keystore, scheme=DEFAULT_RSA_SIGNATURE_SCHEME + auth_repo, + keystore, + scheme=DEFAULT_RSA_SIGNATURE_SCHEME, + prompt_for_keys=prompt_for_keys, ) auth_repo.commit(f"Remove {target_name} target") # commit_message = input("\nEnter commit message and press ENTER\n\n") - remove_paths(path, [target_name], keystore, commit=False) + remove_paths( + path, [target_name], keystore, commit=False, prompt_for_keys=prompt_for_keys + ) update_snapshot_and_timestamp( - auth_repo, keystore, scheme=DEFAULT_RSA_SIGNATURE_SCHEME + auth_repo, + keystore, + scheme=DEFAULT_RSA_SIGNATURE_SCHEME, + prompt_for_keys=prompt_for_keys, ) auth_repo.commit(f"Remove {target_name} from delegated paths") # update snapshot and timestamp calls write_all, so targets updates will be saved too @@ -434,8 +457,9 @@ def _save_top_commit_of_repo_to_target( @log_on_end(DEBUG, "Finished updating target files", logger=taf_logger) @log_on_error( ERROR, - "An error occurred while updating target files: {e!r}", + "An error occurred while updating target files: {e}", logger=taf_logger, + on_exceptions=TAFError, reraise=True, ) @check_if_clean @@ -445,6 +469,7 @@ def update_target_repos_from_repositories_json( keystore, add_branch=True, scheme=DEFAULT_RSA_SIGNATURE_SCHEME, + prompt_for_keys=False, ): """ Create or update target files by reading the latest commit's repositories.json @@ -473,15 +498,18 @@ def update_target_repos_from_repositories_json( ) for repo_name in repositories_json.get("repositories"): _save_top_commit_of_repo_to_target(library_dir, repo_name, path, add_branch) - register_target_files(path, keystore, None, True, scheme, write=True) + register_target_files( + path, keystore, None, True, scheme, write=True, prompt_for_keys=prompt_for_keys + ) @log_on_start(DEBUG, "Updating target files", logger=taf_logger) @log_on_end(DEBUG, "Finished updating target files", logger=taf_logger) @log_on_error( ERROR, - "An error occurred while updating target files: {e!r}", + "An error occurred while updating target files: {e}", logger=taf_logger, + on_exceptions=TAFError, reraise=True, ) @check_if_clean @@ -492,6 +520,7 @@ def update_and_sign_targets( keystore: str, roles_key_infos: str, scheme: str, + prompt_for_keys: bool = False, ): """ Save the top commit of specified target repositories to the corresponding target files and sign. @@ -536,7 +565,15 @@ def update_and_sign_targets( for target_name in target_names: _save_top_commit_of_repo_to_target(library_dir, target_name, path, True) print(f"Updated {target_name} target file") - register_target_files(path, keystore, roles_key_infos, True, scheme, write=True) + register_target_files( + path, + keystore, + roles_key_infos, + True, + scheme, + write=True, + prompt_for_keys=prompt_for_keys, + ) def _update_target_repos(repo_path, targets_dir, target_repo_path, add_branch): diff --git a/taf/git.py b/taf/git.py index 37fa857a..ab4c2689 100644 --- a/taf/git.py +++ b/taf/git.py @@ -65,7 +65,7 @@ def __init__( if name is not None: self.name = self._validate_repo_name(name) self.path = self._validate_repo_path(library_dir, name, path) - self.library_dir = library_dir.resolve() + self.library_dir = library_dir.expanduser().resolve() elif path is None: raise InvalidRepositoryError( "Either specify library dir and name pair or path!" @@ -1147,7 +1147,7 @@ def _validate_repo_path(self, library_dir, name, path=None): raise InvalidRepositoryError( f"Repository path/name {library_dir}/{name} is not valid" ) - repo_dir = Path(repo_dir).resolve() + repo_dir = Path(repo_dir).expanduser().resolve() if path is not None and path != repo_dir: raise InvalidRepositoryError( "Both library dir and name pair and path specified and are not equal. Omit the path." diff --git a/taf/keys.py b/taf/keys.py index 95027846..6b78b732 100644 --- a/taf/keys.py +++ b/taf/keys.py @@ -3,7 +3,7 @@ from pathlib import Path from tuf.repository_tool import generate_and_write_unencrypted_rsa_keypair from taf.constants import DEFAULT_ROLE_SETUP_PARAMS, DEFAULT_RSA_SIGNATURE_SCHEME -from taf.exceptions import KeystoreError +from taf.exceptions import KeystoreError, SigningError from taf.keystore import ( key_cmd_prompt, read_private_key_from_keystore, @@ -98,6 +98,9 @@ def _sort_roles(key_info, repository): else: yubikey_roles.append((role_name, role_key_info)) if "delegations" in role_key_info: + delegations = role_key_info["delegations"] + if "roles" not in delegations: + continue delegated_keystore_role, delegated_yubikey_roles = _sort_roles( role_key_info["delegations"]["roles"], repository ) @@ -144,6 +147,7 @@ def load_signing_keys( keystore=None, loaded_yubikeys=None, scheme=DEFAULT_RSA_SIGNATURE_SCHEME, + prompt_for_keys=False, ): """ Load role's signing keys. Make sure that at least the threshold of keys was @@ -163,7 +167,7 @@ def load_signing_keys( # if the keystore file is not found, ask the user if they want to sign # using yubikey and to insert it if that is the case - keystore = Path(keystore) + keystore = Path(keystore).expanduser().resolve() def _load_from_keystore(key_name): if (keystore / key_name).is_file(): @@ -220,9 +224,12 @@ def _load_and_append_yubikeys(key_name, role, retry_on_failure): num_of_signatures += 1 continue - key = key_cmd_prompt(key_name, role, taf_repo, keys, scheme) - keys.append(key) - num_of_signatures += 1 + if prompt_for_keys and click.confirm(f"Manually enter {role} key?"): + key = key_cmd_prompt(key_name, role, taf_repo, keys, scheme) + keys.append(key) + num_of_signatures += 1 + else: + raise SigningError(f"Cannot load keys of role {role}") return keys, yubikeys @@ -274,6 +281,7 @@ def _invalid_key_message(key_name, keystore, is_public): print(f"{key_path} is not a file!") if keystore is not None: + keystore = Path(keystore).expanduser().resolve() while public_key is None and private_key is None: try: public_key = read_public_key_from_keystore(keystore, key_name, scheme) diff --git a/taf/keystore.py b/taf/keystore.py index 553c5850..5b52d6cc 100644 --- a/taf/keystore.py +++ b/taf/keystore.py @@ -89,7 +89,7 @@ def read_private_key_from_keystore( scheme=DEFAULT_RSA_SIGNATURE_SCHEME, password=None, ): - key_path = Path(keystore, key_name) + key_path = Path(keystore, key_name).expanduser().resolve() if not key_path.is_file(): raise KeystoreError(f"{str(key_path)} does not exist") @@ -129,7 +129,7 @@ def _read_key_or_keystore_error(path, password, scheme): def read_public_key_from_keystore( keystore, key_name, scheme=DEFAULT_RSA_SIGNATURE_SCHEME ): - pub_key_path = Path(keystore, f"{key_name}.pub") + pub_key_path = Path(keystore, f"{key_name}.pub").expanduser().resolve() if not pub_key_path.is_file(): raise KeystoreError(f"{str(pub_key_path)} does not exist") try: diff --git a/taf/tools/cli/__init__.py b/taf/tools/cli/__init__.py index e69de29b..e64430fa 100644 --- a/taf/tools/cli/__init__.py +++ b/taf/tools/cli/__init__.py @@ -0,0 +1,17 @@ +import click +from functools import partial, wraps + + +def catch_cli_exception(func=None, *, handle, print_error=False): + if not func: + return partial(catch_cli_exception, handle=handle) + + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except handle as e: + if print_error: + click.echo(e) + + return wrapper diff --git a/taf/tools/dependencies/__init__.py b/taf/tools/dependencies/__init__.py index 82021c4e..ce50bf25 100644 --- a/taf/tools/dependencies/__init__.py +++ b/taf/tools/dependencies/__init__.py @@ -3,6 +3,8 @@ add_dependency, remove_dependency ) +from taf.exceptions import TAFError +from taf.tools.cli import catch_cli_exception def attach_to_group(group): @@ -15,14 +17,18 @@ def dependencies(): ignore_unknown_options=True, allow_extra_args=True, )) + @catch_cli_exception(handle=TAFError) @click.argument("dependency_name") @click.option("--path", default=".", help="Authentication repository's location. If not specified, set to the current directory") @click.option("--branch-name", default=None, help="Name of the branch which contains the out-of-band commit") @click.option("--out-of-band-commit", default=None, help="Out-of-band commit SHA") @click.option("--dependency-path", default=None, help="Dependency's filesystem path") @click.option("--keystore", default=None, help="Location of the keystore files") + @click.option("--keystore", default=None, help="Location of the keystore files") + @click.option("--prompt-for-keys", is_flag=True, default=False, help="Whether to ask the user to enter their key if not " + "located inside the keystore directory") @click.pass_context - def add(ctx, dependency_name, path, branch_name, out_of_band_commit, dependency_path, keystore): + def add(ctx, dependency_name, path, branch_name, out_of_band_commit, dependency_path, keystore, prompt_for_keys): """Add a dependency (an authentication repository) to dependencies.json or update it if it was already added to this file. Update and sign targets metadata, snapshot and timestamp using yubikeys or keys loaded from the specified keystore location. Information that is added to dependencies.json includes out-of-band authentication commit and name @@ -57,18 +63,27 @@ def add(ctx, dependency_name, path, branch_name, out_of_band_commit, dependency_ out_of_band_commit=out_of_band_commit, dependency_path=dependency_path, keystore=keystore, - custom=custom + custom=custom, + prompt_for_keys=prompt_for_keys, ) @dependencies.command() + @catch_cli_exception(handle=TAFError) @click.argument("dependency-name") @click.option("--path", default=".", help="Authentication repository's location. If not specified, set to the current directory") @click.option("--keystore", default=None, help="Location of the keystore files") - def remove(dependency_name, path, keystore): + @click.option("--prompt-for-keys", is_flag=True, default=False, help="Whether to ask the user to enter their key if not " + "located inside the keystore directory") + def remove(dependency_name, path, keystore, prompt_for_keys): """Remove a dependency from dependencies.json. Update and sign targets metadata, snapshot and timestamp using yubikeys or keys loaded from the specified keystore location. `taf dependencies remove auth-path namespace1/auth --keystore keystore-path` """ - remove_dependency(path, dependency_name, keystore) + remove_dependency( + path=path, + dependency_name=dependency_name, + keystore=keystore, + prompt_for_keys=prompt_for_keys, + ) diff --git a/taf/tools/metadata/__init__.py b/taf/tools/metadata/__init__.py index dde3b002..20be2853 100644 --- a/taf/tools/metadata/__init__.py +++ b/taf/tools/metadata/__init__.py @@ -1,6 +1,8 @@ import click from taf.api.metadata import update_metadata_expiration_date, check_expiration_dates as check_metadata_expiration_dates from taf.constants import DEFAULT_RSA_SIGNATURE_SCHEME +from taf.exceptions import SigningError +from taf.tools.cli import catch_cli_exception from taf.utils import ISO_DATE_PARAM_TYPE as ISO_DATE import datetime @@ -37,6 +39,7 @@ def check_expiration_dates(path, interval, start_date): check_metadata_expiration_dates(path=path, interval=interval, start_date=start_date) @metadata.command() + @catch_cli_exception(handle=SigningError) @click.option("--path", default=".", help="Authentication repository's location. If not specified, set to the current directory") @click.option("--role", multiple=True, help="A list of roles which expiration date should get updated") @click.option("--interval", default=None, help="Number of days added to the start date", @@ -48,7 +51,9 @@ def check_expiration_dates(path, interval, start_date): "interval is added", type=ISO_DATE) @click.option("--no-commit", is_flag=True, default=False, help="Indicates if the changes should not be " "committed automatically") - def update_expiration_dates(path, role, interval, keystore, scheme, start_date, no_commit): + @click.option("--prompt-for-keys", is_flag=True, default=False, help="Whether to ask the user to enter their key if not " + "located inside the keystore directory") + def update_expiration_dates(path, role, interval, keystore, scheme, start_date, no_commit, prompt_for_keys): """ \b Update expiration date of the metadata file corresponding to the specified role. @@ -69,5 +74,13 @@ def update_expiration_dates(path, role, interval, keystore, scheme, start_date, if not len(role): print("Specify at least one role") return - update_metadata_expiration_date(path, role, interval, keystore, - scheme, start_date, no_commit) + update_metadata_expiration_date( + path=path, + roles=role, + interval=interval, + keystore=keystore, + scheme=scheme, + start_date=start_date, + no_commit=no_commit, + prompt_for_keys=prompt_for_keys + ) diff --git a/taf/tools/repo/__init__.py b/taf/tools/repo/__init__.py index 84ececcb..bcfcc1ef 100644 --- a/taf/tools/repo/__init__.py +++ b/taf/tools/repo/__init__.py @@ -1,6 +1,8 @@ import click import json from taf.api.repository import create_repository +from taf.exceptions import TAFError +from taf.tools.cli import catch_cli_exception from taf.updater.updater import update_repository, validate_repository, UpdateType @@ -11,6 +13,7 @@ def repo(): pass @repo.command() + @catch_cli_exception(handle=TAFError) @click.option("--path", default=".", help="Authentication repository's location. If not specified, set to the current directory") @click.option("--keys-description", help="A dictionary containing information about the " "keys or a path to a json file which stores the needed information") @@ -62,7 +65,13 @@ def create(path, keys_description, keystore, commit, test): If the test flag is set, a special target file will be created. This means that when calling the updater, it'll be necessary to use the --authenticate-test-repo flag. """ - create_repository(path, keystore, keys_description, commit, test) + create_repository( + path=path, + keystore=keystore, + roles_key_infos=keys_description, + commit=commit, + test=test, + ) @repo.command() @click.option("--path", default=".", help="Authentication repository's location. If not specified, set to the current directory") diff --git a/taf/tools/roles/__init__.py b/taf/tools/roles/__init__.py index 75ff2939..5323e696 100644 --- a/taf/tools/roles/__init__.py +++ b/taf/tools/roles/__init__.py @@ -2,6 +2,8 @@ from taf.api.roles import add_role, add_roles, remove_role, add_signing_key as add_roles_signing_key from taf.constants import DEFAULT_RSA_SIGNATURE_SCHEME +from taf.exceptions import TAFError +from taf.tools.cli import catch_cli_exception def attach_to_group(group): @@ -11,6 +13,7 @@ def roles(): pass @roles.command() + @catch_cli_exception(handle=TAFError) @click.argument("role") @click.option("--path", default=".", help="Authentication repository's location. If not specified, set to the current directory") @click.option("--parent-role", default="targets", help="Parent targets role of this role. Defaults to targets") @@ -21,7 +24,9 @@ def roles(): "role whose signatures are required in order to consider a file as being properly signed by that role") @click.option("--yubikey", is_flag=True, default=None, help="A flag determining if the new role should be signed using a Yubikey") @click.option("--scheme", default=DEFAULT_RSA_SIGNATURE_SCHEME, help="A signature scheme used for signing") - def add(role, path, parent_role, delegated_path, keystore, keys_number, threshold, yubikey, scheme): + @click.option("--prompt-for-keys", is_flag=True, default=False, help="Whether to ask the user to enter their key if not " + "located inside the keystore directory") + def add(role, path, parent_role, delegated_path, keystore, keys_number, threshold, yubikey, scheme, prompt_for_keys): """Add a new delegated target role, specifying which paths are delegated to the new role. Its parent role, number of signing keys and signatures threshold can also be defined. Update and sign all metadata files and commit. @@ -30,15 +35,29 @@ def add(role, path, parent_role, delegated_path, keystore, keys_number, threshol print("Specify at least one path") return - add_role(path, role, parent_role, delegated_path, keys_number, threshold, yubikey, keystore, scheme) + add_role( + path=path, + role=role, + parent_role=parent_role, + paths=delegated_path, + keys_number=keys_number, + threshold=threshold, + yubikey=yubikey, + keystore=keystore, + scheme=scheme, + prompt_for_keys=prompt_for_keys + ) @roles.command() + @catch_cli_exception(handle=TAFError) @click.option("--path", default=".", help="Authentication repository's location. If not specified, set to the current directory") @click.argument("keys-description") @click.option("--keystore", default=None, help="Location of the keystore files") @click.option("--scheme", default=DEFAULT_RSA_SIGNATURE_SCHEME, help="A signature scheme " "used for signing") - def add_multiple(path, keystore, keys_description, scheme): + @click.option("--prompt-for-keys", is_flag=True, default=False, help="Whether to ask the user to enter their key if not " + "located inside the keystore directory") + def add_multiple(path, keystore, keys_description, scheme, prompt_for_keys): """Add one or more target roles. Information about the roles can be provided through a dictionary - either specified directly or contained by a .json file whose path is specified when calling this command. This allows @@ -69,9 +88,16 @@ def add_multiple(path, keystore, keys_description, scheme): "keystore": "keystore_path" } """ - add_roles(path, keystore, keys_description, scheme) + add_roles( + path=path, + keystore=keystore, + roles_key_infos=keys_description, + scheme=scheme, + prompt_for_keys=prompt_for_keys + ) @roles.command() + @catch_cli_exception(handle=TAFError) @click.argument("role") @click.option("--path", default=".", help="Authentication repository's location. If not specified, set to the current directory") @click.option("--keystore", default=None, help="Location of the keystore files") @@ -79,15 +105,26 @@ def add_multiple(path, keystore, keys_description, scheme): "used for signing") @click.option("--remove-targets/--no-remove-targets", default=True, help="Should targets delegated to this " "role also be removed. If not removed, they are signed by the parent role") - def remove(role, path, keystore, scheme, remove_targets): + @click.option("--prompt-for-keys", is_flag=True, default=False, help="Whether to ask the user to enter their key if not " + "located inside the keystore directory",) + def remove(role, path, keystore, scheme, remove_targets, prompt_for_keys): """Remove a delegated target role, and, optionally, its targets (depending on the remove-targets parameter). If targets should also be deleted, target files are remove and their corresponding entires are removed from repositoires.json. If targets should not get removed, the target files are signed using the removed role's parent role """ - remove_role(path, role, keystore, scheme=scheme, remove_targets=remove_targets, commit=True) + remove_role( + path=path, + role=role, + keystore=keystore, + scheme=scheme, + remove_targets=remove_targets, + commit=True, + prompt_for_keys=prompt_for_keys, + ) @roles.command() + @catch_cli_exception(handle=TAFError) @click.argument("path") @click.option("--role", multiple=True, help="A list of roles to whose list of signing keys " "the new key should be added") @@ -98,7 +135,9 @@ def remove(role, path, keystore, scheme, remove_targets): "keys or a path to a json file which stores the needed information") @click.option("--scheme", default=DEFAULT_RSA_SIGNATURE_SCHEME, help="A signature scheme " "used for signing") - def add_signing_key(path, role, pub_key_path, keystore, keys_description, scheme): + @click.option("--prompt-for-keys", is_flag=True, default=False, help="Whether to ask the user to enter their key if not " + "located inside the keystore directory") + def add_signing_key(path, role, pub_key_path, keystore, keys_description, scheme, prompt_for_keys): """ Add a new signing key. This will make it possible to a sign metadata files corresponding to the specified roles with another key. Although private keys are @@ -111,5 +150,12 @@ def add_signing_key(path, role, pub_key_path, keystore, keys_description, scheme if not len(role): print("Specify at least one role") return - add_roles_signing_key(path, role, pub_key_path, keystore, - keys_description, scheme) + add_roles_signing_key( + path=path, + roles=role, + pub_key_path=pub_key_path, + keystore=keystore, + roles_key_infos=keys_description, + scheme=scheme, + prompt_for_keys=prompt_for_keys + ) diff --git a/taf/tools/targets/__init__.py b/taf/tools/targets/__init__.py index c2c6b372..c18ca64f 100644 --- a/taf/tools/targets/__init__.py +++ b/taf/tools/targets/__init__.py @@ -10,6 +10,7 @@ ) from taf.constants import DEFAULT_RSA_SIGNATURE_SCHEME from taf.exceptions import TAFError +from taf.tools.cli import catch_cli_exception def attach_to_group(group): @@ -22,14 +23,17 @@ def targets(): ignore_unknown_options=True, allow_extra_args=True, )) + @catch_cli_exception(handle=TAFError) @click.option("--path", default=".", help="Authentication repository's location. If not specified, set to the current directory") @click.option("--target-name", default=None, help="Namespace prefixed name of the target repository") @click.option("--target-path", default=None, help="Target repository's filesystem path") @click.option("--role", default="targets", help="Signing role of the corresponding target file. " "Can be a new role, in which case it will be necessary to enter its information when prompted") @click.option("--keystore", default=None, help="Location of the keystore files") + @click.option("--prompt-for-keys", is_flag=True, default=False, help="Whether to ask the user to enter their key if not " + "located inside the keystore directory") @click.pass_context - def add_repo(ctx, path, target_path, target_name, role, keystore): + def add_repo(ctx, path, target_path, target_name, role, keystore, prompt_for_keys): """Add a new repository by adding it to repositories.json, creating a delegation (if targets is not its signing role) and adding and signing initial target files if the repository is found on the filesystem. All additional information that should be saved as the repository's custom content in `repositories.json` @@ -56,7 +60,8 @@ def add_repo(ctx, path, target_path, target_name, role, keystore): library_dir=None, role=role, keystore=keystore, - custom=custom + custom=custom, + prompt_for_keys=prompt_for_keys, ) @targets.command() @@ -102,22 +107,33 @@ def list(path, library_dir): list_targets(path, library_dir) @targets.command() + @catch_cli_exception(handle=TAFError) @click.option("--path", default=".", help="Authentication repository's location. If not specified, set to the current directory") @click.argument("target-name") @click.option("--keystore", default=None, help="Location of the keystore files") - def remove_repo(path, target_name, keystore): + @click.option("--prompt-for-keys", is_flag=True, default=False, help="Whether to ask the user to enter their key if not " + "located inside the keystore directory") + def remove_repo(path, target_name, keystore, prompt_for_keys): """Remove a target repository (from repsoitories.json and target file) and sign """ - remove_target_repo(path, target_name, keystore) + remove_target_repo( + path=path, + target_name=target_name, + keystore=keystore, + prompt_for_keys=prompt_for_keys, + ) @targets.command() + @catch_cli_exception(handle=TAFError) @click.option("--path", default=".", help="Authentication repository's location. If not specified, set to the current directory") @click.option("--keystore", default=None, help="Location of the keystore files") @click.option("--keys-description", help="A dictionary containing information about the " "keys or a path to a json file which stores the needed information") @click.option("--scheme", default=DEFAULT_RSA_SIGNATURE_SCHEME, help="A signature scheme " "used for signing") - def sign(path, keystore, keys_description, scheme): + @click.option("--prompt-for-keys", is_flag=True, default=False, help="Whether to ask the user to enter their key if not " + "located inside the keystore directory") + def sign(path, keystore, keys_description, scheme, prompt_for_keys): """ Register and sign target files. This means that all targets metadata files corresponding to roles responsible for updated target files are updated. Once the targets @@ -127,15 +143,21 @@ def sign(path, keystore, keys_description, scheme): by manually entering the key or by using a Yubikey. """ try: - register_target_files(path, keystore=keystore, - roles_key_infos=keys_description, - scheme=scheme, write=True) + register_target_files( + path=path, + keystore=keystore, + roles_key_infos=keys_description, + scheme=scheme, + write=True, + prompt_for_keys=prompt_for_keys + ) except TAFError as e: click.echo() click.echo(str(e)) click.echo() @targets.command() + @catch_cli_exception(handle=TAFError) @click.option("--path", default=".", help="Authentication repository's location. If not specified, set to the current directory") @click.option("--library-dir", default=None, help="Directory where target repositories and, " "optionally, authentication repository are located. If omitted it is " @@ -149,7 +171,9 @@ def sign(path, keystore, keys_description, scheme): "keys or a path to a json file which stores the needed information") @click.option("--scheme", default=DEFAULT_RSA_SIGNATURE_SCHEME, help="A signature scheme " "used for signing") - def update_and_sign(path, library_dir, target_type, keystore, keys_description, scheme): + @click.option("--prompt-for-keys", is_flag=True, default=False, help="Whether to ask the user to enter their key if not " + "located inside the keystore directory") + def update_and_sign(path, library_dir, target_type, keystore, keys_description, scheme, prompt_for_keys): """ Update target files corresponding to target repositories specified through the target type parameter by writing the current top commit and branch name to the target files. Sign the updated files @@ -181,10 +205,18 @@ def update_and_sign(path, library_dir, target_type, keystore, keys_description, target_type, keystore=keystore, roles_key_infos=keys_description, - scheme=scheme) + scheme=scheme, + prompt_for_keys=prompt_for_keys + ) else: update_target_repos_from_repositories_json( - path, library_dir, add_branch=True, keystore=keystore, scheme=scheme) + path, + library_dir, + add_branch=True, + keystore=keystore, + scheme=scheme, + prompt_for_keys=prompt_for_keys + ) except TAFError as e: click.echo() click.echo(str(e))