Skip to content

Commit 4932181

Browse files
committed
feat(auth): jwt validation and auth-aware service wrapper
1 parent c5ae7ae commit 4932181

File tree

12 files changed

+1140
-40
lines changed

12 files changed

+1140
-40
lines changed

Cargo.lock

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

objectstore-server/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ figment = { version = "0.10.19", features = ["env", "test", "yaml"] }
2121
futures-util = { workspace = true }
2222
humantime = { workspace = true }
2323
humantime-serde = { workspace = true }
24+
jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] }
2425
merni = { workspace = true }
2526
mimalloc = { workspace = true }
2627
num_cpus = "1.17.0"
@@ -38,6 +39,7 @@ secrecy = { version = "0.10.3", features = ["serde"] }
3839
serde = { workspace = true }
3940
serde_json = { workspace = true }
4041
serde_with = "3.14.1"
42+
thiserror = "2.0.17"
4143
tokio = { workspace = true, features = ["full"] }
4244
tokio-stream = { workspace = true }
4345
tower = { version = "0.5.2" }

objectstore-server/src/auth.rs

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
use axum::extract::FromRequestParts;
2+
use axum::http::{StatusCode, header, request::Parts};
3+
use objectstore_service::BackendStream;
4+
use objectstore_service::{ObjectPath, StorageService};
5+
use objectstore_types::Metadata;
6+
7+
use super::{AuthContext, AuthError, Permission};
8+
use crate::state::ServiceState;
9+
10+
const BEARER_PREFIX: &str = "Bearer ";
11+
12+
/// Wrapper around [`objectstore_service::StorageService`] that ensures each storage operation is
13+
/// authorized according to the request's authorization details. See also: [`AuthContext`].
14+
///
15+
/// When [`crate::config::AuthZ::enforce`] is false, authorization failures are logged but any
16+
/// unauthorized operations are still allowed to proceed.
17+
///
18+
/// Objectstore API endpoints can use `AuthAwareService` simply by adding it to their handler
19+
/// function's argument list like so:
20+
/// ```no_run
21+
/// # use axum::extract::Path;
22+
/// # use axum::response::IntoResponse;
23+
/// # use axum::http::StatusCode;
24+
/// # use objectstore_server::{auth::AuthAwareService, error::ApiResult};
25+
/// # use objectstore_service::ObjectPath;
26+
/// async fn delete_object(
27+
/// service: AuthAwareService, // <- Constructed automatically from request parts
28+
/// Path(path): Path<ObjectPath>,
29+
/// ) -> ApiResult<impl IntoResponse> {
30+
/// service.delete_object(&path).await?;
31+
///
32+
/// Ok(StatusCode::NO_CONTENT)
33+
/// }
34+
/// ```
35+
pub struct AuthAwareService {
36+
service: StorageService,
37+
38+
enforce: bool,
39+
40+
auth_context: Option<AuthContext>,
41+
}
42+
43+
impl AuthAwareService {
44+
fn assert_authorized(&self, perm: Permission, path: &ObjectPath) -> anyhow::Result<()> {
45+
let auth_result = self
46+
.auth_context
47+
.as_ref()
48+
.ok_or(AuthError::VerificationFailure)
49+
.and_then(|ac| ac.assert_authorized(perm, path));
50+
if self.enforce {
51+
return Ok(auth_result?);
52+
}
53+
Ok(())
54+
}
55+
56+
/// Auth-aware wrapper around [`StorageService::put_object`].
57+
pub async fn put_object(
58+
&self,
59+
path: ObjectPath,
60+
metadata: &Metadata,
61+
stream: BackendStream,
62+
) -> anyhow::Result<ObjectPath> {
63+
self.assert_authorized(Permission::ObjectWrite, &path)?;
64+
65+
self.service.put_object(path, metadata, stream).await
66+
}
67+
68+
/// Auth-aware wrapper around [`StorageService::get_object`].
69+
pub async fn get_object(
70+
&self,
71+
path: &ObjectPath,
72+
) -> anyhow::Result<Option<(Metadata, BackendStream)>> {
73+
self.assert_authorized(Permission::ObjectRead, path)?;
74+
75+
self.service.get_object(path).await
76+
}
77+
78+
/// Auth-aware wrapper around [`StorageService::delete_object`].
79+
pub async fn delete_object(&self, path: &ObjectPath) -> anyhow::Result<()> {
80+
self.assert_authorized(Permission::ObjectDelete, path)?;
81+
82+
self.service.delete_object(path).await
83+
}
84+
}
85+
86+
impl FromRequestParts<ServiceState> for AuthAwareService {
87+
type Rejection = StatusCode;
88+
89+
async fn from_request_parts(
90+
parts: &mut Parts,
91+
state: &ServiceState,
92+
) -> Result<Self, Self::Rejection> {
93+
let encoded_token = parts
94+
.headers
95+
.get(header::AUTHORIZATION)
96+
.and_then(|v| v.to_str().ok())
97+
// TODO: Handle case-insensitive bearer prefix
98+
.and_then(|v| v.strip_prefix(BEARER_PREFIX));
99+
100+
let auth_context = AuthContext::from_encoded_jwt(encoded_token, &state.config.auth);
101+
if auth_context.is_err() && state.config.auth.enforce {
102+
tracing::debug!("Authorization failed when enforcement is enabled");
103+
return Err(StatusCode::UNAUTHORIZED);
104+
}
105+
106+
Ok(AuthAwareService {
107+
service: state.authless_service.clone(),
108+
enforce: state.config.auth.enforce,
109+
auth_context: auth_context.ok(),
110+
})
111+
}
112+
}

0 commit comments

Comments
 (0)