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