Add profiles to passphrase catalog

Change-Id: Id6e7cddd123e31f0df963167ddf3fa8f33e9060c
This commit is contained in:
Alexander Hughes 2019-09-16 11:07:03 -05:00 committed by Ian Pittwood
parent ee2b822a6d
commit 14f8600e37
7 changed files with 296 additions and 9 deletions

View File

@ -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

View File

@ -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()

View File

@ -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."""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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