Skip to content

Commit def94a8

Browse files
authored
object_store: Add support for requester pays buckets (#6768)
* Add support for requester pays buckets * Add tests * fix rustdoc
1 parent 7ef302d commit def94a8

File tree

4 files changed

+143
-2
lines changed

4 files changed

+143
-2
lines changed

object_store/src/aws/builder.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ pub struct AmazonS3Builder {
170170
encryption_bucket_key_enabled: Option<ConfigValue<bool>>,
171171
/// base64-encoded 256-bit customer encryption key for SSE-C.
172172
encryption_customer_key_base64: Option<String>,
173+
/// When set to true, charge requester for bucket operations
174+
request_payer: ConfigValue<bool>,
173175
}
174176

175177
/// Configuration keys for [`AmazonS3Builder`]
@@ -330,6 +332,13 @@ pub enum AmazonS3ConfigKey {
330332
/// - `s3_express`
331333
S3Express,
332334

335+
/// Enable Support for S3 Requester Pays
336+
///
337+
/// Supported keys:
338+
/// - `aws_request_payer`
339+
/// - `request_payer`
340+
RequestPayer,
341+
333342
/// Client options
334343
Client(ClientConfigKey),
335344

@@ -358,6 +367,7 @@ impl AsRef<str> for AmazonS3ConfigKey {
358367
Self::CopyIfNotExists => "aws_copy_if_not_exists",
359368
Self::ConditionalPut => "aws_conditional_put",
360369
Self::DisableTagging => "aws_disable_tagging",
370+
Self::RequestPayer => "aws_request_payer",
361371
Self::Client(opt) => opt.as_ref(),
362372
Self::Encryption(opt) => opt.as_ref(),
363373
}
@@ -389,6 +399,7 @@ impl FromStr for AmazonS3ConfigKey {
389399
"aws_copy_if_not_exists" | "copy_if_not_exists" => Ok(Self::CopyIfNotExists),
390400
"aws_conditional_put" | "conditional_put" => Ok(Self::ConditionalPut),
391401
"aws_disable_tagging" | "disable_tagging" => Ok(Self::DisableTagging),
402+
"aws_request_payer" | "request_payer" => Ok(Self::RequestPayer),
392403
// Backwards compatibility
393404
"aws_allow_http" => Ok(Self::Client(ClientConfigKey::AllowHttp)),
394405
"aws_server_side_encryption" => Ok(Self::Encryption(
@@ -510,6 +521,9 @@ impl AmazonS3Builder {
510521
AmazonS3ConfigKey::ConditionalPut => {
511522
self.conditional_put = Some(ConfigValue::Deferred(value.into()))
512523
}
524+
AmazonS3ConfigKey::RequestPayer => {
525+
self.request_payer = ConfigValue::Deferred(value.into())
526+
}
513527
AmazonS3ConfigKey::Encryption(key) => match key {
514528
S3EncryptionConfigKey::ServerSideEncryption => {
515529
self.encryption_type = Some(ConfigValue::Deferred(value.into()))
@@ -567,6 +581,7 @@ impl AmazonS3Builder {
567581
self.conditional_put.as_ref().map(ToString::to_string)
568582
}
569583
AmazonS3ConfigKey::DisableTagging => Some(self.disable_tagging.to_string()),
584+
AmazonS3ConfigKey::RequestPayer => Some(self.request_payer.to_string()),
570585
AmazonS3ConfigKey::Encryption(key) => match key {
571586
S3EncryptionConfigKey::ServerSideEncryption => {
572587
self.encryption_type.as_ref().map(ToString::to_string)
@@ -845,6 +860,14 @@ impl AmazonS3Builder {
845860
self
846861
}
847862

863+
/// Set whether to charge requester for bucket operations.
864+
///
865+
/// <https://docs.aws.amazon.com/AmazonS3/latest/userguide/RequesterPaysBuckets.html>
866+
pub fn with_request_payer(mut self, enabled: bool) -> Self {
867+
self.request_payer = ConfigValue::Parsed(enabled);
868+
self
869+
}
870+
848871
/// Create a [`AmazonS3`] instance from the provided values,
849872
/// consuming `self`.
850873
pub fn build(mut self) -> Result<AmazonS3> {
@@ -996,6 +1019,7 @@ impl AmazonS3Builder {
9961019
copy_if_not_exists,
9971020
conditional_put: put_precondition,
9981021
encryption_headers,
1022+
request_payer: self.request_payer.get()?,
9991023
};
10001024

10011025
let client = Arc::new(S3Client::new(config)?);

object_store/src/aws/client.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ pub(crate) struct S3Config {
202202
pub checksum: Option<Checksum>,
203203
pub copy_if_not_exists: Option<S3CopyIfNotExists>,
204204
pub conditional_put: Option<S3ConditionalPut>,
205+
pub request_payer: bool,
205206
pub(super) encryption_headers: S3EncryptionHeaders,
206207
}
207208

@@ -249,7 +250,8 @@ impl<'a> SessionCredential<'a> {
249250
fn authorizer(&self) -> Option<AwsAuthorizer<'_>> {
250251
let mut authorizer =
251252
AwsAuthorizer::new(self.credential.as_deref()?, "s3", &self.config.region)
252-
.with_sign_payload(self.config.sign_payload);
253+
.with_sign_payload(self.config.sign_payload)
254+
.with_request_payer(self.config.request_payer);
253255

254256
if self.session_token {
255257
let token = HeaderName::from_static("x-amz-s3session-token");

object_store/src/aws/credential.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,14 @@ pub struct AwsAuthorizer<'a> {
101101
region: &'a str,
102102
token_header: Option<HeaderName>,
103103
sign_payload: bool,
104+
request_payer: bool,
104105
}
105106

106107
static DATE_HEADER: HeaderName = HeaderName::from_static("x-amz-date");
107108
static HASH_HEADER: HeaderName = HeaderName::from_static("x-amz-content-sha256");
108109
static TOKEN_HEADER: HeaderName = HeaderName::from_static("x-amz-security-token");
110+
static REQUEST_PAYER_HEADER: HeaderName = HeaderName::from_static("x-amz-request-payer");
111+
static REQUEST_PAYER_HEADER_VALUE: HeaderValue = HeaderValue::from_static("requester");
109112
const ALGORITHM: &str = "AWS4-HMAC-SHA256";
110113

111114
impl<'a> AwsAuthorizer<'a> {
@@ -118,6 +121,7 @@ impl<'a> AwsAuthorizer<'a> {
118121
date: None,
119122
sign_payload: true,
120123
token_header: None,
124+
request_payer: false,
121125
}
122126
}
123127

@@ -134,6 +138,14 @@ impl<'a> AwsAuthorizer<'a> {
134138
self
135139
}
136140

141+
/// Set whether to include requester pays headers
142+
///
143+
/// <https://docs.aws.amazon.com/AmazonS3/latest/userguide/ObjectsinRequesterPaysBuckets.html>
144+
pub fn with_request_payer(mut self, request_payer: bool) -> Self {
145+
self.request_payer = request_payer;
146+
self
147+
}
148+
137149
/// Authorize `request` with an optional pre-calculated SHA256 digest by attaching
138150
/// the relevant [AWS SigV4] headers
139151
///
@@ -180,6 +192,15 @@ impl<'a> AwsAuthorizer<'a> {
180192
let header_digest = HeaderValue::from_str(&digest).unwrap();
181193
request.headers_mut().insert(&HASH_HEADER, header_digest);
182194

195+
if self.request_payer {
196+
// For DELETE, GET, HEAD, POST, and PUT requests, include x-amz-request-payer :
197+
// requester in the header
198+
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/ObjectsinRequesterPaysBuckets.html
199+
request
200+
.headers_mut()
201+
.insert(&REQUEST_PAYER_HEADER, REQUEST_PAYER_HEADER_VALUE.clone());
202+
}
203+
183204
let (signed_headers, canonical_headers) = canonicalize_headers(request.headers());
184205

185206
let scope = self.scope(date);
@@ -226,6 +247,13 @@ impl<'a> AwsAuthorizer<'a> {
226247
.append_pair("X-Amz-Expires", &expires_in.as_secs().to_string())
227248
.append_pair("X-Amz-SignedHeaders", "host");
228249

250+
if self.request_payer {
251+
// For signed URLs, include x-amz-request-payer=requester in the request
252+
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/ObjectsinRequesterPaysBuckets.html
253+
url.query_pairs_mut()
254+
.append_pair("x-amz-request-payer", "requester");
255+
}
256+
229257
// For S3, you must include the X-Amz-Security-Token query parameter in the URL if
230258
// using credentials sourced from the STS service.
231259
if let Some(ref token) = self.credential.token {
@@ -763,12 +791,53 @@ mod tests {
763791
region: "us-east-1",
764792
sign_payload: true,
765793
token_header: None,
794+
request_payer: false,
766795
};
767796

768797
signer.authorize(&mut request, None);
769798
assert_eq!(request.headers().get(&AUTHORIZATION).unwrap(), "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20220806/us-east-1/ec2/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=a3c787a7ed37f7fdfbfd2d7056a3d7c9d85e6d52a2bfbec73793c0be6e7862d4")
770799
}
771800

801+
#[test]
802+
fn test_sign_with_signed_payload_request_payer() {
803+
let client = Client::new();
804+
805+
// Test credentials from https://docs.aws.amazon.com/AmazonS3/latest/userguide/RESTAuthentication.html
806+
let credential = AwsCredential {
807+
key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
808+
secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string(),
809+
token: None,
810+
};
811+
812+
// method = 'GET'
813+
// service = 'ec2'
814+
// host = 'ec2.amazonaws.com'
815+
// region = 'us-east-1'
816+
// endpoint = 'https://ec2.amazonaws.com'
817+
// request_parameters = ''
818+
let date = DateTime::parse_from_rfc3339("2022-08-06T18:01:34Z")
819+
.unwrap()
820+
.with_timezone(&Utc);
821+
822+
let mut request = client
823+
.request(Method::GET, "https://ec2.amazon.com/")
824+
.build()
825+
.unwrap();
826+
827+
let signer = AwsAuthorizer {
828+
date: Some(date),
829+
credential: &credential,
830+
service: "ec2",
831+
region: "us-east-1",
832+
sign_payload: true,
833+
token_header: None,
834+
request_payer: true,
835+
};
836+
837+
signer.authorize(&mut request, None);
838+
assert_eq!(request.headers().get(&AUTHORIZATION).unwrap(), "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20220806/us-east-1/ec2/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-request-payer, Signature=7030625a9e9b57ed2a40e63d749f4a4b7714b6e15004cab026152f870dd8565d")
839+
}
840+
772841
#[test]
773842
fn test_sign_with_unsigned_payload() {
774843
let client = Client::new();
@@ -802,6 +871,7 @@ mod tests {
802871
region: "us-east-1",
803872
token_header: None,
804873
sign_payload: false,
874+
request_payer: false,
805875
};
806876

807877
authorizer.authorize(&mut request, None);
@@ -828,6 +898,7 @@ mod tests {
828898
region: "us-east-1",
829899
token_header: None,
830900
sign_payload: false,
901+
request_payer: false,
831902
};
832903

833904
let mut url = Url::parse("https://examplebucket.s3.amazonaws.com/test.txt").unwrap();
@@ -848,6 +919,48 @@ mod tests {
848919
);
849920
}
850921

922+
#[test]
923+
fn signed_get_url_request_payer() {
924+
// Values from https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
925+
let credential = AwsCredential {
926+
key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
927+
secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string(),
928+
token: None,
929+
};
930+
931+
let date = DateTime::parse_from_rfc3339("2013-05-24T00:00:00Z")
932+
.unwrap()
933+
.with_timezone(&Utc);
934+
935+
let authorizer = AwsAuthorizer {
936+
date: Some(date),
937+
credential: &credential,
938+
service: "s3",
939+
region: "us-east-1",
940+
token_header: None,
941+
sign_payload: false,
942+
request_payer: true,
943+
};
944+
945+
let mut url = Url::parse("https://examplebucket.s3.amazonaws.com/test.txt").unwrap();
946+
authorizer.sign(Method::GET, &mut url, Duration::from_secs(86400));
947+
948+
assert_eq!(
949+
url,
950+
Url::parse(
951+
"https://examplebucket.s3.amazonaws.com/test.txt?\
952+
X-Amz-Algorithm=AWS4-HMAC-SHA256&\
953+
X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&\
954+
X-Amz-Date=20130524T000000Z&\
955+
X-Amz-Expires=86400&\
956+
X-Amz-SignedHeaders=host&\
957+
x-amz-request-payer=requester&\
958+
X-Amz-Signature=9ad7c781cc30121f199b47d35ed3528473e4375b63c5d91cd87c927803e4e00a"
959+
)
960+
.unwrap()
961+
);
962+
}
963+
851964
#[test]
852965
fn test_sign_port() {
853966
let client = Client::new();
@@ -880,6 +993,7 @@ mod tests {
880993
region: "us-east-1",
881994
token_header: None,
882995
sign_payload: true,
996+
request_payer: false,
883997
};
884998

885999
authorizer.authorize(&mut request, None);

object_store/src/aws/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,8 @@ impl Signer for AmazonS3 {
136136
/// ```
137137
async fn signed_url(&self, method: Method, path: &Path, expires_in: Duration) -> Result<Url> {
138138
let credential = self.credentials().get_credential().await?;
139-
let authorizer = AwsAuthorizer::new(&credential, "s3", &self.client.config.region);
139+
let authorizer = AwsAuthorizer::new(&credential, "s3", &self.client.config.region)
140+
.with_request_payer(self.client.config.request_payer);
140141

141142
let path_url = self.path_url(path);
142143
let mut url = Url::parse(&path_url).map_err(|e| crate::Error::Generic {

0 commit comments

Comments
 (0)