Skip to content

Commit abb2cf6

Browse files
adopt request-in/request-out Authenticate interface
1 parent 6a9cd32 commit abb2cf6

8 files changed

Lines changed: 192 additions & 176 deletions

File tree

src/iceberg/catalog/rest/auth/auth_properties.h

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,23 +54,13 @@ class ICEBERG_REST_EXPORT AuthProperties : public ConfigBase<AuthProperties> {
5454

5555
// ---- SigV4 entries ----
5656

57-
inline static const std::string kSigV4Region = "rest.auth.sigv4.region";
58-
inline static const std::string kSigV4Service = "rest.auth.sigv4.service";
5957
inline static const std::string kSigV4DelegateAuthType =
6058
"rest.auth.sigv4.delegate-auth-type";
61-
62-
// ---- SigV4 AWS credential entries ----
63-
64-
/// AWS region for SigV4 signing.
6559
inline static const std::string kSigV4SigningRegion = "rest.signing-region";
66-
/// AWS service name for SigV4 signing.
6760
inline static const std::string kSigV4SigningName = "rest.signing-name";
6861
inline static const std::string kSigV4SigningNameDefault = "execute-api";
69-
/// Static access key ID for SigV4 signing.
7062
inline static const std::string kSigV4AccessKeyId = "rest.access-key-id";
71-
/// Static secret access key for SigV4 signing.
7263
inline static const std::string kSigV4SecretAccessKey = "rest.secret-access-key";
73-
/// Optional session token for SigV4 signing.
7464
inline static const std::string kSigV4SessionToken = "rest.session-token";
7565

7666
// ---- OAuth2 entries ----

src/iceberg/catalog/rest/auth/auth_session.cc

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,12 @@ class DefaultAuthSession : public AuthSession {
3333
explicit DefaultAuthSession(std::unordered_map<std::string, std::string> headers)
3434
: headers_(std::move(headers)) {}
3535

36-
Status Authenticate(
37-
std::unordered_map<std::string, std::string>& headers,
38-
[[maybe_unused]] const HTTPRequestContext& request_context) override {
36+
Result<HTTPRequest> Authenticate(const HTTPRequest& request) override {
37+
HTTPRequest authenticated = request;
3938
for (const auto& [key, value] : headers_) {
40-
headers.try_emplace(key, value);
39+
authenticated.headers.try_emplace(key, value);
4140
}
42-
return {};
41+
return authenticated;
4342
}
4443

4544
private:

src/iceberg/catalog/rest/auth/auth_session.h

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,14 @@
3333

3434
namespace iceberg::rest::auth {
3535

36-
/// \brief Context about the HTTP request being authenticated.
37-
struct ICEBERG_REST_EXPORT HTTPRequestContext {
36+
/// \brief An outgoing HTTP request passed through an AuthSession. Mirrors the
37+
/// HTTPRequest type used by the Java reference implementation so signing
38+
/// implementations like SigV4 can operate on method, url, headers, and body
39+
/// as a single value.
40+
struct ICEBERG_REST_EXPORT HTTPRequest {
3841
HttpMethod method = HttpMethod::kGet;
3942
std::string url;
43+
std::unordered_map<std::string, std::string> headers;
4044
std::string body;
4145
};
4246

@@ -45,26 +49,21 @@ class ICEBERG_REST_EXPORT AuthSession {
4549
public:
4650
virtual ~AuthSession() = default;
4751

48-
/// \brief Authenticate the given request headers.
52+
/// \brief Authenticate an outgoing HTTP request.
4953
///
50-
/// This method adds authentication information (e.g., Authorization header)
51-
/// to the provided headers map. The implementation should be idempotent.
54+
/// Returns a new request with authentication information (e.g., an
55+
/// Authorization header) added. Implementations must be idempotent and must
56+
/// not mutate the input request.
5257
///
53-
/// \param[in,out] headers The headers map to add authentication information to.
54-
/// \return Status indicating success or one of the following errors:
58+
/// \param request The request to authenticate.
59+
/// \return The authenticated request on success, or one of:
5560
/// - AuthenticationFailed: General authentication failure (invalid credentials,
5661
/// etc.)
5762
/// - TokenExpired: Authentication token has expired and needs refresh
5863
/// - NotAuthorized: Not authenticated (401)
5964
/// - IOError: Network or connection errors when reaching auth server
6065
/// - RestError: HTTP errors from authentication service
61-
virtual Status Authenticate(std::unordered_map<std::string, std::string>& headers,
62-
const HTTPRequestContext& request_context) = 0;
63-
64-
/// \brief Convenience overload for callers that don't need a request context.
65-
Status Authenticate(std::unordered_map<std::string, std::string>& headers) {
66-
return Authenticate(headers, HTTPRequestContext{});
67-
}
66+
virtual Result<HTTPRequest> Authenticate(const HTTPRequest& request) = 0;
6867

6968
/// \brief Close the session and release any resources.
7069
///

src/iceberg/catalog/rest/auth/sigv4_auth_manager.cc

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -82,21 +82,18 @@ std::unordered_map<std::string, std::string> MergeProperties(
8282
return merged;
8383
}
8484

85-
/// SigV4 signer reproducing Java RESTSigV4AuthSession's
86-
/// SignerChecksumParams(SHA256, X_AMZ_CONTENT_SHA256) output: canonical
87-
/// headers carry Base64(SHA256(body)), canonical request trailer uses hex.
85+
/// Matches Java RESTSigV4AuthSession: canonical headers carry
86+
/// Base64(SHA256(body)), canonical request trailer uses hex.
8887
class RestSigV4Signer : public Aws::Client::AWSAuthV4Signer {
8988
public:
9089
RestSigV4Signer(const std::shared_ptr<Aws::Auth::AWSCredentialsProvider>& creds,
9190
const char* service_name, const Aws::String& region)
9291
: Aws::Client::AWSAuthV4Signer(creds, service_name, region,
9392
PayloadSigningPolicy::Always,
9493
/*urlEscapePath=*/false) {
95-
// AWSAuthV4Signer normally overwrites x-amz-content-sha256 with the hex
96-
// body hash right before canonicalization, which would clobber the Base64
97-
// value the caller pre-sets. Clearing this flag skips that overwrite so
98-
// canonical headers see the caller's Base64, while ComputePayloadHash
99-
// still feeds hex into the canonical request trailer.
94+
// Skip the signer's hex overwrite of x-amz-content-sha256 so canonical
95+
// headers see the caller's Base64; ComputePayloadHash still feeds hex
96+
// into the canonical request trailer.
10097
m_includeSha256HashHeader = false;
10198
}
10299
};
@@ -118,41 +115,37 @@ SigV4AuthSession::SigV4AuthSession(
118115

119116
SigV4AuthSession::~SigV4AuthSession() = default;
120117

121-
Status SigV4AuthSession::Authenticate(
122-
std::unordered_map<std::string, std::string>& headers,
123-
const HTTPRequestContext& request_context) {
124-
ICEBERG_RETURN_UNEXPECTED(delegate_->Authenticate(headers, request_context));
125-
auto original_headers = headers;
118+
Result<HTTPRequest> SigV4AuthSession::Authenticate(const HTTPRequest& request) {
119+
ICEBERG_ASSIGN_OR_RAISE(auto delegate_request, delegate_->Authenticate(request));
120+
const auto& original_headers = delegate_request.headers;
126121

127122
// Relocate any delegate-set Authorization so SigV4 takes precedence.
128123
std::unordered_map<std::string, std::string> signing_headers;
129-
for (const auto& [name, value] : headers) {
124+
for (const auto& [name, value] : original_headers) {
130125
if (StringUtils::EqualsIgnoreCase(name, "Authorization")) {
131126
signing_headers[std::string(kRelocatedHeaderPrefix) + name] = value;
132127
} else {
133128
signing_headers[name] = value;
134129
}
135130
}
136131

137-
Aws::Http::URI aws_uri(request_context.url.c_str());
132+
Aws::Http::URI aws_uri(delegate_request.url.c_str());
138133
auto aws_request = std::make_shared<Aws::Http::Standard::StandardHttpRequest>(
139-
aws_uri, ToAwsMethod(request_context.method));
134+
aws_uri, ToAwsMethod(delegate_request.method));
140135
for (const auto& [name, value] : signing_headers) {
141136
aws_request->SetHeaderValue(Aws::String(name.c_str()), Aws::String(value.c_str()));
142137
}
143138

144139
// Empty body uses hex EMPTY_BODY_SHA256 (Java workaround for the signer
145-
// producing an invalid checksum for empty bodies); non-empty body uses
146-
// Base64(SHA256(body)). See RestSigV4Signer doc for why this value survives
147-
// signing to land in the canonical headers unchanged.
148-
if (request_context.body.empty()) {
140+
// producing an invalid checksum on empty bodies); non-empty uses Base64.
141+
if (delegate_request.body.empty()) {
149142
aws_request->SetHeaderValue("x-amz-content-sha256", Aws::String(kEmptyBodySha256));
150143
} else {
151144
auto body_stream =
152-
Aws::MakeShared<std::stringstream>("SigV4Body", request_context.body);
145+
Aws::MakeShared<std::stringstream>("SigV4Body", delegate_request.body);
153146
aws_request->AddContentBody(body_stream);
154147
auto sha256 = Aws::Utils::HashingUtils::CalculateSHA256(
155-
Aws::String(request_context.body.data(), request_context.body.size()));
148+
Aws::String(delegate_request.body.data(), delegate_request.body.size()));
156149
aws_request->SetHeaderValue("x-amz-content-sha256",
157150
Aws::Utils::HashingUtils::Base64Encode(sha256));
158151
}
@@ -162,21 +155,25 @@ Status SigV4AuthSession::Authenticate(
162155
Error{ErrorKind::kAuthenticationFailed, "SigV4 signing failed"});
163156
}
164157

165-
// Merge signed headers back; relocate any original value that conflicts.
166-
headers.clear();
158+
// Fill headers with the signed set, relocating any conflicting originals.
159+
HTTPRequest signed_request{.method = delegate_request.method,
160+
.url = std::move(delegate_request.url),
161+
.headers = {},
162+
.body = std::move(delegate_request.body)};
167163
for (const auto& [aws_name, aws_value] : aws_request->GetHeaders()) {
168164
std::string name(aws_name.c_str(), aws_name.size());
169165
std::string value(aws_value.c_str(), aws_value.size());
170166
for (const auto& [orig_name, orig_value] : original_headers) {
171167
if (StringUtils::EqualsIgnoreCase(orig_name, name) && orig_value != value) {
172-
headers[std::string(kRelocatedHeaderPrefix) + orig_name] = orig_value;
168+
signed_request.headers[std::string(kRelocatedHeaderPrefix) + orig_name] =
169+
orig_value;
173170
break;
174171
}
175172
}
176-
headers[std::move(name)] = std::move(value);
173+
signed_request.headers[std::move(name)] = std::move(value);
177174
}
178175

179-
return {};
176+
return signed_request;
180177
}
181178

182179
Status SigV4AuthSession::Close() { return delegate_->Close(); }
@@ -243,7 +240,6 @@ SigV4AuthManager::MakeCredentialsProvider(
243240
bool has_ak = access_key_it != properties.end() && !access_key_it->second.empty();
244241
bool has_sk = secret_key_it != properties.end() && !secret_key_it->second.empty();
245242

246-
// Reject partial credentials — providing only one of AK/SK is a misconfiguration.
247243
ICEBERG_PRECHECK(
248244
has_ak == has_sk, "Both '{}' and '{}' must be set together, or neither",
249245
AuthProperties::kSigV4AccessKeyId, AuthProperties::kSigV4SecretAccessKey);
@@ -263,14 +259,10 @@ SigV4AuthManager::MakeCredentialsProvider(
263259

264260
std::string SigV4AuthManager::ResolveSigningRegion(
265261
const std::unordered_map<std::string, std::string>& properties) {
266-
auto it = properties.find(AuthProperties::kSigV4SigningRegion);
267-
if (it != properties.end() && !it->second.empty()) {
262+
if (auto it = properties.find(AuthProperties::kSigV4SigningRegion);
263+
it != properties.end() && !it->second.empty()) {
268264
return it->second;
269265
}
270-
auto legacy_it = properties.find(AuthProperties::kSigV4Region);
271-
if (legacy_it != properties.end() && !legacy_it->second.empty()) {
272-
return legacy_it->second;
273-
}
274266
if (const char* env = std::getenv("AWS_REGION")) {
275267
return std::string(env);
276268
}
@@ -282,14 +274,10 @@ std::string SigV4AuthManager::ResolveSigningRegion(
282274

283275
std::string SigV4AuthManager::ResolveSigningName(
284276
const std::unordered_map<std::string, std::string>& properties) {
285-
auto it = properties.find(AuthProperties::kSigV4SigningName);
286-
if (it != properties.end() && !it->second.empty()) {
277+
if (auto it = properties.find(AuthProperties::kSigV4SigningName);
278+
it != properties.end() && !it->second.empty()) {
287279
return it->second;
288280
}
289-
auto legacy_it = properties.find(AuthProperties::kSigV4Service);
290-
if (legacy_it != properties.end() && !legacy_it->second.empty()) {
291-
return legacy_it->second;
292-
}
293281
return AuthProperties::kSigV4SigningNameDefault;
294282
}
295283

src/iceberg/catalog/rest/auth/sigv4_auth_manager.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,7 @@ class ICEBERG_REST_EXPORT SigV4AuthSession : public AuthSession {
6565

6666
~SigV4AuthSession() override;
6767

68-
Status Authenticate(std::unordered_map<std::string, std::string>& headers,
69-
const HTTPRequestContext& request_context) override;
68+
Result<HTTPRequest> Authenticate(const HTTPRequest& request) override;
7069

7170
Status Close() override;
7271

src/iceberg/catalog/rest/http_client.cc

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,22 @@ namespace {
7070
/// \brief Default error type for unparseable REST responses.
7171
constexpr std::string_view kRestExceptionType = "RESTException";
7272

73-
/// \brief Prepare headers for an HTTP request.
74-
Result<cpr::Header> BuildHeaders(
75-
const std::unordered_map<std::string, std::string>& request_headers,
73+
/// \brief Merge default headers with per-request headers (per-request wins).
74+
std::unordered_map<std::string, std::string> MergeHeaders(
7675
const std::unordered_map<std::string, std::string>& default_headers,
77-
auth::AuthSession& session, const auth::HTTPRequestContext& request_context) {
78-
std::unordered_map<std::string, std::string> headers(default_headers);
76+
const std::unordered_map<std::string, std::string>& request_headers) {
77+
std::unordered_map<std::string, std::string> merged(default_headers);
7978
for (const auto& [key, val] : request_headers) {
80-
headers.insert_or_assign(key, val);
79+
merged.insert_or_assign(key, val);
8180
}
82-
ICEBERG_RETURN_UNEXPECTED(session.Authenticate(headers, request_context));
83-
return cpr::Header(headers.begin(), headers.end());
81+
return merged;
82+
}
83+
84+
/// \brief Authenticate the request and return the final cpr::Header.
85+
Result<cpr::Header> AuthenticateRequest(const auth::HTTPRequest& request,
86+
auth::AuthSession& session) {
87+
ICEBERG_ASSIGN_OR_RAISE(auto authenticated, session.Authenticate(request));
88+
return cpr::Header(authenticated.headers.begin(), authenticated.headers.end());
8489
}
8590

8691
/// \brief Converts a map of string key-value pairs to cpr::Parameters.
@@ -171,8 +176,11 @@ Result<HttpResponse> HttpClient::Get(
171176
const ErrorHandler& error_handler, auth::AuthSession& session) {
172177
ICEBERG_ASSIGN_OR_RAISE(
173178
auto all_headers,
174-
BuildHeaders(headers, default_headers_, session,
175-
{HttpMethod::kGet, AppendQueryString(path, params), ""}));
179+
AuthenticateRequest({.method = HttpMethod::kGet,
180+
.url = AppendQueryString(path, params),
181+
.headers = MergeHeaders(default_headers_, headers),
182+
.body = ""},
183+
session));
176184
cpr::Response response =
177185
cpr::Get(cpr::Url{path}, GetParameters(params), all_headers, *connection_pool_);
178186

@@ -188,7 +196,11 @@ Result<HttpResponse> HttpClient::Post(
188196
const ErrorHandler& error_handler, auth::AuthSession& session) {
189197
ICEBERG_ASSIGN_OR_RAISE(
190198
auto all_headers,
191-
BuildHeaders(headers, default_headers_, session, {HttpMethod::kPost, path, body}));
199+
AuthenticateRequest({.method = HttpMethod::kPost,
200+
.url = path,
201+
.headers = MergeHeaders(default_headers_, headers),
202+
.body = body},
203+
session));
192204
cpr::Response response =
193205
cpr::Post(cpr::Url{path}, cpr::Body{body}, all_headers, *connection_pool_);
194206

@@ -214,9 +226,13 @@ Result<HttpResponse> HttpClient::PostForm(
214226
// actual payload sent over the wire.
215227
cpr::Payload payload(pair_list.begin(), pair_list.end());
216228
std::string encoded_body = payload.GetContent();
217-
ICEBERG_ASSIGN_OR_RAISE(auto all_headers,
218-
BuildHeaders(form_headers, default_headers_, session,
219-
{HttpMethod::kPost, path, encoded_body}));
229+
ICEBERG_ASSIGN_OR_RAISE(
230+
auto all_headers,
231+
AuthenticateRequest({.method = HttpMethod::kPost,
232+
.url = path,
233+
.headers = MergeHeaders(default_headers_, form_headers),
234+
.body = encoded_body},
235+
session));
220236
cpr::Response response =
221237
cpr::Post(cpr::Url{path}, std::move(payload), all_headers, *connection_pool_);
222238

@@ -231,7 +247,11 @@ Result<HttpResponse> HttpClient::Head(
231247
const ErrorHandler& error_handler, auth::AuthSession& session) {
232248
ICEBERG_ASSIGN_OR_RAISE(
233249
auto all_headers,
234-
BuildHeaders(headers, default_headers_, session, {HttpMethod::kHead, path, ""}));
250+
AuthenticateRequest({.method = HttpMethod::kHead,
251+
.url = path,
252+
.headers = MergeHeaders(default_headers_, headers),
253+
.body = ""},
254+
session));
235255
cpr::Response response = cpr::Head(cpr::Url{path}, all_headers, *connection_pool_);
236256

237257
ICEBERG_RETURN_UNEXPECTED(HandleFailureResponse(response, error_handler));
@@ -246,8 +266,11 @@ Result<HttpResponse> HttpClient::Delete(
246266
const ErrorHandler& error_handler, auth::AuthSession& session) {
247267
ICEBERG_ASSIGN_OR_RAISE(
248268
auto all_headers,
249-
BuildHeaders(headers, default_headers_, session,
250-
{HttpMethod::kDelete, AppendQueryString(path, params), ""}));
269+
AuthenticateRequest({.method = HttpMethod::kDelete,
270+
.url = AppendQueryString(path, params),
271+
.headers = MergeHeaders(default_headers_, headers),
272+
.body = ""},
273+
session));
251274
cpr::Response response =
252275
cpr::Delete(cpr::Url{path}, GetParameters(params), all_headers, *connection_pool_);
253276

0 commit comments

Comments
 (0)