Add profiles to passphrase catalog
Change-Id: Id6e7cddd123e31f0df963167ddf3fa8f33e9060c
This commit is contained in:
parent
ee2b822a6d
commit
14f8600e37
|
@ -15,6 +15,7 @@
|
|||
import logging
|
||||
|
||||
from pegleg.engine.catalogs.base_catalog import BaseCatalog
|
||||
from pegleg.engine.catalogs import passphrase_profiles
|
||||
from pegleg.engine import exceptions
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
@ -34,6 +35,8 @@ P_DEFAULT_REGENERABLE = True
|
|||
P_DEFAULT_PROMPT = False
|
||||
VALID_PASSPHRASE_TYPES = ['passphrase', 'base64', 'uuid']
|
||||
VALID_BOOLEAN_FIELDS = [True, False]
|
||||
P_PROFILE = 'profile'
|
||||
P_DEFAULT_PROFILE = 'default'
|
||||
|
||||
__all__ = ['PassphraseCatalog']
|
||||
|
||||
|
@ -169,3 +172,25 @@ class PassphraseCatalog(BaseCatalog):
|
|||
validvalues=VALID_BOOLEAN_FIELDS)
|
||||
else:
|
||||
return passphrase_prompt
|
||||
|
||||
def get_passphrase_profile(self, passphrase_name):
|
||||
"""Return the profile field of the ``passphrase_name``.
|
||||
|
||||
Determine which profile this passphrase should use when selecting
|
||||
the pool to generate passphrase from.
|
||||
If no option is specified, use default profile. See
|
||||
pegleg.engine.catalogs.passphrase_profiles for default and valid
|
||||
options.
|
||||
"""
|
||||
|
||||
for c_doc in self._catalog_docs:
|
||||
for passphrase in c_doc['data']['passphrases']:
|
||||
if passphrase[P_DOCUMENT_NAME] == passphrase_name:
|
||||
profile = passphrase.get(P_PROFILE,
|
||||
P_DEFAULT_PROFILE).lower()
|
||||
if profile not in passphrase_profiles.VALID_PROFILES:
|
||||
raise exceptions.InvalidPassphraseProfile(
|
||||
pprofile=profile,
|
||||
validvalues=passphrase_profiles.VALID_PROFILES)
|
||||
else:
|
||||
return profile
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# Copyright 2019 AT&T Intellectual Property. All other rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import string
|
||||
|
||||
PROFILES = {
|
||||
'default': string.ascii_letters + string.digits + '@#&-+=?',
|
||||
'alphanumeric': string.ascii_letters + string.digits,
|
||||
'alphanumeric_lower': string.ascii_lowercase + string.digits,
|
||||
'alphanumeric_upper': string.ascii_uppercase + string.digits,
|
||||
'all': string.ascii_letters + string.digits + string.punctuation,
|
||||
'hex_lower': 'abcdef0123456789',
|
||||
'hex_upper': 'ABCDEF0123456789'
|
||||
}
|
||||
|
||||
VALID_PROFILES = PROFILES.keys()
|
|
@ -113,6 +113,13 @@ class InvalidPassphraseRegeneration(PeglegBaseException):
|
|||
'values are: %(validvalues)s.')
|
||||
|
||||
|
||||
class InvalidPassphraseProfile(PeglegBaseException):
|
||||
"""Invalid Passphrase profile field"""
|
||||
message = (
|
||||
'Invalid Passphrase profile %(pprofile)s specified. '
|
||||
'Valid values are: %(validvalues)s.')
|
||||
|
||||
|
||||
class GenesisBundleEncryptionException(PeglegBaseException):
|
||||
"""Exception raised when encryption of the genesis bundle fails."""
|
||||
|
||||
|
|
|
@ -54,7 +54,6 @@ class PassphraseGenerator(BaseGenerator):
|
|||
self).__init__(sitename, save_location, author)
|
||||
self._catalog = PassphraseCatalog(
|
||||
self._sitename, documents=self._documents)
|
||||
self._pass_util = CryptoString()
|
||||
|
||||
def generate(self, interactive=False, force_cleartext=False):
|
||||
"""
|
||||
|
@ -80,6 +79,7 @@ class PassphraseGenerator(BaseGenerator):
|
|||
passphrase = None
|
||||
passphrase_type = self._catalog.get_passphrase_type(p_name)
|
||||
prompt = self._catalog.is_passphrase_prompt(p_name)
|
||||
profile = self._catalog.get_passphrase_profile(p_name)
|
||||
if interactive and prompt:
|
||||
auto_allowed = regenerable
|
||||
|
||||
|
@ -111,7 +111,7 @@ class PassphraseGenerator(BaseGenerator):
|
|||
if passphrase_type == 'uuid': # nosec
|
||||
passphrase = uuidutils.generate_uuid()
|
||||
else:
|
||||
passphrase = self._pass_util.get_crypto_string(
|
||||
passphrase = CryptoString(profile).get_crypto_string(
|
||||
self._catalog.get_length(p_name))
|
||||
if passphrase_type == 'base64': # nosec
|
||||
# Take the randomly generated string and convert to a
|
||||
|
|
|
@ -15,14 +15,30 @@
|
|||
import random
|
||||
import string
|
||||
|
||||
from pegleg.engine.catalogs import passphrase_profiles
|
||||
from pegleg.engine import exceptions
|
||||
|
||||
__all__ = ['CryptoString']
|
||||
|
||||
|
||||
class CryptoString(object):
|
||||
def __init__(self):
|
||||
punctuation = '@#&-+=?'
|
||||
self._pool = string.ascii_letters + string.digits + punctuation
|
||||
def __init__(self, profile=None):
|
||||
if profile and profile.lower() in passphrase_profiles.VALID_PROFILES:
|
||||
self._pool = passphrase_profiles.PROFILES[profile.lower()]
|
||||
elif profile:
|
||||
raise exceptions.InvalidPassphraseProfile(
|
||||
pprofile=profile.lower(),
|
||||
validvalues=passphrase_profiles.VALID_PROFILES)
|
||||
else:
|
||||
self._pool = passphrase_profiles.PROFILES['default']
|
||||
self._random = random.SystemRandom()
|
||||
self.determine_char_sets()
|
||||
|
||||
def determine_char_sets(self):
|
||||
self.test_upper = self.has_upper(self._pool)
|
||||
self.test_lower = self.has_lower(self._pool)
|
||||
self.test_number = self.has_number(self._pool)
|
||||
self.test_symbol = self.has_symbol(self._pool)
|
||||
|
||||
def has_upper(self, crypto_str):
|
||||
"""Check if string contains an uppercase letter
|
||||
|
@ -69,13 +85,20 @@ class CryptoString(object):
|
|||
|
||||
:param str crypto_str: The string to test.
|
||||
:returns: True if string contains at least one each: uppercase letter,
|
||||
lowercase letter, number and symbol
|
||||
lowercase letter, number and symbol if that character set is
|
||||
present in the original passphrase pool.
|
||||
:rtype: boolean
|
||||
"""
|
||||
|
||||
for test in [self.has_upper, self.has_lower, self.has_number,
|
||||
self.has_symbol]:
|
||||
if not test(crypto_str):
|
||||
test_cases = [
|
||||
self.test_upper, self.test_lower, self.test_number,
|
||||
self.test_symbol
|
||||
]
|
||||
tests = [
|
||||
self.has_upper, self.has_lower, self.has_number, self.has_symbol
|
||||
]
|
||||
for (test_case, test) in zip(test_cases, tests):
|
||||
if test_case and not test(crypto_str):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
|
@ -119,6 +119,50 @@ data:
|
|||
...
|
||||
""")
|
||||
|
||||
TEST_PROFILES_CATALOG = yaml.safe_load(
|
||||
"""
|
||||
---
|
||||
schema: pegleg/PassphraseCatalog/v1
|
||||
metadata:
|
||||
schema: metadata/Document/v1
|
||||
name: cluster-passphrases
|
||||
layeringDefinition:
|
||||
abstract: false
|
||||
layer: global
|
||||
storagePolicy: cleartext
|
||||
data:
|
||||
passphrases:
|
||||
- description: 'default profile'
|
||||
document_name: default_passphrase
|
||||
encrypted: true
|
||||
profile: default
|
||||
- description: 'alphanumeric profile'
|
||||
document_name: alphanumeric_passphrase
|
||||
encrypted: true
|
||||
profile: alphanumeric
|
||||
- description: 'alphanumeric_lower profile'
|
||||
document_name: alphanumeric_lower_passphrase
|
||||
encrypted: true
|
||||
profile: alphanumeric_lower
|
||||
- description: 'alphanumeric_upper profile'
|
||||
document_name: alphanumeric_upper_passphrase
|
||||
encrypted: true
|
||||
profile: alphanumeric_upper
|
||||
- description: 'all profile'
|
||||
document_name: all_passphrase
|
||||
encrypted: true
|
||||
profile: all
|
||||
- description: 'hex_lower profile'
|
||||
document_name: hex_lower_passphrase
|
||||
encrypted: true
|
||||
profile: hex_lower
|
||||
- description: 'hex_upper profile'
|
||||
document_name: hex_upper_passphrase
|
||||
encrypted: true
|
||||
profile: hex_upper
|
||||
...
|
||||
""")
|
||||
|
||||
TEST_REPOSITORIES = {
|
||||
'repositories': {
|
||||
'global': {
|
||||
|
@ -156,6 +200,7 @@ TEST_GLOBAL_SITE_DOCUMENTS = [
|
|||
TEST_SITE_DEFINITION, TEST_GLOBAL_PASSPHRASES_CATALOG
|
||||
]
|
||||
TEST_TYPE_SITE_DOCUMENTS = [TEST_SITE_DEFINITION, TEST_TYPES_CATALOG]
|
||||
TEST_PROFILES_SITE_DOCUMENTS = [TEST_SITE_DEFINITION, TEST_PROFILES_CATALOG]
|
||||
|
||||
|
||||
@mock.patch.object(
|
||||
|
@ -329,3 +374,93 @@ def test_uuid_passphrase_catalog(*_):
|
|||
os.environ['PEGLEG_SALT'].encode())
|
||||
if passphrase_file_name == "uuid_passphrase_doc.yaml":
|
||||
assert uuid.UUID(decrypted_passphrase.decode()).version == 4
|
||||
|
||||
|
||||
@mock.patch.object(
|
||||
util.definition,
|
||||
'documents_for_site',
|
||||
autospec=True,
|
||||
return_value=TEST_PROFILES_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_global_repo/site/cicd/passphrases/passphrase-catalog.yaml',
|
||||
])
|
||||
@mock.patch.dict(
|
||||
os.environ, {
|
||||
'PEGLEG_PASSPHRASE': 'ytrr89erARAiPE34692iwUMvWqqBvC',
|
||||
'PEGLEG_SALT': 'MySecretSalt1234567890]['
|
||||
})
|
||||
def test_profiles_catalog(*_):
|
||||
_dir = tempfile.mkdtemp()
|
||||
os.makedirs(os.path.join(_dir, 'cicd_site_repo'), exist_ok=True)
|
||||
PassphraseGenerator('cicd', _dir, 'test_author').generate()
|
||||
s_util = CryptoString()
|
||||
|
||||
for passphrase in TEST_PROFILES_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)
|
||||
decrypted_passphrase = encryption.decrypt(
|
||||
doc['data']['managedDocument']['data'],
|
||||
os.environ['PEGLEG_PASSPHRASE'].encode(),
|
||||
os.environ['PEGLEG_SALT'].encode()).decode()
|
||||
assert len(decrypted_passphrase) == 24
|
||||
if passphrase_file_name == "default_passphrase.yaml":
|
||||
assert s_util.has_lower(decrypted_passphrase) is True
|
||||
assert s_util.has_upper(decrypted_passphrase) is True
|
||||
assert s_util.has_number(decrypted_passphrase) is True
|
||||
assert s_util.has_symbol(decrypted_passphrase) is True
|
||||
bad_symbols = any(
|
||||
char in '!"$%()*,./:;<>[]^_`{|}~\''
|
||||
for char in decrypted_passphrase)
|
||||
assert not bad_symbols
|
||||
elif passphrase_file_name == "alphanumeric_passphrase.yaml":
|
||||
assert s_util.has_lower(decrypted_passphrase) is True
|
||||
assert s_util.has_upper(decrypted_passphrase) is True
|
||||
assert s_util.has_number(decrypted_passphrase) is True
|
||||
assert s_util.has_symbol(decrypted_passphrase) is False
|
||||
elif passphrase_file_name == "alphanumeric_lower_passphrase.yaml":
|
||||
assert s_util.has_lower(decrypted_passphrase) is True
|
||||
assert s_util.has_upper(decrypted_passphrase) is False
|
||||
assert s_util.has_number(decrypted_passphrase) is True
|
||||
assert s_util.has_symbol(decrypted_passphrase) is False
|
||||
elif passphrase_file_name == "alphanumeric_upper_passphrase.yaml":
|
||||
assert s_util.has_lower(decrypted_passphrase) is False
|
||||
assert s_util.has_upper(decrypted_passphrase) is True
|
||||
assert s_util.has_number(decrypted_passphrase) is True
|
||||
assert s_util.has_symbol(decrypted_passphrase) is False
|
||||
elif passphrase_file_name == "all_passphrase.yaml":
|
||||
assert s_util.has_lower(decrypted_passphrase) is True
|
||||
assert s_util.has_upper(decrypted_passphrase) is True
|
||||
assert s_util.has_number(decrypted_passphrase) is True
|
||||
assert s_util.has_symbol(decrypted_passphrase) is True
|
||||
elif passphrase_file_name == "hex_lower_passphrase.yaml":
|
||||
assert s_util.has_lower(decrypted_passphrase) is True
|
||||
assert s_util.has_upper(decrypted_passphrase) is False
|
||||
assert s_util.has_number(decrypted_passphrase) is True
|
||||
assert s_util.has_symbol(decrypted_passphrase) is False
|
||||
bad_letters = any(
|
||||
char in 'ghijklmnopqrstuvwxyz'
|
||||
for char in decrypted_passphrase)
|
||||
assert not bad_letters
|
||||
elif passphrase_file_name == "hex_upper_passphrase.yaml":
|
||||
assert s_util.has_lower(decrypted_passphrase) is False
|
||||
assert s_util.has_upper(decrypted_passphrase) is True
|
||||
assert s_util.has_number(decrypted_passphrase) is True
|
||||
assert s_util.has_symbol(decrypted_passphrase) is False
|
||||
bad_letters = any(
|
||||
char in 'GHIJKLMNOPQRSTUVWXYZ'
|
||||
for char in decrypted_passphrase)
|
||||
assert not bad_letters
|
||||
|
|
|
@ -93,3 +93,73 @@ def test_cryptostring_has_all():
|
|||
assert s_util.validate_crypto_str(crypto_string) is False
|
||||
crypto_string = 'ThisPasswordH4sNoSymbols'
|
||||
assert s_util.validate_crypto_str(crypto_string) is False
|
||||
|
||||
|
||||
def test_cryptostring_default_profile():
|
||||
s_util = CryptoString(profile='default')
|
||||
crypto_string = s_util.get_crypto_string()
|
||||
assert s_util.has_lower(crypto_string) is True
|
||||
assert s_util.has_upper(crypto_string) is True
|
||||
assert s_util.has_number(crypto_string) is True
|
||||
assert s_util.has_symbol(crypto_string) is True
|
||||
bad_symbols = any(
|
||||
char in '!"$%()*,./:;<>[]^_`{|}~\'' for char in crypto_string)
|
||||
assert not bad_symbols
|
||||
|
||||
|
||||
def test_cryptostring_alphanumeric_profile():
|
||||
s_util = CryptoString(profile='alphanumeric')
|
||||
crypto_string = s_util.get_crypto_string()
|
||||
assert s_util.has_lower(crypto_string) is True
|
||||
assert s_util.has_upper(crypto_string) is True
|
||||
assert s_util.has_number(crypto_string) is True
|
||||
assert s_util.has_symbol(crypto_string) is False
|
||||
|
||||
|
||||
def test_cryptostring_alphanumeric_lower_profile():
|
||||
s_util = CryptoString(profile='alphanumeric_lower')
|
||||
crypto_string = s_util.get_crypto_string()
|
||||
assert s_util.has_lower(crypto_string) is True
|
||||
assert s_util.has_upper(crypto_string) is False
|
||||
assert s_util.has_number(crypto_string) is True
|
||||
assert s_util.has_symbol(crypto_string) is False
|
||||
|
||||
|
||||
def test_cryptostring_alphanumeric_upper_profile():
|
||||
s_util = CryptoString(profile='alphanumeric_upper')
|
||||
crypto_string = s_util.get_crypto_string()
|
||||
assert s_util.has_lower(crypto_string) is False
|
||||
assert s_util.has_upper(crypto_string) is True
|
||||
assert s_util.has_number(crypto_string) is True
|
||||
assert s_util.has_symbol(crypto_string) is False
|
||||
|
||||
|
||||
def test_cryptostring_all_profile():
|
||||
s_util = CryptoString(profile='all')
|
||||
crypto_string = s_util.get_crypto_string()
|
||||
assert s_util.has_lower(crypto_string) is True
|
||||
assert s_util.has_upper(crypto_string) is True
|
||||
assert s_util.has_number(crypto_string) is True
|
||||
assert s_util.has_symbol(crypto_string) is True
|
||||
|
||||
|
||||
def test_cryptostring_hex_lower_profile():
|
||||
s_util = CryptoString(profile='hex_lower')
|
||||
crypto_string = s_util.get_crypto_string()
|
||||
assert s_util.has_lower(crypto_string) is True
|
||||
assert s_util.has_upper(crypto_string) is False
|
||||
assert s_util.has_number(crypto_string) is True
|
||||
assert s_util.has_symbol(crypto_string) is False
|
||||
bad_letters = any(char in 'ghijklmnopqrstuvwxyz' for char in crypto_string)
|
||||
assert not bad_letters
|
||||
|
||||
|
||||
def test_cryptostring_hex_upper_profile():
|
||||
s_util = CryptoString(profile='hex_upper')
|
||||
crypto_string = s_util.get_crypto_string()
|
||||
assert s_util.has_lower(crypto_string) is False
|
||||
assert s_util.has_upper(crypto_string) is True
|
||||
assert s_util.has_number(crypto_string) is True
|
||||
assert s_util.has_symbol(crypto_string) is False
|
||||
bad_letters = any(char in 'GHIJKLMNOPQRSTUVWXYZ' for char in crypto_string)
|
||||
assert not bad_letters
|
||||
|
|
Loading…
Reference in New Issue