You've already forked pgbackrest
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:
@@ -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"/>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -17,6 +17,7 @@ Key type
|
||||
***********************************************************************************************************************************/
|
||||
typedef enum
|
||||
{
|
||||
storageAzureKeyTypeAuto = STRID5("auto", 0x7d2a10),
|
||||
storageAzureKeyTypeShared = STRID5("shared", 0x85905130),
|
||||
storageAzureKeyTypeSas = STRID5("sas", 0x4c330),
|
||||
} StorageAzureKeyType;
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user