From 2fa6a1a7bd05fc9816ca488bf1a3ccad4061263c Mon Sep 17 00:00:00 2001 From: "Ian H. Pittwood" Date: Mon, 6 May 2019 11:30:59 -0500 Subject: [PATCH] Allow decryption of directories This change allows users to specify a directory or file to be decrypted. Allows directory decryption. Adds flag to overwrite encrypted file with decrypted data. Intelligently recognizes paths vs files in CLI input and outputs data accordingly. Change-Id: I0d5e77f0eb1adb42165aa9b214aa90a0db0a3131 --- doc/source/cli/cli.rst | 22 +++++++++------- pegleg/cli.py | 42 ++++++++++++++++++++++--------- pegleg/engine/secrets.py | 33 +++++++++++++++++------- tests/unit/engine/test_secrets.py | 9 +++---- tests/unit/test_cli.py | 4 +-- 5 files changed, 72 insertions(+), 38 deletions(-) diff --git a/doc/source/cli/cli.rst b/doc/source/cli/cli.rst index 2f4401e6..d950b9b8 100644 --- a/doc/source/cli/cli.rst +++ b/doc/source/cli/cli.rst @@ -662,9 +662,9 @@ Example without optional save location: Decrypt ^^^^^^^ -Unwrap an encrypted secrets document from a `Pegleg Managed Documents`_, -decrypt the encrypted secrets, and dump the cleartext secrets file to -``stdout``. +Unwrap one or more encrypted secrets document from +`Pegleg Managed Documents`_, decrypt the encrypted secrets, and dump the +cleartext to stdout or a specified location. **site_name** (Required). @@ -673,21 +673,25 @@ repository folder structure. This is used to ensure the correct revision of the site and global repositories are used, as specified in the site's :file:`site-definition.yaml`. -**-f / filename** (Required). +**-p / --path** (Required). -The absolute path to the pegleg managed encrypted secrets file. +Path to pegleg managed encrypted secrets file or directory of files. +**-s / --save-location** (Optional). -**-s / save-location** (Optional). +The desired output path for the decrypted file. If not specified, decrypted +data will output to stdout. -The desired output path for the decrypted file. If not specified, it will be -printed to stdout. +**-o / --overwrite** (Optional). False by default. + +When set, encrypted file(s) at the specified path will be overwritten with +the decrypted data. Overrides ``--save-location`` option. Usage: :: - ./pegleg.sh site secrets decrypt -f + ./pegleg.sh site secrets decrypt -p [-s ] Examples diff --git a/pegleg/cli.py b/pegleg/cli.py index 685e8fef..02d11cd1 100644 --- a/pegleg/cli.py +++ b/pegleg/cli.py @@ -679,27 +679,45 @@ def encrypt(*, save_location, author, site_name): help='Command to unwrap and decrypt one site ' 'secrets document and print it to stdout.') @click.option( - '-f', - '--filename', - 'file_name', - help='The file to decrypt') + '-p', + '--path', + 'path', + type=click.Path(exists=True, readable=True), + required=True, + help='The file or directory path to decrypt.') @click.option( '-s', '--save-location', 'save_location', default=None, - help='The destination where the decrypted file should be saved. ' - 'If not specified, it will be printed to stdout.') + help='The destination where the decrypted file(s) should be saved. ' + 'If not specified, decrypted data will output to stdout.') +@click.option( + '-o', + '--overwrite', + 'overwrite', + is_flag=True, + default=False, + help='Overwrites original file(s) at path with decrypted data when set. ' + 'Overrides --save-location option.') @click.argument('site_name') -def decrypt(*, file_name, save_location, site_name): +def decrypt(*, path, save_location, overwrite, site_name): engine.repository.process_repositories(site_name) - decrypted = engine.secrets.decrypt(file_name) - if save_location is None: - click.echo(decrypted) + decrypted = engine.secrets.decrypt(path) + if overwrite: + for key, value in decrypted.items(): + files.write(key, value) + os.chmod(key, 0o600) + elif save_location is None: + for value in decrypted.values(): + click.echo(value) else: - files.write(save_location, decrypted) - os.chmod(save_location, 0o600) + for key, value in decrypted.items(): + file_name = os.path.split(key) + file_save_location = os.path.join(save_location, file_name) + files.write(file_save_location, value) + os.chmod(file_save_location, 0o600) @main.group(help="Miscellaneous generate commands") diff --git a/pegleg/engine/secrets.py b/pegleg/engine/secrets.py index af22e484..349cd7cd 100644 --- a/pegleg/engine/secrets.py +++ b/pegleg/engine/secrets.py @@ -12,11 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +from glob import glob import logging import os -import yaml from prettytable import PrettyTable +import yaml from pegleg.engine.catalog.pki_utility import PKIUtility from pegleg.engine.generators.passphrase_generator import PassphraseGenerator @@ -68,7 +69,7 @@ def encrypt(save_location, author, site_name): 'No secret documents were found for site: {}'.format(site_name)) -def decrypt(file_path): +def decrypt(path): """Decrypt one secrets file, and print the decrypted file to standard out. Search the specified file_path for a file. @@ -77,17 +78,31 @@ def decrypt(file_path): If the file is found, but it is not encrypted, print the contents of the file to standard out. Passphrase and salt for the decryption are read from environment variables. - :param file_path: Path to the file to be unwrapped and decrypted. - :type file_path: string + :param path: Path to the file to be unwrapped and decrypted. + :type path: string :return: The decrypted secrets - :rtype: list + :rtype: dict """ LOG.info('Started decrypting...') - if os.path.isfile(file_path): - return PeglegSecretManagement(file_path).decrypt_secrets() + file_dict = {} + + if not os.path.exists(path): + LOG.error('Path: {} was not found. Check your path and site name, ' + 'and try again.'.format(path)) + return file_dict + + if os.path.isfile(path): + file_dict[path] = PeglegSecretManagement(path).decrypt_secrets() else: - LOG.info('File: {} was not found. Check your file path and name, ' - 'and try again.'.format(file_path)) + match = os.path.join(path, '**', '*.yaml') + file_list = glob(match, recursive=True) + if not file_list: + LOG.warning('No YAML files were discovered in path: {}' + .format(path)) + for file_path in file_list: + file_dict[file_path] = PeglegSecretManagement( + file_path).decrypt_secrets() + return file_dict def _get_dest_path(repo_base, file_path, save_location): diff --git a/tests/unit/engine/test_secrets.py b/tests/unit/engine/test_secrets.py index 4ef38071..520737c3 100644 --- a/tests/unit/engine/test_secrets.py +++ b/tests/unit/engine/test_secrets.py @@ -113,11 +113,10 @@ data: {0}-password encrypted_files = listdir(save_location_str) assert len(encrypted_files) > 0 - # for _file in encrypted_files: - decrypted = secrets.decrypt(str(save_location.join( - "site/cicd/secrets/passphrases/" - "cicd-passphrase-encrypted.yaml"))) - assert yaml.load(decrypted) == yaml.load(passphrase_doc) + encrypted_path = str(save_location.join("site/cicd/secrets/passphrases/" + "cicd-passphrase-encrypted.yaml")) + decrypted = secrets.decrypt(encrypted_path) + assert yaml.load(decrypted[encrypted_path]) == yaml.load(passphrase_doc) def test_pegleg_secret_management_constructor(): diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 3874fba6..0d3bffdb 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -555,9 +555,7 @@ class TestSiteSecretsActions(BaseCLIActionTest): assert "encrypted" in ceph_fsid["data"] assert "managedDocument" in ceph_fsid["data"] - relative_file_path = os.path.join("secrets", "passphrases", - "ceph_fsid.yaml") - secrets_opts = ['secrets', 'decrypt', '-f', relative_file_path, + secrets_opts = ['secrets', 'decrypt', '-p', file_path, self.site_name] result = self.runner.invoke(cli.site, ['-r', repo_path] + secrets_opts) assert result.exit_code == 0, result.output