objectstore_server/auth/
service.rs

1use objectstore_service::id::{ObjectContext, ObjectId};
2use objectstore_service::service::{DeleteResponse, GetResponse, InsertResponse, MetadataResponse};
3use objectstore_service::{ClientStream, StorageService};
4use objectstore_types::auth::Permission;
5use objectstore_types::metadata::Metadata;
6
7use crate::auth::{AuthContext, AuthError};
8use crate::endpoints::common::ApiResult;
9
10/// Wrapper around [`StorageService`] that ensures each operation is authorized.
11///
12/// Authorization is performed according to the request's authorization details, see also
13/// [`AuthContext`]. When [`crate::config::AuthZ::enforce`] is false, authorization failures are
14/// logged but any unauthorized operations are still allowed to proceed.
15///
16/// Objectstore API endpoints can use `AuthAwareService` simply by adding it to their handler
17/// function's argument list like so:
18///
19/// ```
20/// use axum::http::StatusCode;
21/// use objectstore_server::auth::AuthAwareService;
22///
23/// async fn my_endpoint(service: AuthAwareService) -> Result<StatusCode, StatusCode> {
24///     service.delete_object(todo!("pass some ID"))
25///         .await
26///         .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
27///
28///     Ok(StatusCode::NO_CONTENT)
29/// }
30/// ```
31#[derive(Debug)]
32pub struct AuthAwareService {
33    service: StorageService,
34    context: Option<AuthContext>,
35    enforce: bool,
36}
37
38impl AuthAwareService {
39    /// Creates a new `AuthAwareService` using the given [`StorageService`], [`AuthContext`], and
40    /// enforcement setting.
41    ///
42    /// If enforcement is enabled, an `AuthContext` must be provided and its checks must succeed
43    /// for an operation to be permitted.
44    ///
45    /// If enforcement is disabled, an `AuthContext` is not required. If one is provided, its
46    /// checks will be run but their results ignored. All operations will be permitted.
47    pub fn new(
48        service: StorageService,
49        context: Option<AuthContext>,
50        enforce: bool,
51    ) -> ApiResult<Self> {
52        if enforce && context.is_none() {
53            let err = AuthError::InternalError("Missing auth context".into());
54            err.log(None, None, enforce);
55            Err(err.into())
56        } else {
57            Ok(Self {
58                service,
59                context,
60                enforce,
61            })
62        }
63    }
64
65    fn assert_authorized(&self, perm: Permission, context: &ObjectContext) -> ApiResult<()> {
66        let auth_result = match &self.context {
67            Some(auth) => auth.assert_authorized(perm, context),
68            None => Ok(()),
69        }
70        .inspect_err(|err| err.log(Some(perm), Some(context.usecase.as_str()), self.enforce));
71
72        match self.enforce {
73            true => Ok(auth_result?),
74            false => Ok(()),
75        }
76    }
77
78    /// Checks whether the request is authorized for the given permission on the given context.
79    ///
80    /// Returns `Ok(())` if authorized, or otherwise an error indicating the reason.
81    /// Equivalent to the internal `assert_authorized` check but exposed for callers
82    /// that validate operations individually before delegating to a lower-level service.
83    pub fn check_permission(&self, perm: Permission, context: &ObjectContext) -> ApiResult<()> {
84        self.assert_authorized(perm, context)
85    }
86
87    /// Auth-aware wrapper around [`StorageService::insert_object`].
88    pub async fn insert_object(
89        &self,
90        context: ObjectContext,
91        key: Option<String>,
92        metadata: Metadata,
93        stream: ClientStream,
94    ) -> ApiResult<InsertResponse> {
95        self.assert_authorized(Permission::ObjectWrite, &context)?;
96        Ok(self
97            .service
98            .insert_object(context, key, metadata, stream)
99            .await?)
100    }
101
102    /// Auth-aware wrapper around [`StorageService::get_metadata`].
103    pub async fn get_metadata(&self, id: ObjectId) -> ApiResult<MetadataResponse> {
104        self.assert_authorized(Permission::ObjectRead, id.context())?;
105        Ok(self.service.get_metadata(id).await?)
106    }
107
108    /// Auth-aware wrapper around [`StorageService::get_object`].
109    pub async fn get_object(&self, id: ObjectId) -> ApiResult<GetResponse> {
110        self.assert_authorized(Permission::ObjectRead, id.context())?;
111        Ok(self.service.get_object(id).await?)
112    }
113
114    /// Auth-aware wrapper around [`StorageService::delete_object`].
115    pub async fn delete_object(&self, id: ObjectId) -> ApiResult<DeleteResponse> {
116        self.assert_authorized(Permission::ObjectDelete, id.context())?;
117        Ok(self.service.delete_object(id).await?)
118    }
119}