Skip to main content

objectstore_server/auth/
service.rs

1use objectstore_service::id::{ObjectContext, ObjectId};
2use objectstore_service::multipart::{
3    AbortMultipartResponse, CompleteMultipartResponse, CompletedPart, InitiateMultipartResponse,
4    ListPartsResponse, PartNumber, UploadId, UploadPartResponse,
5};
6use objectstore_service::service::{DeleteResponse, GetResponse, InsertResponse, MetadataResponse};
7use objectstore_service::{ClientStream, StorageService};
8use objectstore_types::auth::Permission;
9use objectstore_types::metadata::Metadata;
10use objectstore_types::range::ByteRange;
11
12use crate::auth::{AuthContext, AuthError};
13use crate::endpoints::common::ApiResult;
14
15/// Wrapper around [`StorageService`] that ensures each operation is authorized.
16///
17/// Authorization is performed according to the request's authorization details, see also
18/// [`AuthContext`]. When [`crate::config::AuthZ::enforce`] is false, authorization failures are
19/// logged but any unauthorized operations are still allowed to proceed.
20///
21/// Objectstore API endpoints can use `AuthAwareService` simply by adding it to their handler
22/// function's argument list like so:
23///
24/// ```
25/// use axum::http::StatusCode;
26/// use objectstore_server::auth::AuthAwareService;
27///
28/// async fn my_endpoint(service: AuthAwareService) -> Result<StatusCode, StatusCode> {
29///     service.delete_object(todo!("pass some ID"))
30///         .await
31///         .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
32///
33///     Ok(StatusCode::NO_CONTENT)
34/// }
35/// ```
36#[derive(Debug)]
37pub struct AuthAwareService {
38    service: StorageService,
39    context: Option<AuthContext>,
40    enforce: bool,
41}
42
43impl AuthAwareService {
44    /// Creates a new `AuthAwareService` using the given [`StorageService`], [`AuthContext`], and
45    /// enforcement setting.
46    ///
47    /// If enforcement is enabled, an `AuthContext` must be provided and its checks must succeed
48    /// for an operation to be permitted.
49    ///
50    /// If enforcement is disabled, an `AuthContext` is not required. If one is provided, its
51    /// checks will be run but their results ignored. All operations will be permitted.
52    pub fn new(
53        service: StorageService,
54        context: Option<AuthContext>,
55        enforce: bool,
56    ) -> ApiResult<Self> {
57        if enforce && context.is_none() {
58            let err = AuthError::InternalError("Missing auth context".into());
59            err.log(None, None, enforce);
60            Err(err.into())
61        } else {
62            Ok(Self {
63                service,
64                context,
65                enforce,
66            })
67        }
68    }
69
70    fn assert_authorized(&self, perm: Permission, context: &ObjectContext) -> ApiResult<()> {
71        let auth_result = match &self.context {
72            Some(auth) => auth.assert_authorized(perm, context),
73            None => Ok(()),
74        }
75        .inspect_err(|err| err.log(Some(perm), Some(context.usecase.as_str()), self.enforce));
76
77        match self.enforce {
78            true => Ok(auth_result?),
79            false => Ok(()),
80        }
81    }
82
83    /// Checks whether the request is authorized for the given permission on the given context.
84    ///
85    /// Returns `Ok(())` if authorized, or otherwise an error indicating the reason.
86    /// Equivalent to the internal `assert_authorized` check but exposed for callers
87    /// that validate operations individually before delegating to a lower-level service.
88    pub fn check_permission(&self, perm: Permission, context: &ObjectContext) -> ApiResult<()> {
89        self.assert_authorized(perm, context)
90    }
91
92    /// Auth-aware wrapper around [`StorageService::insert_object`].
93    pub async fn insert_object(
94        &self,
95        context: ObjectContext,
96        key: Option<String>,
97        metadata: Metadata,
98        stream: ClientStream,
99    ) -> ApiResult<InsertResponse> {
100        self.assert_authorized(Permission::ObjectWrite, &context)?;
101        Ok(self
102            .service
103            .insert_object(context, key, metadata, stream)
104            .await?)
105    }
106
107    /// Auth-aware wrapper around [`StorageService::get_metadata`].
108    pub async fn get_metadata(&self, id: ObjectId) -> ApiResult<MetadataResponse> {
109        self.assert_authorized(Permission::ObjectRead, id.context())?;
110        Ok(self.service.get_metadata(id).await?)
111    }
112
113    /// Auth-aware wrapper around [`StorageService::get_object`].
114    pub async fn get_object(
115        &self,
116        id: ObjectId,
117        range: Option<ByteRange>,
118    ) -> ApiResult<GetResponse> {
119        self.assert_authorized(Permission::ObjectRead, id.context())?;
120        Ok(self.service.get_object(id, range).await?)
121    }
122
123    /// Auth-aware wrapper around [`StorageService::delete_object`].
124    pub async fn delete_object(&self, id: ObjectId) -> ApiResult<DeleteResponse> {
125        self.assert_authorized(Permission::ObjectDelete, id.context())?;
126        Ok(self.service.delete_object(id).await?)
127    }
128
129    // --- Multipart upload operations ---
130
131    /// Auth-aware wrapper around [`StorageService::initiate_multipart`].
132    pub async fn initiate_multipart(
133        &self,
134        id: ObjectId,
135        metadata: Metadata,
136    ) -> ApiResult<InitiateMultipartResponse> {
137        self.assert_authorized(Permission::ObjectWrite, id.context())?;
138        Ok(self.service.initiate_multipart(id, metadata).await?)
139    }
140
141    /// Auth-aware wrapper around [`StorageService::upload_part`].
142    pub async fn upload_part(
143        &self,
144        id: ObjectId,
145        upload_id: UploadId,
146        part_number: PartNumber,
147        content_length: u64,
148        content_md5: Option<String>,
149        body: ClientStream,
150    ) -> ApiResult<UploadPartResponse> {
151        self.assert_authorized(Permission::ObjectWrite, id.context())?;
152        Ok(self
153            .service
154            .upload_part(
155                id,
156                upload_id,
157                part_number,
158                content_length,
159                content_md5,
160                body,
161            )
162            .await?)
163    }
164
165    /// Auth-aware wrapper around [`StorageService::list_parts`].
166    pub async fn list_parts(
167        &self,
168        id: ObjectId,
169        upload_id: UploadId,
170        max_parts: Option<u32>,
171        part_number_marker: Option<PartNumber>,
172    ) -> ApiResult<ListPartsResponse> {
173        self.assert_authorized(Permission::ObjectWrite, id.context())?;
174        Ok(self
175            .service
176            .list_parts(id, upload_id, max_parts, part_number_marker)
177            .await?)
178    }
179
180    /// Auth-aware wrapper around [`StorageService::abort_multipart`].
181    pub async fn abort_multipart(
182        &self,
183        id: ObjectId,
184        upload_id: UploadId,
185    ) -> ApiResult<AbortMultipartResponse> {
186        self.assert_authorized(Permission::ObjectWrite, id.context())?;
187        Ok(self.service.abort_multipart(id, upload_id).await?)
188    }
189
190    /// Auth-aware wrapper around [`StorageService::complete_multipart`].
191    pub async fn complete_multipart(
192        &self,
193        id: ObjectId,
194        upload_id: UploadId,
195        parts: Vec<CompletedPart>,
196    ) -> ApiResult<CompleteMultipartResponse> {
197        self.assert_authorized(Permission::ObjectWrite, id.context())?;
198        Ok(self
199            .service
200            .complete_multipart(id, upload_id, parts)
201            .await?)
202    }
203}