Skip to content

Commit d144694

Browse files
phillipleblancsgrebnov
authored andcommitted
Implement AWS Sigv4 support for Iceberg REST catalogs on Glue
Original change: #1 Upstream PR: apache#917
1 parent 460de45 commit d144694

File tree

10 files changed

+689
-9
lines changed

10 files changed

+689
-9
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ aws-sdk-glue = "1.39"
5757
aws-sdk-s3tables = "1.28.0"
5858
backon = "1.5.1"
5959
base64 = "0.22.1"
60+
aws-sdk-sts = "1.63"
61+
aws-sigv4 = { version = "1.2", features = ["sign-http"] }
62+
aws-credential-types = "1.2"
6063
bimap = "0.6"
6164
bytes = "1.10"
6265
chrono = "0.4.41"
@@ -101,6 +104,7 @@ pretty_assertions = "1.4"
101104
rand = "0.8.5"
102105
regex = "1.10.5"
103106
reqwest = { version = "0.12.12", default-features = false, features = ["json"] }
107+
reqwest-middleware = { version = "0.4.0", features = ["json"] }
104108
roaring = { version = "0.11" }
105109
rust_decimal = "1.37.1"
106110
serde = { version = "1.0.219", features = ["rc"] }

crates/catalog/rest/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,18 @@ license = { workspace = true }
2929
repository = { workspace = true }
3030

3131
[dependencies]
32+
anyhow = { workspace = true }
3233
async-trait = { workspace = true }
34+
aws-config = { workspace = true, optional = true }
35+
aws-sigv4 = { workspace = true, optional = true }
36+
aws-credential-types = { workspace = true, optional = true }
37+
aws-sdk-sts = { workspace = true, optional = true }
3338
chrono = { workspace = true }
3439
http = { workspace = true }
3540
iceberg = { workspace = true }
3641
itertools = { workspace = true }
3742
reqwest = { workspace = true }
43+
reqwest-middleware = { workspace = true }
3844
serde = { workspace = true }
3945
serde_derive = { workspace = true }
4046
serde_json = { workspace = true }
@@ -49,3 +55,7 @@ iceberg_test_utils = { path = "../../test_utils", features = ["tests"] }
4955
mockito = { workspace = true }
5056
port_scanner = { workspace = true }
5157
tokio = { workspace = true }
58+
wiremock = "=0.6.4"
59+
[features]
60+
default = ["sigv4"]
61+
sigv4 = ["aws-config", "aws-sigv4", "aws-credential-types", "aws-sdk-sts"]

crates/catalog/rest/src/catalog.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ impl RestCatalogConfig {
153153
.join("/")
154154
}
155155

156+
pub(crate) fn base_url(&self) -> String {
157+
[&self.uri, PATH_V1].join("/")
158+
}
159+
156160
fn config_endpoint(&self) -> String {
157161
[&self.uri, PATH_V1, "config"].join("/")
158162
}
@@ -308,6 +312,45 @@ impl RestCatalogConfig {
308312
}
309313
}
310314

315+
#[cfg(feature = "sigv4")]
316+
impl RestCatalogConfig {
317+
pub(crate) fn sigv4_enabled(&self) -> bool {
318+
self.props
319+
.get("rest.sigv4-enabled")
320+
.is_some_and(|v| v == "true")
321+
}
322+
323+
pub(crate) fn signing_region(&self) -> Option<String> {
324+
self.props.get("rest.signing-region").cloned()
325+
}
326+
327+
pub(crate) fn signing_name(&self) -> Option<String> {
328+
self.props.get("rest.signing-name").cloned()
329+
}
330+
331+
pub(crate) fn access_key_id(&self) -> Option<String> {
332+
self.props.get("rest.access-key-id").cloned()
333+
}
334+
335+
pub(crate) fn secret_access_key(&self) -> Option<String> {
336+
self.props.get("rest.secret-access-key").cloned()
337+
}
338+
339+
pub(crate) fn session_token(&self) -> Option<String> {
340+
self.props.get("rest.session-token").cloned()
341+
}
342+
343+
pub(crate) fn role_arn(&self) -> Option<String> {
344+
self.props.get("rest.client.assume-role.arn").cloned()
345+
}
346+
347+
pub(crate) fn role_session_name(&self) -> Option<String> {
348+
self.props
349+
.get("rest.client.assume-role.session-name")
350+
.cloned()
351+
}
352+
}
353+
311354
#[derive(Debug)]
312355
struct RestContext {
313356
client: HttpClient,

crates/catalog/rest/src/client.rs

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@ use std::fmt::{Debug, Formatter};
2121
use http::StatusCode;
2222
use iceberg::{Error, ErrorKind, Result};
2323
use reqwest::header::HeaderMap;
24-
use reqwest::{Client, IntoUrl, Method, Request, RequestBuilder, Response};
24+
use reqwest::{IntoUrl, Method, Request, Response};
25+
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, RequestBuilder};
2526
use serde::de::DeserializeOwned;
2627
use tokio::sync::Mutex;
2728

2829
use crate::RestCatalogConfig;
2930
use crate::types::{ErrorResponse, TokenResponse};
3031

3132
pub(crate) struct HttpClient {
32-
client: Client,
33+
client: ClientWithMiddleware,
3334

3435
/// The token to be used for authentication.
3536
///
@@ -56,10 +57,39 @@ impl Debug for HttpClient {
5657

5758
impl HttpClient {
5859
/// Create a new http client.
60+
#[allow(unused_mut)]
5961
pub fn new(cfg: &RestCatalogConfig) -> Result<Self> {
6062
let extra_headers = cfg.extra_headers()?;
63+
let mut client_builder = ClientBuilder::new(cfg.client().unwrap_or_default());
64+
65+
#[cfg(feature = "sigv4")]
66+
if cfg.sigv4_enabled() {
67+
let mut sigv4_middleware = crate::middleware::sigv4::SigV4Middleware::new(
68+
&cfg.base_url(),
69+
cfg.signing_name().as_deref().unwrap_or("glue"),
70+
cfg.signing_region().as_deref(),
71+
);
72+
73+
if let (Some(access_key_id), Some(secret_access_key)) =
74+
(cfg.access_key_id(), cfg.secret_access_key())
75+
{
76+
sigv4_middleware = sigv4_middleware.with_credentials(
77+
access_key_id,
78+
secret_access_key,
79+
cfg.session_token(),
80+
);
81+
}
82+
83+
if let Some(role_arn) = cfg.role_arn() {
84+
sigv4_middleware = sigv4_middleware.with_role(role_arn, cfg.role_session_name());
85+
}
86+
87+
client_builder = client_builder.with(sigv4_middleware);
88+
}
89+
6190
Ok(HttpClient {
62-
client: cfg.client().unwrap_or_default(),
91+
client: client_builder.build(),
92+
6393
token: Mutex::new(cfg.token()),
6494
token_endpoint: cfg.get_token_endpoint(),
6595
credential: cfg.credential(),
@@ -77,8 +107,26 @@ impl HttpClient {
77107
.then(|| cfg.extra_headers())
78108
.transpose()?
79109
.unwrap_or(self.extra_headers);
110+
111+
let client = match cfg.client() {
112+
Some(client) => {
113+
let mut client_builder = ClientBuilder::new(client);
114+
#[cfg(feature = "sigv4")]
115+
if cfg.sigv4_enabled() {
116+
client_builder =
117+
client_builder.with(crate::middleware::sigv4::SigV4Middleware::new(
118+
&cfg.base_url(),
119+
cfg.signing_name().as_deref().unwrap_or("glue"),
120+
cfg.signing_region().as_deref(),
121+
));
122+
}
123+
client_builder.build()
124+
}
125+
None => ClientBuilder::from_client(self.client).build(),
126+
};
127+
80128
Ok(HttpClient {
81-
client: cfg.client().unwrap_or(self.client),
129+
client,
82130
token: Mutex::new(cfg.token().or_else(|| self.token.into_inner())),
83131
token_endpoint: if !cfg.get_token_endpoint().is_empty() {
84132
cfg.get_token_endpoint()

crates/catalog/rest/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353

5454
mod catalog;
5555
mod client;
56+
mod middleware;
5657
mod types;
5758

5859
pub use catalog::*;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
//! Middleware that is applied on requests to the Rest Catalog API.
19+
20+
#[cfg(feature = "sigv4")]
21+
pub(crate) mod sigv4;

0 commit comments

Comments
 (0)