Skip to content

Commit 4c5a133

Browse files
authored
Merge pull request #158 from magento/MQE-1070
MQE-1070: Hide Sensitive Creds in Allure Report
2 parents 712ab3e + 32a4c56 commit 4c5a133

File tree

14 files changed

+207
-33
lines changed

14 files changed

+207
-33
lines changed

dev/tests/functional/_bootstrap.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* See COPYING.txt for license details.
55
*/
66

7-
define('PROJECT_ROOT', dirname(dirname(dirname(__DIR__))));
7+
defined('PROJECT_ROOT') || define('PROJECT_ROOT', dirname(dirname(dirname(__DIR__))));
88
require_once realpath(PROJECT_ROOT . '/vendor/autoload.php');
99

1010
//Load constants from .env file
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace tests\unit\Magento\FunctionalTestFramework\DataGenerator\Handlers;
8+
9+
use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore;
10+
use Magento\FunctionalTestingFramework\Util\MagentoTestCase;
11+
use AspectMock\Test as AspectMock;
12+
13+
class CredentialStoreTest extends MagentoTestCase
14+
{
15+
16+
/**
17+
* Test basic encryption/decryption functionality in CredentialStore class.
18+
*/
19+
public function testBasicEncryptDecrypt()
20+
{
21+
$testKey = 'myKey';
22+
$testValue = 'myValue';
23+
24+
AspectMock::double(CredentialStore::class, [
25+
'readInCredentialsFile' => ["$testKey=$testValue"]
26+
]);
27+
28+
$encryptedCred = CredentialStore::getInstance()->getSecret($testKey);
29+
30+
// assert the value we've gotten is in fact not identical to our test value
31+
$this->assertNotEquals($testValue, $encryptedCred);
32+
33+
$actualValue = CredentialStore::getInstance()->decryptSecretValue($encryptedCred);
34+
35+
// assert that we are able to successfully decrypt our secret value
36+
$this->assertEquals($testValue, $actualValue);
37+
}
38+
}

dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ public function testInvalidMergeOrderReference()
4848
try {
4949
$this->testActionObjectExtractor->extractActions($invalidArray, 'TestWithSelfReferencingStepKey');
5050
} catch (\Exception $e) {
51-
TestLoggingUtil::getInstance()->validateMockLogStatement(
51+
TestLoggingUtil::getInstance()->validateMockLogStatmentRegex(
5252
'error',
53-
'Line 108: Invalid ordering configuration in test',
53+
'/Line \d*: Invalid ordering configuration in test/',
5454
[
5555
'test' => 'TestWithSelfReferencingStepKey',
5656
'stepKey' => ['invalidTestAction1']

dev/tests/unit/Util/TestLoggingUtil.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@ public function validateMockLogStatement($type, $message, $context)
8282
$this->assertEquals($context, $record['context']);
8383
}
8484

85+
public function validateMockLogStatmentRegex($type, $regex, $context)
86+
{
87+
$records = $this->testLogHandler->getRecords();
88+
$record = $records[count($records)-1]; // we assume the latest record is what requires validation
89+
$this->assertEquals(strtoupper($type), $record['level_name']);
90+
$this->assertRegExp($regex, $record['message']);
91+
$this->assertEquals($context, $record['context']);
92+
}
93+
8594
/**
8695
* Function which clears the test logger context from the LogginUtil class. Should be run after a test class has
8796
* executed.

dev/tests/verification/Resources/PersistedReplacementTest.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class PersistedReplacementTestCest
5353
$I->fillField("#selector", "StringBefore " . $createdData->getCreatedDataByName('firstname') . " StringAfter");
5454
$I->fillField("#" . $createdData->getCreatedDataByName('firstname'), "input");
5555
$I->fillField("#" . getenv("MAGENTO_BASE_URL") . "#" . $createdData->getCreatedDataByName('firstname'), "input");
56-
$I->fillField("#" . CredentialStore::getInstance()->getSecret("SECRET_PARAM") . "#" . $createdData->getCreatedDataByName('firstname'), "input");
56+
$I->fillSecretField("#" . CredentialStore::getInstance()->getSecret("SECRET_PARAM") . "#" . $createdData->getCreatedDataByName('firstname'), "input");
5757
$I->dragAndDrop("#" . $createdData->getCreatedDataByName('firstname'), $createdData->getCreatedDataByName('lastname'));
5858
$I->conditionalClick($createdData->getCreatedDataByName('lastname'), "#" . $createdData->getCreatedDataByName('firstname'), true);
5959
$I->amOnUrl($createdData->getCreatedDataByName('firstname') . ".html");

src/Magento/FunctionalTestingFramework/Config/Reader/MftfFilesystem.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ public function readFiles($fileList)
4848
}
4949
}
5050
$exceptionCollector->throwException();
51-
$this->validateSchema($configMerger, $fileList->getFilename());
51+
if ($fileList->valid()) {
52+
$this->validateSchema($configMerger, $fileList->getFilename());
53+
}
5254

5355
$output = [];
5456
if ($configMerger) {

src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Symfony\Component\Console\Input\InputInterface;
1414
use Symfony\Component\Console\Input\InputOption;
1515
use Symfony\Component\Console\Output\OutputInterface;
16+
use Symfony\Component\Debug\Debug;
1617
use Symfony\Component\Process\Process;
1718

1819
class RunTestCommand extends Command

src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,29 @@
1313

1414
class CredentialStore
1515
{
16+
const ENCRYPTION_ALGO = "AES-256-CBC";
17+
1618
/**
17-
* Singletone instnace
19+
* Singleton instance
1820
*
1921
* @var CredentialStore
2022
*/
2123
private static $INSTANCE = null;
2224

25+
/**
26+
* Initial vector for open_ssl encryption.
27+
*
28+
* @var string
29+
*/
30+
private $iv = null;
31+
32+
/**
33+
* Key for open_ssl encryption/decryption
34+
*
35+
* @var string
36+
*/
37+
private $encodedKey = null;
38+
2339
/**
2440
* Key/Value paris of credential names and their corresponding values
2541
*
@@ -46,7 +62,10 @@ public static function getInstance()
4662
*/
4763
private function __construct()
4864
{
49-
$this->readInCredentialsFile();
65+
$this->encodedKey = base64_encode(openssl_random_pseudo_bytes(16));
66+
$this->iv = substr(hash('sha256', $this->encodedKey), 0, 16);
67+
$creds = $this->readInCredentialsFile();
68+
$this->credentials = $this->encryptCredFileContents($creds);
5069
}
5170

5271
/**
@@ -77,7 +96,7 @@ public function getSecret($key)
7796
/**
7897
* Private function which reads in secret key/values from .credentials file and stores in memory as key/value pair.
7998
*
80-
* @return void
99+
* @return array
81100
* @throws TestFrameworkException
82101
*/
83102
private function readInCredentialsFile()
@@ -95,16 +114,46 @@ private function readInCredentialsFile()
95114
);
96115
}
97116

98-
$credContents = file($credsFilePath, FILE_IGNORE_NEW_LINES);
117+
return file($credsFilePath, FILE_IGNORE_NEW_LINES);
118+
}
119+
120+
/**
121+
* Function which takes the contents of the credentials file and encrypts the entries.
122+
*
123+
* @param array $credContents
124+
* @return array
125+
*/
126+
private function encryptCredFileContents($credContents)
127+
{
128+
$encryptedCreds = [];
99129
foreach ($credContents as $credValue) {
100130
if (substr($credValue, 0, 1) === '#' || empty($credValue)) {
101131
continue;
102132
}
103133

104134
list($key, $value) = explode("=", $credValue);
105135
if (!empty($value)) {
106-
$this->credentials[$key] = $value;
136+
$encryptedCreds[$key] = openssl_encrypt(
137+
$value,
138+
self::ENCRYPTION_ALGO,
139+
$this->encodedKey,
140+
0,
141+
$this->iv
142+
);
107143
}
108144
}
145+
146+
return $encryptedCreds;
147+
}
148+
149+
/**
150+
* Takes a value encrypted at runtime and descrypts using the object's initial vector.
151+
*
152+
* @param string $value
153+
* @return string
154+
*/
155+
public function decryptSecretValue($value)
156+
{
157+
return openssl_decrypt($value, self::ENCRYPTION_ALGO, $this->encodedKey, 0, $this->iv);
109158
}
110159
}

src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,18 @@
66

77
namespace Magento\FunctionalTestingFramework\Module;
88

9-
use Codeception\Events;
109
use Codeception\Module\WebDriver;
1110
use Codeception\Test\Descriptor;
1211
use Codeception\TestInterface;
13-
use Facebook\WebDriver\WebDriverSelect;
14-
use Facebook\WebDriver\WebDriverBy;
15-
use Facebook\WebDriver\Exception\NoSuchElementException;
1612
use Facebook\WebDriver\Interactions\WebDriverActions;
17-
use Codeception\Exception\ElementNotFound;
1813
use Codeception\Exception\ModuleConfigException;
1914
use Codeception\Exception\ModuleException;
2015
use Codeception\Util\Uri;
21-
use Codeception\Util\ActionSequence;
16+
use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore;
2217
use Magento\FunctionalTestingFramework\DataGenerator\Persist\Curl\WebapiExecutor;
2318
use Magento\FunctionalTestingFramework\Util\Protocol\CurlTransport;
2419
use Magento\FunctionalTestingFramework\Util\Protocol\CurlInterface;
25-
use Magento\Setup\Exception;
2620
use Magento\FunctionalTestingFramework\Util\ConfigSanitizerUtil;
27-
use Yandex\Allure\Adapter\Event\TestCaseFinishedEvent;
2821
use Yandex\Allure\Adapter\Support\AttachmentSupport;
2922

3023
/**
@@ -44,6 +37,8 @@
4437
* password: admin_password
4538
* browser: chrome
4639
* ```
40+
*
41+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
4742
*/
4843
class MagentoWebDriver extends WebDriver
4944
{
@@ -596,6 +591,23 @@ public function dragAndDrop($source, $target, $xOffset = null, $yOffset = null)
596591
}
597592
}
598593

594+
/**
595+
* Function used to fill sensitive crednetials with user data, data is decrypted immediately prior to fill to avoid
596+
* exposure in console or log.
597+
*
598+
* @param string $field
599+
* @param string $value
600+
* @return void
601+
*/
602+
public function fillSecretField($field, $value)
603+
{
604+
// to protect any secrets from being printed to console the values are executed only at the webdriver level as a
605+
// decrypted value
606+
607+
$decryptedValue = CredentialStore::getInstance()->decryptSecretValue($value);
608+
$this->fillField($field, $decryptedValue);
609+
}
610+
599611
/**
600612
* Override for _failed method in Codeception method. Adds png and html attachments to allure report
601613
* following parent execution of test failure processing.

src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler;
1010
use Magento\FunctionalTestingFramework\Test\Util\ActionMergeUtil;
1111
use Magento\FunctionalTestingFramework\Test\Util\ActionObjectExtractor;
12+
use Magento\FunctionalTestingFramework\Test\Util\TestHookObjectExtractor;
13+
use Magento\FunctionalTestingFramework\Test\Util\TestObjectExtractor;
1214

1315
/**
1416
* Class TestObject
@@ -183,12 +185,10 @@ public function getEstimatedDuration()
183185
}
184186

185187
$hookTime = 0;
186-
if (array_key_exists('before', $this->hooks)) {
187-
$hookTime += $this->calculateWeightedActionTimes($this->hooks['before']->getActions());
188-
}
189-
190-
if (array_key_exists('after', $this->hooks)) {
191-
$hookTime += $this->calculateWeightedActionTimes($this->hooks['after']->getActions());
188+
foreach ([TestObjectExtractor::TEST_BEFORE_HOOK, TestObjectExtractor::TEST_AFTER_HOOK] as $hookName) {
189+
if (array_key_exists($hookName, $this->hooks)) {
190+
$hookTime += $this->calculateWeightedActionTimes($this->hooks[$hookName]->getActions());
191+
}
192192
}
193193

194194
$testTime = $this->calculateWeightedActionTimes($this->getOrderedActions());

src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,66 @@ public function resolveActionSteps($parsedSteps, $skipActionGroupResolution = fa
8383
return $this->orderedSteps;
8484
}
8585

86-
return $this->resolveActionGroups($this->orderedSteps);
86+
$resolvedActions = $this->resolveActionGroups($this->orderedSteps);
87+
return $this->resolveSecretFieldAccess($resolvedActions);
88+
}
89+
90+
/**
91+
* Takes an array of actions and resolves any references to secret fields. The function then validates whether the
92+
* refernece is valid and replaces the function name accordingly to hide arguments at runtime.
93+
*
94+
* @param ActionObject[] $resolvedActions
95+
* @return ActionObject[]
96+
* @throws TestReferenceException
97+
*/
98+
private function resolveSecretFieldAccess($resolvedActions)
99+
{
100+
$actions = [];
101+
foreach ($resolvedActions as $resolvedAction) {
102+
$action = $resolvedAction;
103+
$hasSecretRef = $this->actionAttributeContainsSecretRef($resolvedAction->getCustomActionAttributes());
104+
105+
if ($resolvedAction->getType() !== 'fillField' && $hasSecretRef) {
106+
throw new TestReferenceException("You cannot reference secret data outside of fill field actions");
107+
}
108+
109+
if ($resolvedAction->getType() === 'fillField' && $hasSecretRef) {
110+
$action = new ActionObject(
111+
$action->getStepKey(),
112+
'fillSecretField',
113+
$action->getCustomActionAttributes(),
114+
$action->getLinkedAction(),
115+
$action->getActionOrigin()
116+
);
117+
}
118+
119+
$actions[$action->getStepKey()] = $action;
120+
}
121+
122+
return $actions;
123+
}
124+
125+
/**
126+
* Returns a boolean based on whether or not the action attributes contain a reference to a secret field.
127+
*
128+
* @param array $actionAttributes
129+
* @return boolean
130+
*/
131+
private function actionAttributeContainsSecretRef($actionAttributes)
132+
{
133+
foreach ($actionAttributes as $actionAttribute) {
134+
if (is_array($actionAttribute)) {
135+
return $this->actionAttributeContainsSecretRef($actionAttribute);
136+
}
137+
138+
preg_match_all("/{{_CREDS\.([\w]+)}}/", $actionAttribute, $matches);
139+
140+
if (!empty($matches[0])) {
141+
return true;
142+
}
143+
}
144+
145+
return false;
87146
}
88147

89148
/**

0 commit comments

Comments
 (0)