objectstore_server/extractors/
id.rs1use std::borrow::Cow;
2
3use axum::extract::rejection::PathRejection;
4use axum::extract::{FromRequestParts, Path};
5use axum::http::request::Parts;
6use axum::response::{IntoResponse, Response};
7use objectstore_service::id::{ObjectContext, ObjectId};
8use objectstore_types::scope::{EMPTY_SCOPES, Scope, Scopes};
9use serde::{Deserialize, de};
10
11use crate::extractors::Xt;
12use crate::state::ServiceState;
13
14#[derive(Debug)]
15pub enum ObjectRejection {
16 Path(PathRejection),
17 Killswitched,
18 RateLimited,
19}
20
21impl IntoResponse for ObjectRejection {
22 fn into_response(self) -> Response {
23 match self {
24 ObjectRejection::Path(rejection) => rejection.into_response(),
25 ObjectRejection::Killswitched => (
26 axum::http::StatusCode::FORBIDDEN,
27 "Object access is disabled for this scope through killswitches",
28 )
29 .into_response(),
30 ObjectRejection::RateLimited => (
31 axum::http::StatusCode::TOO_MANY_REQUESTS,
32 "Object access is rate limited",
33 )
34 .into_response(),
35 }
36 }
37}
38
39impl From<PathRejection> for ObjectRejection {
40 fn from(rejection: PathRejection) -> Self {
41 ObjectRejection::Path(rejection)
42 }
43}
44
45impl FromRequestParts<ServiceState> for Xt<ObjectId> {
46 type Rejection = ObjectRejection;
47
48 async fn from_request_parts(
49 parts: &mut Parts,
50 state: &ServiceState,
51 ) -> Result<Self, Self::Rejection> {
52 let Path(params) = Path::<ObjectParams>::from_request_parts(parts, state).await?;
53 let id = ObjectId::from_parts(params.usecase, params.scopes, params.key);
54
55 populate_sentry_context(id.context());
56 sentry::configure_scope(|s| s.set_extra("key", id.key().into()));
57
58 if state.config.killswitches.matches(id.context()) {
59 tracing::debug!("Request rejected due to killswitches");
60 return Err(ObjectRejection::Killswitched);
61 }
62
63 if !state.rate_limiter.check(id.context()) {
64 tracing::debug!("Request rejected due to rate limits");
65 return Err(ObjectRejection::RateLimited);
66 }
67
68 Ok(Xt(id))
69 }
70}
71
72#[derive(Clone, Debug, Deserialize)]
76struct ObjectParams {
77 usecase: String,
78 #[serde(deserialize_with = "deserialize_scopes")]
79 scopes: Scopes,
80 key: String,
81}
82
83fn deserialize_scopes<'de, D>(deserializer: D) -> Result<Scopes, D::Error>
88where
89 D: de::Deserializer<'de>,
90{
91 let s = Cow::<str>::deserialize(deserializer)?;
92 if s == EMPTY_SCOPES {
93 return Ok(Scopes::empty());
94 }
95
96 let scopes = s
97 .split(';')
98 .map(|s| {
99 let (key, value) = s
100 .split_once("=")
101 .ok_or_else(|| de::Error::custom("scope must be 'key=value'"))?;
102
103 Scope::create(key, value).map_err(de::Error::custom)
104 })
105 .collect::<Result<_, _>>()?;
106
107 Ok(scopes)
108}
109
110impl FromRequestParts<ServiceState> for Xt<ObjectContext> {
111 type Rejection = ObjectRejection;
112
113 async fn from_request_parts(
114 parts: &mut Parts,
115 state: &ServiceState,
116 ) -> Result<Self, Self::Rejection> {
117 let Path(params) = Path::<ContextParams>::from_request_parts(parts, state).await?;
118 let context = ObjectContext {
119 usecase: params.usecase,
120 scopes: params.scopes,
121 };
122
123 populate_sentry_context(&context);
124
125 if state.config.killswitches.matches(&context) {
126 tracing::debug!("Request rejected due to killswitches");
127 return Err(ObjectRejection::Killswitched);
128 }
129
130 if !state.rate_limiter.check(&context) {
131 tracing::debug!("Request rejected due to rate limits");
132 return Err(ObjectRejection::RateLimited);
133 }
134
135 Ok(Xt(context))
136 }
137}
138
139#[derive(Clone, Debug, Deserialize)]
143struct ContextParams {
144 usecase: String,
145 #[serde(deserialize_with = "deserialize_scopes")]
146 scopes: Scopes,
147}
148
149fn populate_sentry_context(context: &ObjectContext) {
150 sentry::configure_scope(|s| {
151 s.set_tag("usecase", &context.usecase);
152 for scope in &context.scopes {
153 s.set_tag(&format!("scope.{}", scope.name()), scope.value());
154 }
155 });
156}