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