Skip to main content

objectstore_server/auth/
service.rs

1use objectstore_service::backend::common::{ExpiryUpdate, SetExpiryResponse};
2use objectstore_service::id::{ObjectContext, ObjectId};
3use objectstore_service::service::{
4    CreateUploadSessionResponse, DeleteResponse, GetResponse, InsertResponse, MetadataResponse,
5};
6
7use objectstore_service::{ClientStream, StorageService};
8use objectstore_types::auth::Permission;
9use objectstore_types::metadata::Metadata;
10use objectstore_types::range::ByteRange;
11use objectstore_types::resumable::{SessionToken, UploadProgress};
12use objectstore_types::time::Timestamp;
13
14use crate::auth::AuthContext;
15use crate::endpoints::common::ApiResult;
16
17/// Wrapper around [`StorageService`] that ensures each operation is authorized.
18///
19/// Authorization is performed according to the request's authorization details, see also
20/// [`AuthContext`]. When [`crate::config::AuthZ::enforce`] is false, authorization failures are
21/// logged but any unauthorized operations are still allowed to proceed.
22///
23/// Objectstore API endpoints can use `AuthAwareService` simply by adding it to their handler
24/// function's argument list like so:
25///
26/// ```
27/// use axum::http::StatusCode;
28/// use objectstore_server::auth::AuthAwareService;
29/// use objectstore_server::extractors::request_time::RequestTime;
30///
31/// async fn my_endpoint(
32///     service: AuthAwareService,
33///     RequestTime(access_time): RequestTime,
34/// ) -> Result<StatusCode, StatusCode> {
35///     service.delete_object(todo!("pass some ID"), access_time)
36///         .await
37///         .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
38///
39///     Ok(StatusCode::NO_CONTENT)
40/// }
41/// ```
42#[derive(Debug)]
43pub struct AuthAwareService {
44    service: StorageService,
45    context: AuthContext,
46    enforce: bool,
47}
48
49impl AuthAwareService {
50    /// Creates a new `AuthAwareService` using the given [`StorageService`], [`AuthContext`],
51    /// and enforcement setting.
52    ///
53    /// The `context` is used to check whether each operation is authorized. `enforce` controls
54    /// whether authorization failures will result in an error response or be ignored.
55    ///
56    /// With an [`AuthContext::Disabled`] context, all operations are permitted.
57    pub fn new(service: StorageService, context: AuthContext, enforce: bool) -> Self {
58        Self {
59            service,
60            context,
61            enforce,
62        }
63    }
64
65    /// Checks whether the request is authorized for the given permission on the given context.
66    ///
67    /// Returns `Ok(())` if authorized, or an error indicating the reason.
68    pub fn check_permission(&self, perm: Permission, context: &ObjectContext) -> ApiResult<()> {
69        if let Err(error) = self.context.assert_authorized(perm, context) {
70            sentry::with_scope(
71                |s| s.set_tag("perm", perm.to_string()),
72                || error.log(!self.enforce),
73            );
74
75            if self.enforce {
76                return Err(error.into());
77            }
78        }
79
80        Ok(())
81    }
82
83    /// Auth-aware wrapper around [`StorageService::insert_object`].
84    pub async fn insert_object(
85        &self,
86        context: ObjectContext,
87        key: Option<String>,
88        metadata: Metadata,
89        stream: ClientStream,
90        access_time: Timestamp,
91    ) -> ApiResult<InsertResponse> {
92        self.check_permission(Permission::ObjectWrite, &context)?;
93        Ok(self
94            .service
95            .insert_object(context, key, metadata, stream, access_time)
96            .await?)
97    }
98
99    /// Auth-aware wrapper around [`StorageService::get_metadata`].
100    pub async fn get_metadata(
101        &self,
102        id: ObjectId,
103        access_time: Timestamp,
104    ) -> ApiResult<MetadataResponse> {
105        self.check_permission(Permission::ObjectRead, id.context())?;
106        Ok(self.service.get_metadata(id, access_time).await?)
107    }
108
109    /// Auth-aware wrapper around [`StorageService::set_expiry`].
110    pub async fn set_expiry(
111        &self,
112        id: ObjectId,
113        target: ExpiryUpdate,
114        access_time: Timestamp,
115    ) -> ApiResult<SetExpiryResponse> {
116        self.check_permission(Permission::ObjectWrite, id.context())?;
117        Ok(self.service.set_expiry(id, target, access_time).await?)
118    }
119
120    /// Auth-aware wrapper around [`StorageService::get_object`].
121    pub async fn get_object(
122        &self,
123        id: ObjectId,
124        access_time: Timestamp,
125        range: Option<ByteRange>,
126    ) -> ApiResult<GetResponse> {
127        self.check_permission(Permission::ObjectRead, id.context())?;
128        Ok(self.service.get_object(id, access_time, range).await?)
129    }
130
131    /// Auth-aware wrapper around [`StorageService::delete_object`].
132    pub async fn delete_object(
133        &self,
134        id: ObjectId,
135        access_time: Timestamp,
136    ) -> ApiResult<DeleteResponse> {
137        self.check_permission(Permission::ObjectDelete, id.context())?;
138        Ok(self.service.delete_object(id, access_time).await?)
139    }
140
141    // --- Resumable upload operations ---
142
143    /// Auth-aware wrapper around [`StorageService::create_upload_session`].
144    pub async fn create_upload_session(
145        &self,
146        id: ObjectId,
147        metadata: Metadata,
148        total_length: u64,
149    ) -> ApiResult<Option<CreateUploadSessionResponse>> {
150        self.check_permission(Permission::ObjectWrite, id.context())?;
151        Ok(self
152            .service
153            .create_upload_session(id, metadata, total_length)
154            .await?)
155    }
156
157    /// Auth-aware wrapper around [`StorageService::put_chunk`].
158    pub async fn put_chunk(
159        &self,
160        id: ObjectId,
161        token: SessionToken,
162        offset: u64,
163        content_length: u64,
164        body: ClientStream,
165    ) -> ApiResult<UploadProgress> {
166        self.check_permission(Permission::ObjectWrite, id.context())?;
167        Ok(self
168            .service
169            .put_chunk(id, token, offset, content_length, body)
170            .await?)
171    }
172
173    /// Auth-aware wrapper around [`StorageService::upload_offset`].
174    pub async fn upload_offset(
175        &self,
176        id: ObjectId,
177        token: SessionToken,
178    ) -> ApiResult<UploadProgress> {
179        // A status query can detect an upload to be complete and cause logical object creation.
180        self.check_permission(Permission::ObjectWrite, id.context())?;
181        Ok(self.service.upload_offset(id, token).await?)
182    }
183
184    /// Auth-aware wrapper around [`StorageService::cancel_upload`].
185    pub async fn cancel_upload(&self, id: ObjectId, token: SessionToken) -> ApiResult<()> {
186        // Canceling a session discards an in-progress upload rather than deleting an object.
187        self.check_permission(Permission::ObjectWrite, id.context())?;
188        Ok(self.service.cancel_upload(id, token).await?)
189    }
190}