# Copyright 2018 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 base64 import os import tempfile from unittest import mock import uuid from cryptography import fernet import pytest from testfixtures import log_capture import yaml from pegleg.engine.generators.passphrase_generator import PassphraseGenerator from pegleg.engine.util.cryptostring import CryptoString from pegleg.engine.util import encryption from pegleg.engine import util import pegleg TEST_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: ceph_swift_keystone_password encrypted: true - 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_barbican_oslo_db_password encrypted: true length: 23 - description: 'short description of the passphrase' document_name: osh_cinder_password encrypted: true length: 25 - description: 'short description of the passphrase' document_name: osh_oslo_db_admin_password encrypted: true length: 0 - description: 'short description of the passphrase' document_name: osh_placement_password encrypted: true length: 32 ... """) 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( """ --- schema: pegleg/PassphraseCatalog/v1 metadata: schema: metadata/Document/v1 name: cluster-passphrases layeringDefinition: abstract: false layer: global storagePolicy: cleartext data: passphrases: - description: 'description of passphrase from global' document_name: passphrase_from_global encrypted: true ... """) TEST_TYPES_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: 'description of base64 required passphrases' document_name: base64_encoded_passphrase_doc encrypted: true type: base64 - description: 'description of uuid secret' document_name: uuid_passphrase_doc encrypted: true encoding: none type: uuid - description: 'description of random passphrase' document_name: passphrase_doc encrypted: true type: passphrase - description: 'description of default random passphrase' document_name: default_passphrase_doc encrypted: true ... """) 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': { 'revision': '843d1a50106e1f17f3f722e2ef1634ae442fe68f', 'url': 'ssh://REPO_USERNAME@gerrit:29418/aic-clcp-manifests.git' }, 'secrets': { 'revision': 'master', 'url': ( 'ssh://REPO_USERNAME@gerrit:29418/aic-clcp-security-' 'manifests.git') } } } TEST_SITE_DEFINITION = { 'data': { 'revision': 'v1.0', 'site_type': 'cicd', }, 'metadata': { 'layeringDefinition': { 'abstract': 'false', 'layer': 'site', }, 'name': 'test-site', 'schema': 'metadata/Document/v1', 'storagePolicy': 'cleartext', }, 'schema': 'pegleg/SiteDefinition/v1', } TEST_SITE_DOCUMENTS = [TEST_SITE_DEFINITION, TEST_PASSPHRASES_CATALOG] 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( 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(*_): _dir = tempfile.mkdtemp() 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( _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 @log_capture() def test_generate_passphrases_exception(capture): unenc_data = uuid.uuid4().bytes passphrase1 = uuid.uuid4().bytes passphrase2 = uuid.uuid4().bytes salt1 = uuid.uuid4().bytes salt2 = uuid.uuid4().bytes # Generate random data and encrypt it enc_data = encryption.encrypt(unenc_data, passphrase1, salt1) # Decrypt using the wrong key to see to see the InvalidToken error with pytest.raises(fernet.InvalidToken): encryption.decrypt(enc_data, passphrase2, salt2) capture.check( ( 'pegleg.engine.util.encryption', 'ERROR', ( 'Signature verification to decrypt secrets failed. ' 'Please check your provided passphrase and salt and ' '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', autospec=True, return_value=TEST_GLOBAL_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_global_passphrase_catalog(*_): _dir = tempfile.mkdtemp() os.makedirs(os.path.join(_dir, 'cicd_site_repo'), exist_ok=True) PassphraseGenerator('cicd', _dir, 'test_author').generate() for passphrase in TEST_GLOBAL_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 == "passphrase_from_global.yaml": assert len(decrypted_passphrase) == 24 @mock.patch.object( util.definition, 'documents_for_site', autospec=True, return_value=TEST_TYPE_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_uuid_passphrase_catalog(*_): _dir = tempfile.mkdtemp() os.makedirs(os.path.join(_dir, 'cicd_site_repo'), exist_ok=True) PassphraseGenerator('cicd', _dir, 'test_author').generate() for passphrase in TEST_TYPES_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()) 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