diff --git a/doc/source/cli/cli.rst b/doc/source/cli/cli.rst index 6ed746d2..60b7a6d4 100644 --- a/doc/source/cli/cli.rst +++ b/doc/source/cli/cli.rst @@ -642,7 +642,13 @@ repository folder structure. The ``encrypt`` command looks up the whose ``encryptionPolicy`` is set to ``encrypted``), and encrypts the documents in those files. -**-a / \\-\\-author** (Required) +**-p / \\-\\-path** (Optional). + +The file or directory path to encrypt. If a path is not provided, all +applicable files discovered in the user specified repositories for +``site_name`` will be encrypted. + +**-a / \\-\\-author** (Required). Author is the identifier for the program or the person, who is encrypting the secrets documents. diff --git a/pegleg/cli/commands.py b/pegleg/cli/commands.py index a16f4b8f..34553e50 100644 --- a/pegleg/cli/commands.py +++ b/pegleg/cli/commands.py @@ -307,7 +307,7 @@ def secrets(): default=False, show_default=True, help='Force Pegleg to regenerate all PKI items.') -@click.argument('site_name') +@utils.SITE_REPOSITORY_ARGUMENT def generate_pki_deprecated(site_name, author, days, regenerate_all): """Generate certificates, certificate authorities and keypairs for a given site. @@ -361,7 +361,7 @@ def generate_pki_deprecated(site_name, author, days, regenerate_all): default=True, show_default=True, help='Whether to encrypt the wrapped file.') -@click.argument('site_name') +@utils.SITE_REPOSITORY_ARGUMENT def wrap_secret_cli( *, site_name, author, filename, output_path, schema, name, layer, encrypt): @@ -401,7 +401,7 @@ def genesis_bundle(*, build_dir, validators, site_name): 'days', default=60, help='The number of days past today to check if certificates are valid.') -@click.argument('site_name') +@utils.SITE_REPOSITORY_ARGUMENT def check_pki_certs(site_name, days): """Check PKI certificates of a site for expiration.""" expiring_certs_exist, cert_results = pegleg_main.run_check_pki_certs( @@ -496,7 +496,7 @@ def generate(): 'generated, wrapped, and encrypted passphrases files will be saved ' 'in: /site//secrets/certificates/ ' 'directory. Defaults to site repository path if no value given.') -@click.argument('site_name') +@utils.SITE_REPOSITORY_ARGUMENT def generate_pki(site_name, author, days, regenerate_all, save_location): """Generate certificates, certificate authorities and keypairs for a given site. @@ -508,7 +508,7 @@ def generate_pki(site_name, author, days, regenerate_all, save_location): @generate.command('passphrases', help='Command to generate site passphrases') -@click.argument('site_name') +@utils.SITE_REPOSITORY_ARGUMENT @click.option( '-s', '--save-location', @@ -562,11 +562,20 @@ def generate_passphrases( help='Command to encrypt and wrap site secrets ' 'documents with metadata.storagePolicy set ' 'to encrypted, in pegleg managed documents.') +@click.option( + '-p', + '--path', + 'path', + type=click.Path(exists=True, readable=True), + required=False, + help='The file or directory path to encrypt. ' + 'If path is not provided, all applicable files for the site ' + 'will be encrypted.') @click.option( '-s', '--save-location', 'save_location', - default=None, + required=True, help='Directory to output the encrypted site secrets files. Created ' 'automatically if it does not already exist. ' 'If save_location is not provided, the output encrypted files will ' @@ -578,9 +587,9 @@ def generate_passphrases( required=True, help='Identifier for the program or person who is encrypting the secrets ' 'documents') -@click.argument('site_name') -def encrypt(*, save_location, author, site_name): - pegleg_main.run_encrypt(author, save_location, site_name) +@utils.SITE_REPOSITORY_ARGUMENT +def encrypt(*, path, save_location, author, site_name): + pegleg_main.run_encrypt(author, save_location, site_name, path=path) @secrets.command( @@ -608,7 +617,7 @@ def encrypt(*, save_location, author, site_name): default=False, help='Overwrites original file(s) at path with decrypted data when set. ' 'Overrides --save-location option.') -@click.argument('site_name') +@utils.SITE_REPOSITORY_ARGUMENT def decrypt(*, path, save_location, overwrite, site_name): data = pegleg_main.run_decrypt(overwrite, path, save_location, site_name) if data: diff --git a/pegleg/engine/secrets.py b/pegleg/engine/secrets.py index 222e77c5..ce39702d 100644 --- a/pegleg/engine/secrets.py +++ b/pegleg/engine/secrets.py @@ -37,7 +37,7 @@ __all__ = ('encrypt', 'decrypt', 'generate_passphrases', 'wrap_secret') LOG = logging.getLogger(__name__) -def encrypt(save_location, author, site_name): +def encrypt(save_location, author, site_name, path=None): """ Encrypt all secrets documents for a site identifies by site_name. @@ -56,21 +56,52 @@ def encrypt(save_location, author, site_name): :param str author: Identifies the individual or application, who encrypts the secrets documents. :param str site_name: The name of the site to encrypt its secrets files. + :param str path: The path to the directory or file to be encrypted. """ files.check_file_save_location(save_location) + LOG.debug('Save location is %s', save_location) + + file_sets = [] + path_exists = path and os.path.exists(path) + if path_exists: + if os.path.isfile(path): + LOG.debug('Specified path is a file') + file_sets = [(None, path)] + elif os.path.isdir(path): + LOG.debug('Specified path is a directory') + file_sets = [] + for filename in glob(os.path.join(path, '**/*.yaml'), + recursive=True): + LOG.debug('Discovered %s', filename) + file_sets.append((None, filename)) + else: + LOG.debug('No path specified, searching all repos') + file_sets = list(definition.site_files_by_repo(site_name)) + LOG.info('Started encrypting...') secrets_found = False - for repo_base, file_path in definition.site_files_by_repo(site_name): + for repo_base, file_path in file_sets: + LOG.debug('Looking at %s in %s repo', file_path, repo_base) secrets_found = True - PeglegSecretManagement( - file_path=file_path, author=author, - site_name=site_name).encrypt_secrets( - _get_dest_path(repo_base, file_path, save_location)) + secret = PeglegSecretManagement( + file_path=file_path, author=author, site_name=site_name) + if path_exists: + if save_location: + output_path = os.path.join( + save_location.rstrip(os.path.sep), + file_path.lstrip(os.path.sep)) + else: + output_path = file_path + else: + output_path = _get_dest_path(repo_base, file_path, save_location) + LOG.debug('Outputting encrypted data to %s', output_path) + secret.encrypt_secrets(output_path) + if secrets_found: LOG.info('Encryption of all secret files was completed.') else: - LOG.warn( + LOG.warning( 'No secret documents were found for site: {}'.format(site_name)) diff --git a/pegleg/pegleg_main.py b/pegleg/pegleg_main.py index 0d8e9e00..eff9f6a8 100644 --- a/pegleg/pegleg_main.py +++ b/pegleg/pegleg_main.py @@ -368,7 +368,7 @@ def run_generate_passphrases( force_cleartext=force_cleartext) -def run_encrypt(author, save_location, site_name): +def run_encrypt(author, save_location, site_name, path=None): """Wraps and encrypts site secret documents :param author: identifies author generating new certificates for @@ -376,12 +376,14 @@ def run_encrypt(author, save_location, site_name): :param save_location: path to save encrypted documents to, if None the original documents are overwritten :param site_name: site name to process + :param path: path to the document(s) to encrypt :return: """ config.set_global_enc_keys(site_name) - if save_location is None: + if save_location is None and path is None: save_location = config.get_site_repo() - engine.secrets.encrypt(save_location, author, site_name=site_name) + engine.secrets.encrypt( + save_location, author, site_name=site_name, path=path) def run_decrypt(overwrite, path, save_location, site_name): diff --git a/tests/unit/cli/test_commands.py b/tests/unit/cli/test_commands.py index 546053a1..bf1d5c78 100644 --- a/tests/unit/cli/test_commands.py +++ b/tests/unit/cli/test_commands.py @@ -586,7 +586,10 @@ class TestSiteSecretsActions(BaseCLIActionTest): with open(file_path, "w") as ceph_fsid_fi: yaml.dump(ceph_fsid, ceph_fsid_fi) - secrets_opts = ['secrets', 'encrypt', '-a', 'test', self.site_name] + secrets_opts = [ + 'secrets', 'encrypt', '--save-location', repo_path, '-a', 'test', + self.site_name + ] result = self.runner.invoke( commands.site, ['--no-decrypt', '-r', repo_path] + secrets_opts) @@ -955,14 +958,15 @@ class TestCliSiteSubcommandsWithDecryptOption(BaseCLIActionTest): @pytest.mark.skipif( not pki_utility.PKIUtility.cfssl_exists(), reason='cfssl must be installed to execute these tests') - def test_check_pki_certs_expired_using_decrypt_option(self): + def test_check_pki_certs_expired_using_decrypt_option(self, tmpdir): repo_path = self.treasuremap_path secrets_opts = ['secrets', 'check-pki-certs', self.site_name] result = self.runner.invoke( - commands.site, ['--decrypt', '-r', repo_path] + secrets_opts) + commands.site, + ['--decrypt', '-r', repo_path, '-p', tmpdir] + secrets_opts) assert result.exit_code == 1, result.output assert self._validate_no_files_encrypted( - os.path.join(repo_path, 'site', 'seaworthy', 'secrets')) + os.path.join(tmpdir, 'site', 'seaworthy', 'secrets')) def test_genesis_bundle_using_decrypt_option(self, tmpdir): repo_path = self.treasuremap_path