Skip to content

Commit 0c5c886

Browse files
committed
Support unauthenticated access to public buckets
Use of a basic authentication header in requests to the Reductionist API is now optional. If basic auth credentials are provided, they will be used as S3 access/secret keys as before. Otherwise, requests to S3 will be unauthenticated. Attempts to access a private bucket without authentication will return a 401.
1 parent 375bcd7 commit 0c5c886

File tree

5 files changed

+77
-45
lines changed

5 files changed

+77
-45
lines changed

benches/s3_client.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use aws_types::region::Region;
66
use axum::body::Bytes;
77
use criterion::{black_box, criterion_group, criterion_main, Criterion};
88
use reductionist::resource_manager::ResourceManager;
9-
use reductionist::s3_client::{S3Client, S3ClientMap};
9+
use reductionist::s3_client::{S3Client, S3ClientMap, S3Credentials};
1010
use url::Url;
1111
// Bring trait into scope to use as_bytes method.
1212
use zerocopy::AsBytes;
@@ -40,6 +40,7 @@ fn criterion_benchmark(c: &mut Criterion) {
4040
let url = Url::parse("http://localhost:9000").unwrap();
4141
let username = "minioadmin";
4242
let password = "minioadmin";
43+
let credentials = S3Credentials::access_key(username, password);
4344
let bucket = "s3-client-bench";
4445
let runtime = tokio::runtime::Runtime::new().unwrap();
4546
let map = S3ClientMap::new();
@@ -53,7 +54,7 @@ fn criterion_benchmark(c: &mut Criterion) {
5354
let name = format!("s3_client({})", size);
5455
c.bench_function(&name, |b| {
5556
b.to_async(&runtime).iter(|| async {
56-
let client = S3Client::new(&url, username, password).await;
57+
let client = S3Client::new(&url, credentials.clone()).await;
5758
client
5859
.download_object(black_box(bucket), &key, None, &resource_manager, &mut None)
5960
.await
@@ -63,7 +64,7 @@ fn criterion_benchmark(c: &mut Criterion) {
6364
let name = format!("s3_client_map({})", size);
6465
c.bench_function(&name, |b| {
6566
b.to_async(&runtime).iter(|| async {
66-
let client = map.get(&url, username, password).await;
67+
let client = map.get(&url, credentials.clone()).await;
6768
client
6869
.download_object(black_box(bucket), &key, None, &resource_manager, &mut None)
6970
.await

docs/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ The request body should be a JSON object of the form:
7373
```
7474

7575
Request authentication is implemented using [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication) with the username and password consisting of your S3 Access Key ID and Secret Access Key, respectively.
76+
Unauthenticated access to S3 is possible by omitting the basic auth header.
7677

7778
On success, all operations return HTTP 200 OK with the response using the same datatype as specified in the request except for `count` which always returns the result as `int64`.
7879
The server returns the following headers with the HTTP response:

docs/contributing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ Proxy functionality can be tested using the [S3 active storage compliance suite]
150150

151151
### Making requests to active storage endpoints
152152

153-
Request authentication is implemented using [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication) with the username and password consisting of your S3 Access Key ID and Secret Access Key, respectively. These credentials are then used internally to authenticate with the upstream S3 source using [standard AWS authentication methods](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html)
153+
Request authentication is implemented using [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication) with the username and password consisting of your S3 Access Key ID and Secret Access Key, respectively. If provided, these credentials are then used internally to authenticate with the upstream S3 source using [standard AWS authentication methods](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html). If no basic auth header is provided, an unauthenticated request will be made to S3.
154154

155155
A basic Python client is provided in `scripts/client.py`.
156156
First install dependencies in a Python virtual environment:

src/app.rs

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,10 @@ use crate::validated_json::ValidatedJson;
1414

1515
use axum::middleware;
1616
use axum::{
17-
body::{Body, Bytes},
17+
body::Bytes,
1818
extract::{Path, State},
1919
headers::authorization::{Authorization, Basic},
2020
http::header,
21-
http::Request,
22-
http::StatusCode,
2321
response::{IntoResponse, Response},
2422
routing::{get, post},
2523
Router, TypedHeader,
@@ -31,7 +29,6 @@ use tower::Layer;
3129
use tower::ServiceBuilder;
3230
use tower_http::normalize_path::NormalizePathLayer;
3331
use tower_http::trace::TraceLayer;
34-
use tower_http::validate_request::ValidateRequestHeaderLayer;
3532
use tracing::debug_span;
3633
use tracing::Instrument;
3734

@@ -113,8 +110,6 @@ pub fn init(args: &CommandLineArgs) {
113110
/// The router is populated with all routes as well as the following middleware:
114111
///
115112
/// * a [tower_http::trace::TraceLayer] for tracing requests and responses
116-
/// * a [tower_http::validate_request::ValidateRequestHeaderLayer] for validating authorisation
117-
/// headers
118113
fn router(args: &CommandLineArgs) -> Router {
119114
fn v1(state: SharedAppState) -> Router {
120115
Router::new()
@@ -124,20 +119,7 @@ fn router(args: &CommandLineArgs) -> Router {
124119
.route("/select", post(operation_handler::<operations::Select>))
125120
.route("/sum", post(operation_handler::<operations::Sum>))
126121
.route("/:operation", post(unknown_operation_handler))
127-
.layer(
128-
ServiceBuilder::new()
129-
.layer(TraceLayer::new_for_http())
130-
.layer(ValidateRequestHeaderLayer::custom(
131-
// Validate that an authorization header has been provided.
132-
|request: &mut Request<Body>| {
133-
if request.headers().contains_key(header::AUTHORIZATION) {
134-
Ok(())
135-
} else {
136-
Err(StatusCode::UNAUTHORIZED.into_response())
137-
}
138-
},
139-
)),
140-
)
122+
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
141123
.with_state(state)
142124
}
143125

@@ -183,7 +165,7 @@ async fn schema() -> &'static str {
183165
///
184166
/// # Arguments
185167
///
186-
/// * `auth`: Basic authentication credentials
168+
/// * `client`: S3 client object
187169
/// * `request_data`: RequestData object for the request
188170
#[tracing::instrument(
189171
level = "DEBUG",
@@ -220,18 +202,23 @@ async fn download_object<'a>(
220202
///
221203
/// # Arguments
222204
///
223-
/// * `auth`: Basic authorization header
205+
/// * `auth`: Optional basic authentication header
224206
/// * `request_data`: RequestData object for the request
225207
async fn operation_handler<T: operation::Operation>(
226208
State(state): State<SharedAppState>,
227-
TypedHeader(auth): TypedHeader<Authorization<Basic>>,
209+
auth: Option<TypedHeader<Authorization<Basic>>>,
228210
ValidatedJson(request_data): ValidatedJson<models::RequestData>,
229211
) -> Result<models::Response, ActiveStorageError> {
230212
let memory = request_data.size.unwrap_or(0);
231213
let mut _mem_permits = state.resource_manager.memory(memory).await?;
214+
let credentials = if let Some(TypedHeader(auth)) = auth {
215+
s3_client::S3Credentials::access_key(auth.username(), auth.password())
216+
} else {
217+
s3_client::S3Credentials::None
218+
};
232219
let s3_client = state
233220
.s3_client_map
234-
.get(&request_data.source, auth.username(), auth.password())
221+
.get(&request_data.source, credentials)
235222
.instrument(tracing::Span::current())
236223
.await;
237224
let data = download_object(

src/s3_client.rs

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,36 @@ use tokio::sync::{RwLock, SemaphorePermit};
1313
use tracing::Instrument;
1414
use url::Url;
1515

16+
#[derive(Clone, Eq, Hash, PartialEq)]
17+
pub enum S3Credentials {
18+
AccessKey {
19+
access_key: String,
20+
secret_key: String,
21+
},
22+
None,
23+
}
24+
25+
impl S3Credentials {
26+
/// Create an access key credential.
27+
pub fn access_key(access_key: &str, secret_key: &str) -> Self {
28+
S3Credentials::AccessKey {
29+
access_key: access_key.to_string(),
30+
secret_key: secret_key.to_string(),
31+
}
32+
}
33+
}
34+
1635
/// A map containing initialised S3Client objects.
1736
///
1837
/// The [aws_sdk_s3::Client] object is relatively expensive to create, so we reuse them where
1938
/// possible. This type provides a map for storing the clients objects.
2039
///
21-
/// The map's key is a 3-tuple of the S3 URL, username and password.
40+
/// The map's key is a 2-tuple of the S3 URL and credentials.
2241
/// The value is the corresponding client object.
2342
pub struct S3ClientMap {
2443
/// A [hashbrown::HashMap] for storing the S3 clients. A read-write lock synchronises access to
2544
/// the map, optimised for reads.
26-
map: RwLock<HashMap<(Url, String, String), S3Client>>,
45+
map: RwLock<HashMap<(Url, S3Credentials), S3Client>>,
2746
}
2847

2948
// FIXME: Currently clients are never removed from the map. If a large number of endpoints or
@@ -43,10 +62,9 @@ impl S3ClientMap {
4362
/// # Arguments
4463
///
4564
/// * `url`: Object storage API URL
46-
/// * `username`: Object storage account username
47-
/// * `password`: Object storage account password
48-
pub async fn get(&self, url: &Url, username: &str, password: &str) -> S3Client {
49-
let key = (url.clone(), username.to_string(), password.to_string());
65+
/// * `credentials`: Object storage account credentials
66+
pub async fn get(&self, url: &Url, credentials: S3Credentials) -> S3Client {
67+
let key = (url.clone(), credentials.clone());
5068
// Common case: return an existing client from the map.
5169
{
5270
let map = self.map.read().await;
@@ -61,7 +79,7 @@ impl S3ClientMap {
6179
client.clone()
6280
} else {
6381
tracing::info!("Creating new S3 client for {}", url);
64-
let client = S3Client::new(url, username, password).await;
82+
let client = S3Client::new(url, credentials).await;
6583
let (_, client) = map.insert_unique_unchecked(key, client);
6684
client.clone()
6785
}
@@ -81,13 +99,21 @@ impl S3Client {
8199
/// # Arguments
82100
///
83101
/// * `url`: Object storage API URL
84-
/// * `username`: Object storage account username
85-
/// * `password`: Object storage account password
86-
pub async fn new(url: &Url, username: &str, password: &str) -> Self {
87-
let credentials = Credentials::from_keys(username, password, None);
102+
/// * `credentials`: Object storage account credentials
103+
pub async fn new(url: &Url, credentials: S3Credentials) -> Self {
88104
let region = Region::new("us-east-1");
89-
let s3_config = aws_sdk_s3::Config::builder()
90-
.credentials_provider(credentials)
105+
let builder = aws_sdk_s3::Config::builder();
106+
let builder = match credentials {
107+
S3Credentials::AccessKey {
108+
access_key,
109+
secret_key,
110+
} => {
111+
let credentials = Credentials::from_keys(access_key, secret_key, None);
112+
builder.credentials_provider(credentials)
113+
}
114+
S3Credentials::None => builder,
115+
};
116+
let s3_config = builder
91117
.region(Some(region))
92118
.endpoint_url(url.to_string())
93119
.force_path_style(true)
@@ -178,21 +204,38 @@ mod tests {
178204
use super::*;
179205
use url::Url;
180206

207+
fn make_access_key() -> S3Credentials {
208+
S3Credentials::access_key("user", "password")
209+
}
210+
211+
fn make_alt_access_key() -> S3Credentials {
212+
S3Credentials::access_key("user2", "password")
213+
}
214+
181215
#[tokio::test]
182216
async fn s3_client_map() {
183217
let url = Url::parse("http://example.com").unwrap();
184218
let map = S3ClientMap::new();
185-
map.get(&url, "user", "password").await;
186-
map.get(&url, "user", "password").await;
219+
map.get(&url, make_access_key()).await;
220+
map.get(&url, make_access_key()).await;
187221
assert_eq!(map.map.read().await.len(), 1);
188-
map.get(&url, "user2", "password2").await;
222+
map.get(&url, make_alt_access_key()).await;
189223
assert_eq!(map.map.read().await.len(), 2);
224+
map.get(&url, S3Credentials::None).await;
225+
map.get(&url, S3Credentials::None).await;
226+
assert_eq!(map.map.read().await.len(), 3);
190227
}
191228

192229
#[tokio::test]
193230
async fn new() {
194231
let url = Url::parse("http://example.com").unwrap();
195-
S3Client::new(&url, "user", "password").await;
232+
S3Client::new(&url, make_access_key()).await;
233+
}
234+
235+
#[tokio::test]
236+
async fn new_no_auth() {
237+
let url = Url::parse("http://example.com").unwrap();
238+
S3Client::new(&url, S3Credentials::None).await;
196239
}
197240

198241
#[test]

0 commit comments

Comments
 (0)