objectstore_server/extractors/
id.rs

1use 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/// Path parameters used for object-level endpoints.
61///
62/// This is meant to be used with the axum `Path` extractor.
63#[derive(Clone, Debug, Deserialize)]
64struct ObjectParams {
65    usecase: String,
66    #[serde(deserialize_with = "deserialize_scopes")]
67    scopes: Scopes,
68    key: String,
69}
70
71/// Deserializes a `Scopes` instance from a string representation.
72///
73/// The string representation is a semicolon-separated list of `key=value` pairs, following the
74/// Matrix URIs proposal. An empty scopes string (`"_"`) represents no scopes.
75fn 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/// Path parameters used for collection-level endpoints without a key.
122///
123/// This is meant to be used with the axum `Path` extractor.
124#[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}