1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2026-06-16 00:25:48 +02:00

Support for S3 EKS pod identity.

Fetch credentials automatically using EKS pod identity, which removes the need for static configuration. Credentials are automatically updated before they expire to support long-running commands.
This commit is contained in:
wolrajhti
2026-01-09 09:18:28 +01:00
committed by GitHub
parent 91ad65537f
commit 79544f64a3
10 changed files with 220 additions and 21 deletions
+12
View File
@@ -57,6 +57,18 @@
<p>Support for <proper>Azure</proper> managed identities.</p>
</release-item>
<release-item>
<github-issue id="2718"/>
<github-pull-request id="2719"/>
<release-item-contributor-list>
<release-item-contributor id="pierre.bouteloup"/>
<release-item-reviewer id="david.steele"/>
</release-item-contributor-list>
<p><i><b>Experimental</b></i> support for <proper>S3</proper> EKS pod identity.</p>
</release-item>
<release-item>
<github-issue id="2697"/>
<github-pull-request id="2700"/>
+5
View File
@@ -935,6 +935,11 @@
<contributor-id type="github">philrhurst</contributor-id>
</contributor>
<contributor id="pierre.bouteloup">
<contributor-name-display>Pierre BOUTELOUP</contributor-name-display>
<contributor-id type="github">wolrajhti</contributor-id>
</contributor>
<contributor id="pierre.ducroquet">
<contributor-name-display>Pierre Ducroquet</contributor-name-display>
<contributor-id type="github">PierreDucroquet</contributor-id>
+1
View File
@@ -2311,6 +2311,7 @@ option:
- shared
- auto
- web-id
- pod-id
repo-s3-key:
section: global
+2
View File
@@ -284,6 +284,8 @@ Option value constants
#define CFGOPTVAL_REPO_S3_KEY_TYPE_AUTO STRID5("auto", 0x7d2a10)
#define CFGOPTVAL_REPO_S3_KEY_TYPE_AUTO_Z "auto"
#define CFGOPTVAL_REPO_S3_KEY_TYPE_POD_ID STRID5("pod-id", 0x89d91f00)
#define CFGOPTVAL_REPO_S3_KEY_TYPE_POD_ID_Z "pod-id"
#define CFGOPTVAL_REPO_S3_KEY_TYPE_SHARED STRID5("shared", 0x85905130)
#define CFGOPTVAL_REPO_S3_KEY_TYPE_SHARED_Z "shared"
#define CFGOPTVAL_REPO_S3_KEY_TYPE_WEB_ID STRID5("web-id", 0x89d88b70)
+6
View File
@@ -109,6 +109,7 @@ static const StringPubConst parseRuleValueStr[] =
PARSE_RULE_STRPUB("pause"), // val/str
PARSE_RULE_STRPUB("pg"), // val/str
PARSE_RULE_STRPUB("pgbackrest"), // val/str
PARSE_RULE_STRPUB("pod-id"), // val/str
PARSE_RULE_STRPUB("posix"), // val/str
PARSE_RULE_STRPUB("postgres"), // val/str
PARSE_RULE_STRPUB("prefer"), // val/str
@@ -243,6 +244,7 @@ typedef enum
parseRuleValStrQT_pause_QT, // val/str/enum
parseRuleValStrQT_pg_QT, // val/str/enum
parseRuleValStrQT_pgbackrest_QT, // val/str/enum
parseRuleValStrQT_pod_DS_id_QT, // val/str/enum
parseRuleValStrQT_posix_QT, // val/str/enum
parseRuleValStrQT_postgres_QT, // val/str/enum
parseRuleValStrQT_prefer_QT, // val/str/enum
@@ -318,6 +320,7 @@ static const StringId parseRuleValueStrId[] =
STRID5("path", 0x450300), // val/strid
STRID5("pause", 0x59d4300), // val/strid
STRID5("pg", 0xf00), // val/strid
STRID5("pod-id", 0x89d91f00), // val/strid
STRID5("posix", 0x184cdf00), // val/strid
STRID5("prefer", 0x245316500), // val/strid
STRID5("preserve", 0x2da45996500), // val/strid
@@ -383,6 +386,7 @@ static const uint8_t parseRuleValueStrIdStrMap[] =
parseRuleValStrQT_path_QT, // val/strid/strmap
parseRuleValStrQT_pause_QT, // val/strid/strmap
parseRuleValStrQT_pg_QT, // val/strid/strmap
parseRuleValStrQT_pod_DS_id_QT, // val/strid/strmap
parseRuleValStrQT_posix_QT, // val/strid/strmap
parseRuleValStrQT_prefer_QT, // val/strid/strmap
parseRuleValStrQT_preserve_QT, // val/strid/strmap
@@ -448,6 +452,7 @@ typedef enum
parseRuleValStrIdPath, // val/strid/enum
parseRuleValStrIdPause, // val/strid/enum
parseRuleValStrIdPg, // val/strid/enum
parseRuleValStrIdPodId, // val/strid/enum
parseRuleValStrIdPosix, // val/strid/enum
parseRuleValStrIdPrefer, // val/strid/enum
parseRuleValStrIdPreserve, // val/strid/enum
@@ -8033,6 +8038,7 @@ static const ParseRuleOption parseRuleOption[CFG_OPTION_TOTAL] =
PARSE_RULE_VAL_STRID(Shared), // opt/repo-s3-key-type
PARSE_RULE_VAL_STRID(Auto), // opt/repo-s3-key-type
PARSE_RULE_VAL_STRID(WebId), // opt/repo-s3-key-type
PARSE_RULE_VAL_STRID(PodId), // opt/repo-s3-key-type
), // opt/repo-s3-key-type
// opt/repo-s3-key-type
PARSE_RULE_OPTIONAL_DEFAULT // opt/repo-s3-key-type
+26 -3
View File
@@ -57,7 +57,8 @@ storageS3Helper(const unsigned int repoIdx, const bool write, StoragePathExpress
// Get role and token
const StorageS3KeyType keyType = (StorageS3KeyType)cfgOptionIdxStrId(cfgOptRepoS3KeyType, repoIdx);
const String *role = cfgOptionIdxStrNull(cfgOptRepoS3Role, repoIdx);
const String *webIdTokenFile = NULL;
const String *tokenFile = NULL;
const String *credUrl = NULL;
// If web identity authentication then load the role and token filename from environment variables documented here:
// https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts-technical-overview.html
@@ -79,7 +80,29 @@ storageS3Helper(const unsigned int repoIdx, const bool write, StoragePathExpress
}
role = strNewZ(roleZ);
webIdTokenFile = strNewZ(webIdTokenFileZ);
tokenFile = strNewZ(webIdTokenFileZ);
}
// If pod identity authentication then load the credentials url and token filename from environment variables documented
// here: https://docs.aws.amazon.com/eks/latest/userguide/pod-id-how-it-works.html
else if (keyType == storageS3KeyTypePodId)
{
#define S3_ENV_AWS_CONTAINER_CREDENTIALS_FULL_URI "AWS_CONTAINER_CREDENTIALS_FULL_URI"
#define S3_ENV_AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE"
const char *const credUrlZ = getenv(S3_ENV_AWS_CONTAINER_CREDENTIALS_FULL_URI);
const char *const podIdTokenFileZ = getenv(S3_ENV_AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE);
if (credUrlZ == NULL || podIdTokenFileZ == NULL)
{
THROW_FMT(
OptionInvalidError,
"option '%s' is '" CFGOPTVAL_REPO_S3_KEY_TYPE_POD_ID_Z "' but '" S3_ENV_AWS_CONTAINER_CREDENTIALS_FULL_URI "'"
" and '" S3_ENV_AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE "' are not set",
cfgOptionIdxName(cfgOptRepoS3KeyType, repoIdx));
}
credUrl = strNewZ(credUrlZ);
tokenFile = strNewZ(podIdTokenFileZ);
}
MEM_CONTEXT_PRIOR_BEGIN()
@@ -90,7 +113,7 @@ storageS3Helper(const unsigned int repoIdx, const bool write, StoragePathExpress
(StorageS3UriStyle)cfgOptionIdxStrId(cfgOptRepoS3UriStyle, repoIdx), cfgOptionIdxStr(cfgOptRepoS3Region, repoIdx),
keyType, cfgOptionIdxStrNull(cfgOptRepoS3Key, repoIdx), cfgOptionIdxStrNull(cfgOptRepoS3KeySecret, repoIdx),
cfgOptionIdxStrNull(cfgOptRepoS3Token, repoIdx), cfgOptionIdxStrNull(cfgOptRepoS3KmsKeyId, repoIdx),
cfgOptionIdxStrNull(cfgOptRepoS3SseCustomerKey, repoIdx), role, webIdTokenFile,
cfgOptionIdxStrNull(cfgOptRepoS3SseCustomerKey, repoIdx), role, tokenFile, credUrl,
(size_t)cfgOptionIdxUInt64(cfgOptRepoStorageUploadChunkSize, repoIdx),
cfgOptionIdxKvNull(cfgOptRepoStorageTag, repoIdx), host, port, ioTimeoutMs(), protocolType,
cfgOptionIdxBool(cfgOptRepoStorageVerifyTls, repoIdx), cfgOptionIdxStrNull(cfgOptRepoStorageCaFile, repoIdx),
+82 -9
View File
@@ -117,8 +117,9 @@ struct StorageS3
// For retrieving temporary security credentials
HttpClient *credHttpClient; // HTTP client to service credential requests
const String *credHost; // Credentials host
const String *credPath; // Credential url path
const String *credRole; // Role to use for credential requests
const String *webIdTokenFile; // File containing token to use for web-id credential requests
const String *tokenFile; // File with token to use for web-id/pod-id credential requests
time_t credExpirationTime; // Time the temporary credentials expire
// Current signing key and date it is valid for
@@ -395,8 +396,7 @@ storageS3AuthWebId(StorageS3 *const this, const HttpHeader *const header)
MEM_CONTEXT_TEMP_BEGIN()
{
// Load the token from the given file for each request since the token may be updated during execution
const String *const webIdToken = strNewBuf(
storageGetP(storageNewReadP(storagePosixNewP(FSLASH_STR), this->webIdTokenFile)));
const String *const webIdToken = strNewBuf(storageGetP(storageNewReadP(storagePosixNewP(FSLASH_STR), this->tokenFile)));
// Get credentials
HttpQuery *const query = httpQueryNewP();
@@ -440,6 +440,56 @@ storageS3AuthWebId(StorageS3 *const this, const HttpHeader *const header)
FUNCTION_LOG_RETURN_VOID();
}
/***********************************************************************************************************************************
Automatically get credentials for an associated pod identity
The AWS documentation does not appear to provide a clear example of how to perform this authorization without using their SDK but
this article proved helpful: https://securitylabs.datadoghq.com/articles/eks-pod-identity-deep-dive
***********************************************************************************************************************************/
static void
storageS3AuthPodId(StorageS3 *const this, const HttpHeader *const header)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(STORAGE_S3, this);
FUNCTION_LOG_PARAM(HTTP_HEADER, header);
FUNCTION_LOG_END();
MEM_CONTEXT_TEMP_BEGIN()
{
// Load the token from the given file for each request since the token may be updated during execution
const String *const podIdToken = strNewBuf(storageGetP(storageNewReadP(storagePosixNewP(FSLASH_STR), this->tokenFile)));
// Get credentials
HttpRequest *const request = httpRequestNewP(
this->credHttpClient, HTTP_VERB_GET_STR, this->credPath,
.header = httpHeaderAdd(httpHeaderDup(header, NULL), HTTP_HEADER_AUTHORIZATION_STR, podIdToken));
HttpResponse *const response = httpRequestResponse(request, true);
CHECK(FormatError, httpResponseCode(response) != HTTP_RESPONSE_CODE_NOT_FOUND, "invalid response code");
const KeyValue *const kvResponse = varKv(jsonToVar(strNewBuf(httpResponseContent(response))));
// Copy credentials
MEM_CONTEXT_OBJ_BEGIN(this)
{
this->accessKey = strDup(varStr(kvGet(kvResponse, VARSTRDEF("AccessKeyId"))));
CHECK(FormatError, this->accessKey != NULL, "access key missing");
this->secretAccessKey = strDup(varStr(kvGet(kvResponse, VARSTRDEF("SecretAccessKey"))));
CHECK(FormatError, this->secretAccessKey != NULL, "secret access key missing");
this->securityToken = strDup(varStr(kvGet(kvResponse, VARSTRDEF("Token"))));
CHECK(FormatError, this->securityToken != NULL, "token missing");
}
MEM_CONTEXT_OBJ_END();
// Update expiration time
const String *const credExpirationTime = varStr(kvGet(kvResponse, VARSTRDEF("Expiration")));
CHECK(FormatError, credExpirationTime != NULL, "expiration missing");
this->credExpirationTime = storageS3CvtTime(credExpirationTime);
}
MEM_CONTEXT_TEMP_END();
FUNCTION_LOG_RETURN_VOID();
}
/***********************************************************************************************************************************
Process S3 request
***********************************************************************************************************************************/
@@ -530,6 +580,11 @@ storageS3RequestAsync(StorageS3 *const this, const String *const verb, const Str
storageS3AuthAuto(this, credHeader);
break;
// Pod identity authentication
case storageS3KeyTypePodId:
storageS3AuthPodId(this, credHeader);
break;
// Web identity authentication
default:
{
@@ -1185,9 +1240,9 @@ storageS3New(
const String *const bucket, const String *const endPoint, const StorageS3UriStyle uriStyle, const String *const region,
const StorageS3KeyType keyType, const String *const accessKey, const String *const secretAccessKey,
const String *const securityToken, const String *const kmsKeyId, const String *sseCustomerKey, const String *const credRole,
const String *const webIdTokenFile, const size_t partSize, const KeyValue *const tag, const String *host,
const unsigned int port, const TimeMSec timeout, const HttpProtocolType protocolType, const bool verifyPeer,
const String *const caFile, const String *const caPath, const bool requesterPays)
const String *const tokenFile, const String *const credUrl, const size_t partSize, const KeyValue *const tag,
const String *host, const unsigned int port, const TimeMSec timeout, const HttpProtocolType protocolType,
const bool verifyPeer, const String *const caFile, const String *const caPath, const bool requesterPays)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(STRING, path);
@@ -1205,7 +1260,8 @@ storageS3New(
FUNCTION_TEST_PARAM(STRING, kmsKeyId);
FUNCTION_TEST_PARAM(STRING, sseCustomerKey);
FUNCTION_TEST_PARAM(STRING, credRole);
FUNCTION_TEST_PARAM(STRING, webIdTokenFile);
FUNCTION_TEST_PARAM(STRING, tokenFile);
FUNCTION_TEST_PARAM(STRING, credUrl);
FUNCTION_LOG_PARAM(SIZE, partSize);
FUNCTION_LOG_PARAM(KEY_VALUE, tag);
FUNCTION_LOG_PARAM(STRING, host);
@@ -1291,10 +1347,10 @@ storageS3New(
{
ASSERT(accessKey == NULL && secretAccessKey == NULL && securityToken == NULL);
ASSERT(credRole != NULL);
ASSERT(webIdTokenFile != NULL);
ASSERT(tokenFile != NULL);
this->credRole = strDup(credRole);
this->webIdTokenFile = strDup(webIdTokenFile);
this->tokenFile = strDup(tokenFile);
this->credHost = S3_STS_HOST_STR;
this->credExpirationTime = time(NULL);
this->credHttpClient = httpClientNew(
@@ -1306,6 +1362,23 @@ storageS3New(
break;
}
// Create the HTTP client used to retrieve pod identity security credentials
case storageS3KeyTypePodId:
{
ASSERT(accessKey == NULL && secretAccessKey == NULL && securityToken == NULL);
ASSERT(credUrl != NULL);
ASSERT(tokenFile != NULL);
const HttpUrl *const url = httpUrlNewParseP(credUrl);
this->tokenFile = strDup(tokenFile);
this->credHost = httpUrlHost(url);
this->credPath = strDup(httpUrlPath(url));
this->credExpirationTime = time(NULL);
this->credHttpClient = httpClientNew(sckClientNew(this->credHost, httpUrlPort(url), timeout, timeout), timeout);
break;
}
// Set shared key credentials
default:
{
+4 -3
View File
@@ -20,6 +20,7 @@ typedef enum
storageS3KeyTypeShared = STRID5("shared", 0x85905130),
storageS3KeyTypeAuto = STRID5("auto", 0x7d2a10),
storageS3KeyTypeWebId = STRID5("web-id", 0x89d88b70),
storageS3KeyTypePodId = STRID5("pod-id", 0x89d91f00),
} StorageS3KeyType;
/***********************************************************************************************************************************
@@ -38,8 +39,8 @@ FN_EXTERN Storage *storageS3New(
const String *path, bool write, time_t targetTime, StoragePathExpressionCallback pathExpressionFunction, const String *bucket,
const String *endPoint, StorageS3UriStyle uriStyle, const String *region, StorageS3KeyType keyType, const String *accessKey,
const String *secretAccessKey, const String *securityToken, const String *kmsKeyId, const String *sseCustomerKey,
const String *credRole, const String *webIdTokenFile, size_t partSize, const KeyValue *tag, const String *host,
unsigned int port, TimeMSec timeout, HttpProtocolType protocolType, bool verifyPeer, const String *caFile, const String *caPath,
bool requesterPays);
const String *credRole, const String *tokenFile, const String *credUrl, size_t partSize, const KeyValue *tag,
const String *host, unsigned int port, TimeMSec timeout, HttpProtocolType protocolType, bool verifyPeer, const String *caFile,
const String *caPath, bool requesterPays);
#endif
+1 -1
View File
@@ -784,7 +784,7 @@ hrnHostConfig(HrnHost *const this)
this->pub.repo1Storage = storageS3New(
hrnHostRepo1Path(this), true, 0, NULL, STRDEF(HRN_HOST_S3_BUCKET), STRDEF(HRN_HOST_S3_ENDPOINT),
storageS3UriStyleHost, STR(HRN_HOST_S3_REGION), storageS3KeyTypeShared, STRDEF(HRN_HOST_S3_ACCESS_KEY),
STRDEF(HRN_HOST_S3_ACCESS_SECRET_KEY), NULL, NULL, NULL, NULL, NULL, 5 * 1024 * 1024, NULL,
STRDEF(HRN_HOST_S3_ACCESS_SECRET_KEY), NULL, NULL, NULL, NULL, NULL, NULL, 5 * 1024 * 1024, NULL,
hrnHostIp(s3), 443, ioTimeoutMs(), httpProtocolTypeHttps, false, NULL, NULL, NULL);
}
MEM_CONTEXT_OBJ_END();
+81 -5
View File
@@ -75,6 +75,7 @@ typedef struct TestRequestParam
const char *content;
const char *accessKey;
const char *securityToken;
const char *authorization;
const char *range;
const char *kms;
const char *sseC;
@@ -149,6 +150,10 @@ testRequest(IoWrite *write, Storage *s3, const char *verb, const char *path, Tes
strCatZ(request, ",Signature=????????????????????????????????????????????????????????????????\r\n");
}
// Add authorization
if (param.authorization != NULL)
strCatFmt(request, "authorization:%s\r\n", param.authorization);
// Add content-length
strCatFmt(request, "content-length:%zu\r\n", param.content != NULL ? strlen(param.content) : 0);
@@ -1072,7 +1077,7 @@ testRun(void)
driver = (StorageS3 *)storageDriver(s3);
TEST_RESULT_STR_Z(driver->credRole, TEST_SERVICE_ROLE, "check role");
TEST_RESULT_STR_Z(driver->webIdTokenFile, TEST_SERVICE_TOKEN_FILE, "check token file");
TEST_RESULT_STR_Z(driver->tokenFile, TEST_SERVICE_TOKEN_FILE, "check token file");
// Set partSize to a small value for testing
driver->partSize = 16;
@@ -1125,9 +1130,6 @@ testRun(void)
TEST_RESULT_UINT(info.size, 9999, "check exists");
TEST_RESULT_INT(info.timeModified, 1445412480, "check time");
// Auth service no longer needed
hrnServerScriptEnd(auth);
// -----------------------------------------------------------------------------------------------------------------
TEST_TITLE("write zero-length file (without kms)");
@@ -1156,10 +1158,71 @@ testRun(void)
storageListP(s3, STRDEF("/"), .errorOnMissing = true), AssertError,
"assertion '!param.errorOnMissing || storageFeature(this, storageFeaturePath)' failed");
// -----------------------------------------------------------------------------------------------------------------
TEST_TITLE("switch to pod-id authentication");
hrnServerScriptClose(service);
#define TEST_PODID_TOKEN "TOKEN-PODID"
#define TEST_PODID_TOKEN_FILE TEST_PATH "/pod-id-token"
#define TEST_PODID_GET "/v1/credentials"
// {uncrustify_off - comment inside string}
#define TEST_PODID_RESPONSE \
"{\n" \
" \"AccessKeyId\": \"gg\",\n" \
" \"SecretAccessKey\": \"hh\",\n" \
" \"Token\": \"mm\",\n" \
" \"AccountId\":\"012345678901\",\n" \
" \"Expiration\": \"%s\"\n" \
"}"
// {uncrustify_on}
HRN_STORAGE_PUT_Z(storagePosixNewP(TEST_PATH_STR, .write = true), TEST_PODID_TOKEN_FILE, TEST_PODID_TOKEN);
argList = strLstDup(commonArgList);
hrnCfgArgRawFmt(argList, cfgOptRepoStorageHost, "%s:%u", strZ(host), testPort);
hrnCfgArgRawStrId(argList, cfgOptRepoS3KeyType, storageS3KeyTypePodId);
HRN_CFG_LOAD(cfgCmdArchivePush, argList);
TEST_ERROR(
storageRepoGet(0, true), OptionInvalidError,
"option 'repo1-s3-key-type' is 'pod-id' but 'AWS_CONTAINER_CREDENTIALS_FULL_URI' and"
" 'AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE' are not set");
setenv(
"AWS_CONTAINER_CREDENTIALS_FULL_URI", zNewFmt("http://%s:%u/v1/credentials", strZ(host), testPortAuth), true);
TEST_ERROR(
storageRepoGet(0, true), OptionInvalidError,
"option 'repo1-s3-key-type' is 'pod-id' but 'AWS_CONTAINER_CREDENTIALS_FULL_URI' and"
" 'AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE' are not set");
setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", TEST_PODID_TOKEN_FILE, true);
s3 = storageRepoGet(0, true);
driver = (StorageS3 *)storageDriver(s3);
TEST_RESULT_STR_Z(driver->tokenFile, TEST_PODID_TOKEN_FILE, "check token file");
// Set partSize to a small value for testing
driver->partSize = 16;
hrnServerScriptAccept(service);
// -----------------------------------------------------------------------------------------------------------------
TEST_TITLE("error without xml");
testRequestP(service, s3, HTTP_VERB_GET, "/?delimiter=%2F&list-type=2");
// Get service credentials
hrnServerScriptAccept(auth);
testRequestP(auth, NULL, HTTP_VERB_GET, TEST_PODID_GET, .authorization = TEST_PODID_TOKEN);
testResponseP(
auth,
.content = zNewFmt(TEST_PODID_RESPONSE, strZ(testS3DateTime(time(NULL) + (S3_CREDENTIAL_RENEW_SEC - 1)))));
hrnServerScriptClose(auth);
testRequestP(service, s3, HTTP_VERB_GET, "/?delimiter=%2F&list-type=2", .accessKey = "gg", .securityToken = "mm");
testResponseP(service, .code = 344);
TEST_ERROR(
@@ -1178,6 +1241,16 @@ testRun(void)
// -----------------------------------------------------------------------------------------------------------------
TEST_TITLE("error with xml");
// Get service credentials
hrnServerScriptAccept(auth);
testRequestP(auth, NULL, HTTP_VERB_GET, TEST_PODID_GET, .authorization = TEST_PODID_TOKEN);
testResponseP(
auth,
.content = zNewFmt(TEST_PODID_RESPONSE, strZ(testS3DateTime(time(NULL) + (S3_CREDENTIAL_RENEW_SEC * 2)))));
hrnServerScriptClose(auth);
testRequestP(service, s3, HTTP_VERB_GET, "/?delimiter=%2F&list-type=2");
testResponseP(
service, .code = 344,
@@ -1204,6 +1277,9 @@ testRun(void)
"*** Response Content ***:\n"
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><Error><Code>SomeOtherCode</Code></Error>");
// Auth service no longer needed
hrnServerScriptEnd(auth);
// -----------------------------------------------------------------------------------------------------------------
TEST_TITLE("list basic level");