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}
19
20impl IntoResponse for ObjectRejection {
21 fn into_response(self) -> Response {
22 match self {
23 ObjectRejection::Path(rejection) => rejection.into_response(),
24 ObjectRejection::Killswitched => (
25 axum::http::StatusCode::FORBIDDEN,
26 "Object access is disabled for this scope through killswitches",
27 )
28 .into_response(),
29 }
30 }
31}
32
33impl From<PathRejection> for ObjectRejection {
34 fn from(rejection: PathRejection) -> Self {
35 ObjectRejection::Path(rejection)
36 }
37}
38
39impl FromRequestParts<ServiceState> for Xt<ObjectId> {
40 type Rejection = ObjectRejection;
41
42 async fn from_request_parts(
43 parts: &mut Parts,
44 state: &ServiceState,
45 ) -> Result<Self, Self::Rejection> {
46 let Path(params) = Path::<ObjectParams>::from_request_parts(parts, state).await?;
47 let id = ObjectId::from_parts(params.usecase, params.scopes, params.key);
48
49 populate_sentry_context(id.context());
50 sentry::configure_scope(|s| s.set_extra("key", id.key().into()));
51
52 if state.config.killswitches.matches(id.context()) {
53 return Err(ObjectRejection::Killswitched);
54 }
55
56 Ok(Xt(id))
57 }
58}
59
60#[derive(Clone, Debug, Deserialize)]
64struct ObjectParams {
65 usecase: String,
66 #[serde(deserialize_with = "deserialize_scopes")]
67 scopes: Scopes,
68 key: String,
69}
70
71fn deserialize_scopes<'de, D>(deserializer: D) -> Result<Scopes, D::Error>
76where
77 D: de::Deserializer<'de>,
78{
79 let s = Cow::<str>::deserialize(deserializer)?;
80 if s == EMPTY_SCOPES {
81 return Ok(Scopes::empty());
82 }
83
84 let scopes = s
85 .split(';')
86 .map(|s| {
87 let (key, value) = s
88 .split_once("=")
89 .ok_or_else(|| de::Error::custom("scope must be 'key=value'"))?;
90
91 Scope::create(key, value).map_err(de::Error::custom)
92 })
93 .collect::<Result<_, _>>()?;
94
95 Ok(scopes)
96}
97
98impl FromRequestParts<ServiceState> for Xt<ObjectContext> {
99 type Rejection = ObjectRejection;
100
101 async fn from_request_parts(
102 parts: &mut Parts,
103 state: &ServiceState,
104 ) -> Result<Self, Self::Rejection> {
105 let Path(params) = Path::<ContextParams>::from_request_parts(parts, state).await?;
106 let context = ObjectContext {
107 usecase: params.usecase,
108 scopes: params.scopes,
109 };
110
111 populate_sentry_context(&context);
112
113 if state.config.killswitches.matches(&context) {
114 return Err(ObjectRejection::Killswitched);
115 }
116
117 Ok(Xt(context))
118 }
119}
120
121#[derive(Clone, Debug, Deserialize)]
125struct ContextParams {
126 usecase: String,
127 #[serde(deserialize_with = "deserialize_scopes")]
128 scopes: Scopes,
129}
130
131fn populate_sentry_context(context: &ObjectContext) {
132 sentry::configure_scope(|s| {
133 s.set_tag("usecase", &context.usecase);
134 for scope in &context.scopes {
135 s.set_tag(&format!("scope.{}", scope.name()), scope.value());
136 }
137 });
138}