Skip to content

Commit 6075248

Browse files
Meson: wire SigV4 behind a feature option
1 parent b59fb32 commit 6075248

7 files changed

Lines changed: 153 additions & 96 deletions

File tree

meson.options

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,11 @@ option(
4444
value: 'disabled',
4545
)
4646

47+
option(
48+
'sigv4',
49+
type: 'feature',
50+
description: 'Build AWS SigV4 authentication support for rest catalog',
51+
value: 'disabled',
52+
)
53+
4754
option('tests', type: 'feature', description: 'Build tests', value: 'enabled')

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

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "iceberg/catalog/rest/auth/sigv4_auth_manager.h"
2121

2222
#include <cstdlib>
23+
#include <mutex>
2324
#include <sstream>
2425

2526
#include <aws/core/Aws.h>
@@ -107,21 +108,22 @@ class RestSigV4Signer : public Aws::Client::AWSAuthV4Signer {
107108
SigV4AuthSession::SigV4AuthSession(
108109
std::shared_ptr<AuthSession> delegate, std::string signing_region,
109110
std::string signing_name,
110-
std::shared_ptr<Aws::Auth::AWSCredentialsProvider> credentials_provider)
111+
std::shared_ptr<Aws::Auth::AWSCredentialsProvider> credentials_provider,
112+
std::unordered_map<std::string, std::string> effective_properties)
111113
: delegate_(std::move(delegate)),
112114
signing_region_(std::move(signing_region)),
113115
signing_name_(std::move(signing_name)),
114116
credentials_provider_(std::move(credentials_provider)),
115117
signer_(std::make_unique<RestSigV4Signer>(
116-
credentials_provider_, signing_name_.c_str(), signing_region_.c_str())) {}
118+
credentials_provider_, signing_name_.c_str(), signing_region_.c_str())),
119+
effective_properties_(std::move(effective_properties)) {}
117120

118121
SigV4AuthSession::~SigV4AuthSession() = default;
119122

120123
Result<HTTPRequest> SigV4AuthSession::Authenticate(const HTTPRequest& request) {
121124
ICEBERG_ASSIGN_OR_RAISE(auto delegate_request, delegate_->Authenticate(request));
122125
const auto& original_headers = delegate_request.headers;
123126

124-
// Relocate any delegate-set Authorization so SigV4 takes precedence.
125127
std::unordered_map<std::string, std::string> signing_headers;
126128
for (const auto& [name, value] : original_headers) {
127129
if (StringUtils::EqualsIgnoreCase(name, "Authorization")) {
@@ -138,8 +140,8 @@ Result<HTTPRequest> SigV4AuthSession::Authenticate(const HTTPRequest& request) {
138140
aws_request->SetHeaderValue(Aws::String(name.c_str()), Aws::String(value.c_str()));
139141
}
140142

141-
// Empty body uses hex EMPTY_BODY_SHA256 (Java workaround for the signer
142-
// producing an invalid checksum on empty bodies); non-empty uses Base64.
143+
// Empty body: hex EMPTY_BODY_SHA256 (Java parity workaround for the signer
144+
// computing an invalid checksum on empty bodies). Non-empty: Base64.
143145
if (delegate_request.body.empty()) {
144146
aws_request->SetHeaderValue("x-amz-content-sha256", Aws::String(kEmptyBodySha256));
145147
} else {
@@ -152,12 +154,14 @@ Result<HTTPRequest> SigV4AuthSession::Authenticate(const HTTPRequest& request) {
152154
Aws::Utils::HashingUtils::Base64Encode(sha256));
153155
}
154156

155-
if (!signer_->SignRequest(*aws_request)) {
156-
return std::unexpected<Error>(
157-
Error{ErrorKind::kAuthenticationFailed, "SigV4 signing failed"});
157+
{
158+
std::lock_guard<std::mutex> lock(signing_mutex_);
159+
if (!signer_->SignRequest(*aws_request)) {
160+
return std::unexpected<Error>(
161+
Error{ErrorKind::kAuthenticationFailed, "SigV4 signing failed"});
162+
}
158163
}
159164

160-
// Fill headers with the signed set, relocating any conflicting originals.
161165
HTTPRequest signed_request{.method = delegate_request.method,
162166
.url = std::move(delegate_request.url),
163167
.headers = {},
@@ -200,7 +204,6 @@ Result<std::shared_ptr<AuthSession>> SigV4AuthManager::CatalogSession(
200204
HttpClient& shared_client,
201205
const std::unordered_map<std::string, std::string>& properties) {
202206
AwsSdkGuard::EnsureInitialized();
203-
catalog_properties_ = properties;
204207
ICEBERG_ASSIGN_OR_RAISE(auto delegate_session,
205208
delegate_->CatalogSession(shared_client, properties));
206209
return WrapSession(std::move(delegate_session), properties);
@@ -214,8 +217,8 @@ Result<std::shared_ptr<AuthSession>> SigV4AuthManager::ContextualSession(
214217
ICEBERG_ASSIGN_OR_RAISE(auto delegate_session, delegate_->ContextualSession(
215218
context, sigv4_parent->delegate()));
216219

217-
auto merged = MergeProperties(catalog_properties_, context);
218-
return WrapSession(std::move(delegate_session), merged);
220+
auto merged = MergeProperties(sigv4_parent->effective_properties(), context);
221+
return WrapSession(std::move(delegate_session), std::move(merged));
219222
}
220223

221224
Result<std::shared_ptr<AuthSession>> SigV4AuthManager::TableSession(
@@ -228,8 +231,8 @@ Result<std::shared_ptr<AuthSession>> SigV4AuthManager::TableSession(
228231
auto delegate_session,
229232
delegate_->TableSession(table, properties, sigv4_parent->delegate()));
230233

231-
auto merged = MergeProperties(catalog_properties_, properties);
232-
return WrapSession(std::move(delegate_session), merged);
234+
auto merged = MergeProperties(sigv4_parent->effective_properties(), properties);
235+
return WrapSession(std::move(delegate_session), std::move(merged));
233236
}
234237

235238
Status SigV4AuthManager::Close() { return delegate_->Close(); }
@@ -285,13 +288,13 @@ std::string SigV4AuthManager::ResolveSigningName(
285288

286289
Result<std::shared_ptr<AuthSession>> SigV4AuthManager::WrapSession(
287290
std::shared_ptr<AuthSession> delegate_session,
288-
const std::unordered_map<std::string, std::string>& properties) {
291+
std::unordered_map<std::string, std::string> properties) {
289292
auto region = ResolveSigningRegion(properties);
290293
auto service = ResolveSigningName(properties);
291294
ICEBERG_ASSIGN_OR_RAISE(auto credentials, MakeCredentialsProvider(properties));
292-
return std::make_shared<SigV4AuthSession>(std::move(delegate_session),
293-
std::move(region), std::move(service),
294-
std::move(credentials));
295+
return std::make_shared<SigV4AuthSession>(
296+
std::move(delegate_session), std::move(region), std::move(service),
297+
std::move(credentials), std::move(properties));
295298
}
296299

297300
Result<std::unique_ptr<AuthManager>> MakeSigV4AuthManager(

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#pragma once
2121

2222
#include <memory>
23+
#include <mutex>
2324
#include <string>
2425
#include <unordered_map>
2526

@@ -47,8 +48,8 @@ namespace iceberg::rest::auth {
4748
///
4849
/// See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv.html
4950
///
50-
/// Thread safety: Authenticate() is NOT thread-safe. Each session should be used
51-
/// from a single thread, or callers must synchronize externally.
51+
/// Thread safety: Authenticate() is thread-safe; concurrent signing calls are
52+
/// serialized by an internal mutex.
5253
class ICEBERG_REST_EXPORT SigV4AuthSession : public AuthSession {
5354
public:
5455
/// SHA-256 hash of empty string, used for requests with no body.
@@ -61,7 +62,8 @@ class ICEBERG_REST_EXPORT SigV4AuthSession : public AuthSession {
6162
SigV4AuthSession(
6263
std::shared_ptr<AuthSession> delegate, std::string signing_region,
6364
std::string signing_name,
64-
std::shared_ptr<Aws::Auth::AWSCredentialsProvider> credentials_provider);
65+
std::shared_ptr<Aws::Auth::AWSCredentialsProvider> credentials_provider,
66+
std::unordered_map<std::string, std::string> effective_properties);
6567

6668
~SigV4AuthSession() override;
6769

@@ -71,20 +73,27 @@ class ICEBERG_REST_EXPORT SigV4AuthSession : public AuthSession {
7173

7274
const std::shared_ptr<AuthSession>& delegate() const { return delegate_; }
7375

76+
/// Merged properties this session was built from. Child sessions inherit
77+
/// from this (not the catalog's) so contextual overrides propagate into
78+
/// table sessions.
79+
const std::unordered_map<std::string, std::string>& effective_properties() const {
80+
return effective_properties_;
81+
}
82+
7483
private:
7584
std::shared_ptr<AuthSession> delegate_;
7685
std::string signing_region_;
7786
std::string signing_name_;
7887
std::shared_ptr<Aws::Auth::AWSCredentialsProvider> credentials_provider_;
7988
std::unique_ptr<Aws::Client::AWSAuthV4Signer> signer_;
89+
std::unordered_map<std::string, std::string> effective_properties_;
90+
// AWSAuthV4Signer::SignRequest mutates shared signer state.
91+
mutable std::mutex signing_mutex_;
8092
};
8193

8294
/// \brief An AuthManager that produces SigV4AuthSession instances.
8395
///
8496
/// Wraps a delegate AuthManager to handle double authentication (e.g., OAuth2 + SigV4).
85-
///
86-
/// Thread safety: CatalogSession() must be called before ContextualSession() or
87-
/// TableSession(). Concurrent calls are NOT safe — callers must synchronize externally.
8897
class ICEBERG_REST_EXPORT SigV4AuthManager : public AuthManager {
8998
public:
9099
explicit SigV4AuthManager(std::unique_ptr<AuthManager> delegate);
@@ -118,10 +127,9 @@ class ICEBERG_REST_EXPORT SigV4AuthManager : public AuthManager {
118127
const std::unordered_map<std::string, std::string>& properties);
119128
Result<std::shared_ptr<AuthSession>> WrapSession(
120129
std::shared_ptr<AuthSession> delegate_session,
121-
const std::unordered_map<std::string, std::string>& properties);
130+
std::unordered_map<std::string, std::string> properties);
122131

123132
std::unique_ptr<AuthManager> delegate_;
124-
std::unordered_map<std::string, std::string> catalog_properties_;
125133
};
126134

127135
} // namespace iceberg::rest::auth

src/iceberg/catalog/rest/http_client.cc

Lines changed: 40 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -81,21 +81,8 @@ std::unordered_map<std::string, std::string> MergeHeaders(
8181
return merged;
8282
}
8383

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());
89-
}
90-
91-
/// \brief Converts a map of string key-value pairs to cpr::Parameters.
92-
cpr::Parameters GetParameters(
93-
const std::unordered_map<std::string, std::string>& params) {
94-
cpr::Parameters cpr_params;
95-
for (const auto& [key, val] : params) {
96-
cpr_params.Add({key, val});
97-
}
98-
return cpr_params;
84+
cpr::Header ToCprHeader(const auth::HTTPRequest& request) {
85+
return cpr::Header(request.headers.begin(), request.headers.end());
9986
}
10087

10188
/// \brief Append URL-encoded query parameters to a URL, sorted by key.
@@ -175,14 +162,13 @@ Result<HttpResponse> HttpClient::Get(
175162
const std::unordered_map<std::string, std::string>& headers,
176163
const ErrorHandler& error_handler, auth::AuthSession& session) {
177164
ICEBERG_ASSIGN_OR_RAISE(
178-
auto all_headers,
179-
AuthenticateRequest({.method = HttpMethod::kGet,
180-
.url = AppendQueryString(path, params),
181-
.headers = MergeHeaders(default_headers_, headers),
182-
.body = ""},
183-
session));
184-
cpr::Response response =
185-
cpr::Get(cpr::Url{path}, GetParameters(params), all_headers, *connection_pool_);
165+
auto authenticated,
166+
session.Authenticate({.method = HttpMethod::kGet,
167+
.url = AppendQueryString(path, params),
168+
.headers = MergeHeaders(default_headers_, headers),
169+
.body = ""}));
170+
cpr::Response response = cpr::Get(cpr::Url{authenticated.url},
171+
ToCprHeader(authenticated), *connection_pool_);
186172

187173
ICEBERG_RETURN_UNEXPECTED(HandleFailureResponse(response, error_handler));
188174
HttpResponse http_response;
@@ -195,14 +181,14 @@ Result<HttpResponse> HttpClient::Post(
195181
const std::unordered_map<std::string, std::string>& headers,
196182
const ErrorHandler& error_handler, auth::AuthSession& session) {
197183
ICEBERG_ASSIGN_OR_RAISE(
198-
auto all_headers,
199-
AuthenticateRequest({.method = HttpMethod::kPost,
200-
.url = path,
201-
.headers = MergeHeaders(default_headers_, headers),
202-
.body = body},
203-
session));
184+
auto authenticated,
185+
session.Authenticate({.method = HttpMethod::kPost,
186+
.url = path,
187+
.headers = MergeHeaders(default_headers_, headers),
188+
.body = body}));
204189
cpr::Response response =
205-
cpr::Post(cpr::Url{path}, cpr::Body{body}, all_headers, *connection_pool_);
190+
cpr::Post(cpr::Url{authenticated.url}, cpr::Body{authenticated.body},
191+
ToCprHeader(authenticated), *connection_pool_);
206192

207193
ICEBERG_RETURN_UNEXPECTED(HandleFailureResponse(response, error_handler));
208194
HttpResponse http_response;
@@ -222,19 +208,18 @@ Result<HttpResponse> HttpClient::PostForm(
222208
for (const auto& [key, val] : form_data) {
223209
pair_list.emplace_back(key, val);
224210
}
225-
// Use cpr's own encoding as the signing body to ensure consistency with the
226-
// actual payload sent over the wire.
227-
cpr::Payload payload(pair_list.begin(), pair_list.end());
228-
std::string encoded_body = payload.GetContent();
211+
// Sign the exact bytes cpr will put on the wire.
212+
std::string encoded_body =
213+
cpr::Payload(pair_list.begin(), pair_list.end()).GetContent();
229214
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));
215+
auto authenticated,
216+
session.Authenticate({.method = HttpMethod::kPost,
217+
.url = path,
218+
.headers = MergeHeaders(default_headers_, form_headers),
219+
.body = std::move(encoded_body)}));
236220
cpr::Response response =
237-
cpr::Post(cpr::Url{path}, std::move(payload), all_headers, *connection_pool_);
221+
cpr::Post(cpr::Url{authenticated.url}, cpr::Body{authenticated.body},
222+
ToCprHeader(authenticated), *connection_pool_);
238223

239224
ICEBERG_RETURN_UNEXPECTED(HandleFailureResponse(response, error_handler));
240225
HttpResponse http_response;
@@ -246,13 +231,13 @@ Result<HttpResponse> HttpClient::Head(
246231
const std::string& path, const std::unordered_map<std::string, std::string>& headers,
247232
const ErrorHandler& error_handler, auth::AuthSession& session) {
248233
ICEBERG_ASSIGN_OR_RAISE(
249-
auto all_headers,
250-
AuthenticateRequest({.method = HttpMethod::kHead,
251-
.url = path,
252-
.headers = MergeHeaders(default_headers_, headers),
253-
.body = ""},
254-
session));
255-
cpr::Response response = cpr::Head(cpr::Url{path}, all_headers, *connection_pool_);
234+
auto authenticated,
235+
session.Authenticate({.method = HttpMethod::kHead,
236+
.url = path,
237+
.headers = MergeHeaders(default_headers_, headers),
238+
.body = ""}));
239+
cpr::Response response = cpr::Head(cpr::Url{authenticated.url},
240+
ToCprHeader(authenticated), *connection_pool_);
256241

257242
ICEBERG_RETURN_UNEXPECTED(HandleFailureResponse(response, error_handler));
258243
HttpResponse http_response;
@@ -265,14 +250,13 @@ Result<HttpResponse> HttpClient::Delete(
265250
const std::unordered_map<std::string, std::string>& headers,
266251
const ErrorHandler& error_handler, auth::AuthSession& session) {
267252
ICEBERG_ASSIGN_OR_RAISE(
268-
auto all_headers,
269-
AuthenticateRequest({.method = HttpMethod::kDelete,
270-
.url = AppendQueryString(path, params),
271-
.headers = MergeHeaders(default_headers_, headers),
272-
.body = ""},
273-
session));
274-
cpr::Response response =
275-
cpr::Delete(cpr::Url{path}, GetParameters(params), all_headers, *connection_pool_);
253+
auto authenticated,
254+
session.Authenticate({.method = HttpMethod::kDelete,
255+
.url = AppendQueryString(path, params),
256+
.headers = MergeHeaders(default_headers_, headers),
257+
.body = ""}));
258+
cpr::Response response = cpr::Delete(cpr::Url{authenticated.url},
259+
ToCprHeader(authenticated), *connection_pool_);
276260

277261
ICEBERG_RETURN_UNEXPECTED(HandleFailureResponse(response, error_handler));
278262
HttpResponse http_response;

src/iceberg/catalog/rest/meson.build

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,26 @@ cpr_needs_static = (
4141
cpr_dep = dependency('cpr', static: cpr_needs_static)
4242

4343
iceberg_rest_build_deps = [iceberg_dep, cpr_dep]
44+
iceberg_rest_compile_defs = []
45+
46+
sigv4_opt = get_option('sigv4')
47+
aws_sdk_core_dep = dependency('aws-cpp-sdk-core', required: sigv4_opt)
48+
if aws_sdk_core_dep.found()
49+
iceberg_rest_sources += files('auth/sigv4_auth_manager.cc')
50+
iceberg_rest_build_deps += aws_sdk_core_dep
51+
iceberg_rest_compile_defs += '-DICEBERG_BUILD_SIGV4'
52+
endif
53+
4454
iceberg_rest_lib = library(
4555
'iceberg_rest',
4656
sources: iceberg_rest_sources,
4757
dependencies: iceberg_rest_build_deps,
4858
gnu_symbol_visibility: 'hidden',
49-
cpp_shared_args: ['-DICEBERG_REST_EXPORTING'],
50-
cpp_static_args: ['-DICEBERG_REST_STATIC'],
59+
cpp_shared_args: ['-DICEBERG_REST_EXPORTING'] + iceberg_rest_compile_defs,
60+
cpp_static_args: ['-DICEBERG_REST_STATIC'] + iceberg_rest_compile_defs,
5161
)
5262

53-
iceberg_rest_compile_args = []
63+
iceberg_rest_compile_args = iceberg_rest_compile_defs
5464
if get_option('default_library') == 'static'
5565
iceberg_rest_compile_args += ['-DICEBERG_REST_STATIC']
5666
endif
@@ -80,13 +90,17 @@ install_headers(
8090
subdir: 'iceberg/catalog/rest',
8191
)
8292

93+
iceberg_rest_auth_headers = [
94+
'auth/auth_manager.h',
95+
'auth/auth_managers.h',
96+
'auth/auth_properties.h',
97+
'auth/auth_session.h',
98+
'auth/oauth2_util.h',
99+
]
100+
if aws_sdk_core_dep.found()
101+
iceberg_rest_auth_headers += ['auth/sigv4_auth_manager.h']
102+
endif
83103
install_headers(
84-
[
85-
'auth/auth_manager.h',
86-
'auth/auth_managers.h',
87-
'auth/auth_properties.h',
88-
'auth/auth_session.h',
89-
'auth/oauth2_util.h',
90-
],
104+
iceberg_rest_auth_headers,
91105
subdir: 'iceberg/catalog/rest/auth',
92106
)

0 commit comments

Comments
 (0)