scufflecloud_core/operations/
mod.rs

1use diesel_async::AsyncConnection;
2use diesel_async::scoped_futures::ScopedFutureExt;
3
4use crate::CoreConfig;
5use crate::cedar::{self, Action, CedarEntity};
6use crate::common::TxError;
7use crate::http_ext::RequestExt;
8use crate::std_ext::ResultExt;
9
10pub(crate) mod login;
11pub(crate) mod organization_invitations;
12pub(crate) mod organizations;
13pub(crate) mod register;
14pub(crate) mod user_session_requests;
15pub(crate) mod user_sessions;
16pub(crate) mod users;
17
18/// This trait defines a framework for operations.
19///
20/// The process of running an operation (calling [`run`](Self::run)) is as follows:
21/// 1. Validate the operation with [`validate`](Self::validate).
22/// 2. Start a new database transaction. (If `Self::TRANSACTION` is `true`)
23/// 3. Load the principal with [`load_principal`](Self::load_principal).
24/// 4. Load the resource with [`load_resource`](Self::load_resource).
25/// 5. Check if the principal is authorized to perform the action on the resource with Cedar.
26/// 6. Execute the operation with [`execute`](Self::execute).
27/// 7. Commit the database transaction. (If `Self::TRANSACTION` is `true`)
28pub(crate) trait Operation<G: CoreConfig>: RequestExt + Sized + Send {
29    /// The cedar principal type for the operation.
30    type Principal: CedarEntity<G> + Send + Sync;
31    /// The cedar resource type for the operation.
32    type Resource: CedarEntity<G> + Send + Sync;
33    /// The response type for the operation.
34    type Response: Send;
35
36    const TRANSACTION: bool = true;
37    /// The action that this operation represents.
38    const ACTION: Action;
39
40    /// Validates the operation request.
41    /// This is called before loading principal and resource and executing the operation.
42    /// If this returns an error, the operation will not be executed.
43    async fn validate(&mut self) -> Result<(), tonic::Status> {
44        Ok(())
45    }
46
47    fn load_principal(
48        &mut self,
49        conn: &mut diesel_async::AsyncPgConnection,
50    ) -> impl Future<Output = Result<Self::Principal, tonic::Status>> + Send;
51
52    fn load_resource(
53        &mut self,
54        conn: &mut diesel_async::AsyncPgConnection,
55    ) -> impl Future<Output = Result<Self::Resource, tonic::Status>> + Send;
56
57    fn execute(
58        self,
59        conn: &mut diesel_async::AsyncPgConnection,
60        principal: Self::Principal,
61        resource: Self::Resource,
62    ) -> impl Future<Output = Result<Self::Response, tonic::Status>> + Send;
63
64    async fn run(mut self) -> Result<Self::Response, tonic::Status> {
65        let global = &self.global::<G>()?;
66
67        self.validate().await?;
68
69        let mut db = global.db().await.into_tonic_internal_err("failed to connect to database")?;
70
71        if Self::TRANSACTION {
72            let resp = db
73                .transaction::<_, TxError, _>(move |tx| {
74                    async move {
75                        let principal = self.load_principal(tx).await?;
76                        let resource = self.load_resource(tx).await?;
77
78                        cedar::is_authorized(global, self.session(), &principal, Self::ACTION, &resource).await?;
79
80                        self.execute(tx, principal, resource).await.map_err(Into::into)
81                    }
82                    .scope_boxed()
83                })
84                .await?;
85
86            Ok(resp)
87        } else {
88            let principal = self.load_principal(&mut db).await?;
89            let resource = self.load_resource(&mut db).await?;
90
91            cedar::is_authorized(global, self.session(), &principal, Self::ACTION, &resource).await?;
92
93            self.execute(&mut db, principal, resource).await
94        }
95    }
96}