objectstore_server/extractors/
service.rs1use std::time::SystemTime;
2
3use axum::extract::{FromRequestParts, OriginalUri, Query};
4use axum::http::{Method, header, request::Parts};
5use objectstore_types::presign::PARAM_SIG;
6use serde::Deserialize;
7
8use crate::auth::{AuthAwareService, AuthContext, AuthError, PresignParams};
9use crate::endpoints::common::ApiError;
10use crate::state::ServiceState;
11
12const BEARER_PREFIX: &str = "Bearer ";
13
14const HEADER_AUTH: &str = "x-os-auth";
19
20#[derive(Debug, Deserialize)]
24struct AuthParams {
25 os_auth: Option<String>,
28}
29
30impl AuthAwareService {
31 fn from_token(parts: &mut Parts, state: &ServiceState) -> Result<AuthContext, AuthError> {
32 let query_token = Query::<AuthParams>::try_from_uri(&parts.uri)
33 .map_err(|_| AuthError::BadRequest("invalid query string"))?
34 .0
35 .os_auth;
36
37 let header_token = match query_token {
38 Some(_) => None,
39 None => parts
40 .headers
41 .get(HEADER_AUTH)
42 .or_else(|| parts.headers.get(header::AUTHORIZATION))
43 .and_then(|v| v.to_str().ok())
44 .and_then(strip_bearer)
45 .map(str::to_owned),
46 };
47
48 let token = query_token.as_deref().or(header_token.as_deref());
49
50 AuthContext::from_encoded_jwt(token, &state.key_directory)
51 }
52
53 async fn from_presigned_request(
54 parts: &mut Parts,
55 state: &ServiceState,
56 ) -> Result<AuthContext, AuthError> {
57 if !matches!(&parts.method, &Method::GET | &Method::HEAD) {
58 return Err(AuthError::UnsupportedPresignedMethod);
59 }
60
61 let Query(params) = Query::<PresignParams>::from_request_parts(parts, state)
62 .await
63 .map_err(|_| {
64 AuthError::BadRequest("presigned URL has missing or invalid parameters")
65 })?;
66
67 let path = parts
70 .extensions
71 .get::<OriginalUri>()
72 .ok_or(AuthError::InternalError(
73 "OriginalUri extension missing".into(),
74 ))?
75 .0
76 .path();
77
78 AuthContext::from_presigned_request(
79 &parts.method,
80 path,
81 parts.uri.query(),
82 ¶ms,
83 &state.key_directory,
84 SystemTime::now(),
85 )
86 }
87}
88
89impl FromRequestParts<ServiceState> for AuthAwareService {
90 type Rejection = ApiError;
91
92 async fn from_request_parts(
93 parts: &mut Parts,
94 state: &ServiceState,
95 ) -> Result<Self, Self::Rejection> {
96 let enforce = state.config.auth.enforce;
97 if !state.config.auth.is_active() {
98 return Ok(AuthAwareService::new(
99 state.service.clone(),
100 AuthContext::Disabled,
101 enforce,
102 ));
103 }
104
105 let auth_result = if has_signature(parts.uri.query()) {
106 AuthAwareService::from_presigned_request(parts, state).await
107 } else {
108 AuthAwareService::from_token(parts, state)
109 }
110 .inspect_err(|e| e.log(!enforce));
111
112 let auth = match auth_result {
114 Ok(auth) => auth,
115 Err(error) if enforce => return Err(ApiError::Auth(error)),
116 Err(_) => AuthContext::Disabled,
117 };
118
119 Ok(AuthAwareService::new(state.service.clone(), auth, enforce))
120 }
121}
122
123fn has_signature(query: Option<&str>) -> bool {
125 query.is_some_and(|query| {
126 query
127 .split('&')
128 .any(|pair| pair.split_once('=').map_or(pair, |(key, _)| key) == PARAM_SIG)
129 })
130}
131
132fn strip_bearer(header_value: &str) -> Option<&str> {
133 let (prefix, tail) = header_value.split_at_checked(BEARER_PREFIX.len())?;
134 if prefix.eq_ignore_ascii_case(BEARER_PREFIX) {
135 Some(tail)
136 } else {
137 None
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn test_strip_bearer() {
147 assert_eq!(strip_bearer("Bearer tokenvalue"), Some("tokenvalue"));
149 assert_eq!(strip_bearer("bearer tokenvalue"), Some("tokenvalue"));
150 assert_eq!(strip_bearer("BEARER tokenvalue"), Some("tokenvalue"));
151
152 assert_eq!(strip_bearer("Token tokenvalue"), None);
154 assert_eq!(strip_bearer("Bearer"), None);
155
156 assert_eq!(strip_bearer("Bearer⚠️tokenvalue"), None);
158 }
159
160 #[test]
161 fn test_has_presign_signature() {
162 assert!(has_signature(Some("os_sig=abc")));
163 assert!(has_signature(Some("os_kid=relay&os_sig=abc")));
164
165 assert!(!has_signature(Some("OS_SIG=abc")));
166 assert!(!has_signature(None));
167 assert!(!has_signature(Some("os_kid=relay")));
168 }
169
170 #[test]
171 fn test_auth_params_from_query() {
172 fn parse(query: &str) -> Option<String> {
173 let uri = format!("http://localhost/?{query}").parse().unwrap();
174 Query::<AuthParams>::try_from_uri(&uri).unwrap().0.os_auth
175 }
176
177 let jwt = "header.payload.signature";
178
179 assert_eq!(parse(&format!("os_auth={jwt}")), Some(jwt.to_owned()));
181 assert_eq!(parse("os_auth=a%2Eb"), Some("a.b".to_owned()));
182 assert_eq!(
183 parse(&format!("foo=bar&os_auth={jwt}")),
184 Some(jwt.to_owned())
185 );
186
187 assert_eq!(parse(""), None);
189 assert_eq!(parse("foo=bar"), None);
190 }
191}