Skip to content

Commit fd2d63e

Browse files
authored
Fix authentication callback (#45)
* Fix authentication callback * Format * Fix test * . * . * Fix test
1 parent b8d451d commit fd2d63e

2 files changed

Lines changed: 90 additions & 45 deletions

File tree

crates/catalog/rest/src/client.rs

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ impl HttpClient {
244244
// Try authenticator first (highest priority)
245245
if let Some(authenticator) = &self.authenticator {
246246
let token = authenticator.get_token().await?;
247+
// Cache the token so that subsequent requests can use it without calling the authenticator
248+
*self.token.lock().await = Some(token.clone());
247249
Self::set_bearer_token(req, &token, "Invalid custom token")?;
248250
return Ok(());
249251
}
@@ -284,41 +286,53 @@ impl HttpClient {
284286
Ok(self.client.execute(request).await?)
285287
}
286288

287-
// Queries the Iceberg REST catalog after authentication with the given `Request` and
288-
// returns a `Response`.
289+
// Queries the Iceberg REST catalog with authentication and returns a `Response`.
289290
//
290-
// If a custom authenticator is configured, the first request is sent without authentication.
291-
// If it fails with a 401/403 permission denied error, the custom authenticator is called
292-
// to get a fresh token and the request is retried.
291+
// For custom authenticators:
292+
// - On the first request, fetches a token from the authenticator and caches it.
293+
// - On subsequent requests, reuses the cached token without calling the authenticator.
294+
// - If a request returns 401/403, invalidates the cache and fetches a fresh token.
293295
//
294296
// For other authentication methods (static token, OAuth credentials), authentication
295297
// is applied to all requests as before.
296298
pub async fn query_catalog(&self, mut request: Request) -> Result<Response> {
297-
let has_custom_authenticator = self.authenticator.is_some();
298-
let token_is_set = self.token.lock().await.is_some();
299+
if self.authenticator.is_some() {
300+
// For custom authenticators, use cached token if available
301+
let token_is_set = self.token.lock().await.is_some();
302+
303+
if token_is_set {
304+
// We have a cached token, use it by applying the cached authorization
305+
// without calling the authenticator again
306+
let cached_token = self.token.lock().await.clone();
307+
if let Some(token) = cached_token {
308+
HttpClient::set_bearer_token(&mut request, &token, "Invalid cached token")?;
309+
}
310+
} else {
311+
// No cached token, fetch one from the authenticator
312+
self.authenticate(&mut request).await?;
313+
}
299314

300-
if has_custom_authenticator && token_is_set {
301-
// For custom authenticators with a cached token, try without authentication first
302-
// to avoid unnecessary token fetches
303-
let cloned_request = request
304-
.try_clone()
305-
.ok_or_else(|| Error::new(ErrorKind::DataInvalid, "Unable to clone request"))?;
306-
let response = self.execute(cloned_request).await?;
315+
// Send request with authentication
316+
let response =
317+
self.execute(request.try_clone().ok_or_else(|| {
318+
Error::new(ErrorKind::DataInvalid, "Unable to clone request")
319+
})?)
320+
.await?;
307321

308322
// Check if we got a permission denied error
309323
if matches!(
310324
response.status(),
311325
StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN
312326
) {
313-
// Retry with authentication from the custom authenticator
327+
// Token was rejected, invalidate and get a fresh one from the authenticator
328+
self.invalidate_token().await?;
314329
self.authenticate(&mut request).await?;
315330
return self.execute(request).await;
316331
}
317332

318333
Ok(response)
319334
} else {
320-
// For custom authenticators without a token, or other auth methods:
321-
// authenticate on every request
335+
// Other auth methods: authenticate on every request
322336
self.authenticate(&mut request).await?;
323337
self.execute(request).await
324338
}

crates/catalog/rest/tests/rest_catalog_test.rs

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -530,33 +530,58 @@ async fn test_authenticator_token_refresh() {
530530
count: token_request_count_clone,
531531
});
532532

533-
let catalog_with_auth = get_catalog(Some(authenticator)).await;
533+
let catalog_with_auth = get_catalog(Some(authenticator.clone())).await;
534534

535-
// Perform multiple operations that should trigger token requests
536-
let ns1 = Namespace::with_properties(
537-
NamespaceIdent::from_strs(["test_refresh_1"]).unwrap(),
538-
HashMap::new(),
539-
);
535+
// Clean up from any previous test runs
536+
let ns1_ident = NamespaceIdent::from_strs(["test_refresh_1"]).unwrap();
537+
let ns2_ident = NamespaceIdent::from_strs(["test_refresh_2"]).unwrap();
538+
cleanup_namespace(&catalog_with_auth, &ns1_ident).await;
539+
cleanup_namespace(&catalog_with_auth, &ns2_ident).await;
540+
541+
// Perform multiple operations that should reuse the cached token
542+
let ns1 = Namespace::with_properties(ns1_ident.clone(), HashMap::new());
540543
catalog_with_auth
541544
.create_namespace(ns1.name(), HashMap::new())
542545
.await
543546
.unwrap();
544547

545-
let ns2 = Namespace::with_properties(
546-
NamespaceIdent::from_strs(["test_refresh_2"]).unwrap(),
547-
HashMap::new(),
548-
);
548+
let ns2 = Namespace::with_properties(ns2_ident.clone(), HashMap::new());
549549
catalog_with_auth
550550
.create_namespace(ns2.name(), HashMap::new())
551551
.await
552552
.unwrap();
553553

554-
// Verify authenticator was called multiple times
554+
// With lazy authentication, the token is fetched once and cached for reuse
555+
// across multiple operations, rather than being called on every request
555556
let count = *token_request_count.lock().unwrap();
556-
assert!(
557-
count >= 2,
558-
"Authenticator should have been called at least twice, but was called {count} times"
557+
assert_eq!(
558+
count, 1,
559+
"Authenticator should have been called once for lazy token caching, but was called {count} times"
559560
);
561+
562+
// Test that token is refreshed when invalidated
563+
catalog_with_auth.invalidate_token().await.unwrap();
564+
565+
let ns3_ident = NamespaceIdent::from_strs(["test_refresh_3"]).unwrap();
566+
cleanup_namespace(&catalog_with_auth, &ns3_ident).await;
567+
568+
let ns3 = Namespace::with_properties(ns3_ident.clone(), HashMap::new());
569+
catalog_with_auth
570+
.create_namespace(ns3.name(), HashMap::new())
571+
.await
572+
.unwrap();
573+
574+
// After invalidating and making another request, authenticator should be called again
575+
let count = *token_request_count.lock().unwrap();
576+
assert_eq!(
577+
count, 2,
578+
"Authenticator should have been called twice (once initial, once after invalidation), but was called {count} times"
579+
);
580+
581+
// Clean up
582+
cleanup_namespace(&catalog_with_auth, &ns1_ident).await;
583+
cleanup_namespace(&catalog_with_auth, &ns2_ident).await;
584+
cleanup_namespace(&catalog_with_auth, &ns3_ident).await;
560585
}
561586

562587
#[tokio::test]
@@ -570,40 +595,46 @@ async fn test_authenticator_persists_across_operations() {
570595

571596
let catalog_with_auth = get_catalog(Some(authenticator)).await;
572597

598+
// Clean up from any previous test runs
599+
let ns_ident = NamespaceIdent::from_strs(["test_persist", "auth"]).unwrap();
600+
let parent_ident = NamespaceIdent::from_strs(["test_persist"]).unwrap();
601+
cleanup_namespace(&catalog_with_auth, &ns_ident).await;
602+
573603
// Create a namespace
574-
let ns = Namespace::with_properties(
575-
NamespaceIdent::from_strs(["test_persist", "auth"]).unwrap(),
576-
HashMap::new(),
577-
);
604+
let ns = Namespace::with_properties(ns_ident.clone(), HashMap::new());
578605
catalog_with_auth
579606
.create_namespace(ns.name(), HashMap::new())
580607
.await
581608
.unwrap();
582609

583610
let count_after_create = *operation_count.lock().unwrap();
584611

585-
// List the namespace children (should use the same authenticator)
612+
// List the namespace children (should reuse the cached token from the create operation)
586613
// We need to list children of "test_persist" to find "auth"
587614
let list_result = catalog_with_auth
588-
.list_namespaces(Some(&NamespaceIdent::from_strs(["test_persist"]).unwrap()))
615+
.list_namespaces(Some(&parent_ident))
589616
.await
590617
.unwrap();
591618
assert!(
592-
list_result.contains(&NamespaceIdent::from_strs(["test_persist", "auth"]).unwrap()),
619+
list_result.contains(&ns_ident),
593620
"Namespace {:?} not found in list {:?}",
594621
ns.name(),
595622
list_result
596623
);
597624

598625
let count_after_list = *operation_count.lock().unwrap();
599626

600-
// Verify authenticator was used for both operations
601-
assert!(
602-
count_after_create > 0,
603-
"Authenticator should be used for create"
627+
// With lazy authentication, the token is fetched once on the first operation
628+
// and then reused for subsequent operations without calling the authenticator again
629+
assert_eq!(
630+
count_after_create, 1,
631+
"Authenticator should be called once for the create operation"
604632
);
605-
assert!(
606-
count_after_list > count_after_create,
607-
"Authenticator should be used for list operation too"
633+
assert_eq!(
634+
count_after_list, 1,
635+
"Authenticator should still have been called only once (token is cached and reused for list)"
608636
);
637+
638+
// Clean up
639+
cleanup_namespace(&catalog_with_auth, &ns_ident).await;
609640
}

0 commit comments

Comments
 (0)