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    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/// Path parameters used for object-level endpoints.
73///
74/// This is meant to be used with the axum `Path` extractor.
75#[derive(Clone, Debug, Deserialize)]
76struct ObjectParams {
77    usecase: String,
78    #[serde(deserialize_with = "deserialize_scopes")]
79    scopes: Scopes,
80    key: String,
81}
82
83/// Deserializes a `Scopes` instance from a string representation.
84///
85/// The string representation is a semicolon-separated list of `key=value` pairs, following the
86/// Matrix URIs proposal. An empty scopes string (`"_"`) represents no scopes.
87fn 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/// Path parameters used for collection-level endpoints without a key.
140///
141/// This is meant to be used with the axum `Path` extractor.
142#[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}