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:
parent
87d24d530a
commit
9163ef08ca
@ -949,7 +949,6 @@ catalog.
|
|||||||
|
|
||||||
**-a / --author** (Required)
|
**-a / --author** (Required)
|
||||||
|
|
||||||
|
|
||||||
``Author`` is intended to document the application or the individual, who
|
``Author`` is intended to document the application or the individual, who
|
||||||
generates the site passphrase documents, mostly for tracking purposes. It
|
generates the site passphrase documents, mostly for tracking purposes. It
|
||||||
is expected to be leveraged in an operator-specific manner.
|
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>
|
<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.
|
**-i / --interactive** (Optional). False by default.
|
||||||
|
|
||||||
Enables input prompts for "prompt: true" passphrases. Input prompts are
|
Enables input prompts for "prompt: true" passphrases. Input prompts are
|
||||||
|
@ -707,6 +707,15 @@ def generate_pki(site_name, author, days, regenerate_all, save_location):
|
|||||||
required=True,
|
required=True,
|
||||||
help='Identifier for the program or person who is generating the secrets '
|
help='Identifier for the program or person who is generating the secrets '
|
||||||
'documents')
|
'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(
|
@click.option(
|
||||||
'-i',
|
'-i',
|
||||||
'--interactive',
|
'--interactive',
|
||||||
@ -722,11 +731,13 @@ def generate_pki(site_name, author, days, regenerate_all, save_location):
|
|||||||
show_default=True,
|
show_default=True,
|
||||||
help='Force cleartext generation of passphrases. This is not recommended.')
|
help='Force cleartext generation of passphrases. This is not recommended.')
|
||||||
def generate_passphrases(
|
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)
|
engine.repository.process_repositories(site_name)
|
||||||
config.set_global_enc_keys(site_name)
|
config.set_global_enc_keys(site_name)
|
||||||
engine.secrets.generate_passphrases(
|
engine.secrets.generate_passphrases(
|
||||||
site_name, save_location, author, interactive, force_cleartext)
|
site_name, save_location, author, passphrase_catalog, interactive,
|
||||||
|
force_cleartext)
|
||||||
|
|
||||||
|
|
||||||
@secrets.command(
|
@secrets.command(
|
||||||
|
@ -40,7 +40,12 @@ class PassphraseGenerator(BaseGenerator):
|
|||||||
Generates passphrases for a given environment, specified in a
|
Generates passphrases for a given environment, specified in a
|
||||||
passphrase catalog.
|
passphrase catalog.
|
||||||
"""
|
"""
|
||||||
def __init__(self, sitename, save_location, author):
|
def __init__(
|
||||||
|
self,
|
||||||
|
sitename,
|
||||||
|
save_location,
|
||||||
|
author,
|
||||||
|
override_passphrase_catalog=None):
|
||||||
"""Constructor for ``PassphraseGenerator``.
|
"""Constructor for ``PassphraseGenerator``.
|
||||||
|
|
||||||
:param str sitename: Site name for which passphrases are generated.
|
: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
|
:param str author: Identifying name of the author generating new
|
||||||
certificates.
|
certificates.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super(PassphraseGenerator,
|
super(PassphraseGenerator,
|
||||||
self).__init__(sitename, save_location, author)
|
self).__init__(sitename, save_location, author)
|
||||||
self._catalog = PassphraseCatalog(
|
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):
|
def generate(self, interactive=False, force_cleartext=False):
|
||||||
"""
|
"""
|
||||||
|
@ -138,7 +138,11 @@ def _get_dest_path(repo_base, file_path, save_location):
|
|||||||
|
|
||||||
|
|
||||||
def generate_passphrases(
|
def generate_passphrases(
|
||||||
site_name, save_location, author, interactive=False,
|
site_name,
|
||||||
|
save_location,
|
||||||
|
author,
|
||||||
|
passphrase_catalog=None,
|
||||||
|
interactive=False,
|
||||||
force_cleartext=False):
|
force_cleartext=False):
|
||||||
"""
|
"""
|
||||||
Look for the site passphrase catalogs, and for every passphrase entry in
|
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 site_name: The site to read from
|
||||||
:param str save_location: Location to write files to
|
:param str save_location: Location to write files to
|
||||||
:param str author: Author who's generating the files
|
: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 interactive: Whether to allow user input for passphrases
|
||||||
:param bool force_cleartext: Whether to generate results in clear text
|
: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(
|
PassphraseGenerator(
|
||||||
interactive=interactive, force_cleartext=force_cleartext)
|
site_name, save_location, author,
|
||||||
|
override_passphrase_catalog).generate(
|
||||||
|
interactive=interactive, force_cleartext=force_cleartext)
|
||||||
|
|
||||||
|
|
||||||
def generate_crypto_string(length):
|
def generate_crypto_string(length):
|
||||||
|
@ -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(
|
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)
|
os.makedirs(os.path.join(_dir, 'cicd_site_repo'), exist_ok=True)
|
||||||
PassphraseGenerator('cicd', _dir, 'test_author').generate()
|
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']:
|
for passphrase in TEST_PASSPHRASES_CATALOG['data']['passphrases']:
|
||||||
passphrase_file_name = '{}.yaml'.format(passphrase['document_name'])
|
passphrase_file_name = '{}.yaml'.format(passphrase['document_name'])
|
||||||
passphrase_file_path = os.path.join(
|
passphrase_file_path = os.path.join(
|
||||||
@ -281,6 +313,68 @@ def test_generate_passphrases_exception(capture):
|
|||||||
'try again.')))
|
'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(
|
@mock.patch.object(
|
||||||
util.definition,
|
util.definition,
|
||||||
'documents_for_site',
|
'documents_for_site',
|
||||||
|
Loading…
Reference in New Issue
Block a user