Skip to content

Commit fbfb7e9

Browse files
address review: SigV4 session lifecycle via Make(), per-table sessions, Base64 wording, CI toolchain
1 parent c23e3af commit fbfb7e9

7 files changed

Lines changed: 115 additions & 40 deletions

File tree

‎ci/scripts/build_iceberg.sh‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ if is_windows; then
6060
CMAKE_ARGS+=("-DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake")
6161
CMAKE_ARGS+=("-DCMAKE_BUILD_TYPE=Release")
6262
else
63+
# Pass an externally provided toolchain (e.g. vcpkg for the SigV4 job)
64+
# explicitly instead of relying on CMake >= 3.21 reading the env var.
65+
if [[ -n "${CMAKE_TOOLCHAIN_FILE:-}" ]]; then
66+
CMAKE_ARGS+=("-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}")
67+
fi
6368
CMAKE_ARGS+=("-DCMAKE_BUILD_TYPE=Debug")
6469
fi
6570

‎meson.options‎

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

47+
# Resolves a system-installed AWS SDK via its CMake config; the bundled-AWS
48+
# path (ICEBERG_AWSSDK_SOURCE=BUNDLED) is CMake-only.
4749
option(
4850
'sigv4',
4951
type: 'feature',

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

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,7 @@ SigV4AuthSession::SigV4AuthSession(
172172
signer_(std::make_unique<RestSigV4Signer>(
173173
credentials_provider_, signing_name_.c_str(), signing_region_.c_str())) {}
174174

175-
SigV4AuthSession::~SigV4AuthSession() {
176-
if (owns_sdk_registration_) {
177-
AwsSdkLifecycle::Instance().UnregisterSession();
178-
}
179-
}
175+
SigV4AuthSession::~SigV4AuthSession() { AwsSdkLifecycle::Instance().UnregisterSession(); }
180176

181177
Result<HttpRequest> SigV4AuthSession::Authenticate(const HttpRequest& request) {
182178
ICEBERG_ASSIGN_OR_RAISE(auto delegate_request, delegate_->Authenticate(request));
@@ -198,8 +194,12 @@ Result<HttpRequest> SigV4AuthSession::Authenticate(const HttpRequest& request) {
198194
aws_request->SetHeaderValue(Aws::String(name.c_str()), Aws::String(value.c_str()));
199195
}
200196

201-
// Empty body: hex EMPTY_BODY_SHA256 (Java parity workaround for the signer
202-
// computing an invalid checksum on empty bodies). Non-empty: Base64.
197+
// Java parity: for non-empty bodies the signed x-amz-content-sha256 header
198+
// carries Base64(SHA256(body)) — matching the Java client's
199+
// SignerChecksumParams behavior — while the canonical request's payload hash
200+
// line remains lowercase hex per SigV4. Empty bodies use the hex
201+
// EMPTY_BODY_SHA256 constant (workaround for the signer computing an invalid
202+
// checksum on empty bodies).
203203
if (delegate_request.body.empty()) {
204204
aws_request->SetHeaderValue("x-amz-content-sha256", Aws::String(kEmptyBodySha256));
205205
} else {
@@ -411,11 +411,23 @@ class SessionSlot {
411411

412412
} // namespace
413413

414+
Result<std::shared_ptr<SigV4AuthSession>> SigV4AuthSession::Make(
415+
std::shared_ptr<AuthSession> delegate, std::string signing_region,
416+
std::string signing_name,
417+
std::shared_ptr<Aws::Auth::AWSCredentialsProvider> credentials_provider) {
418+
ICEBERG_ASSIGN_OR_RAISE(auto slot, SessionSlot::Reserve());
419+
auto session = std::shared_ptr<SigV4AuthSession>(
420+
new SigV4AuthSession(std::move(delegate), std::move(signing_region),
421+
std::move(signing_name), std::move(credentials_provider)));
422+
// The session's destructor now owns the unregister.
423+
slot.Release();
424+
return session;
425+
}
426+
414427
Result<std::shared_ptr<AuthSession>> SigV4AuthManager::WrapSession(
415428
std::shared_ptr<AuthSession> delegate_session,
416429
const std::unordered_map<std::string, std::string>& properties,
417430
std::shared_ptr<Aws::Auth::AWSCredentialsProvider> reuse_credentials) {
418-
ICEBERG_ASSIGN_OR_RAISE(auto slot, SessionSlot::Reserve());
419431
ICEBERG_ASSIGN_OR_RAISE(auto region, ResolveSigningRegion(properties));
420432
auto service = ResolveSigningName(properties);
421433

@@ -441,12 +453,9 @@ Result<std::shared_ptr<AuthSession>> SigV4AuthManager::WrapSession(
441453
AuthProperties::kSigV4SecretAccessKey +
442454
"' or configure the AWS credentials chain"});
443455
}
444-
auto session =
445-
std::make_shared<SigV4AuthSession>(std::move(delegate_session), std::move(region),
446-
std::move(service), std::move(credentials));
447-
// The reserved slot's unregister responsibility now belongs to the session.
448-
session->owns_sdk_registration_ = true;
449-
slot.Release();
456+
ICEBERG_ASSIGN_OR_RAISE(
457+
auto session, SigV4AuthSession::Make(std::move(delegate_session), std::move(region),
458+
std::move(service), std::move(credentials)));
450459
return session;
451460
}
452461

‎src/iceberg/catalog/rest/auth/sigv4_auth_manager_internal.h‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ class ICEBERG_REST_EXPORT SigV4AuthSession : public AuthSession {
5858
/// Prefix prepended to relocated headers that conflict with SigV4-signed headers.
5959
static constexpr std::string_view kRelocatedHeaderPrefix = "Original-";
6060

61-
SigV4AuthSession(
61+
/// \brief Creates a session registered with the AWS SDK lifecycle.
62+
///
63+
/// Fails if the SDK is not initialized. Every session owns its lifecycle
64+
/// registration and unregisters on destruction.
65+
static Result<std::shared_ptr<SigV4AuthSession>> Make(
6266
std::shared_ptr<AuthSession> delegate, std::string signing_region,
6367
std::string signing_name,
6468
std::shared_ptr<Aws::Auth::AWSCredentialsProvider> credentials_provider);
@@ -78,17 +82,16 @@ class ICEBERG_REST_EXPORT SigV4AuthSession : public AuthSession {
7882
}
7983

8084
private:
81-
// WrapSession() reserves an AwsSdkLifecycle slot and transfers it here.
82-
friend class SigV4AuthManager;
85+
SigV4AuthSession(
86+
std::shared_ptr<AuthSession> delegate, std::string signing_region,
87+
std::string signing_name,
88+
std::shared_ptr<Aws::Auth::AWSCredentialsProvider> credentials_provider);
8389

8490
std::shared_ptr<AuthSession> delegate_;
8591
std::string signing_region_;
8692
std::string signing_name_;
8793
std::shared_ptr<Aws::Auth::AWSCredentialsProvider> credentials_provider_;
8894
std::unique_ptr<Aws::Client::AWSAuthV4Signer> signer_;
89-
// Only WrapSession()-created sessions registered a slot; directly-constructed
90-
// ones (e.g. tests) must not unregister and underflow the count.
91-
bool owns_sdk_registration_ = false;
9295
};
9396

9497
/// \brief An AuthManager that produces SigV4AuthSession instances.

‎src/iceberg/catalog/rest/rest_catalog.cc‎

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,56 @@ Result<bool> CaptureNoSuchNamespace(const auto& status) {
118118
return CaptureNoSuchObject(status, ErrorKind::kNoSuchNamespace);
119119
}
120120

121+
std::string TableSessionKey(const TableIdentifier& identifier) {
122+
std::string key;
123+
for (const auto& level : identifier.ns.levels) {
124+
key += level;
125+
key += '\x1f';
126+
}
127+
key += identifier.name;
128+
return key;
129+
}
130+
121131
} // namespace
122132

123133
RestCatalog::~RestCatalog() {
134+
for (auto& [key, session] : table_sessions_) {
135+
if (session) {
136+
std::ignore = session->Close();
137+
}
138+
}
124139
if (catalog_session_) {
125140
std::ignore = catalog_session_->Close();
126141
}
127142
}
128143

144+
Status RestCatalog::RememberTableSession(
145+
const TableIdentifier& identifier,
146+
const std::unordered_map<std::string, std::string>& config) {
147+
ICEBERG_ASSIGN_OR_RAISE(
148+
auto session, auth_manager_->TableSession(identifier, config, catalog_session_));
149+
if (session == catalog_session_) {
150+
return {};
151+
}
152+
std::shared_ptr<auth::AuthSession> replaced;
153+
{
154+
std::lock_guard<std::mutex> lock(table_sessions_mutex_);
155+
auto& slot = table_sessions_[TableSessionKey(identifier)];
156+
replaced = std::exchange(slot, std::move(session));
157+
}
158+
if (replaced) {
159+
std::ignore = replaced->Close();
160+
}
161+
return {};
162+
}
163+
164+
std::shared_ptr<auth::AuthSession> RestCatalog::SessionFor(
165+
const TableIdentifier& identifier) {
166+
std::lock_guard<std::mutex> lock(table_sessions_mutex_);
167+
auto it = table_sessions_.find(TableSessionKey(identifier));
168+
return it != table_sessions_.end() ? it->second : catalog_session_;
169+
}
170+
129171
Result<std::shared_ptr<RestCatalog>> RestCatalog::Make(
130172
const RestCatalogProperties& config) {
131173
ICEBERG_ASSIGN_OR_RAISE(auto uri, config.Uri());
@@ -355,7 +397,9 @@ Result<LoadTableResult> RestCatalog::CreateTableInternal(
355397
*catalog_session_));
356398

357399
ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(response.body()));
358-
return LoadTableResultFromJson(json);
400+
ICEBERG_ASSIGN_OR_RAISE(auto load_result, LoadTableResultFromJson(json));
401+
ICEBERG_RETURN_UNEXPECTED(RememberTableSession(identifier, load_result.config));
402+
return load_result;
359403
}
360404

361405
Result<std::shared_ptr<Table>> RestCatalog::CreateTable(
@@ -391,7 +435,7 @@ Result<std::shared_ptr<Table>> RestCatalog::UpdateTable(
391435
ICEBERG_ASSIGN_OR_RAISE(
392436
const auto response,
393437
client_->Post(path, json_request, /*headers=*/{}, *TableErrorHandler::Instance(),
394-
*catalog_session_));
438+
*SessionFor(identifier)));
395439

396440
ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(response.body()));
397441
ICEBERG_ASSIGN_OR_RAISE(auto commit_response, CommitTableResponseFromJson(json));
@@ -479,6 +523,7 @@ Result<std::shared_ptr<Table>> RestCatalog::LoadTable(const TableIdentifier& ide
479523
ICEBERG_ASSIGN_OR_RAISE(const auto body, LoadTableInternal(identifier));
480524
ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(body));
481525
ICEBERG_ASSIGN_OR_RAISE(auto load_result, LoadTableResultFromJson(json));
526+
ICEBERG_RETURN_UNEXPECTED(RememberTableSession(identifier, load_result.config));
482527
/// FIXME: support per-table FileIO creation
483528
return Table::Make(identifier, std::move(load_result.metadata),
484529
std::move(load_result.metadata_location), file_io_,
@@ -503,6 +548,7 @@ Result<std::shared_ptr<Table>> RestCatalog::RegisterTable(
503548

504549
ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(response.body()));
505550
ICEBERG_ASSIGN_OR_RAISE(auto load_result, LoadTableResultFromJson(json));
551+
ICEBERG_RETURN_UNEXPECTED(RememberTableSession(identifier, load_result.config));
506552
return Table::Make(identifier, std::move(load_result.metadata),
507553
std::move(load_result.metadata_location), file_io_,
508554
shared_from_this());

‎src/iceberg/catalog/rest/rest_catalog.h‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
#pragma once
2121

2222
#include <memory>
23+
#include <mutex>
2324
#include <string>
25+
#include <unordered_map>
2426
#include <unordered_set>
2527

2628
#include "iceberg/catalog.h"
@@ -109,6 +111,14 @@ class ICEBERG_REST_EXPORT RestCatalog : public Catalog,
109111

110112
Result<std::string> LoadTableInternal(const TableIdentifier& identifier) const;
111113

114+
/// \brief Derives and caches a per-table auth session from a table response
115+
/// `config`, per the REST spec (`config.token` must be used for table requests).
116+
Status RememberTableSession(const TableIdentifier& identifier,
117+
const std::unordered_map<std::string, std::string>& config);
118+
119+
/// \brief Returns the cached per-table session, or the catalog session.
120+
std::shared_ptr<auth::AuthSession> SessionFor(const TableIdentifier& identifier);
121+
112122
Result<LoadTableResult> CreateTableInternal(
113123
const TableIdentifier& identifier, const std::shared_ptr<Schema>& schema,
114124
const std::shared_ptr<PartitionSpec>& spec, const std::shared_ptr<SortOrder>& order,
@@ -124,6 +134,8 @@ class ICEBERG_REST_EXPORT RestCatalog : public Catalog,
124134
std::unique_ptr<auth::AuthManager> auth_manager_;
125135
std::shared_ptr<auth::AuthSession> catalog_session_;
126136
SnapshotMode snapshot_mode_;
137+
std::mutex table_sessions_mutex_;
138+
std::unordered_map<std::string, std::shared_ptr<auth::AuthSession>> table_sessions_;
127139
};
128140

129141
} // namespace iceberg::rest

‎src/iceberg/test/sigv4_auth_test.cc‎

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -72,28 +72,23 @@ TEST_F(SigV4AuthTest, LifecycleFinalizeRefusesWhileSessionsAlive) {
7272
EXPECT_TRUE(IsAwsSdkInitialized());
7373
}
7474

75-
TEST_F(SigV4AuthTest, DirectlyConstructedSessionDoesNotCorruptLifecycleCount) {
76-
// A directly-constructed session never registered with AwsSdkLifecycle, so
77-
// destroying it must not decrement the count. Otherwise it underflows, and a
78-
// later real session wraps it back to zero, letting FinalizeAwsSdk() shut the
79-
// SDK down while a session is still alive.
75+
TEST_F(SigV4AuthTest, SessionRegistrationBalancesLifecycleCount) {
8076
{
8177
auto delegate = AuthSession::MakeDefault({});
8278
auto credentials = std::make_shared<Aws::Auth::SimpleAWSCredentialsProvider>(
8379
Aws::Auth::AWSCredentials("id", "secret"));
84-
auto direct = std::make_shared<SigV4AuthSession>(delegate, "us-east-1", "execute-api",
85-
credentials);
86-
} // destroyed here — must leave the lifecycle count untouched
80+
auto session_result =
81+
SigV4AuthSession::Make(delegate, "us-east-1", "execute-api", credentials);
82+
ASSERT_THAT(session_result, IsOk());
83+
EXPECT_THAT(FinalizeAwsSdk(), IsError(ErrorKind::kInvalid));
84+
}
8785

8886
auto properties = MakeSigV4Properties();
8987
auto manager_result = AuthManagers::Load("test-catalog", properties);
9088
ASSERT_THAT(manager_result, IsOk());
9189
auto session_result = manager_result.value()->CatalogSession(client_, properties);
9290
ASSERT_THAT(session_result, IsOk());
9391

94-
// Exactly one live (registered) session, so Finalize must refuse. With the
95-
// underflow bug the count would have wrapped and Finalize could wrongly
96-
// succeed and shut the SDK down.
9792
EXPECT_THAT(FinalizeAwsSdk(), IsError(ErrorKind::kInvalid));
9893
EXPECT_TRUE(IsAwsSdkInitialized());
9994
}
@@ -338,7 +333,8 @@ TEST_F(SigV4AuthTest, AuthenticateWithBodyDetailedHeaders) {
338333
ASSERT_NE(auth_it, headers.end());
339334
EXPECT_TRUE(auth_it->second.starts_with("AWS4-HMAC-SHA256 Credential="));
340335

341-
// x-amz-content-sha256 should be Base64-encoded body SHA256 (matching Java)
336+
// Java parity: the x-amz-content-sha256 header is Base64(SHA256(body)) for
337+
// non-empty bodies; the canonical request payload hash stays lowercase hex.
342338
auto sha_it = headers.find("x-amz-content-sha256");
343339
ASSERT_NE(sha_it, headers.end());
344340
EXPECT_NE(sha_it->second, SigV4AuthSession::kEmptyBodySha256);
@@ -390,8 +386,10 @@ TEST_F(SigV4AuthTest, ConflictingSigV4HeadersRelocated) {
390386
auto credentials =
391387
std::make_shared<Aws::Auth::SimpleAWSCredentialsProvider>(Aws::Auth::AWSCredentials(
392388
"AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"));
393-
auto session = std::make_shared<SigV4AuthSession>(delegate, "us-east-1", "execute-api",
394-
credentials);
389+
auto session_result =
390+
SigV4AuthSession::Make(delegate, "us-east-1", "execute-api", credentials);
391+
ASSERT_THAT(session_result, IsOk());
392+
auto session = session_result.value();
395393

396394
HttpRequest request{.method = HttpMethod::kGet, .url = "http://localhost:8080/path"};
397395
auto auth_result = session->Authenticate(request);
@@ -418,11 +416,11 @@ TEST_F(SigV4AuthTest, SessionCloseDelegatesToInner) {
418416
auto delegate = AuthSession::MakeDefault({});
419417
auto credentials = std::make_shared<Aws::Auth::SimpleAWSCredentialsProvider>(
420418
Aws::Auth::AWSCredentials("id", "secret"));
421-
auto session = std::make_shared<SigV4AuthSession>(delegate, "us-east-1", "execute-api",
422-
credentials);
419+
auto session_result =
420+
SigV4AuthSession::Make(delegate, "us-east-1", "execute-api", credentials);
421+
ASSERT_THAT(session_result, IsOk());
423422

424-
// Close should succeed without error
425-
EXPECT_THAT(session->Close(), IsOk());
423+
EXPECT_THAT(session_result.value()->Close(), IsOk());
426424
}
427425

428426
TEST_F(SigV4AuthTest, CreateCustomDelegateNone) {

0 commit comments

Comments
 (0)