Skip to content

Commit e58189f

Browse files
feat(rest): introduce AuthManager/AuthSession and rework SigV4 as a wrapping manager
Mirrors Java's AuthManager API: Noop/OAuth2/SigV4 managers selected via rest.auth.type (or the legacy rest.sigv4-enabled), injectable through RestCatalogBuilder::with_auth_manager. OAuth2 token handling moves out of HttpClient into OAuth2Manager with the cached token surviving the config handshake. SigV4 now wraps a delegate session, relocating its Authorization header to X-Iceberg-Authorization before signing, so token auth composes with SigV4 instead of being mutually exclusive.
1 parent a4c4d8e commit e58189f

8 files changed

Lines changed: 809 additions & 329 deletions

File tree

crates/catalog/rest/public-api.txt

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,18 @@ impl serde_core::ser::Serialize for iceberg_catalog_rest::NamespaceResponse
193193
pub fn iceberg_catalog_rest::NamespaceResponse::serialize<__S>(&self, __serializer: __S) -> core::result::Result<<__S as serde_core::ser::Serializer>::Ok, <__S as serde_core::ser::Serializer>::Error> where __S: serde_core::ser::Serializer
194194
impl<'de> serde_core::de::Deserialize<'de> for iceberg_catalog_rest::NamespaceResponse
195195
pub fn iceberg_catalog_rest::NamespaceResponse::deserialize<__D>(__deserializer: __D) -> core::result::Result<Self, <__D as serde_core::de::Deserializer>::Error> where __D: serde_core::de::Deserializer<'de>
196+
pub struct iceberg_catalog_rest::NoopAuthManager
197+
impl core::fmt::Debug for iceberg_catalog_rest::NoopAuthManager
198+
pub fn iceberg_catalog_rest::NoopAuthManager::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
199+
impl iceberg_catalog_rest::AuthManager for iceberg_catalog_rest::NoopAuthManager
200+
pub fn iceberg_catalog_rest::NoopAuthManager::catalog_session<'life0, 'life1, 'async_trait>(&'life0 self, _props: &'life1 std::collections::hash::map::HashMap<alloc::string::String, alloc::string::String>) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = iceberg::error::Result<alloc::sync::Arc<dyn iceberg_catalog_rest::AuthSession>>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait
201+
pub fn iceberg_catalog_rest::NoopAuthManager::init_session<'life0, 'async_trait>(&'life0 self) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = iceberg::error::Result<alloc::sync::Arc<dyn iceberg_catalog_rest::AuthSession>>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait
202+
pub struct iceberg_catalog_rest::OAuth2Manager
203+
impl core::fmt::Debug for iceberg_catalog_rest::OAuth2Manager
204+
pub fn iceberg_catalog_rest::OAuth2Manager::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
205+
impl iceberg_catalog_rest::AuthManager for iceberg_catalog_rest::OAuth2Manager
206+
pub fn iceberg_catalog_rest::OAuth2Manager::catalog_session<'life0, 'life1, 'async_trait>(&'life0 self, props: &'life1 std::collections::hash::map::HashMap<alloc::string::String, alloc::string::String>) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = iceberg::error::Result<alloc::sync::Arc<dyn iceberg_catalog_rest::AuthSession>>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait
207+
pub fn iceberg_catalog_rest::OAuth2Manager::init_session<'life0, 'async_trait>(&'life0 self) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = iceberg::error::Result<alloc::sync::Arc<dyn iceberg_catalog_rest::AuthSession>>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait
196208
pub struct iceberg_catalog_rest::RegisterTableRequest
197209
pub iceberg_catalog_rest::RegisterTableRequest::metadata_location: alloc::string::String
198210
pub iceberg_catalog_rest::RegisterTableRequest::name: alloc::string::String
@@ -248,8 +260,8 @@ pub fn iceberg_catalog_rest::RestCatalog::update_namespace<'life0, 'life1, 'asyn
248260
pub fn iceberg_catalog_rest::RestCatalog::update_table<'life0, 'async_trait>(&'life0 self, commit: iceberg::catalog::TableCommit) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = iceberg::error::Result<iceberg::table::Table>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait
249261
pub struct iceberg_catalog_rest::RestCatalogBuilder
250262
impl iceberg_catalog_rest::RestCatalogBuilder
263+
pub fn iceberg_catalog_rest::RestCatalogBuilder::with_auth_manager(self, auth_manager: alloc::sync::Arc<dyn iceberg_catalog_rest::AuthManager>) -> Self
251264
pub fn iceberg_catalog_rest::RestCatalogBuilder::with_client(self, client: reqwest::async_impl::client::Client) -> Self
252-
pub fn iceberg_catalog_rest::RestCatalogBuilder::with_signer(self, signer: alloc::sync::Arc<dyn iceberg_catalog_rest::HttpRequestSigner>) -> Self
253265
impl core::default::Default for iceberg_catalog_rest::RestCatalogBuilder
254266
pub fn iceberg_catalog_rest::RestCatalogBuilder::default() -> Self
255267
impl core::fmt::Debug for iceberg_catalog_rest::RestCatalogBuilder
@@ -260,15 +272,22 @@ pub fn iceberg_catalog_rest::RestCatalogBuilder::load(self, name: impl core::con
260272
pub fn iceberg_catalog_rest::RestCatalogBuilder::with_kms_client_factory(self, kms_client_factory: alloc::sync::Arc<dyn iceberg::encryption::kms::factory::KmsClientFactory>) -> Self
261273
pub fn iceberg_catalog_rest::RestCatalogBuilder::with_runtime(self, runtime: iceberg::runtime::Runtime) -> Self
262274
pub fn iceberg_catalog_rest::RestCatalogBuilder::with_storage_factory(self, storage_factory: alloc::sync::Arc<dyn iceberg::io::storage::StorageFactory>) -> Self
275+
pub struct iceberg_catalog_rest::SigV4AuthManager
276+
impl iceberg_catalog_rest::SigV4AuthManager
277+
pub fn iceberg_catalog_rest::SigV4AuthManager::new(delegate: alloc::sync::Arc<dyn iceberg_catalog_rest::AuthManager>, signer: iceberg_catalog_rest::SigV4Signer) -> Self
278+
impl core::fmt::Debug for iceberg_catalog_rest::SigV4AuthManager
279+
pub fn iceberg_catalog_rest::SigV4AuthManager::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
280+
impl iceberg_catalog_rest::AuthManager for iceberg_catalog_rest::SigV4AuthManager
281+
pub fn iceberg_catalog_rest::SigV4AuthManager::catalog_session<'life0, 'life1, 'async_trait>(&'life0 self, props: &'life1 std::collections::hash::map::HashMap<alloc::string::String, alloc::string::String>) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = iceberg::error::Result<alloc::sync::Arc<dyn iceberg_catalog_rest::AuthSession>>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait
282+
pub fn iceberg_catalog_rest::SigV4AuthManager::init_session<'life0, 'async_trait>(&'life0 self) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = iceberg::error::Result<alloc::sync::Arc<dyn iceberg_catalog_rest::AuthSession>>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait
263283
pub struct iceberg_catalog_rest::SigV4Signer
264284
impl iceberg_catalog_rest::SigV4Signer
265285
pub fn iceberg_catalog_rest::SigV4Signer::new(credentials: iceberg_catalog_rest::AwsCredentials, region: alloc::string::String, service: alloc::string::String, mode: iceberg_catalog_rest::PayloadHashMode) -> Self
286+
pub fn iceberg_catalog_rest::SigV4Signer::sign(&self, request: &mut reqwest::async_impl::request::Request) -> iceberg::error::Result<()>
266287
impl core::clone::Clone for iceberg_catalog_rest::SigV4Signer
267288
pub fn iceberg_catalog_rest::SigV4Signer::clone(&self) -> iceberg_catalog_rest::SigV4Signer
268289
impl core::fmt::Debug for iceberg_catalog_rest::SigV4Signer
269290
pub fn iceberg_catalog_rest::SigV4Signer::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
270-
impl iceberg_catalog_rest::HttpRequestSigner for iceberg_catalog_rest::SigV4Signer
271-
pub fn iceberg_catalog_rest::SigV4Signer::sign(&self, request: &mut reqwest::async_impl::request::Request) -> iceberg::error::Result<()>
272291
pub struct iceberg_catalog_rest::StorageCredential
273292
pub iceberg_catalog_rest::StorageCredential::config: std::collections::hash::map::HashMap<alloc::string::String, alloc::string::String>
274293
pub iceberg_catalog_rest::StorageCredential::prefix: alloc::string::String
@@ -315,6 +334,10 @@ impl serde_core::ser::Serialize for iceberg_catalog_rest::UpdateNamespacePropert
315334
pub fn iceberg_catalog_rest::UpdateNamespacePropertiesResponse::serialize<__S>(&self, __serializer: __S) -> core::result::Result<<__S as serde_core::ser::Serializer>::Ok, <__S as serde_core::ser::Serializer>::Error> where __S: serde_core::ser::Serializer
316335
impl<'de> serde_core::de::Deserialize<'de> for iceberg_catalog_rest::UpdateNamespacePropertiesResponse
317336
pub fn iceberg_catalog_rest::UpdateNamespacePropertiesResponse::deserialize<__D>(__deserializer: __D) -> core::result::Result<Self, <__D as serde_core::de::Deserializer>::Error> where __D: serde_core::de::Deserializer<'de>
337+
pub const iceberg_catalog_rest::AUTH_TYPE_NONE: &str
338+
pub const iceberg_catalog_rest::AUTH_TYPE_OAUTH2: &str
339+
pub const iceberg_catalog_rest::AUTH_TYPE_SIGV4: &str
340+
pub const iceberg_catalog_rest::REST_CATALOG_PROP_AUTH_TYPE: &str
318341
pub const iceberg_catalog_rest::REST_CATALOG_PROP_DISABLE_HEADER_REDACTION: &str
319342
pub const iceberg_catalog_rest::REST_CATALOG_PROP_SIGNING_ACCESS_KEY: &str
320343
pub const iceberg_catalog_rest::REST_CATALOG_PROP_SIGNING_NAME: &str
@@ -324,7 +347,19 @@ pub const iceberg_catalog_rest::REST_CATALOG_PROP_SIGNING_SESSION_TOKEN: &str
324347
pub const iceberg_catalog_rest::REST_CATALOG_PROP_SIGV4_ENABLED: &str
325348
pub const iceberg_catalog_rest::REST_CATALOG_PROP_URI: &str
326349
pub const iceberg_catalog_rest::REST_CATALOG_PROP_WAREHOUSE: &str
327-
pub trait iceberg_catalog_rest::HttpRequestSigner: core::marker::Send + core::marker::Sync + core::fmt::Debug
328-
pub fn iceberg_catalog_rest::HttpRequestSigner::sign(&self, request: &mut reqwest::async_impl::request::Request) -> iceberg::error::Result<()>
329-
impl iceberg_catalog_rest::HttpRequestSigner for iceberg_catalog_rest::SigV4Signer
330-
pub fn iceberg_catalog_rest::SigV4Signer::sign(&self, request: &mut reqwest::async_impl::request::Request) -> iceberg::error::Result<()>
350+
pub trait iceberg_catalog_rest::AuthManager: core::fmt::Debug + core::marker::Send + core::marker::Sync
351+
pub fn iceberg_catalog_rest::AuthManager::catalog_session<'life0, 'life1, 'async_trait>(&'life0 self, props: &'life1 std::collections::hash::map::HashMap<alloc::string::String, alloc::string::String>) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = iceberg::error::Result<alloc::sync::Arc<dyn iceberg_catalog_rest::AuthSession>>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait
352+
pub fn iceberg_catalog_rest::AuthManager::init_session<'life0, 'async_trait>(&'life0 self) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = iceberg::error::Result<alloc::sync::Arc<dyn iceberg_catalog_rest::AuthSession>>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait
353+
impl iceberg_catalog_rest::AuthManager for iceberg_catalog_rest::NoopAuthManager
354+
pub fn iceberg_catalog_rest::NoopAuthManager::catalog_session<'life0, 'life1, 'async_trait>(&'life0 self, _props: &'life1 std::collections::hash::map::HashMap<alloc::string::String, alloc::string::String>) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = iceberg::error::Result<alloc::sync::Arc<dyn iceberg_catalog_rest::AuthSession>>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait
355+
pub fn iceberg_catalog_rest::NoopAuthManager::init_session<'life0, 'async_trait>(&'life0 self) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = iceberg::error::Result<alloc::sync::Arc<dyn iceberg_catalog_rest::AuthSession>>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait
356+
impl iceberg_catalog_rest::AuthManager for iceberg_catalog_rest::OAuth2Manager
357+
pub fn iceberg_catalog_rest::OAuth2Manager::catalog_session<'life0, 'life1, 'async_trait>(&'life0 self, props: &'life1 std::collections::hash::map::HashMap<alloc::string::String, alloc::string::String>) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = iceberg::error::Result<alloc::sync::Arc<dyn iceberg_catalog_rest::AuthSession>>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait
358+
pub fn iceberg_catalog_rest::OAuth2Manager::init_session<'life0, 'async_trait>(&'life0 self) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = iceberg::error::Result<alloc::sync::Arc<dyn iceberg_catalog_rest::AuthSession>>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait
359+
impl iceberg_catalog_rest::AuthManager for iceberg_catalog_rest::SigV4AuthManager
360+
pub fn iceberg_catalog_rest::SigV4AuthManager::catalog_session<'life0, 'life1, 'async_trait>(&'life0 self, props: &'life1 std::collections::hash::map::HashMap<alloc::string::String, alloc::string::String>) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = iceberg::error::Result<alloc::sync::Arc<dyn iceberg_catalog_rest::AuthSession>>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait
361+
pub fn iceberg_catalog_rest::SigV4AuthManager::init_session<'life0, 'async_trait>(&'life0 self) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = iceberg::error::Result<alloc::sync::Arc<dyn iceberg_catalog_rest::AuthSession>>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait
362+
pub trait iceberg_catalog_rest::AuthSession: core::fmt::Debug + core::marker::Send + core::marker::Sync
363+
pub fn iceberg_catalog_rest::AuthSession::authenticate<'life0, 'life1, 'async_trait>(&'life0 self, request: &'life1 mut reqwest::async_impl::request::Request) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = iceberg::error::Result<()>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait
364+
pub fn iceberg_catalog_rest::AuthSession::invalidate<'life0, 'async_trait>(&'life0 self) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = iceberg::error::Result<()>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait
365+
pub fn iceberg_catalog_rest::AuthSession::refresh<'life0, 'async_trait>(&'life0 self) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = iceberg::error::Result<()>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
//! Pluggable authentication for the REST catalog, mirroring Iceberg Java's
19+
//! `AuthManager`/`AuthSession` API.
20+
21+
mod oauth2;
22+
mod sigv4;
23+
24+
use std::collections::HashMap;
25+
use std::fmt::Debug;
26+
use std::sync::Arc;
27+
28+
use async_trait::async_trait;
29+
use iceberg::Result;
30+
pub use oauth2::OAuth2Manager;
31+
use reqwest::Request;
32+
pub use sigv4::SigV4AuthManager;
33+
34+
/// `rest.auth.type` value disabling authentication.
35+
pub const AUTH_TYPE_NONE: &str = "none";
36+
/// `rest.auth.type` value selecting OAuth2 token authentication.
37+
pub const AUTH_TYPE_OAUTH2: &str = "oauth2";
38+
/// `rest.auth.type` value selecting AWS SigV4 request signing.
39+
pub const AUTH_TYPE_SIGV4: &str = "sigv4";
40+
41+
/// Creates the [`AuthSession`]s used to authenticate REST catalog requests.
42+
///
43+
/// A manager is created once per catalog, either from the `rest.auth.type`
44+
/// property or injected through `RestCatalogBuilder::with_auth_manager`, and
45+
/// lives for the lifetime of the catalog.
46+
#[async_trait]
47+
pub trait AuthManager: Debug + Send + Sync {
48+
/// Session used for the initial `/v1/config` handshake, built from the
49+
/// user-supplied configuration.
50+
async fn init_session(&self) -> Result<Arc<dyn AuthSession>>;
51+
52+
/// Session used for all subsequent catalog requests, given the properties
53+
/// merged from the user configuration and the server's config response.
54+
///
55+
/// Implementations may carry state (e.g. a cached token) over from the
56+
/// init session.
57+
async fn catalog_session(
58+
&self,
59+
props: &HashMap<String, String>,
60+
) -> Result<Arc<dyn AuthSession>>;
61+
}
62+
63+
/// Authenticates outgoing REST catalog requests.
64+
#[async_trait]
65+
pub trait AuthSession: Debug + Send + Sync {
66+
/// Applies authentication to the request (adds headers, signs, ...).
67+
async fn authenticate(&self, request: &mut Request) -> Result<()>;
68+
69+
/// Drops any cached credentials so the next request re-authenticates.
70+
async fn invalidate(&self) -> Result<()> {
71+
Ok(())
72+
}
73+
74+
/// Proactively refreshes cached credentials (e.g. re-exchanges an OAuth2
75+
/// client credential for a new token), leaving them intact on failure.
76+
async fn refresh(&self) -> Result<()> {
77+
Ok(())
78+
}
79+
}
80+
81+
/// [`AuthManager`] that performs no authentication.
82+
#[derive(Debug)]
83+
pub struct NoopAuthManager;
84+
85+
/// [`AuthSession`] that performs no authentication.
86+
#[derive(Debug)]
87+
struct NoopSession;
88+
89+
#[async_trait]
90+
impl AuthManager for NoopAuthManager {
91+
async fn init_session(&self) -> Result<Arc<dyn AuthSession>> {
92+
Ok(Arc::new(NoopSession))
93+
}
94+
95+
async fn catalog_session(
96+
&self,
97+
_props: &HashMap<String, String>,
98+
) -> Result<Arc<dyn AuthSession>> {
99+
Ok(Arc::new(NoopSession))
100+
}
101+
}
102+
103+
#[async_trait]
104+
impl AuthSession for NoopSession {
105+
async fn authenticate(&self, _request: &mut Request) -> Result<()> {
106+
Ok(())
107+
}
108+
}

0 commit comments

Comments
 (0)