Skip to content

Commit 49ee0c5

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

File tree

4 files changed

+129
-28
lines changed

4 files changed

+129
-28
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: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,31 @@ class CredentialStore
2828
*/
2929
private $credStorage = [];
3030

31+
/**
32+
* Boolean to indicate if credential storage have been initialized
33+
*
34+
* @var boolean
35+
*/
36+
private $initialized;
37+
3138
/**
3239
* Singleton instance
3340
*
3441
* @var CredentialStore
3542
*/
3643
private static $INSTANCE = null;
3744

45+
/**
46+
* Exception contexts
47+
*
48+
* @var array
49+
*/
50+
private $exceptionContexts = [
51+
self::ARRAY_KEY_FOR_FILE => [],
52+
self::ARRAY_KEY_FOR_VAULT => [],
53+
self::ARRAY_KEY_FOR_AWS_SECRETS_MANAGER => [],
54+
];
55+
3856
/**
3957
* Static singleton getter for CredentialStore Instance
4058
*
@@ -57,16 +75,7 @@ public static function getInstance()
5775
*/
5876
private function __construct()
5977
{
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-
}
78+
$this->initialized = false;
7079
}
7180

7281
/**
@@ -78,6 +87,9 @@ private function __construct()
7887
*/
7988
public function getSecret($key)
8089
{
90+
// Initialize credential storage if it's not been done
91+
$this->initializeCredentialStorage();
92+
8193
// Get secret data from storage according to the order they are stored which follows this precedence:
8294
// FileStorage > VaultStorage > AwsSecretsManagerStorage
8395
foreach ($this->credStorage as $storage) {
@@ -87,9 +99,31 @@ public function getSecret($key)
8799
}
88100
}
89101

102+
// Attach all exceptions collected
103+
$exceptionMessage = '';
104+
foreach ($this->exceptionContexts as $type => $exceptionContexts) {
105+
if (empty($this->exceptionContexts[$type])) {
106+
continue;
107+
}
108+
109+
$exceptionMessage .= "\nException from ";
110+
if ($type == self::ARRAY_KEY_FOR_FILE) {
111+
$exceptionMessage .= "File Storage: \n";
112+
}
113+
if ($type == self::ARRAY_KEY_FOR_VAULT) {
114+
$exceptionMessage .= "Vault Storage: \n";
115+
}
116+
if ($type == self::ARRAY_KEY_FOR_AWS_SECRETS_MANAGER) {
117+
$exceptionMessage .= "AWS Secrets Manager Storage: \n";
118+
}
119+
120+
$exceptionMessage .= implode("\n", $this->exceptionContexts[$type]) . "\n";
121+
}
122+
90123
throw new TestFrameworkException(
91124
"{$key} not found. " . self::CREDENTIAL_STORAGE_INFO
92-
. ' and ensure key, value exists to use _CREDS in tests.'
125+
. " and ensure key, value exists to use _CREDS in tests."
126+
. $exceptionMessage
93127
);
94128
}
95129

@@ -121,6 +155,43 @@ public function decryptAllSecretsInString($string)
121155
}
122156
}
123157

158+
/**
159+
* Setter for exception contexts
160+
*
161+
* @param string $type
162+
* @param string $context
163+
* @return void
164+
*/
165+
public function setExceptionContexts($type, $context)
166+
{
167+
if (array_key_exists($type, $this->exceptionContexts) && !empty($context)) {
168+
$this->exceptionContexts[$type][] = $context;
169+
}
170+
}
171+
172+
/**
173+
* Initialize all available credential storage
174+
*
175+
* @return void
176+
* @throws TestFrameworkException
177+
*/
178+
private function initializeCredentialStorage()
179+
{
180+
if (!$this->initialized) {
181+
// Initialize credential storage by defined order of precedence as the following
182+
$this->initializeFileStorage();
183+
$this->initializeVaultStorage();
184+
$this->initializeAwsSecretsManagerStorage();
185+
$this->initialized = true;
186+
if (empty($this->credStorage)) {
187+
$this->initialized = true;
188+
throw new TestFrameworkException(
189+
'Invalid Credential Storage. ' . self::CREDENTIAL_STORAGE_INFO . '.'
190+
);
191+
}
192+
}
193+
}
194+
124195
/**
125196
* Initialize file storage
126197
*
@@ -132,6 +203,10 @@ private function initializeFileStorage()
132203
try {
133204
$this->credStorage[self::ARRAY_KEY_FOR_FILE] = new FileStorage();
134205
} catch (TestFrameworkException $e) {
206+
// Print error message in console
207+
print_r($e->getMessage());
208+
// Save to exception context for Allure report
209+
$this->setExceptionContexts('file', $e->getMessage());
135210
}
136211
}
137212

@@ -152,6 +227,10 @@ private function initializeVaultStorage()
152227
'/' . trim($cvSecretPath, '/')
153228
);
154229
} 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('vault', $e->getMessage());
155234
}
156235
}
157236
}

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

Lines changed: 9 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,25 @@ 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('aws', $errMessage);
124129
} catch (\Exception $e) {
125130
$errMessage = "\nException:\n" . $e->getMessage()
126131
. "\nUnable to read value for key {$key} from AWS Secrets Manager\n";
132+
// Print error message in console
127133
print_r($errMessage);
134+
// Add error message in mftf log if verbose is enable
128135
if (MftfApplicationConfig::getConfig()->verboseEnabled()) {
129136
LoggingUtil::getInstance()->getLogger(AwsSecretsManagerStorage::class)->debug($errMessage);
130137
}
138+
// Save to exception context for Allure report
139+
CredentialStore::getInstance()->setExceptionContexts('aws', $errMessage);
131140
}
132141
return $reValue;
133142
}

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

Lines changed: 12 additions & 3 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 Vault\Client;
@@ -123,10 +124,14 @@ public function getEncryptedValue($key)
123124
$reValue = openssl_encrypt($value, parent::ENCRYPTION_ALGO, parent::$encodedKey, 0, parent::$iv);
124125
parent::$cachedSecretData[$key] = $reValue;
125126
} catch (\Exception $e) {
127+
$errMessage = "\nUnable to read secret for key name {$key} from vault." . $e->getMessage();
128+
// Print error message in console
129+
print_r($errMessage);
130+
// Save to exception context for Allure report
131+
CredentialStore::getInstance()->setExceptionContexts('vault', $errMessage);
132+
// Add error message in mftf log if verbose is enable
126133
if (MftfApplicationConfig::getConfig()->verboseEnabled()) {
127-
LoggingUtil::getInstance()->getLogger(VaultStorage::class)->debug(
128-
"Unable to read secret for key name {$key} from vault"
129-
);
134+
LoggingUtil::getInstance()->getLogger(VaultStorage::class)->debug($errMessage);
130135
}
131136
}
132137
return $reValue;
@@ -149,6 +154,10 @@ private function authenticated()
149154
return true;
150155
}
151156
} catch (\Exception $e) {
157+
// Print error message in console
158+
print_r($e->getMessage());
159+
// Save to exception context for Allure report
160+
CredentialStore::getInstance()->setExceptionContexts('vault', $e->getMessage());
152161
}
153162
return false;
154163
}

0 commit comments

Comments
 (0)