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::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/// Path parameters used for object-level endpoints.
82///
83/// This is meant to be used with the axum `Path` extractor.
84#[derive(Clone, Debug, Deserialize)]
85struct ObjectParams {
86    usecase: String,
87    #[serde(deserialize_with = "deserialize_scopes")]
88    scopes: Scopes,
89    key: String,
90}
91
92/// Deserializes a `Scopes` instance from a string representation.
93///
94/// The string representation is a semicolon-separated list of `key=value` pairs, following the
95/// Matrix URIs proposal. An empty scopes string (`"_"`) represents no scopes.
96fn 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/// Path parameters used for collection-level endpoints without a key.
157///
158/// This is meant to be used with the axum `Path` extractor.
159#[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}