Add passphrase catalog override option

Adds an option to specify a passphrase catalog to override catalogs
discovered in the site repository. This allows the generation of a
specified subset of passphrases instead of the entire site's catalog.

Change-Id: I797107234292eea8ca788b7a94ed5e2c90566bf5
This commit is contained in:
Ian H. Pittwood 2019-11-20 11:18:02 -06:00 committed by Ian Pittwood
parent 87d24d530a
commit 9163ef08ca
5 changed files with 138 additions and 9 deletions

View File

@ -949,7 +949,6 @@ catalog.
**-a / --author** (Required)
``Author`` is intended to document the application or the individual, who
generates the site passphrase documents, mostly for tracking purposes. It
is expected to be leveraged in an operator-specific manner.
@ -965,6 +964,15 @@ are placed in the following folder structure under ``save_location``:
<save_location>/site/<site_name>/secrets/passphrases/<passphrase_name.yaml>
**-c / --passphrase-catalog** (Optional).
Specifies a path for a passphrase catalog file to use instead of the catalogs
found in the repositories specified by the user. The specified catalog
will be used when this option is specified and all other discovered catalogs
will be disregarded. This can be used to specify a subset of passphrases to
generate instead of the whole catalog or for testing new passphrases before
merging them into production.
**-i / --interactive** (Optional). False by default.
Enables input prompts for "prompt: true" passphrases. Input prompts are

View File

@ -707,6 +707,15 @@ def generate_pki(site_name, author, days, regenerate_all, save_location):
required=True,
help='Identifier for the program or person who is generating the secrets '
'documents')
@click.option(
'-c',
'--passphrase-catalog',
'passphrase_catalog',
required=False,
type=click.Path(exists=True, dir_okay=False, readable=True),
help='Path to a specific passphrase catalog to generate passphrases from. '
'If not specified, defaults to use catalogs discovered in the '
'repositories.')
@click.option(
'-i',
'--interactive',
@ -722,11 +731,13 @@ def generate_pki(site_name, author, days, regenerate_all, save_location):
show_default=True,
help='Force cleartext generation of passphrases. This is not recommended.')
def generate_passphrases(
*, site_name, save_location, author, interactive, force_cleartext):
*, site_name, save_location, author, passphrase_catalog, interactive,
force_cleartext):
engine.repository.process_repositories(site_name)
config.set_global_enc_keys(site_name)
engine.secrets.generate_passphrases(
site_name, save_location, author, interactive, force_cleartext)
site_name, save_location, author, passphrase_catalog, interactive,
force_cleartext)
@secrets.command(

View File

@ -40,7 +40,12 @@ class PassphraseGenerator(BaseGenerator):
Generates passphrases for a given environment, specified in a
passphrase catalog.
"""
def __init__(self, sitename, save_location, author):
def __init__(
self,
sitename,
save_location,
author,
override_passphrase_catalog=None):
"""Constructor for ``PassphraseGenerator``.
:param str sitename: Site name for which passphrases are generated.
@ -49,11 +54,11 @@ class PassphraseGenerator(BaseGenerator):
:param str author: Identifying name of the author generating new
certificates.
"""
super(PassphraseGenerator,
self).__init__(sitename, save_location, author)
self._catalog = PassphraseCatalog(
self._sitename, documents=self._documents)
self._sitename,
documents=override_passphrase_catalog or self._documents)
def generate(self, interactive=False, force_cleartext=False):
"""

View File

@ -138,7 +138,11 @@ def _get_dest_path(repo_base, file_path, save_location):
def generate_passphrases(
site_name, save_location, author, interactive=False,
site_name,
save_location,
author,
passphrase_catalog=None,
interactive=False,
force_cleartext=False):
"""
Look for the site passphrase catalogs, and for every passphrase entry in
@ -149,12 +153,19 @@ def generate_passphrases(
:param str site_name: The site to read from
:param str save_location: Location to write files to
:param str author: Author who's generating the files
:param path-like passphrase_catalog: Path to file overriding any other
discovered passphrase catalogs
:param bool interactive: Whether to allow user input for passphrases
:param bool force_cleartext: Whether to generate results in clear text
"""
override_passphrase_catalog = passphrase_catalog
if passphrase_catalog:
override_passphrase_catalog = files.read(passphrase_catalog)
PassphraseGenerator(site_name, save_location, author).generate(
interactive=interactive, force_cleartext=force_cleartext)
PassphraseGenerator(
site_name, save_location, author,
override_passphrase_catalog).generate(
interactive=interactive, force_cleartext=force_cleartext)
def generate_crypto_string(length):

View File

@ -68,6 +68,34 @@ data:
...
""")
TEST_OVERRIDE_PASSPHRASES_CATALOG = yaml.safe_load(
"""
---
schema: pegleg/PassphraseCatalog/v1
metadata:
schema: metadata/Document/v1
name: cluster-passphrases
layeringDefinition:
abstract: false
layer: site
storagePolicy: cleartext
data:
passphrases:
- description: 'short description of the passphrase'
document_name: ucp_keystone_admin_password
encrypted: true
length: 24
- description: 'short description of the passphrase'
document_name: osh_cinder_password
encrypted: true
length: 25
- description: 'short description of the passphrase'
document_name: osh_placement_password
encrypted: true
length: 32
...
""")
TEST_GLOBAL_PASSPHRASES_CATALOG = yaml.safe_load(
"""
---
@ -230,6 +258,10 @@ def test_generate_passphrases(*_):
os.makedirs(os.path.join(_dir, 'cicd_site_repo'), exist_ok=True)
PassphraseGenerator('cicd', _dir, 'test_author').generate()
passphrase_dir = os.path.join(
_dir, 'site', 'cicd', 'secrets', 'passphrases')
assert 6 == len(os.listdir(passphrase_dir))
for passphrase in TEST_PASSPHRASES_CATALOG['data']['passphrases']:
passphrase_file_name = '{}.yaml'.format(passphrase['document_name'])
passphrase_file_path = os.path.join(
@ -281,6 +313,68 @@ def test_generate_passphrases_exception(capture):
'try again.')))
@mock.patch.object(
util.definition,
'documents_for_site',
autospec=True,
return_value=TEST_SITE_DOCUMENTS)
@mock.patch.object(
pegleg.config,
'get_site_repo',
autospec=True,
return_value='cicd_site_repo')
@mock.patch.object(
util.definition,
'site_files',
autospec=True,
return_value=[
'cicd_site_repo/site/cicd/passphrases/passphrase-catalog.yaml',
])
@mock.patch.dict(
os.environ, {
'PEGLEG_PASSPHRASE': 'ytrr89erARAiPE34692iwUMvWqqBvC',
'PEGLEG_SALT': 'MySecretSalt1234567890]['
})
def test_generate_passphrases_with_overidden_passphrase_catalog(*_):
_dir = tempfile.mkdtemp()
os.makedirs(os.path.join(_dir, 'cicd_site_repo'), exist_ok=True)
PassphraseGenerator(
'cicd', _dir, 'test_author',
[TEST_OVERRIDE_PASSPHRASES_CATALOG]).generate()
passphrase_dir = os.path.join(
_dir, 'site', 'cicd', 'secrets', 'passphrases')
assert 3 == len(os.listdir(passphrase_dir))
for passphrase in TEST_OVERRIDE_PASSPHRASES_CATALOG['data']['passphrases']:
passphrase_file_name = '{}.yaml'.format(passphrase['document_name'])
passphrase_file_path = os.path.join(
_dir, 'site', 'cicd', 'secrets', 'passphrases',
passphrase_file_name)
assert os.path.isfile(passphrase_file_path)
with open(passphrase_file_path) as stream:
doc = yaml.safe_load(stream)
assert doc['schema'] == 'pegleg/PeglegManagedDocument/v1'
assert doc['metadata']['storagePolicy'] == 'cleartext'
assert 'encrypted' in doc['data']
assert doc['data']['encrypted']['by'] == 'test_author'
assert 'generated' in doc['data']
assert doc['data']['generated']['by'] == 'test_author'
assert 'managedDocument' in doc['data']
assert doc['data']['managedDocument']['metadata'][
'storagePolicy'] == 'encrypted'
decrypted_passphrase = encryption.decrypt(
doc['data']['managedDocument']['data'],
os.environ['PEGLEG_PASSPHRASE'].encode(),
os.environ['PEGLEG_SALT'].encode())
if passphrase_file_name == 'osh_placement_password.yaml':
assert len(decrypted_passphrase) == 32
elif passphrase_file_name == 'osh_cinder_password.yaml':
assert len(decrypted_passphrase) == 25
else:
assert len(decrypted_passphrase) == 24
@mock.patch.object(
util.definition,
'documents_for_site',