Skip to content

Commit c0a885c

Browse files
committed
MQE-1919: MFTF AWS Secrets Manager - CI Use
1 parent 93e221c commit c0a885c

File tree

9 files changed

+231
-53
lines changed

9 files changed

+231
-53
lines changed

docs/credentials.md

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -147,31 +147,33 @@ AWS Secrets Manager offers secret management that supports:
147147

148148
#### Use AWS Secrets Manager from your own AWS account
149149

150-
- AWS account with Secrets Manager service available
151-
- IAM User or Role is created with appropriate AWS Secrets Manger access permission
150+
- An AWS account with Secrets Manager service
151+
- An IAM user with AWS Secrets Manager access permission
152152

153-
#### Use AWS Secrets Manager from other AWS account
153+
#### Use AWS Secrets Manager in CI/CD
154154

155155
- AWS account ID where the AWS Secrets Manager service is hosted
156-
- IAM User or Role with appropriate access permission
156+
- Authorized CI/CD EC2 instances with AWS Secrets Manager service access IAM role attached
157157

158158
### Store secrets in AWS Secrets Manager
159159

160-
161160
#### Secrets format
162161

163162
`Secret Name` and `Secret Value` are two key pieces of information for creating a secret.
164163

165164
`Secret Value` can be either plaintext or key/value pairs in JSON format.
166165

167-
`Secrets Name` must use the following format:
166+
`Secret Name` must use the following format:
168167

169168
```conf
170169
mftf/<VENDOR>/<YOUR/SECRET/KEY>
171170
```
172171

173-
`Secrets Value` in plaintext format can be any content you want to secure. `Secrets Value` in key/value pairs format, however, the `key` must be same as the `Secret Name` with `mftf/<VENDOR>/` part removed.
174-
e.g. in above example, `key` should be `<YOUR/SECRET/KEY>`
172+
`Secret Value` can be stored in two different formats: plaintext or key/value pairs.
173+
174+
For plaintext format, `Secret Value` can be any string you want to secure.
175+
176+
For key/value pairs format, `Secret Value` is a key/value pair with `key` the same as `Secret Name` without `mftf/<VENDOR>/` prefix, which is `<YOUR/SECRET/KEY>`, and value can be any string you want to secure.
175177

176178
##### Create Secrets using AWS CLI
177179

@@ -181,8 +183,11 @@ aws secretsmanager create-secret --name "mftf/magento/shipping/carriers_usps_use
181183

182184
##### Create Secrets using AWS Console
183185

184-
To save the same secret in key/value JSON format, you should use
185-
186+
- Sign in to the AWS Secrets Manager console
187+
- Choose Store a new secret
188+
- In the Select secret type section, specify "Other type of secret"
189+
- For `Secret Name`, `Secret Key` and `Secret Value` field, for example, to save the same secret in key/value JSON format, you should use
190+
186191
```conf
187192
# Secret Name
188193
mftf/magento/shipping/carriers_usps_userid
@@ -210,9 +215,8 @@ CREDENTIAL_AWS_SECRETS_MANAGER_PROFILE=default
210215

211216
### Optionally set CREDENTIAL_AWS_ACCOUNT_ID environment variable
212217

213-
Full AWS KMS ([Key Management Service][]) key ARN ([Amazon Resource Name][]) is required when accessing secrets stored in other AWS account.
214-
If this is the case, you will also need to set `CREDENTIAL_AWS_ACCOUNT_ID` environment variable so that MFTF can construct the full ARN.
215-
This is also commonly used in CI system.
218+
In case AWS credentials cannot resolve to a valid AWS account, full AWS KMS ([Key Management Service][]) key ARN ([Amazon Resource Name][]) is required.
219+
You will also need to set `CREDENTIAL_AWS_ACCOUNT_ID` environment variable so that MFTF can construct the full ARN. This is mostly used for CI/CD.
216220

217221
```bash
218222
export CREDENTIAL_AWS_ACCOUNT_ID=<Your_12_Digits_AWS_Account_ID>
@@ -236,7 +240,7 @@ Define the value as a reference to the corresponding key in the credentials file
236240

237241
- `_CREDS` is an environment constant pointing to the `.credentials` file
238242
- `my_data_key` is a key in the the `.credentials` file or vault that contains the value to be used in a test step
239-
- for File Storage, ensure your key contains the vendor prefix, i.e. `vendor/my_data_key`
243+
- for File Storage, ensure your key contains the vendor prefix, which is `vendor/my_data_key`
240244

241245
For example, to reference secret data in the [`fillField`][] action, use the `userInput` attribute using a typical File Storage:
242246

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

Lines changed: 131 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
namespace Magento\FunctionalTestingFramework\DataGenerator\Handlers;
88

9+
use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector;
910
use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException;
11+
use Magento\FunctionalTestingFramework\DataGenerator\Handlers\SecretStorage\BaseStorage;
1012
use Magento\FunctionalTestingFramework\DataGenerator\Handlers\SecretStorage\FileStorage;
1113
use Magento\FunctionalTestingFramework\DataGenerator\Handlers\SecretStorage\VaultStorage;
1214
use Magento\FunctionalTestingFramework\DataGenerator\Handlers\SecretStorage\AwsSecretsManagerStorage;
@@ -24,22 +26,35 @@ class CredentialStore
2426
/**
2527
* Credential storage array
2628
*
27-
* @var array
29+
* @var BaseStorage[]
2830
*/
2931
private $credStorage = [];
3032

33+
/**
34+
* Boolean to indicate if credential storage have been initialized
35+
*
36+
* @var boolean
37+
*/
38+
private $initialized;
39+
3140
/**
3241
* Singleton instance
3342
*
3443
* @var CredentialStore
3544
*/
3645
private static $INSTANCE = null;
3746

47+
/**
48+
* Exception contexts
49+
*
50+
* @var ExceptionCollector
51+
*/
52+
private $exceptionContexts;
53+
3854
/**
3955
* Static singleton getter for CredentialStore Instance
4056
*
4157
* @return CredentialStore
42-
* @throws TestFrameworkException
4358
*/
4459
public static function getInstance()
4560
{
@@ -52,21 +67,11 @@ public static function getInstance()
5267

5368
/**
5469
* CredentialStore constructor
55-
*
56-
* @throws TestFrameworkException
5770
*/
5871
private function __construct()
5972
{
60-
// Initialize credential storage by defined order of precedence as the following
61-
$this->initializeFileStorage();
62-
$this->initializeVaultStorage();
63-
$this->initializeAwsSecretsManagerStorage();
64-
65-
if (empty($this->credStorage)) {
66-
throw new TestFrameworkException(
67-
'Invalid Credential Storage. ' . self::CREDENTIAL_STORAGE_INFO . '.'
68-
);
69-
}
73+
$this->initialized = false;
74+
$this->exceptionContexts = new ExceptionCollector();
7075
}
7176

7277
/**
@@ -78,6 +83,9 @@ private function __construct()
7883
*/
7984
public function getSecret($key)
8085
{
86+
// Initialize credential storage if it's not been done
87+
$this->initializeCredentialStorage();
88+
8189
// Get secret data from storage according to the order they are stored which follows this precedence:
8290
// FileStorage > VaultStorage > AwsSecretsManagerStorage
8391
foreach ($this->credStorage as $storage) {
@@ -87,38 +95,125 @@ public function getSecret($key)
8795
}
8896
}
8997

98+
$exceptionContexts = $this->getExceptionContexts();
99+
$this->resetExceptionContext();
90100
throw new TestFrameworkException(
91101
"{$key} not found. " . self::CREDENTIAL_STORAGE_INFO
92-
. ' and ensure key, value exists to use _CREDS in tests.'
102+
. " and ensure key, value exists to use _CREDS in tests."
103+
. $exceptionContexts
93104
);
94105
}
95106

96107
/**
97108
* Return decrypted input value
98109
*
99110
* @param string $value
100-
* @return string
111+
* @return string|false The decrypted string on success or false on failure
112+
* @throws TestFrameworkException
101113
*/
102114
public function decryptSecretValue($value)
103115
{
104-
// Loop through storage to decrypt value
105-
foreach ($this->credStorage as $storage) {
106-
return $storage->getDecryptedValue($value);
107-
}
116+
// Initialize credential storage if it's not been done
117+
$this->initializeCredentialStorage();
118+
119+
// Decrypt secret value
120+
return BaseStorage::getDecryptedValue($value);
108121
}
109122

110123
/**
111124
* Return decrypted values for all occurrences from input string
112125
*
113126
* @param string $string
114-
* @return mixed
127+
* @return string|false The decrypted string on success or false on failure
128+
* @throws TestFrameworkException
115129
*/
116130
public function decryptAllSecretsInString($string)
117131
{
118-
// Loop through storage to decrypt all occurrences from input string
119-
foreach ($this->credStorage as $storage) {
120-
return $storage->getAllDecryptedValuesInString($string);
132+
// Initialize credential storage if it's not been done
133+
$this->initializeCredentialStorage();
134+
135+
// Decrypt all secret values in string
136+
return BaseStorage::getAllDecryptedValuesInString($string);
137+
}
138+
139+
/**
140+
* Setter for exception contexts
141+
*
142+
* @param string $type
143+
* @param string $context
144+
* @return void
145+
*/
146+
public function setExceptionContexts($type, $context)
147+
{
148+
$typeArray = [self::ARRAY_KEY_FOR_FILE, self::ARRAY_KEY_FOR_VAULT, self::ARRAY_KEY_FOR_AWS_SECRETS_MANAGER];
149+
if (in_array($type, $typeArray) && !empty($context)) {
150+
$this->exceptionContexts->addError($type, $context);
151+
}
152+
}
153+
154+
/**
155+
* Return collected exception contexts
156+
*
157+
* @return string
158+
*/
159+
private function getExceptionContexts()
160+
{
161+
// Gather all exceptions collected
162+
$exceptionMessage = "\n";
163+
foreach ($this->exceptionContexts->getErrors() as $type => $exceptions) {
164+
$exceptionMessage .= "\nException from ";
165+
if ($type == self::ARRAY_KEY_FOR_FILE) {
166+
$exceptionMessage .= "File Storage: \n";
167+
}
168+
if ($type == self::ARRAY_KEY_FOR_VAULT) {
169+
$exceptionMessage .= "Vault Storage: \n";
170+
}
171+
if ($type == self::ARRAY_KEY_FOR_AWS_SECRETS_MANAGER) {
172+
$exceptionMessage .= "AWS Secrets Manager Storage: \n";
173+
}
174+
175+
if (is_array($exceptions)) {
176+
$exceptionMessage .= implode("\n", $exceptions) . "\n";
177+
} else {
178+
$exceptionMessage .= $exceptions . "\n";
179+
}
180+
}
181+
return $exceptionMessage;
182+
}
183+
184+
/**
185+
* Reset exception contexts to empty array
186+
*
187+
* @return void
188+
*/
189+
private function resetExceptionContext()
190+
{
191+
$this->exceptionContexts->reset();
192+
}
193+
194+
/**
195+
* Initialize all available credential storage
196+
*
197+
* @return void
198+
* @throws TestFrameworkException
199+
*/
200+
private function initializeCredentialStorage()
201+
{
202+
if (!$this->initialized) {
203+
// Initialize credential storage by defined order of precedence as the following
204+
$this->initializeFileStorage();
205+
$this->initializeVaultStorage();
206+
$this->initializeAwsSecretsManagerStorage();
207+
$this->initialized = true;
208+
}
209+
210+
if (empty($this->credStorage)) {
211+
throw new TestFrameworkException(
212+
'Invalid Credential Storage. ' . self::CREDENTIAL_STORAGE_INFO
213+
. '.' . $this->getExceptionContexts()
214+
);
121215
}
216+
$this->resetExceptionContext();
122217
}
123218

124219
/**
@@ -132,6 +227,10 @@ private function initializeFileStorage()
132227
try {
133228
$this->credStorage[self::ARRAY_KEY_FOR_FILE] = new FileStorage();
134229
} catch (TestFrameworkException $e) {
230+
// Print error message in console
231+
print_r($e->getMessage());
232+
// Save to exception context for Allure report
233+
$this->setExceptionContexts(self::ARRAY_KEY_FOR_FILE, $e->getMessage());
135234
}
136235
}
137236

@@ -152,6 +251,10 @@ private function initializeVaultStorage()
152251
'/' . trim($cvSecretPath, '/')
153252
);
154253
} catch (TestFrameworkException $e) {
254+
// Print error message in console
255+
print_r($e->getMessage());
256+
// Save to exception context for Allure report
257+
$this->setExceptionContexts(self::ARRAY_KEY_FOR_VAULT, $e->getMessage());
155258
}
156259
}
157260
}
@@ -181,6 +284,10 @@ private function initializeAwsSecretsManagerStorage()
181284
$awsId
182285
);
183286
} catch (TestFrameworkException $e) {
287+
// Print error message in console
288+
print_r($e->getMessage());
289+
// Save to exception context for Allure report
290+
$this->setExceptionContexts(self::ARRAY_KEY_FOR_AWS_SECRETS_MANAGER, $e->getMessage());
184291
}
185292
}
186293
}

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,12 @@ public function createEntity(
8989

9090
foreach ($overrideFields as $index => $field) {
9191
try {
92-
$overrideFields[$index] = CredentialStore::getInstance()->decryptAllSecretsInString($field);
92+
$decrptedField = CredentialStore::getInstance()->decryptAllSecretsInString($field);
93+
if ($decrptedField !== false) {
94+
$overrideFields[$index] = $decrptedField;
95+
}
9396
} catch (TestFrameworkException $e) {
94-
//do not rethrow if Credentials are not defined
95-
$overrideFields[$index] = $field;
97+
//catch exception if Credentials are not defined
9698
}
9799
}
98100

src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/AwsSecretsManagerStorage.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
namespace Magento\FunctionalTestingFramework\DataGenerator\Handlers\SecretStorage;
88

99
use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig;
10+
use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore;
1011
use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException;
1112
use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil;
1213
use Aws\SecretsManager\SecretsManagerClient;
@@ -117,17 +118,31 @@ public function getEncryptedValue($key)
117118
} catch (AwsException $e) {
118119
$errMessage = "\nAWS Exception:\n" . $e->getAwsErrorMessage()
119120
. "\nUnable to read value for key {$key} from AWS Secrets Manager\n";
121+
// Print error message in console
120122
print_r($errMessage);
123+
// Add error message in mftf log if verbose is enable
121124
if (MftfApplicationConfig::getConfig()->verboseEnabled()) {
122125
LoggingUtil::getInstance()->getLogger(AwsSecretsManagerStorage::class)->debug($errMessage);
123126
}
127+
// Save to exception context for Allure report
128+
CredentialStore::getInstance()->setExceptionContexts(
129+
CredentialStore::ARRAY_KEY_FOR_AWS_SECRETS_MANAGER,
130+
$errMessage
131+
);
124132
} catch (\Exception $e) {
125133
$errMessage = "\nException:\n" . $e->getMessage()
126134
. "\nUnable to read value for key {$key} from AWS Secrets Manager\n";
135+
// Print error message in console
127136
print_r($errMessage);
137+
// Add error message in mftf log if verbose is enable
128138
if (MftfApplicationConfig::getConfig()->verboseEnabled()) {
129139
LoggingUtil::getInstance()->getLogger(AwsSecretsManagerStorage::class)->debug($errMessage);
130140
}
141+
// Save to exception context for Allure report
142+
CredentialStore::getInstance()->setExceptionContexts(
143+
CredentialStore::ARRAY_KEY_FOR_AWS_SECRETS_MANAGER,
144+
$errMessage
145+
);
131146
}
132147
return $reValue;
133148
}

0 commit comments

Comments
 (0)