diff --git a/doc/xml/release/2020s/2026/2.58.0.xml b/doc/xml/release/2020s/2026/2.58.0.xml index 2024b844e..3f9ff6849 100644 --- a/doc/xml/release/2020s/2026/2.58.0.xml +++ b/doc/xml/release/2020s/2026/2.58.0.xml @@ -57,6 +57,18 @@

Support for Azure managed identities.

+ + + + + + + + + +

Experimental support for S3 EKS pod identity.

+
+ diff --git a/doc/xml/release/contributor.xml b/doc/xml/release/contributor.xml index d2fa8ad4d..048a30e18 100644 --- a/doc/xml/release/contributor.xml +++ b/doc/xml/release/contributor.xml @@ -935,6 +935,11 @@ philrhurst + + Pierre BOUTELOUP + wolrajhti + + Pierre Ducroquet PierreDucroquet diff --git a/src/build/config/config.yaml b/src/build/config/config.yaml index 857c725d8..ce4c2bc0d 100644 --- a/src/build/config/config.yaml +++ b/src/build/config/config.yaml @@ -2311,6 +2311,7 @@ option: - shared - auto - web-id + - pod-id repo-s3-key: section: global diff --git a/src/config/config.auto.h b/src/config/config.auto.h index 6a7c3f181..c1ce532c8 100644 --- a/src/config/config.auto.h +++ b/src/config/config.auto.h @@ -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) diff --git a/src/config/parse.auto.c.inc b/src/config/parse.auto.c.inc index 0ff01c5e0..8dd027b4e 100644 --- a/src/config/parse.auto.c.inc +++ b/src/config/parse.auto.c.inc @@ -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 diff --git a/src/storage/s3/helper.c b/src/storage/s3/helper.c index 117501206..9c7d9131e 100644 --- a/src/storage/s3/helper.c +++ b/src/storage/s3/helper.c @@ -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), diff --git a/src/storage/s3/storage.c b/src/storage/s3/storage.c index 8e653744a..c0ded9329 100644 --- a/src/storage/s3/storage.c +++ b/src/storage/s3/storage.c @@ -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: { diff --git a/src/storage/s3/storage.h b/src/storage/s3/storage.h index 3c2d02ea0..dfa5766c3 100644 --- a/src/storage/s3/storage.h +++ b/src/storage/s3/storage.h @@ -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 diff --git a/test/src/common/harnessHost.c b/test/src/common/harnessHost.c index 071f2463b..c9eb6881d 100644 --- a/test/src/common/harnessHost.c +++ b/test/src/common/harnessHost.c @@ -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(); diff --git a/test/src/module/storage/s3Test.c b/test/src/module/storage/s3Test.c index d7a311f7a..92b7cf4fd 100644 --- a/test/src/module/storage/s3Test.c +++ b/test/src/module/storage/s3Test.c @@ -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" "SomeOtherCode"); + // Auth service no longer needed + hrnServerScriptEnd(auth); + // ----------------------------------------------------------------------------------------------------------------- TEST_TITLE("list basic level");