From dae535e31cf373cb1d65b49da7b3bf2178815e80 Mon Sep 17 00:00:00 2001 From: Daniel Elero Date: Tue, 28 Jan 2020 11:25:52 +0100 Subject: [PATCH] Minor CLI fixes --- CHANGELOG.md | 2 +- README.md | 8 ++- taf-complete.sh | 8 +++ taf/auth_repo.py | 8 +-- taf/developer_tool.py | 6 +-- taf/git.py | 2 +- taf/tools/keystore/__init__.py | 31 ++++++------ taf/tools/repo/__init__.py | 92 +++++++++++++++++----------------- taf/tools/targets/__init__.py | 14 +++--- taf/tools/yubikey/__init__.py | 2 +- 10 files changed, 95 insertions(+), 78 deletions(-) create mode 100644 taf-complete.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f512b989..77076ee67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning][semver]. ### Changed -- Separated commands into subcommands +- Separated commands into sub-commands ([96]) - Use `root-dir` and `namespace` instead of `target-dir` ([96]) ### Fixed diff --git a/README.md b/README.md index 1168677de..55aa98a64 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,12 @@ Install extra dependencies when using _Yubikey_: pip install taf[yubikey] ``` +Add bash completion: + +1. copy `taf-complete.sh` to user's directory +1. add `source ./taf-complete.sh` to `~/.bash_profile` or `~/.bashrc` +1. source `~/.bash_profile` + ## Development Setup We are using [pre-commit](https://pre-commit.com/) to run _black_ code formatter, _flake8_ and _bandit_ code quality checks. @@ -70,7 +76,7 @@ pytest To run tests with real Yubikey: 1. Insert **test** Yubikey -2. Run `taf setup_test_yubikey` +2. Run `taf setup_test_key` WARNING: This command will import targets private key to signature slot of your Yubikey, as well as new self-signed x509 certificate! 3. Run `REAL_YK=True pytest` or `set REAL_YK=True pytest` depending on platform. diff --git a/taf-complete.sh b/taf-complete.sh new file mode 100644 index 000000000..b7bc855d0 --- /dev/null +++ b/taf-complete.sh @@ -0,0 +1,8 @@ +_taf_completion() { + COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \ + COMP_CWORD=$COMP_CWORD \ + _TAF_COMPLETE=complete $1 ) ) + return 0 +} + +complete -F _taf_completion -o default taf; diff --git a/taf/auth_repo.py b/taf/auth_repo.py index 8a1e192df..6d9921421 100644 --- a/taf/auth_repo.py +++ b/taf/auth_repo.py @@ -174,8 +174,8 @@ def __init__( default_branch="master", ): super().__init__(repo_path, repo_urls, additional_info, default_branch) - self.targets_path = targets_path - self.metadata_path = metadata_path + self.targets_path = str(Path(self.repo_path, targets_path)) + self.metadata_path = str(Path(self.repo_path, metadata_path)) class NamedAuthenticationRepo(AuthRepoMixin, NamedGitRepository): @@ -193,5 +193,5 @@ def __init__( super().__init__( root_dir, repo_name, repo_urls, additional_info, default_branch ) - self.targets_path = targets_path - self.metadata_path = metadata_path + self.targets_path = str(Path(self.repo_path, targets_path)) + self.metadata_path = str(Path(self.repo_path, metadata_path)) diff --git a/taf/developer_tool.py b/taf/developer_tool.py index 3985f6bd0..a120ffbbd 100644 --- a/taf/developer_tool.py +++ b/taf/developer_tool.py @@ -351,7 +351,7 @@ def create_repository( return tuf.repository_tool.METADATA_STAGED_DIRECTORY_NAME = METADATA_DIRECTORY_NAME - repository = create_new_repository(repo_path) + repository = create_new_repository(repo.repo_path) def _register_roles_keys(role_name, key_info, repository): num_of_keys = key_info.get("number", 1) @@ -396,8 +396,7 @@ def _register_roles_keys(role_name, key_info, repository): # if the repository is a test repository, add a target file called test-auth-repo if test: - target_paths = Path(repo_path) / "targets" - test_auth_file = target_paths / "test-auth-repo" + test_auth_file = Path(repo.targets_path) / "test-auth-repo" test_auth_file.touch() targets_obj = _role_obj("targets", repository) targets_obj.add_target(str(test_auth_file)) @@ -480,6 +479,7 @@ def export_yk_certificate(certs_dir, key): def _get_namespace_and_root(repo_path, namespace, root_dir): + repo_path = Path(repo_path) if namespace is None: namespace = repo_path.parent.name if root_dir is None: diff --git a/taf/git.py b/taf/git.py index 8537db8c5..bb401363a 100644 --- a/taf/git.py +++ b/taf/git.py @@ -24,7 +24,7 @@ def __init__( additional_info: a dictionary containing other data (optional) default_branch: repository's default branch """ - self.repo_path = str(repo_path) + self.repo_path = str(Path(repo_path).resolve()) self.default_branch = default_branch if repo_urls is not None: if settings.update_from_filesystem is False: diff --git a/taf/tools/keystore/__init__.py b/taf/tools/keystore/__init__.py index e673b1b84..2622ddf02 100644 --- a/taf/tools/keystore/__init__.py +++ b/taf/tools/keystore/__init__.py @@ -16,21 +16,22 @@ def generate(path, keys_description): It is necessary to either directly specify this dictionary when calling this command or to provide a path to a `.json` file which contains the needed information. - Keys description example: \n - {\n - "root": {\n - "number": 3,\n - "length": 2048,\n - "passwords": ["password1", "password2", "password3"]\n - "threshold": 2,\n - },\n - "targets": {\n - "length": 2048\n - },\n - "snapshot": {},\n - "timestamp": {}\n - }\n + \b + Keys description example: + { + "root": { + "number": 3, + "length": 2048, + "passwords": ["password1", "password2", "password3"], + "threshold": 2 + }, + "targets": { + "length": 2048 + }, + "snapshot": {}, + "timestamp": {} + } - Default number of keys and threshold are 1, length 3072 and password is an emtpy string + Default number of keys and threshold are 1, length 3072 and password is an empty string. """ developer_tool.generate_keys(path, keys_description) diff --git a/taf/tools/repo/__init__.py b/taf/tools/repo/__init__.py index 3845cedd2..ff5771023 100644 --- a/taf/tools/repo/__init__.py +++ b/taf/tools/repo/__init__.py @@ -15,37 +15,39 @@ def repo(): @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("--keystore", default=None, help="Location of the keystore files") - @click.option("--commit", is_flag=True, help="Indicates if the changes should be " + @click.option("--commit", is_flag=True, default=False, help="Indicates if the changes should be " "committed automatically") @click.option("--test", is_flag=True, default=False, help="Indicates if the created repository " "is a test authentication repository") def create(path, keys_description, keystore, commit, test): """ + \b Create a new authentication repository at the specified location by registering signing keys and generating initial metadata files. 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 - definition of: \n - - total number of keys per role \n - - threshold of signatures per role \n - - should keys of a role be on Yubikeys or should keystore files be used \n - - scheme (the default scheme is rsa-pkcs1v15-sha256) \n - - For example:\n - {\n - "root": {\n - "number": 3,\n - "length": 2048,\n - "passwords": ["password1", "password2", "password3"]\n - "threshold": 2,\n - "yubikey": true\n - },\n - "targets": {\n - "length": 2048\n - },\n - "snapshot": {},\n - "timestamp": {}\n - }\n + definition of: + - total number of keys per role + - threshold of signatures per role + - should keys of a role be on Yubikeys or should keystore files be used + - scheme (the default scheme is rsa-pkcs1v15-sha256) + + \b + For example: + { + "root": { + "number": 3, + "length": 2048, + "passwords": ["password1", "password2", "password3"], + "threshold": 2, + "yubikey": true + }, + "targets": { + "length": 2048 + }, + "snapshot": {}, + "timestamp": {} + } In cases when this dictionary is not specified, it is necessary to enter the needed information when asked to do so, or confirm that default values should be used. @@ -63,7 +65,7 @@ def create(path, keys_description, keystore, commit, test): @click.option("--root-dir", default=None, help="Directory where target repositories and, " "optionally, authentication repository are located. If omitted it is " "calculated based on authentication repository's path. " - "Authentication repo is persumed to be at root-dir/namespace/auth-repo-name") + "Authentication repo is presumed to be at root-dir/namespace/auth-repo-name") @click.option("--namespace", default=None, help="Namespace of the target repositories. " "If omitted, it will be assumed that namespace matches the name of the " "directory which contains the authentication repository") @@ -85,7 +87,7 @@ def generate_repositories_json(path, root_dir, namespace, targets_rel_dir, custo and namespace is namespace1, target repositories should be in E:\\examples\\root\\namespace1. If the authentication repository and the target repositories are in the same root directory and the authentication repository is also directly inside a namespace directory, then the common root - directory is calculated as two repositories up from the authetication repository's directory. + directory is calculated as two repositories up from the authentication repository's directory. Authentication repository's namespace can, but does not have to be equal to the namespace of target, repositories. If the authentication repository's path is E:\\root\\namespace\\auth-repo, root directory will be determined as E:\\root. If this default value is not correct, it can be redefined @@ -105,14 +107,15 @@ def generate_repositories_json(path, root_dir, namespace, targets_rel_dir, custo dictionary are names of the repositories whose custom data should be set and values are custom data dictionaries. For example: - {\n - "test/html-repo": {\n - "type": "html"\n + \b + { + "test/html-repo": { + "type": "html" }, - "test/xml-repo": {\n - "type": "xml"\n - }\n - }\n + "test/xml-repo": { + "type": "xml" + } + } Note: this command does not automatically register repositories.json as a target file. It is recommended that the content of the file is reviewed before doing so manually. @@ -120,16 +123,14 @@ def generate_repositories_json(path, root_dir, namespace, targets_rel_dir, custo developer_tool.generate_repositories_json(path, root_dir, namespace, targets_rel_dir, custom) @repo.command() - @click.argument('path') + @click.argument("path") @click.option("--root-dir", default=None, help="Directory where target repositories and, " "optionally, authentication repository are located. If omitted it is " "calculated based on authentication repository's path. " - "Authentication repo is persumed to be at root-dir/namespace/auth-repo-name") + "Authentication repo is presumed to be at root-dir/namespace/auth-repo-name") @click.option("--namespace", default=None, help="Namespace of the target repositories. " "If omitted, it will be assumed that namespace matches the name of the " "directory which contains the authentication repository") - @click.option('--targets-rel-dir', default=None, help=' Directory relative to which urls ' - 'of the target repositories are set, if they do not have remote set') @click.option("--targets-rel-dir", default=None, help="Directory relative to which " "urls of the target repositories are calculated. Only useful when " "the target repositories do not have remotes set") @@ -144,17 +145,18 @@ def generate_repositories_json(path, root_dir, namespace, targets_rel_dir, custo "committed automatically") @click.option("--test", is_flag=True, default=False, help="Indicates if the created repository " "is a test authentication repository") - @click.option('--scheme', default=DEFAULT_RSA_SIGNATURE_SCHEME, help='A signature scheme used for signing.') + @click.option("--scheme", default=DEFAULT_RSA_SIGNATURE_SCHEME, help="A signature scheme used for signing.") def initialize(path, root_dir, namespace, targets_rel_dir, custom, add_branch, keystore, keys_description, commit, test, scheme): """ - Create and initialize a new authentication repository:\n - 1. Crete an authentication repository (generate initial metadata files)\n - 2. Commit initial metadata files if commit == True\n - 3. Add target files corresponding to target repositories\n - 4. Generate repositories.json\n - 5. Update metadata files\n - 6. Commit the changes if commit == True\n + \b + Create and initialize a new authentication repository: + 1. Create an authentication repository (generate initial metadata files) + 2. Commit initial metadata files if commit == True + 3. Add target files corresponding to target repositories + 4. Generate repositories.json + 5. Update metadata files + 6. Commit the changes if commit == True Combines create, generate_repositories_json, update_repos and targets sign commands. In order to have greater control over individual steps and be able to review files created in the initialization process, execute the mentioned commands separately. @@ -168,7 +170,7 @@ def initialize(path, root_dir, namespace, targets_rel_dir, custom, add_branch, k @click.option("--clients-root-dir", default=None, help="Directory where target repositories and, " "optionally, authentication repository are located. If omitted it is " "calculated based on authentication repository's path. " - "Authentication repo is persumed to be at root-dir/namespace/auth-repo-name") + "Authentication repo is presumed to be at root-dir/namespace/auth-repo-name") @click.option("--from-fs", is_flag=True, default=False, help="Indicates if the we want to clone a " "repository from the filesystem") @click.option("--authenticate-test-repo", is_flag=True, help="Indicates that the authentication " @@ -183,7 +185,7 @@ def update(url, clients_auth_path, clients_root_dir, from_fs, authenticate_test_ --clients-root-dir option. This means that if authentication repository's path is E:\\root\\namespace\\auth-repo, it will be assumed that E:\\root is the root direcotry if clients-root-dir is not specified. - Names of target repositories (as defined in repositories.json) are appened to the root repository's + Names of target repositories (as defined in repositories.json) are appended to the root repository's path thus defining the location of each target repository. If names of target repositories are namespace/repo1, namespace/repo2 etc and the root directory is E:\\root, path of target repositories will be calculated as E:\\root\\namespace\\repo1, E:\\root\\namespace\\root2 etc. diff --git a/taf/tools/targets/__init__.py b/taf/tools/targets/__init__.py index b6fa39ec9..8e21b2e17 100644 --- a/taf/tools/targets/__init__.py +++ b/taf/tools/targets/__init__.py @@ -18,7 +18,7 @@ def sign(path, keystore, scheme): """ Register and sign target files. This means that the targets metadata file is updated by adding or editing information about all target files located - inside the targets directory of the authenticatino repository. Once the targets + inside the targets directory of the authentication repository. Once the targets file is updated, so are snapshot and timestamp. All files are signed. If the keystore parameter is provided, keys stored in that directory will be used for signing. If a needed key is not in that directory, the file can either be signed @@ -31,7 +31,7 @@ def sign(path, keystore, scheme): @click.option("--root-dir", default=None, help="Directory where target repositories and, " "optionally, authentication repository are located. If omitted it is " "calculated based on authentication repository's path. " - "Authentication repo is persumed to be at root-dir/namespace/auth-repo-name") + "Authentication repo is presumed to be at root-dir/namespace/auth-repo-name") @click.option("--namespace", default=None, help="Namespace of the target repositories. " "If omitted, it will be assumed that namespace matches the name of the " "directory which contains the authentication repository") @@ -39,7 +39,7 @@ def sign(path, keystore, scheme): "the current branch to target files") def update_repos_from_fs(path, root_dir, namespace, add_branch): """ - Update target files corresonding to target repositories by traversing through the root + Update target files corresponding to target repositories by traversing through the root directory. Does not automatically sign the metadata files. Note: if repositories.json exists, it is better to call update_repos_from_repositories_json @@ -48,7 +48,7 @@ def update_repos_from_fs(path, root_dir, namespace, add_branch): and namespace is namespace1, target repositories should be in E:\\examples\\root\\namespace1. If the authentication repository and the target repositories are in the same root directory and the authentication repository is also directly inside a namespace directory, then the common root - directory is calculated as two repositories up from the authetication repository's directory. + directory is calculated as two repositories up from the authentication repository's directory. Authentication repository's namespace can, but does not have to be equal to the namespace of target, repositories. If the authentication repository's path is E:\\root\\namespace\\auth-repo, root directory will be determined as E:\\root. If this default value is not correct, it can be redefined @@ -72,7 +72,7 @@ def update_repos_from_fs(path, root_dir, namespace, add_branch): @click.option("--root-dir", default=None, help="Directory where target repositories and, " "optionally, authentication repository are located. If omitted it is " "calculated based on authentication repository's path. " - "Authentication repo is persumed to be at root-dir/namespace/auth-repo-name") + "Authentication repo is presumed to be at root-dir/namespace/auth-repo-name") @click.option("--namespace", default=None, help="Namespace of the target repositories. " "If omitted, it will be assumed that namespace matches the name of the " "directory which contains the authentication repository") @@ -80,7 +80,7 @@ def update_repos_from_fs(path, root_dir, namespace, add_branch): "the current branch to target files") def update_repos_from_repositories_json(path, root_dir, namespace, add_branch): """ - Update target files corresonding to target repositories by traversing through repositories + Update target files corresponding to target repositories by traversing through repositories specified in repositories.json which are located inside the specified targets directory without signing the metadata files. @@ -89,7 +89,7 @@ def update_repos_from_repositories_json(path, root_dir, namespace, add_branch): and namespace is namespace1, target repositories should be in E:\\examples\\root\\namespace1. If the authentication repository and the target repositories are in the same root directory and the authentication repository is also directly inside a namespace directory, then the common root - directory is calculated as two repositories up from the authetication repository's directory. + directory is calculated as two repositories up from the authentication repository's directory. Authentication repository's namespace can, but does not have to be equal to the namespace of target, repositories. If the authentication repository's path is E:\\root\\namespace\\auth-repo, root directory will be determined as E:\\root. If this default value is not correct, it can be redefined diff --git a/taf/tools/yubikey/__init__.py b/taf/tools/yubikey/__init__.py index 46a85da96..f670ff062 100644 --- a/taf/tools/yubikey/__init__.py +++ b/taf/tools/yubikey/__init__.py @@ -29,7 +29,7 @@ def setup_signing_key(certs_dir): @yubikey.command() @click.argument("key-path") - def setup_test_yubikey(key_path): + def setup_test_key(key_path): """Copies the specified key onto the inserted YubiKey WARNING - this will reset the inserted key.""" developer_tool.setup_test_yubikey(key_path)