Skip to content

Commit 9b56016

Browse files
feat(rest): introduce AuthManager/AuthSession with OAuth2 and SigV4 managers
1 parent d1b8eac commit 9b56016

10 files changed

Lines changed: 1493 additions & 256 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/catalog/rest/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ chrono = { workspace = true }
3535
http = { workspace = true }
3636
iceberg = { workspace = true }
3737
itertools = { workspace = true }
38+
reqsign-core = "3.0.0"
3839
reqwest = { workspace = true }
3940
serde = { workspace = true }
4041
serde_derive = { workspace = true }
4142
serde_json = { workspace = true }
43+
sha2 = "0.10"
4244
tokio = { workspace = true }
4345
typed-builder = { workspace = true }
4446
uuid = { workspace = true, features = ["v4"] }

crates/catalog/rest/public-api.txt

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,22 @@
11
pub mod iceberg_catalog_rest
2+
pub enum iceberg_catalog_rest::PayloadHashMode
3+
pub iceberg_catalog_rest::PayloadHashMode::IcebergRest
4+
pub iceberg_catalog_rest::PayloadHashMode::StandardAws
5+
impl core::clone::Clone for iceberg_catalog_rest::PayloadHashMode
6+
pub fn iceberg_catalog_rest::PayloadHashMode::clone(&self) -> iceberg_catalog_rest::PayloadHashMode
7+
impl core::cmp::Eq for iceberg_catalog_rest::PayloadHashMode
8+
impl core::cmp::PartialEq for iceberg_catalog_rest::PayloadHashMode
9+
pub fn iceberg_catalog_rest::PayloadHashMode::eq(&self, other: &iceberg_catalog_rest::PayloadHashMode) -> bool
10+
impl core::fmt::Debug for iceberg_catalog_rest::PayloadHashMode
11+
pub fn iceberg_catalog_rest::PayloadHashMode::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
12+
impl core::marker::Copy for iceberg_catalog_rest::PayloadHashMode
13+
impl core::marker::StructuralPartialEq for iceberg_catalog_rest::PayloadHashMode
14+
pub struct iceberg_catalog_rest::AwsCredentials
15+
pub iceberg_catalog_rest::AwsCredentials::access_key_id: alloc::string::String
16+
pub iceberg_catalog_rest::AwsCredentials::secret_access_key: alloc::string::String
17+
pub iceberg_catalog_rest::AwsCredentials::session_token: core::option::Option<alloc::string::String>
18+
impl core::clone::Clone for iceberg_catalog_rest::AwsCredentials
19+
pub fn iceberg_catalog_rest::AwsCredentials::clone(&self) -> iceberg_catalog_rest::AwsCredentials
220
pub struct iceberg_catalog_rest::CommitTableRequest
321
pub iceberg_catalog_rest::CommitTableRequest::identifier: core::option::Option<iceberg::catalog::TableIdent>
422
pub iceberg_catalog_rest::CommitTableRequest::requirements: alloc::vec::Vec<iceberg::catalog::TableRequirement>
@@ -175,6 +193,18 @@ impl serde_core::ser::Serialize for iceberg_catalog_rest::NamespaceResponse
175193
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
176194
impl<'de> serde_core::de::Deserialize<'de> for iceberg_catalog_rest::NamespaceResponse
177195
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
178208
pub struct iceberg_catalog_rest::RegisterTableRequest
179209
pub iceberg_catalog_rest::RegisterTableRequest::metadata_location: alloc::string::String
180210
pub iceberg_catalog_rest::RegisterTableRequest::name: alloc::string::String
@@ -230,6 +260,7 @@ pub fn iceberg_catalog_rest::RestCatalog::update_namespace<'life0, 'life1, 'asyn
230260
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
231261
pub struct iceberg_catalog_rest::RestCatalogBuilder
232262
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
233264
pub fn iceberg_catalog_rest::RestCatalogBuilder::with_client(self, client: reqwest::async_impl::client::Client) -> Self
234265
impl core::default::Default for iceberg_catalog_rest::RestCatalogBuilder
235266
pub fn iceberg_catalog_rest::RestCatalogBuilder::default() -> Self
@@ -241,6 +272,22 @@ pub fn iceberg_catalog_rest::RestCatalogBuilder::load(self, name: impl core::con
241272
pub fn iceberg_catalog_rest::RestCatalogBuilder::with_kms_client_factory(self, kms_client_factory: alloc::sync::Arc<dyn iceberg::encryption::kms::factory::KmsClientFactory>) -> Self
242273
pub fn iceberg_catalog_rest::RestCatalogBuilder::with_runtime(self, runtime: iceberg::runtime::Runtime) -> Self
243274
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
283+
pub struct iceberg_catalog_rest::SigV4Signer
284+
impl iceberg_catalog_rest::SigV4Signer
285+
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<()>
287+
impl core::clone::Clone for iceberg_catalog_rest::SigV4Signer
288+
pub fn iceberg_catalog_rest::SigV4Signer::clone(&self) -> iceberg_catalog_rest::SigV4Signer
289+
impl core::fmt::Debug for iceberg_catalog_rest::SigV4Signer
290+
pub fn iceberg_catalog_rest::SigV4Signer::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
244291
pub struct iceberg_catalog_rest::StorageCredential
245292
pub iceberg_catalog_rest::StorageCredential::config: std::collections::hash::map::HashMap<alloc::string::String, alloc::string::String>
246293
pub iceberg_catalog_rest::StorageCredential::prefix: alloc::string::String
@@ -287,6 +334,32 @@ impl serde_core::ser::Serialize for iceberg_catalog_rest::UpdateNamespacePropert
287334
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
288335
impl<'de> serde_core::de::Deserialize<'de> for iceberg_catalog_rest::UpdateNamespacePropertiesResponse
289336
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
290341
pub const iceberg_catalog_rest::REST_CATALOG_PROP_DISABLE_HEADER_REDACTION: &str
342+
pub const iceberg_catalog_rest::REST_CATALOG_PROP_SIGNING_ACCESS_KEY: &str
343+
pub const iceberg_catalog_rest::REST_CATALOG_PROP_SIGNING_NAME: &str
344+
pub const iceberg_catalog_rest::REST_CATALOG_PROP_SIGNING_REGION: &str
345+
pub const iceberg_catalog_rest::REST_CATALOG_PROP_SIGNING_SECRET_KEY: &str
346+
pub const iceberg_catalog_rest::REST_CATALOG_PROP_SIGNING_SESSION_TOKEN: &str
347+
pub const iceberg_catalog_rest::REST_CATALOG_PROP_SIGV4_ENABLED: &str
291348
pub const iceberg_catalog_rest::REST_CATALOG_PROP_URI: &str
292349
pub const iceberg_catalog_rest::REST_CATALOG_PROP_WAREHOUSE: &str
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)