Skip to content

Commit 93e221c

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

File tree

4 files changed

+145
-70
lines changed

4 files changed

+145
-70
lines changed

docs/credentials.md

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -144,32 +144,54 @@ AWS Secrets Manager offers secret management that supports:
144144
- Audit secret rotation centrally for resources in the AWS Cloud, third-party services, and on-premises
145145

146146
### Prerequisites
147-
- AWS account
148-
- AWS Secrets Manger is created and configured
147+
148+
#### Use AWS Secrets Manager from your own AWS account
149+
150+
- AWS account with Secrets Manager service available
149151
- IAM User or Role is created with appropriate AWS Secrets Manger access permission
150152

153+
#### Use AWS Secrets Manager from other AWS account
154+
155+
- AWS account ID where the AWS Secrets Manager service is hosted
156+
- IAM User or Role with appropriate access permission
157+
151158
### Store secrets in AWS Secrets Manager
152159

160+
153161
#### Secrets format
154-
`Secret Name`, `Secret Key`, `Secret Value` are three key pieces of information to construct an AWS Secret.
155-
`Secret Key` and `Secret Value` can be any content you want to secure, `Secret Name` must follow the format:
162+
163+
`Secret Name` and `Secret Value` are two key pieces of information for creating a secret.
164+
165+
`Secret Value` can be either plaintext or key/value pairs in JSON format.
166+
167+
`Secrets Name` must use the following format:
156168

157169
```conf
158-
mftf/<VENDOR>/<SECRET_KEY>
170+
mftf/<VENDOR>/<YOUR/SECRET/KEY>
159171
```
160172

161-
```conf
162-
# Secret name for carriers_usps_userid
163-
mftf/magento/carriers_usps_userid
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>`
175+
176+
##### Create Secrets using AWS CLI
164177

165-
# Secret key for carriers_usps_userid
166-
carriers_usps_userid
178+
```bash
179+
aws secretsmanager create-secret --name "mftf/magento/shipping/carriers_usps_userid" --description "Carriers USPS user id" --secret-string "1234567"
180+
```
181+
182+
##### Create Secrets using AWS Console
183+
184+
To save the same secret in key/value JSON format, you should use
185+
186+
```conf
187+
# Secret Name
188+
mftf/magento/shipping/carriers_usps_userid
167189
168-
# Secret name for carriers_usps_password
169-
mftf/magento/carriers_usps_password
190+
# Secret Key
191+
shipping/carriers_usps_userid
170192
171-
# Secret key for carriers_usps_password
172-
carriers_usps_password
193+
# Secret Value
194+
1234567
173195
```
174196

175197
### Setup MFTF to use AWS Secrets Manager
@@ -186,6 +208,16 @@ CREDENTIAL_AWS_SECRETS_MANAGER_REGION=us-east-1
186208
CREDENTIAL_AWS_SECRETS_MANAGER_PROFILE=default
187209
```
188210

211+
### Optionally set CREDENTIAL_AWS_ACCOUNT_ID environment variable
212+
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.
216+
217+
```bash
218+
export CREDENTIAL_AWS_ACCOUNT_ID=<Your_12_Digits_AWS_Account_ID>
219+
```
220+
189221
## Configure multiple credential storage
190222

191223
It is possible and sometimes useful to setup and use multiple credential storage at the same time.
@@ -239,4 +271,6 @@ The MFTF tests delivered with Magento application do not use credentials and do
239271
[`CREDENTIAL_VAULT_SECRET_BASE_PATH`]: configuration.md#credential_vault_secret_base_path
240272
[credential chain]: https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/guide_credentials.html
241273
[`CREDENTIAL_AWS_SECRETS_MANAGER_PROFILE`]: configuration.md#credential_aws_secrets_manager_profile
242-
[`CREDENTIAL_AWS_SECRETS_MANAGER_REGION`]: configuration.md#credential_aws_secrets_manager_region
274+
[`CREDENTIAL_AWS_SECRETS_MANAGER_REGION`]: configuration.md#credential_aws_secrets_manager_region
275+
[Key Management Service]: https://aws.amazon.com/kms/
276+
[Amazon Resource Name]: https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html

etc/config/.env.example

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ BROWSER=chrome
3737
#*** To use AWS Secrets Manager to manage _CREDS secrets, uncomment and set region, profile is optional, when omitted, AWS default credential provider chain will be used ***#
3838
#CREDENTIAL_AWS_SECRETS_MANAGER_PROFILE=default
3939
#CREDENTIAL_AWS_SECRETS_MANAGER_REGION=us-east-1
40-
#*** If using non-default AWS account ***#
41-
#CREDENTIAL_AWS_ACCOUNT_ID=
4240

4341
#*** Uncomment these properties to set up a dev environment with symlinked projects ***#
4442
#TESTS_BP=

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

Lines changed: 68 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -57,45 +57,10 @@ public static function getInstance()
5757
*/
5858
private function __construct()
5959
{
60-
// Initialize file storage
61-
try {
62-
$this->credStorage[self::ARRAY_KEY_FOR_FILE] = new FileStorage();
63-
} catch (TestFrameworkException $e) {
64-
}
65-
66-
// Initialize vault storage
67-
$cvAddress = getenv('CREDENTIAL_VAULT_ADDRESS');
68-
$cvSecretPath = getenv('CREDENTIAL_VAULT_SECRET_BASE_PATH');
69-
if ($cvAddress !== false && $cvSecretPath !== false) {
70-
try {
71-
$this->credStorage[self::ARRAY_KEY_FOR_VAULT] = new VaultStorage(
72-
UrlFormatter::format($cvAddress, false),
73-
'/' . trim($cvSecretPath, '/')
74-
);
75-
} catch (TestFrameworkException $e) {
76-
}
77-
}
78-
79-
// Initialize AWS Secrets Manager storage
80-
$awsRegion = getenv('CREDENTIAL_AWS_SECRETS_MANAGER_REGION');
81-
$awsProfile = getenv('CREDENTIAL_AWS_SECRETS_MANAGER_PROFILE');
82-
$awsId = getenv('CREDENTIAL_AWS_ACCOUNT_ID');
83-
if ($awsRegion !== false) {
84-
if ($awsProfile === false) {
85-
$awsProfile = null;
86-
}
87-
if ($awsId === false) {
88-
$awsId = null;
89-
}
90-
try {
91-
$this->credStorage[self::ARRAY_KEY_FOR_AWS_SECRETS_MANAGER] = new AwsSecretsManagerStorage(
92-
$awsRegion,
93-
$awsProfile,
94-
$awsId
95-
);
96-
} catch (TestFrameworkException $e) {
97-
}
98-
}
60+
// Initialize credential storage by defined order of precedence as the following
61+
$this->initializeFileStorage();
62+
$this->initializeVaultStorage();
63+
$this->initializeAwsSecretsManagerStorage();
9964

10065
if (empty($this->credStorage)) {
10166
throw new TestFrameworkException(
@@ -155,4 +120,68 @@ public function decryptAllSecretsInString($string)
155120
return $storage->getAllDecryptedValuesInString($string);
156121
}
157122
}
123+
124+
/**
125+
* Initialize file storage
126+
*
127+
* @return void
128+
*/
129+
private function initializeFileStorage()
130+
{
131+
// Initialize file storage
132+
try {
133+
$this->credStorage[self::ARRAY_KEY_FOR_FILE] = new FileStorage();
134+
} catch (TestFrameworkException $e) {
135+
}
136+
}
137+
138+
/**
139+
* Initialize Vault storage
140+
*
141+
* @return void
142+
*/
143+
private function initializeVaultStorage()
144+
{
145+
// Initialize vault storage
146+
$cvAddress = getenv('CREDENTIAL_VAULT_ADDRESS');
147+
$cvSecretPath = getenv('CREDENTIAL_VAULT_SECRET_BASE_PATH');
148+
if ($cvAddress !== false && $cvSecretPath !== false) {
149+
try {
150+
$this->credStorage[self::ARRAY_KEY_FOR_VAULT] = new VaultStorage(
151+
UrlFormatter::format($cvAddress, false),
152+
'/' . trim($cvSecretPath, '/')
153+
);
154+
} catch (TestFrameworkException $e) {
155+
}
156+
}
157+
}
158+
159+
/**
160+
* Initialize AWS Secrets Manager storage
161+
*
162+
* @return void
163+
*/
164+
private function initializeAwsSecretsManagerStorage()
165+
{
166+
// Initialize AWS Secrets Manager storage
167+
$awsRegion = getenv('CREDENTIAL_AWS_SECRETS_MANAGER_REGION');
168+
$awsProfile = getenv('CREDENTIAL_AWS_SECRETS_MANAGER_PROFILE');
169+
$awsId = getenv('CREDENTIAL_AWS_ACCOUNT_ID');
170+
if (!empty($awsRegion)) {
171+
if (empty($awsProfile)) {
172+
$awsProfile = null;
173+
}
174+
if (empty($awsId)) {
175+
$awsId = null;
176+
}
177+
try {
178+
$this->credStorage[self::ARRAY_KEY_FOR_AWS_SECRETS_MANAGER] = new AwsSecretsManagerStorage(
179+
$awsRegion,
180+
$awsProfile,
181+
$awsId
182+
);
183+
} catch (TestFrameworkException $e) {
184+
}
185+
}
186+
}
158187
}

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

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -115,17 +115,18 @@ public function getEncryptedValue($key)
115115
$reValue = openssl_encrypt($value, parent::ENCRYPTION_ALGO, parent::$encodedKey, 0, parent::$iv);
116116
parent::$cachedSecretData[$key] = $reValue;
117117
} catch (AwsException $e) {
118-
$error = $e->getAwsErrorCode();
118+
$errMessage = "\nAWS Exception:\n" . $e->getAwsErrorMessage()
119+
. "\nUnable to read value for key {$key} from AWS Secrets Manager\n";
120+
print_r($errMessage);
119121
if (MftfApplicationConfig::getConfig()->verboseEnabled()) {
120-
LoggingUtil::getInstance()->getLogger(AwsSecretsManagerStorage::class)->debug(
121-
"AWS error code: {$error}. Unable to read value for key {$key} from AWS Secrets Manager"
122-
);
122+
LoggingUtil::getInstance()->getLogger(AwsSecretsManagerStorage::class)->debug($errMessage);
123123
}
124124
} catch (\Exception $e) {
125+
$errMessage = "\nException:\n" . $e->getMessage()
126+
. "\nUnable to read value for key {$key} from AWS Secrets Manager\n";
127+
print_r($errMessage);
125128
if (MftfApplicationConfig::getConfig()->verboseEnabled()) {
126-
LoggingUtil::getInstance()->getLogger(AwsSecretsManagerStorage::class)->debug(
127-
"Unable to read value for key {$key} from AWS Secrets Manager"
128-
);
129+
LoggingUtil::getInstance()->getLogger(AwsSecretsManagerStorage::class)->debug($errMessage);
129130
}
130131
}
131132
return $reValue;
@@ -145,13 +146,22 @@ private function parseAwsSecretResult($awsResult, $key)
145146
if (isset($awsResult['SecretString'])) {
146147
$rawSecret = $awsResult['SecretString'];
147148
} else {
148-
throw new TestFrameworkException("Error parsing result from AWS Secrets Manager");
149+
throw new TestFrameworkException(
150+
"'SecretString' field is not set in AWS Result. Error parsing result from AWS Secrets Manager"
151+
);
149152
}
153+
154+
// Secrets are saved as JSON structures of key/value pairs if using AWS Secrets Manager console, and
155+
// Secrets are saved as plain text if using AWS CLI. We need to handle both cases.
150156
$secret = json_decode($rawSecret, true);
151157
if (isset($secret[$key])) {
152158
return $secret[$key];
159+
} elseif (is_string($rawSecret)) {
160+
return $rawSecret;
153161
}
154-
throw new TestFrameworkException("Error parsing result from AWS Secrets Manager");
162+
throw new TestFrameworkException(
163+
"$key not found or value is not string . Error parsing result from AWS Secrets Manager"
164+
);
155165
}
156166

157167
/**
@@ -169,13 +179,17 @@ private function createAwsSecretsManagerClient($region, $profile)
169179
return;
170180
}
171181

172-
// Create AWS Secrets Manager client
173-
$this->client = new SecretsManagerClient([
174-
'profile' => $profile,
182+
$options = [
175183
'region' => $region,
176-
'version' => self::LATEST_VERSION
177-
]);
184+
'version' => self::LATEST_VERSION,
185+
];
178186

187+
if (!empty($profile)) {
188+
$options['profile'] = $profile;
189+
}
190+
191+
// Create AWS Secrets Manager client
192+
$this->client = new SecretsManagerClient($options);
179193
if ($this->client === null) {
180194
throw new TestFrameworkException("Unable to create AWS Secrets Manager client");
181195
}

0 commit comments

Comments
 (0)