Skip to content

Commit 0dfb1ef

Browse files
author
shuxu.li
committed
feat(auth): implement AuthSessionCache for OAuth2 child sessions
1 parent 85f8a84 commit 0dfb1ef

13 files changed

Lines changed: 990 additions & 35 deletions

‎src/iceberg/catalog/rest/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ set(ICEBERG_REST_SOURCES
2222
auth/auth_managers.cc
2323
auth/auth_properties.cc
2424
auth/auth_session.cc
25+
auth/auth_session_cache.cc
2526
auth/oauth2_util.cc
2627
auth/sigv4_manager.cc
2728
auth/token_refresh_scheduler.cc

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

Lines changed: 93 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828
#include "iceberg/catalog/rest/auth/auth_manager_internal.h"
2929
#include "iceberg/catalog/rest/auth/auth_properties.h"
3030
#include "iceberg/catalog/rest/auth/auth_session.h"
31+
#include "iceberg/catalog/rest/auth/auth_session_cache_internal.h"
3132
#include "iceberg/catalog/rest/auth/auth_session_internal.h"
3233
#include "iceberg/catalog/rest/auth/oauth2_util.h"
34+
#include "iceberg/catalog/rest/auth/token_refresh_scheduler.h"
3335
#include "iceberg/catalog/session_context.h"
3436
#include "iceberg/util/base64.h"
3537
#include "iceberg/util/macros.h"
@@ -156,13 +158,13 @@ class OAuth2Manager : public AuthManager {
156158
start_time_ = std::chrono::steady_clock::now();
157159
ICEBERG_ASSIGN_OR_RAISE(
158160
auth_response_, OAuth2Util::FetchToken(*init_client, *init_session, config));
159-
// TODO(lishuxu): Match Java OAuth2Util.AuthSession.fromTokenResponse here.
161+
// TODO(lishuxu): Build the initialization session from the full token response.
160162
return AuthSession::MakeDefault(
161163
OAuth2Util::AuthHeaders(auth_response_->access_token));
162164
}
163165

164166
if (!config.token().empty()) {
165-
// TODO(lishuxu): Match Java OAuth2Util.AuthSession.fromAccessToken here.
167+
// TODO(lishuxu): Preserve configured access-token expiration in the init session.
166168
return AuthSession::MakeDefault(OAuth2Util::AuthHeaders(config.token()));
167169
}
168170

@@ -176,6 +178,28 @@ class OAuth2Manager : public AuthManager {
176178
ICEBERG_PRECHECK(shared_client != nullptr,
177179
"OAuth2 catalog session HTTP client must not be null");
178180
refresh_client_ = std::move(shared_client);
181+
182+
// Initialize catalog-level configuration
183+
keep_refreshed_ = config.keep_refreshed();
184+
exchange_enabled_ = config.exchange_enabled();
185+
session_timeout_ =
186+
std::chrono::milliseconds(config.Get(AuthProperties::kSessionTimeoutMs));
187+
188+
// Create session cache
189+
ICEBERG_ASSIGN_OR_RAISE(
190+
session_cache_,
191+
internal::AuthSessionCache::Make(
192+
session_timeout_, [](std::shared_ptr<AuthSession> session) {
193+
if (auto oauth2 =
194+
std::dynamic_pointer_cast<internal::OAuth2Session>(session)) {
195+
oauth2->StopRefreshing();
196+
}
197+
}));
198+
199+
// Start periodic sweep to reclaim idle sessions
200+
auto sweep_interval = std::min(session_timeout_, std::chrono::milliseconds(60000));
201+
session_cache_->StartPeriodicSweep(TokenRefreshScheduler::Instance(), sweep_interval);
202+
179203
// Reuse the token response and start time from the init phase.
180204
if (auth_response_.has_value()) {
181205
return internal::MakeOAuth2Session(
@@ -184,8 +208,8 @@ class OAuth2Manager : public AuthManager {
184208
config.optional_oauth_params(), refresh_client_, start_time_);
185209
}
186210

187-
// TODO(lishuxu): Honor token-refresh-enabled for catalog bearer tokens, matching
188-
// Java. If token is provided, use it directly.
211+
// TODO(lishuxu): Honor token-refresh-enabled for catalog bearer tokens.
212+
// If token is provided, use it directly.
189213
if (!config.token().empty()) {
190214
OAuthTokenResponse token_response{
191215
.access_token = config.token(),
@@ -217,21 +241,31 @@ class OAuth2Manager : public AuthManager {
217241

218242
Result<std::shared_ptr<AuthSession>> ContextualSession(
219243
const SessionContext& context, std::shared_ptr<AuthSession> parent) override {
220-
// TODO(lishuxu): Add child-session caching and refresh, matching Java
221-
// AuthSessionCache.
244+
// Use session_id as cache key for contextual sessions
245+
std::string cache_key = context.session_id.empty() ? "" : "ctx:" + context.session_id;
222246
return MaybeCreateChildSession(context.credentials, /*allow_credential=*/true,
223-
std::move(parent));
247+
std::move(parent), cache_key);
224248
}
225249

226250
Result<std::shared_ptr<AuthSession>> TableSession(
227251
[[maybe_unused]] const TableIdentifier& table,
228252
const std::unordered_map<std::string, std::string>& properties,
229253
std::shared_ptr<AuthSession> parent) override {
254+
// Use token value as cache key for table sessions
255+
auto token_it = properties.find(AuthProperties::kToken.key());
256+
std::string cache_key;
257+
if (token_it != properties.end() && !token_it->second.empty()) {
258+
cache_key = "tbl:" + token_it->second;
259+
}
230260
return MaybeCreateChildSession(FilterTableSessionProperties(properties),
231-
/*allow_credential=*/false, std::move(parent));
261+
/*allow_credential=*/false, std::move(parent),
262+
cache_key);
232263
}
233264

234265
Status Close() override {
266+
if (session_cache_) {
267+
session_cache_->Close();
268+
}
235269
refresh_client_.reset();
236270
return {};
237271
}
@@ -267,7 +301,8 @@ class OAuth2Manager : public AuthManager {
267301

268302
Result<std::shared_ptr<AuthSession>> MaybeCreateChildSession(
269303
const std::unordered_map<std::string, std::string>& credentials,
270-
bool allow_credential, std::shared_ptr<AuthSession> parent) {
304+
bool allow_credential, std::shared_ptr<AuthSession> parent,
305+
const std::string& cache_key) {
271306
auto token_it = credentials.find(AuthProperties::kToken.key());
272307
auto credential_it = credentials.find(AuthProperties::kCredential.key());
273308
auto typed_token = FindPreferredTypedToken(credentials);
@@ -283,42 +318,78 @@ class OAuth2Manager : public AuthManager {
283318
ICEBERG_PRECHECK(parent_info.has_value(),
284319
"OAuth2 child session requires OAuth2 parent metadata");
285320

321+
// Use cache if we have a valid cache_key
322+
if (!cache_key.empty() && session_cache_) {
323+
return session_cache_->Get(cache_key,
324+
[&]() -> Result<std::shared_ptr<AuthSession>> {
325+
return CreateChildSessionUncached(
326+
credentials, allow_credential, *parent_info);
327+
});
328+
}
329+
330+
// No cache, create session directly
331+
return CreateChildSessionUncached(credentials, allow_credential, *parent_info);
332+
}
333+
334+
Result<std::shared_ptr<AuthSession>> CreateChildSessionUncached(
335+
const std::unordered_map<std::string, std::string>& credentials,
336+
bool allow_credential, const OAuth2SessionInfo& parent_info) {
337+
auto token_it = credentials.find(AuthProperties::kToken.key());
338+
auto credential_it = credentials.find(AuthProperties::kCredential.key());
339+
auto typed_token = FindPreferredTypedToken(credentials);
340+
286341
if (token_it != credentials.end()) {
287-
ICEBERG_ASSIGN_OR_RAISE(auto config,
288-
ChildConfig(*parent_info, parent_info->credential));
342+
// Token child session: don't include parent credential in config.
343+
// Token-based sessions are not refreshed; when token exchange is enabled,
344+
// refresh will use the token itself rather than the parent credential.
345+
auto properties = parent_info.optional_oauth_params;
346+
properties[AuthProperties::kScope.key()] = parent_info.scope;
347+
properties[AuthProperties::kOAuth2ServerUri.key()] = parent_info.oauth2_server_uri;
348+
ICEBERG_ASSIGN_OR_RAISE(auto config, AuthProperties::FromProperties(properties));
289349
return MakeSession(AccessTokenResponse(token_it->second), config,
290350
/*keep_refreshed=*/false);
291351
}
292352

293353
if (allow_credential && credential_it != credentials.end()) {
294354
ICEBERG_ASSIGN_OR_RAISE(auto config,
295-
ChildConfig(*parent_info, credential_it->second));
296-
ICEBERG_ASSIGN_OR_RAISE(auto response,
297-
OAuth2Util::FetchToken(*refresh_client_, *parent, config));
298-
return MakeSession(response, config, /*keep_refreshed=*/false);
355+
ChildConfig(parent_info, credential_it->second));
356+
// Use parent session headers for FetchToken authentication
357+
auto temp_parent = AuthSession::MakeDefault(parent_info.headers);
358+
ICEBERG_ASSIGN_OR_RAISE(
359+
auto response, OAuth2Util::FetchToken(*refresh_client_, *temp_parent, config));
360+
// Credential child sessions use keep_refreshed from catalog level
361+
return MakeSession(response, config, keep_refreshed_);
299362
}
300363

301364
std::optional<std::string> actor_token;
302365
std::optional<std::string> actor_token_type;
303-
if (!parent_info->token.empty()) {
304-
actor_token = parent_info->token;
305-
actor_token_type = parent_info->issued_token_type;
366+
if (!parent_info.token.empty()) {
367+
actor_token = parent_info.token;
368+
actor_token_type = parent_info.issued_token_type;
306369
}
370+
// Use parent session headers for ExchangeToken authentication
371+
auto temp_parent = AuthSession::MakeDefault(parent_info.headers);
307372
ICEBERG_ASSIGN_OR_RAISE(
308373
auto response,
309-
OAuth2Util::ExchangeToken(*refresh_client_, *parent, {}, typed_token->second,
374+
OAuth2Util::ExchangeToken(*refresh_client_, *temp_parent, {}, typed_token->second,
310375
typed_token->first, actor_token, actor_token_type,
311-
parent_info->scope, parent_info->oauth2_server_uri,
312-
parent_info->optional_oauth_params));
376+
parent_info.scope, parent_info.oauth2_server_uri,
377+
parent_info.optional_oauth_params));
313378
ICEBERG_ASSIGN_OR_RAISE(auto config,
314-
ChildConfig(*parent_info, parent_info->credential));
379+
ChildConfig(parent_info, parent_info.credential));
315380
return MakeSession(response, config, /*keep_refreshed=*/false);
316381
}
317382

318383
/// Token response and start time captured by InitSession.
319384
std::optional<OAuthTokenResponse> auth_response_;
320385
std::optional<std::chrono::steady_clock::time_point> start_time_;
321386
std::shared_ptr<HttpClient> refresh_client_;
387+
388+
// Catalog-level configuration initialized in CatalogSession
389+
bool keep_refreshed_ = true;
390+
bool exchange_enabled_ = true;
391+
std::chrono::milliseconds session_timeout_{3'600'000};
392+
std::shared_ptr<internal::AuthSessionCache> session_cache_;
322393
};
323394

324395
Result<std::unique_ptr<AuthManager>> MakeOAuth2Manager(

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ class ICEBERG_REST_EXPORT AuthProperties : public ConfigBase<AuthProperties> {
8282
inline static Entry<std::string> kAudience{"audience", ""};
8383
inline static Entry<std::string> kResource{"resource", ""};
8484

85+
/// Session cache timeout in milliseconds. Sessions will be eligible for eviction
86+
/// after this duration of inactivity. Default is 1 hour (3,600,000 ms).
87+
inline static Entry<int64_t> kSessionTimeoutMs{"auth.session-timeout-ms", 3'600'000};
88+
8589
// ---- OAuth2 token type constants ----
8690

8791
inline static const std::string kAccessTokenType =

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ namespace iceberg::rest::auth {
3737
/// \brief OAuth2 metadata used to derive child authentication sessions.
3838
struct ICEBERG_REST_EXPORT OAuth2SessionInfo {
3939
std::string token;
40+
std::string token_type;
4041
std::string issued_token_type;
4142
std::string credential;
4243
std::string scope;
4344
std::string oauth2_server_uri;
4445
std::unordered_map<std::string, std::string> optional_oauth_params;
46+
std::unordered_map<std::string, std::string> headers;
4547
};
4648

4749
/// \brief An authentication session that can authenticate outgoing HTTP requests.

0 commit comments

Comments
 (0)