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