1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2026-06-20 01:17:49 +02:00

Support for Azure managed identities.

Fetch credentials automatically using managed identities, which removes the need for static configuration. Credentials are automatically updated before they expire to support long-running commands.
This commit is contained in:
Moiz Ibrar
2026-01-05 16:42:11 +05:00
committed by GitHub
parent 67c8b3462d
commit 38f14d73b6
10 changed files with 254 additions and 26 deletions
+13
View File
@@ -44,6 +44,19 @@
<p>Allow expiration of oldest full backup regardless of current retention.</p>
</release-item>
<release-item>
<github-issue id="2023"/>
<github-pull-request id="2712"/>
<release-item-contributor-list>
<release-item-contributor id="moiz.ibrar"/>
<release-item-contributor id="matthew.mols"/>
<release-item-reviewer id="david.steele"/>
</release-item-contributor-list>
<p>Support for <proper>Azure</proper> managed identities.</p>
</release-item>
<release-item>
<github-pull-request id="2693"/>
+10
View File
@@ -800,6 +800,11 @@
<contributor-id type="github">mtkunkel</contributor-id>
</contributor>
<contributor id="matthew.mols">
<contributor-name-display>Matthew Mols</contributor-name-display>
<contributor-id type="github">mmols</contributor-id>
</contributor>
<contributor id="maxim.michkov">
<contributor-name-display>Maxim Michkov</contributor-name-display>
<contributor-id type="github">silent-observer</contributor-id>
@@ -860,6 +865,11 @@
<contributor-id type="github">melrifa1</contributor-id>
</contributor>
<contributor id="moiz.ibrar">
<contributor-name-display>Moiz Ibrar</contributor-name-display>
<contributor-id type="github">moizpgedge</contributor-id>
</contributor>
<contributor id="navid.golpayegani">
<contributor-name-display>Navid Golpayegani</contributor-name-display>
<contributor-id type="github">golpa</contributor-id>
+7
View File
@@ -2195,6 +2195,12 @@ option:
repo-azure-key:
inherit: repo-azure-account
required: false
depend:
option: repo-azure-key-type
list:
- shared
- sas
repo-azure-key-type:
inherit: repo-azure-container
@@ -2203,6 +2209,7 @@ option:
allow-list:
- shared
- sas
- auto
repo-azure-uri-style:
section: global
+1
View File
@@ -544,6 +544,7 @@
<list>
<list-item><id>shared</id> - Shared key</list-item>
<list-item><id>sas</id> - Shared access signature</list-item>
<list-item><id>auto</id> - Automatically authorize using Azure managed identities</list-item>
</list>
</text>
+2
View File
@@ -239,6 +239,8 @@ Option value constants
#define CFGOPTVAL_REMOTE_TYPE_REPO STRID5("repo", 0x7c0b20)
#define CFGOPTVAL_REMOTE_TYPE_REPO_Z "repo"
#define CFGOPTVAL_REPO_AZURE_KEY_TYPE_AUTO STRID5("auto", 0x7d2a10)
#define CFGOPTVAL_REPO_AZURE_KEY_TYPE_AUTO_Z "auto"
#define CFGOPTVAL_REPO_AZURE_KEY_TYPE_SAS STRID5("sas", 0x4c330)
#define CFGOPTVAL_REPO_AZURE_KEY_TYPE_SAS_Z "sas"
#define CFGOPTVAL_REPO_AZURE_KEY_TYPE_SHARED STRID5("shared", 0x85905130)
+6 -4
View File
@@ -5409,7 +5409,7 @@ static const ParseRuleOption parseRuleOption[CFG_OPTION_TOTAL] =
PARSE_RULE_OPTION_NAME("repo-azure-key"), // opt/repo-azure-key
PARSE_RULE_OPTION_TYPE(String), // opt/repo-azure-key
PARSE_RULE_OPTION_RESET(true), // opt/repo-azure-key
PARSE_RULE_OPTION_REQUIRED(true), // opt/repo-azure-key
PARSE_RULE_OPTION_REQUIRED(false), // opt/repo-azure-key
PARSE_RULE_OPTION_SECTION(Global), // opt/repo-azure-key
PARSE_RULE_OPTION_SECURE(true), // opt/repo-azure-key
PARSE_RULE_OPTION_GROUP_ID(Repo), // opt/repo-azure-key
@@ -5477,8 +5477,9 @@ static const ParseRuleOption parseRuleOption[CFG_OPTION_TOTAL] =
( // opt/repo-azure-key
PARSE_RULE_OPTIONAL_DEPEND // opt/repo-azure-key
( // opt/repo-azure-key
PARSE_RULE_VAL_OPT(RepoType), // opt/repo-azure-key
PARSE_RULE_VAL_STRID(Azure), // opt/repo-azure-key
PARSE_RULE_VAL_OPT(RepoAzureKeyType), // opt/repo-azure-key
PARSE_RULE_VAL_STRID(Shared), // opt/repo-azure-key
PARSE_RULE_VAL_STRID(Sas), // opt/repo-azure-key
), // opt/repo-azure-key
), // opt/repo-azure-key
), // opt/repo-azure-key
@@ -5564,6 +5565,7 @@ static const ParseRuleOption parseRuleOption[CFG_OPTION_TOTAL] =
( // opt/repo-azure-key-type
PARSE_RULE_VAL_STRID(Shared), // opt/repo-azure-key-type
PARSE_RULE_VAL_STRID(Sas), // opt/repo-azure-key-type
PARSE_RULE_VAL_STRID(Auto), // opt/repo-azure-key-type
), // opt/repo-azure-key-type
// opt/repo-azure-key-type
PARSE_RULE_OPTIONAL_DEFAULT // opt/repo-azure-key-type
@@ -11764,7 +11766,6 @@ static const uint8_t optionResolveOrder[] =
cfgOptRepoAzureAccount, // opt-resolve-order
cfgOptRepoAzureContainer, // opt-resolve-order
cfgOptRepoAzureEndpoint, // opt-resolve-order
cfgOptRepoAzureKey, // opt-resolve-order
cfgOptRepoAzureKeyType, // opt-resolve-order
cfgOptRepoAzureUriStyle, // opt-resolve-order
cfgOptRepoBlock, // opt-resolve-order
@@ -11818,6 +11819,7 @@ static const uint8_t optionResolveOrder[] =
cfgOptPgHostCmd, // opt-resolve-order
cfgOptPgHostKeyFile, // opt-resolve-order
cfgOptPgHostPort, // opt-resolve-order
cfgOptRepoAzureKey, // opt-resolve-order
cfgOptRepoGcsBucket, // opt-resolve-order
cfgOptRepoGcsEndpoint, // opt-resolve-order
cfgOptRepoGcsKey, // opt-resolve-order
+1 -1
View File
@@ -54,7 +54,7 @@ storageAzureHelper(const unsigned int repoIdx, const bool write, StoragePathExpr
// Ensure the key is valid base64 when key type is shared
const StorageAzureKeyType keyType = (StorageAzureKeyType)cfgOptionIdxStrId(cfgOptRepoAzureKeyType, repoIdx);
const String *const key = cfgOptionIdxStr(cfgOptRepoAzureKey, repoIdx);
const String *const key = cfgOptionIdxStrNull(cfgOptRepoAzureKey, repoIdx);
if (keyType == storageAzureKeyTypeShared)
{
+111 -12
View File
@@ -14,6 +14,7 @@ Azure Storage
#include "common/io/tls/client.h"
#include "common/log.h"
#include "common/regExp.h"
#include "common/type/json.h"
#include "common/type/object.h"
#include "common/type/xml.h"
#include "storage/azure/read.h"
@@ -24,7 +25,7 @@ Azure http headers
***********************************************************************************************************************************/
STRING_STATIC(AZURE_HEADER_TAGS, "x-ms-tags");
STRING_STATIC(AZURE_HEADER_VERSION_STR, "x-ms-version");
STRING_STATIC(AZURE_HEADER_VERSION_VALUE_STR, "2021-06-08");
STRING_STATIC(AZURE_HEADER_VERSION_VALUE_STR, "2024-08-04");
/***********************************************************************************************************************************
Azure query tokens
@@ -40,6 +41,8 @@ STRING_STATIC(AZURE_QUERY_SIG_STR, "sig");
STRING_STATIC(AZURE_QUERY_VALUE_LIST_STR, "list");
STRING_EXTERN(AZURE_QUERY_VALUE_CONTAINER_STR, AZURE_QUERY_VALUE_CONTAINER);
STRING_STATIC(AZURE_QUERY_VALUE_VERSIONS_STR, "versions");
STRING_STATIC(AZURE_QUERY_API_VERSION, "api-version");
STRING_STATIC(AZURE_QUERY_RESOURCE, "resource");
/***********************************************************************************************************************************
XML tags
@@ -54,6 +57,20 @@ STRING_STATIC(AZURE_XML_TAG_NAME_STR, "Name");
STRING_STATIC(AZURE_XML_TAG_PROPERTIES_STR, "Properties");
STRING_STATIC(AZURE_XML_TAG_VERSION_ID_STR, "VersionId");
/***********************************************************************************************************************************
Constants required for Azure managed identities
Documentation for the response format is found at:
https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-curl
***********************************************************************************************************************************/
STRING_STATIC(AZURE_CREDENTIAL_HOST_STR, "169.254.169.254");
#define AZURE_CREDENTIAL_PORT 80
#define AZURE_CREDENTIAL_PATH "/metadata/identity/oauth2/token"
#define AZURE_CREDENTIAL_API_VERSION "2018-02-01"
VARIANT_STRDEF_STATIC(AZURE_JSON_TAG_ACCESS_TOKEN_VAR, "access_token");
VARIANT_STRDEF_STATIC(AZURE_JSON_TAG_EXPIRES_IN_VAR, "expires_in");
/***********************************************************************************************************************************
Object type
***********************************************************************************************************************************/
@@ -64,6 +81,7 @@ struct StorageAzure
StringList *headerRedactList; // List of headers to redact from logging
StringList *queryRedactList; // List of query keys to redact from logging
StorageAzureKeyType keyType; // Key type (e.g. storageAzureKeyTypeShared)
const String *container; // Container to store data in
const String *account; // Account
const Buffer *sharedKey; // Shared key
@@ -74,6 +92,12 @@ struct StorageAzure
const String *pathPrefix; // Account/container prefix
uint64_t fileId; // Id to used to make file block identifiers unique
// For Azure managed identities authentication
HttpClient *credHttpClient; // HTTP client to service credential requests
const String *credHost; // Credentials host
String *accessToken; // Access token
time_t accessTokenExpirationTime; // Time the access token expires
};
/***********************************************************************************************************************************
@@ -104,16 +128,22 @@ storageAzureAuth(
MEM_CONTEXT_TEMP_BEGIN()
{
// Host header is required for both types of authentication
// Set required headers
httpHeaderPut(httpHeader, HTTP_HEADER_HOST_STR, this->host);
// Shared key authentication
if (this->sharedKey != NULL)
{
// Set required headers
// Date header is required for shared key authentication (for signing)
if (this->keyType == storageAzureKeyTypeShared)
httpHeaderPut(httpHeader, HTTP_HEADER_DATE_STR, dateTime);
// Set version header (required for shared key and auto auth types, not for SAS)
if (this->keyType != storageAzureKeyTypeSas)
httpHeaderPut(httpHeader, AZURE_HEADER_VERSION_STR, AZURE_HEADER_VERSION_VALUE_STR);
// Shared key authentication
if (this->keyType == storageAzureKeyTypeShared)
{
ASSERT(this->sharedKey != NULL);
// Generate canonical headers
String *const headerCanonical = strNew();
const StringList *const headerKeyList = httpHeaderList(httpHeader);
@@ -176,6 +206,58 @@ storageAzureAuth(
"SharedKey %s:%s", strZ(this->account),
strZ(strNewEncode(encodingBase64, cryptoHmacOne(hashTypeSha256, this->sharedKey, BUFSTR(stringToSign))))));
}
// Auto authentication
else if (this->keyType == storageAzureKeyTypeAuto)
{
const time_t timeBegin = time(NULL);
if (timeBegin >= this->accessTokenExpirationTime)
{
// Retrieve the access token via the Managed Identities endpoint
HttpHeader *const authHeader = httpHeaderNew(NULL);
httpHeaderAdd(authHeader, STRDEF("Metadata"), TRUE_STR);
httpHeaderAdd(authHeader, HTTP_HEADER_HOST_STR, this->credHost);
httpHeaderAdd(authHeader, HTTP_HEADER_CONTENT_LENGTH_STR, ZERO_STR);
HttpQuery *const authQuery = httpQueryNewP();
httpQueryAdd(authQuery, AZURE_QUERY_API_VERSION, STRDEF(AZURE_CREDENTIAL_API_VERSION));
httpQueryAdd(authQuery, AZURE_QUERY_RESOURCE, strNewFmt("https://%s", strZ(this->host)));
HttpRequest *const request = httpRequestNewP(
this->credHttpClient, HTTP_VERB_GET_STR, STRDEF(AZURE_CREDENTIAL_PATH), .header = authHeader,
.query = authQuery);
HttpResponse *const response = httpRequestResponse(request, true);
// Set the access_token on success and store an expiration time when we should re-fetch it
if (httpResponseCodeOk(response))
{
// Get credentials and expiration from the JSON response
const KeyValue *const credential = varKv(jsonToVar(strNewBuf(httpResponseContent(response))));
const String *const accessToken = varStr(kvGet(credential, AZURE_JSON_TAG_ACCESS_TOKEN_VAR));
CHECK(FormatError, accessToken != NULL, "access token missing");
const Variant *const expiresInStr = kvGet(credential, AZURE_JSON_TAG_EXPIRES_IN_VAR);
CHECK(FormatError, expiresInStr != NULL, "expiry missing");
MEM_CONTEXT_OBJ_BEGIN(this)
{
strCat(strTrunc(this->accessToken), accessToken);
// Subtract http client timeout * 2 so the token does not expire in the middle of http retries
const time_t clientTimeoutPeriod = ((time_t)(httpClientTimeout(this->httpClient) / MSEC_PER_SEC * 2));
const time_t expiresIn = (time_t)varInt64Force(expiresInStr);
this->accessTokenExpirationTime = timeBegin + expiresIn - clientTimeoutPeriod;
}
MEM_CONTEXT_OBJ_END();
}
else
httpRequestError(request, response);
}
// Add authorization header
httpHeaderPut(httpHeader, HTTP_HEADER_AUTHORIZATION_STR, strNewFmt("Bearer %s", strZ(this->accessToken)));
}
// SAS authentication
else
httpQueryMerge(query, this->sasKey);
@@ -795,7 +877,7 @@ storageAzureNew(
ASSERT(container != NULL);
ASSERT(account != NULL);
ASSERT(endpoint != NULL);
ASSERT(key != NULL);
ASSERT(keyType == storageAzureKeyTypeAuto || key != NULL);
ASSERT(blockSize != 0);
OBJ_NEW_BEGIN(StorageAzure, .childQty = MEM_CONTEXT_QTY_MAX)
@@ -810,6 +892,7 @@ storageAzureNew(
.pathPrefix =
uriStyle == storageAzureUriStyleHost ?
strNewFmt("/%s", strZ(container)) : strNewFmt("/%s/%s", strZ(account), strZ(container)),
.keyType = keyType,
};
// Create tag query string
@@ -820,11 +903,27 @@ storageAzureNew(
httpQueryFree(query);
}
// Store shared key or parse sas query
if (keyType == storageAzureKeyTypeShared)
this->sharedKey = bufNewDecode(encodingBase64, key);
else
this->sasKey = httpQueryNewStr(key);
// Initialization by key type
switch (keyType)
{
// Create authentication client
case storageAzureKeyTypeAuto:
this->accessToken = strNew();
this->credHost = AZURE_CREDENTIAL_HOST_STR;
this->credHttpClient = httpClientNew(
sckClientNew(this->credHost, AZURE_CREDENTIAL_PORT, timeout, timeout), timeout);
break;
// Store shared key
case storageAzureKeyTypeShared:
this->sharedKey = bufNewDecode(encodingBase64, key);
break;
// Parse sas query
case storageAzureKeyTypeSas:
this->sasKey = httpQueryNewStr(key);
break;
}
// Create the http client used to service requests. Use plain socket for HTTP, TLS for HTTPS.
IoClient *ioClient;
+1
View File
@@ -17,6 +17,7 @@ Key type
***********************************************************************************************************************************/
typedef enum
{
storageAzureKeyTypeAuto = STRID5("auto", 0x7d2a10),
storageAzureKeyTypeShared = STRID5("shared", 0x85905130),
storageAzureKeyTypeSas = STRID5("sas", 0x4c330),
} StorageAzureKeyType;
+102 -9
View File
@@ -3,6 +3,8 @@ Test Azure Storage
***********************************************************************************************************************************/
#include "common/io/fdRead.h"
#include "common/io/fdWrite.h"
#include "common/io/http/client.h"
#include "common/io/socket/client.h"
#include "storage/helper.h"
#include "common/harnessConfig.h"
@@ -21,6 +23,7 @@ STRING_STATIC(TEST_CONTAINER_STR, TEST_CONTAIN
STRING_STATIC(TEST_KEY_SAS_STR, TEST_KEY_SAS);
#define TEST_KEY_SHARED "YXpLZXk="
STRING_STATIC(TEST_KEY_SHARED_STR, TEST_KEY_SHARED);
#define TEST_ACCESS_TOKEN "test-access-token-12345"
/***********************************************************************************************************************************
Helper to build test requests
@@ -67,7 +70,9 @@ testRequest(IoWrite *write, const char *verb, const char *path, TestRequestParam
strCatZ(request, " HTTP/1.1\r\nuser-agent:" PROJECT_NAME "/" PROJECT_VERSION "\r\n");
// Add authorization string
if (driver->sharedKey != NULL)
if (driver->credHttpClient)
strCatZ(request, "authorization:Bearer " TEST_ACCESS_TOKEN "\r\n");
else if (driver->sharedKey != NULL)
strCatZ(request, "authorization:SharedKey account:????????????????????????????????????????????\r\n");
// Add content-length
@@ -100,8 +105,8 @@ testRequest(IoWrite *write, const char *verb, const char *path, TestRequestParam
strCatFmt(request, "x-ms-tags:%s\r\n", param.tag);
// Add version
if (driver->sharedKey != NULL)
strCatZ(request, "x-ms-version:2021-06-08\r\n");
if (driver->sharedKey != NULL || driver->credHttpClient != NULL)
strCatZ(request, "x-ms-version:2024-08-04\r\n");
// Complete headers
strCatZ(request, "\r\n");
@@ -413,7 +418,7 @@ testRun(void)
TEST_RESULT_Z(
logBuf,
"{content-length: '0', host: 'account.blob.core.windows.net', date: 'Sun, 21 Jun 2020 12:46:19 GMT'"
", x-ms-version: '2021-06-08', authorization: 'SharedKey account:2HRoJbu+G0rqwMjG+6gsb8WWkVo9rJNrDywsrnkmQAE='}",
", x-ms-version: '2024-08-04', authorization: 'SharedKey account:h9heYMD+ErrcIkJATG97G3L9gwom0TQYx/cEj4lAJG4='}",
"check headers");
// -------------------------------------------------------------------------------------------------------------------------
@@ -429,8 +434,8 @@ testRun(void)
TEST_RESULT_Z(
logBuf,
"{content-length: '44', content-md5: 'b64f49553d5c441652e95697a2c5949e', host: 'account.blob.core.windows.net'"
", date: 'Sun, 21 Jun 2020 12:46:19 GMT', x-ms-version: '2021-06-08'"
", authorization: 'SharedKey account:nuaRe9f/J91zHEE2x734ARyHJxd6Smju1j8qPrueE6o='}",
", date: 'Sun, 21 Jun 2020 12:46:19 GMT', x-ms-version: '2024-08-04'"
", authorization: 'SharedKey account:GrE62U88ziaAGq+chejwUKmaBOAsyj+QCjrykcE+O+c='}",
"check headers");
// -------------------------------------------------------------------------------------------------------------------------
@@ -460,6 +465,7 @@ testRun(void)
HRN_FORK_BEGIN()
{
const unsigned int testPort = hrnServerPortNext();
const unsigned int testPortAuth = hrnServerPortNext();
HRN_FORK_CHILD_BEGIN(.prefix = "azure server", .timeout = 5000)
{
@@ -467,9 +473,18 @@ testRun(void)
}
HRN_FORK_CHILD_END();
HRN_FORK_CHILD_BEGIN(.prefix = "azure auth server", .timeout = 5000)
{
TEST_RESULT_VOID(hrnServerRunP(HRN_FORK_CHILD_READ(), hrnServerProtocolSocket, testPortAuth), "azure auth server");
}
HRN_FORK_CHILD_END();
HRN_FORK_PARENT_BEGIN(.prefix = "azure client")
{
IoWrite *service = hrnServerScriptBegin(HRN_FORK_PARENT_WRITE(0));
IoWrite *service = hrnServerScriptBegin(
ioFdWriteNewOpen(STRDEF("s3 client write"), HRN_FORK_PARENT_WRITE_FD(0), 2000));
IoWrite *auth = hrnServerScriptBegin(
ioFdWriteNewOpen(STRDEF("azure auth client write"), HRN_FORK_PARENT_WRITE_FD(1), 2000));
// -----------------------------------------------------------------------------------------------------------------
TEST_TITLE("test against local host with path-style URIs");
@@ -583,7 +598,7 @@ testRun(void)
"content-length: 0\n"
"date: <redacted>\n"
"host: %s\n"
"x-ms-version: 2021-06-08\n"
"x-ms-version: 2024-08-04\n"
"*** Response Headers ***:\n"
"content-length: 7\n"
"*** Response Content ***:\n"
@@ -611,7 +626,7 @@ testRun(void)
"host: %s\n"
"x-ms-blob-type: BlockBlob\n"
"x-ms-tags: %%20Key%%202=%%20Value%%202&Key1=Value1\n"
"x-ms-version: 2021-06-08",
"x-ms-version: 2024-08-04",
strZ(hrnServerHost()));
// -----------------------------------------------------------------------------------------------------------------
@@ -688,15 +703,90 @@ testRun(void)
TEST_RESULT_BOOL(storageInfoP(storage, NULL, .ignoreMissing = true).exists, false, "info for /");
// -----------------------------------------------------------------------------------------------------------------
TEST_TITLE("switch to managed identity");
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test");
hrnCfgArgRawStrId(argList, cfgOptRepoType, STORAGE_AZURE_TYPE);
hrnCfgArgRawZ(argList, cfgOptRepoPath, "/");
hrnCfgArgRawZ(argList, cfgOptRepoAzureContainer, TEST_CONTAINER);
hrnCfgArgRawFmt(argList, cfgOptRepoStorageHost, "%s:%u", strZ(hrnServerHost()), testPort);
hrnCfgArgRawBool(argList, cfgOptRepoStorageVerifyTls, TEST_IN_CONTAINER);
hrnCfgEnvRawZ(cfgOptRepoAzureAccount, TEST_ACCOUNT);
hrnCfgEnvRawZ(cfgOptRepoAzureKeyType, "auto");
HRN_CFG_LOAD(cfgCmdArchivePush, argList);
hrnCfgEnvRemoveRaw(cfgOptRepoAzureKeyType);
TEST_ASSIGN(storage, storageRepoGet(0, true), "get repo storage");
driver = (StorageAzure *)storageDriver(storage);
// Override cred http client to point to our test server
// Note: In real usage, this would connect to 169.254.169.254:80, but for testing we use our mock server
driver->credHttpClient = httpClientNew(sckClientNew(hrnServerHost(), testPortAuth, 1000, 1000), 1000);
// Update credHost to match test server host since we're using a mock server
driver->credHost = hrnServerHost();
hrnServerScriptAccept(service);
// -----------------------------------------------------------------------------------------------------------------
TEST_TITLE("managed Identity token fetch error");
hrnServerScriptAccept(auth);
// Mock error response from metadata endpoint
String *credRequest = strCatFmt(
strNew(),
"GET /metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%%3A%%2F%%2F%s HTTP/1.1\r\n"
"user-agent:" PROJECT_NAME "/" PROJECT_VERSION "\r\n"
"Metadata:true\r\n"
"content-length:0\r\n"
"host:%s\r\n",
strZ(hrnServerHost()), strZ(hrnServerHost()));
hrnServerScriptExpect(auth, credRequest);
testResponseP(auth, .code = 403, .content = "");
hrnServerScriptClose(auth);
TEST_ERROR_FMT(
storageInfoP(storage, STRDEF("BOGUS"), .ignoreMissing = true), ProtocolError,
"HTTP request failed with 403 (Forbidden):\n"
"*** Path/Query ***:\n"
"GET /metadata/identity/oauth2/token?api-version=2018-02-01&resource="
"https%%3A%%2F%%2F%s\n"
"*** Request Headers ***:\n"
"Metadata: true\n"
"content-length: 0\n"
"host: %s\n"
"*** Response Headers ***:\n"
"content-length: 0",
strZ(hrnServerHost()), strZ(hrnServerHost()));
// -----------------------------------------------------------------------------------------------------------------
TEST_TITLE("info for missing file");
// Check initial credential settings
TEST_RESULT_STR_Z(driver->accessToken, "", "check access token value not set");
TEST_RESULT_INT(driver->accessTokenExpirationTime, 0, "check expiration time not set");
// Get managed identity credentials
hrnServerScriptAccept(auth);
hrnServerScriptExpect(auth, credRequest);
testResponseP(auth, .content = "{\"access_token\":\"" TEST_ACCESS_TOKEN "\",\"expires_in\":\"3600\"}");
// Info for missing file
testRequestP(service, HTTP_VERB_HEAD, "/BOGUS");
testResponseP(service, .code = 404);
TEST_RESULT_BOOL(
storageInfoP(storage, STRDEF("BOGUS"), .ignoreMissing = true).exists, false, "file does not exist");
// Check credential settings after auth (time is a bit imprecise since it is changing but verifies the range)
TEST_RESULT_STR_Z(driver->accessToken, TEST_ACCESS_TOKEN, "check access token value");
TEST_RESULT_BOOL(driver->accessTokenExpirationTime > time(NULL), true, "check expiration time min");
TEST_RESULT_BOOL(driver->accessTokenExpirationTime < time(NULL) + 3600, true, "check expiration time max");
// -----------------------------------------------------------------------------------------------------------------
TEST_TITLE("info for file");
@@ -710,6 +800,9 @@ 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("info check existence only");