diff --git a/dev/tests/functional/_bootstrap.php b/dev/tests/functional/_bootstrap.php index 51dcf2063..5e6621fe0 100755 --- a/dev/tests/functional/_bootstrap.php +++ b/dev/tests/functional/_bootstrap.php @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -define('PROJECT_ROOT', dirname(dirname(dirname(__DIR__)))); +defined('PROJECT_ROOT') || define('PROJECT_ROOT', dirname(dirname(dirname(__DIR__)))); require_once realpath(PROJECT_ROOT . '/vendor/autoload.php'); //Load constants from .env file diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/CredentialStoreTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/CredentialStoreTest.php new file mode 100644 index 000000000..a451f8dc9 --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/CredentialStoreTest.php @@ -0,0 +1,38 @@ + ["$testKey=$testValue"] + ]); + + $encryptedCred = CredentialStore::getInstance()->getSecret($testKey); + + // assert the value we've gotten is in fact not identical to our test value + $this->assertNotEquals($testValue, $encryptedCred); + + $actualValue = CredentialStore::getInstance()->decryptSecretValue($encryptedCred); + + // assert that we are able to successfully decrypt our secret value + $this->assertEquals($testValue, $actualValue); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php index 183c9226d..f584adea9 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php @@ -48,9 +48,9 @@ public function testInvalidMergeOrderReference() try { $this->testActionObjectExtractor->extractActions($invalidArray, 'TestWithSelfReferencingStepKey'); } catch (\Exception $e) { - TestLoggingUtil::getInstance()->validateMockLogStatement( + TestLoggingUtil::getInstance()->validateMockLogStatmentRegex( 'error', - 'Line 108: Invalid ordering configuration in test', + '/Line \d*: Invalid ordering configuration in test/', [ 'test' => 'TestWithSelfReferencingStepKey', 'stepKey' => ['invalidTestAction1'] diff --git a/dev/tests/unit/Util/TestLoggingUtil.php b/dev/tests/unit/Util/TestLoggingUtil.php index 4be16280f..7c2ecc142 100644 --- a/dev/tests/unit/Util/TestLoggingUtil.php +++ b/dev/tests/unit/Util/TestLoggingUtil.php @@ -82,6 +82,15 @@ public function validateMockLogStatement($type, $message, $context) $this->assertEquals($context, $record['context']); } + public function validateMockLogStatmentRegex($type, $regex, $context) + { + $records = $this->testLogHandler->getRecords(); + $record = $records[count($records)-1]; // we assume the latest record is what requires validation + $this->assertEquals(strtoupper($type), $record['level_name']); + $this->assertRegExp($regex, $record['message']); + $this->assertEquals($context, $record['context']); + } + /** * Function which clears the test logger context from the LogginUtil class. Should be run after a test class has * executed. diff --git a/dev/tests/verification/Resources/PersistedReplacementTest.txt b/dev/tests/verification/Resources/PersistedReplacementTest.txt index f4f9b0298..78cdd5029 100644 --- a/dev/tests/verification/Resources/PersistedReplacementTest.txt +++ b/dev/tests/verification/Resources/PersistedReplacementTest.txt @@ -53,7 +53,7 @@ class PersistedReplacementTestCest $I->fillField("#selector", "StringBefore " . $createdData->getCreatedDataByName('firstname') . " StringAfter"); $I->fillField("#" . $createdData->getCreatedDataByName('firstname'), "input"); $I->fillField("#" . getenv("MAGENTO_BASE_URL") . "#" . $createdData->getCreatedDataByName('firstname'), "input"); - $I->fillField("#" . CredentialStore::getInstance()->getSecret("SECRET_PARAM") . "#" . $createdData->getCreatedDataByName('firstname'), "input"); + $I->fillSecretField("#" . CredentialStore::getInstance()->getSecret("SECRET_PARAM") . "#" . $createdData->getCreatedDataByName('firstname'), "input"); $I->dragAndDrop("#" . $createdData->getCreatedDataByName('firstname'), $createdData->getCreatedDataByName('lastname')); $I->conditionalClick($createdData->getCreatedDataByName('lastname'), "#" . $createdData->getCreatedDataByName('firstname'), true); $I->amOnUrl($createdData->getCreatedDataByName('firstname') . ".html"); diff --git a/src/Magento/FunctionalTestingFramework/Config/Reader/MftfFilesystem.php b/src/Magento/FunctionalTestingFramework/Config/Reader/MftfFilesystem.php index 173792d7c..728c4cb3a 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Reader/MftfFilesystem.php +++ b/src/Magento/FunctionalTestingFramework/Config/Reader/MftfFilesystem.php @@ -48,7 +48,9 @@ public function readFiles($fileList) } } $exceptionCollector->throwException(); - $this->validateSchema($configMerger, $fileList->getFilename()); + if ($fileList->valid()) { + $this->validateSchema($configMerger, $fileList->getFilename()); + } $output = []; if ($configMerger) { diff --git a/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php b/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php index 4caf4287a..3c06a8ff4 100644 --- a/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php @@ -13,6 +13,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Debug\Debug; use Symfony\Component\Process\Process; class RunTestCommand extends Command diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php index edd5a7c18..a28547d18 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php @@ -13,13 +13,29 @@ class CredentialStore { + const ENCRYPTION_ALGO = "AES-256-CBC"; + /** - * Singletone instnace + * Singleton instance * * @var CredentialStore */ private static $INSTANCE = null; + /** + * Initial vector for open_ssl encryption. + * + * @var string + */ + private $iv = null; + + /** + * Key for open_ssl encryption/decryption + * + * @var string + */ + private $encodedKey = null; + /** * Key/Value paris of credential names and their corresponding values * @@ -46,7 +62,10 @@ public static function getInstance() */ private function __construct() { - $this->readInCredentialsFile(); + $this->encodedKey = base64_encode(openssl_random_pseudo_bytes(16)); + $this->iv = substr(hash('sha256', $this->encodedKey), 0, 16); + $creds = $this->readInCredentialsFile(); + $this->credentials = $this->encryptCredFileContents($creds); } /** @@ -77,7 +96,7 @@ public function getSecret($key) /** * Private function which reads in secret key/values from .credentials file and stores in memory as key/value pair. * - * @return void + * @return array * @throws TestFrameworkException */ private function readInCredentialsFile() @@ -95,7 +114,18 @@ private function readInCredentialsFile() ); } - $credContents = file($credsFilePath, FILE_IGNORE_NEW_LINES); + return file($credsFilePath, FILE_IGNORE_NEW_LINES); + } + + /** + * Function which takes the contents of the credentials file and encrypts the entries. + * + * @param array $credContents + * @return array + */ + private function encryptCredFileContents($credContents) + { + $encryptedCreds = []; foreach ($credContents as $credValue) { if (substr($credValue, 0, 1) === '#' || empty($credValue)) { continue; @@ -103,8 +133,27 @@ private function readInCredentialsFile() list($key, $value) = explode("=", $credValue); if (!empty($value)) { - $this->credentials[$key] = $value; + $encryptedCreds[$key] = openssl_encrypt( + $value, + self::ENCRYPTION_ALGO, + $this->encodedKey, + 0, + $this->iv + ); } } + + return $encryptedCreds; + } + + /** + * Takes a value encrypted at runtime and descrypts using the object's initial vector. + * + * @param string $value + * @return string + */ + public function decryptSecretValue($value) + { + return openssl_decrypt($value, self::ENCRYPTION_ALGO, $this->encodedKey, 0, $this->iv); } } diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php index ecfb6cadd..f1aae2487 100644 --- a/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php +++ b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php @@ -6,25 +6,18 @@ namespace Magento\FunctionalTestingFramework\Module; -use Codeception\Events; use Codeception\Module\WebDriver; use Codeception\Test\Descriptor; use Codeception\TestInterface; -use Facebook\WebDriver\WebDriverSelect; -use Facebook\WebDriver\WebDriverBy; -use Facebook\WebDriver\Exception\NoSuchElementException; use Facebook\WebDriver\Interactions\WebDriverActions; -use Codeception\Exception\ElementNotFound; use Codeception\Exception\ModuleConfigException; use Codeception\Exception\ModuleException; use Codeception\Util\Uri; -use Codeception\Util\ActionSequence; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use Magento\FunctionalTestingFramework\DataGenerator\Persist\Curl\WebapiExecutor; use Magento\FunctionalTestingFramework\Util\Protocol\CurlTransport; use Magento\FunctionalTestingFramework\Util\Protocol\CurlInterface; -use Magento\Setup\Exception; use Magento\FunctionalTestingFramework\Util\ConfigSanitizerUtil; -use Yandex\Allure\Adapter\Event\TestCaseFinishedEvent; use Yandex\Allure\Adapter\Support\AttachmentSupport; /** @@ -44,6 +37,8 @@ * password: admin_password * browser: chrome * ``` + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class MagentoWebDriver extends WebDriver { @@ -596,6 +591,23 @@ public function dragAndDrop($source, $target, $xOffset = null, $yOffset = null) } } + /** + * Function used to fill sensitive crednetials with user data, data is decrypted immediately prior to fill to avoid + * exposure in console or log. + * + * @param string $field + * @param string $value + * @return void + */ + public function fillSecretField($field, $value) + { + // to protect any secrets from being printed to console the values are executed only at the webdriver level as a + // decrypted value + + $decryptedValue = CredentialStore::getInstance()->decryptSecretValue($value); + $this->fillField($field, $decryptedValue); + } + /** * Override for _failed method in Codeception method. Adds png and html attachments to allure report * following parent execution of test failure processing. diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php index 5ff2c8803..75970f0dd 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php @@ -9,6 +9,8 @@ use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler; use Magento\FunctionalTestingFramework\Test\Util\ActionMergeUtil; use Magento\FunctionalTestingFramework\Test\Util\ActionObjectExtractor; +use Magento\FunctionalTestingFramework\Test\Util\TestHookObjectExtractor; +use Magento\FunctionalTestingFramework\Test\Util\TestObjectExtractor; /** * Class TestObject @@ -183,12 +185,10 @@ public function getEstimatedDuration() } $hookTime = 0; - if (array_key_exists('before', $this->hooks)) { - $hookTime += $this->calculateWeightedActionTimes($this->hooks['before']->getActions()); - } - - if (array_key_exists('after', $this->hooks)) { - $hookTime += $this->calculateWeightedActionTimes($this->hooks['after']->getActions()); + foreach ([TestObjectExtractor::TEST_BEFORE_HOOK, TestObjectExtractor::TEST_AFTER_HOOK] as $hookName) { + if (array_key_exists($hookName, $this->hooks)) { + $hookTime += $this->calculateWeightedActionTimes($this->hooks[$hookName]->getActions()); + } } $testTime = $this->calculateWeightedActionTimes($this->getOrderedActions()); diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php index 99e575491..fb562c626 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php @@ -83,7 +83,66 @@ public function resolveActionSteps($parsedSteps, $skipActionGroupResolution = fa return $this->orderedSteps; } - return $this->resolveActionGroups($this->orderedSteps); + $resolvedActions = $this->resolveActionGroups($this->orderedSteps); + return $this->resolveSecretFieldAccess($resolvedActions); + } + + /** + * Takes an array of actions and resolves any references to secret fields. The function then validates whether the + * refernece is valid and replaces the function name accordingly to hide arguments at runtime. + * + * @param ActionObject[] $resolvedActions + * @return ActionObject[] + * @throws TestReferenceException + */ + private function resolveSecretFieldAccess($resolvedActions) + { + $actions = []; + foreach ($resolvedActions as $resolvedAction) { + $action = $resolvedAction; + $hasSecretRef = $this->actionAttributeContainsSecretRef($resolvedAction->getCustomActionAttributes()); + + if ($resolvedAction->getType() !== 'fillField' && $hasSecretRef) { + throw new TestReferenceException("You cannot reference secret data outside of fill field actions"); + } + + if ($resolvedAction->getType() === 'fillField' && $hasSecretRef) { + $action = new ActionObject( + $action->getStepKey(), + 'fillSecretField', + $action->getCustomActionAttributes(), + $action->getLinkedAction(), + $action->getActionOrigin() + ); + } + + $actions[$action->getStepKey()] = $action; + } + + return $actions; + } + + /** + * Returns a boolean based on whether or not the action attributes contain a reference to a secret field. + * + * @param array $actionAttributes + * @return boolean + */ + private function actionAttributeContainsSecretRef($actionAttributes) + { + foreach ($actionAttributes as $actionAttribute) { + if (is_array($actionAttribute)) { + return $this->actionAttributeContainsSecretRef($actionAttribute); + } + + preg_match_all("/{{_CREDS\.([\w]+)}}/", $actionAttribute, $matches); + + if (!empty($matches[0])) { + return true; + } + } + + return false; } /** diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php index 7642aec31..5cd7b7ac5 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php @@ -59,6 +59,7 @@ public function extractActions($testActions, $testName = null) foreach ($testActions as $actionName => $actionData) { $stepKey = $actionData[self::TEST_STEP_MERGE_KEY]; + $actionType = $actionData[self::NODE_NAME]; if (empty($stepKey)) { throw new XmlException(sprintf(self::STEP_KEY_EMPTY_ERROR_MSG, $actionData['nodeName'])); @@ -79,10 +80,7 @@ public function extractActions($testActions, $testName = null) $actionAttributes['parameterArray'] = $actionData['array']['value']; } - if ($actionData[self::NODE_NAME] === self::ACTION_GROUP_TAG) { - $actionAttributes = $this->processActionGroupArgs($actionAttributes); - } - + $actionAttributes = $this->processActionGroupArgs($actionType, $actionAttributes); $linkedAction = $this->processLinkedActions($actionName, $actionData); $actions = $this->extractFieldActions($actionData, $actions); $actionAttributes = $this->extractFieldReferences($actionData, $actionAttributes); @@ -98,7 +96,7 @@ public function extractActions($testActions, $testName = null) $actions[$stepKey] = new ActionObject( $stepKey, - $actionData[self::NODE_NAME], + $actionType, $actionAttributes, $linkedAction['stepKey'], $linkedAction['order'] @@ -142,11 +140,16 @@ private function processLinkedActions($actionName, $actionData) * Takes the action group reference and parses out arguments as an array that can be passed to override defaults * defined in the action group xml. * - * @param array $actionAttributeData + * @param string $actionType + * @param array $actionAttributeData * @return array */ - private function processActionGroupArgs($actionAttributeData) + private function processActionGroupArgs($actionType, $actionAttributeData) { + if ($actionType !== self::ACTION_GROUP_TAG) { + return $actionAttributeData; + } + $actionAttributeArgData = []; foreach ($actionAttributeData as $attributeDataKey => $attributeDataValues) { if ($attributeDataKey == self::ACTION_GROUP_REF) { diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd index 9aa772858..90671c340 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd @@ -37,6 +37,7 @@ + diff --git a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php index 99baeba41..45b9883ea 100644 --- a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php @@ -210,7 +210,7 @@ private function assembleTestPhp($testObject) $hookPhp = $this->generateHooksPhp($testObject->getHooks()); $testsPhp = $this->generateTestPhp($testObject); } catch (TestReferenceException $e) { - throw new TestReferenceException($e->getMessage() . " in Test \"" . $testObject->getName() . "\""); + throw new TestReferenceException($e->getMessage() . "\n" . $testObject->getFilename()); } $cestPhp = "