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