scufflecloud_core/
operations.rs

1use std::sync::Arc;
2
3use core_cedar::CedarEntity;
4use diesel_async::TransactionManager;
5use ext_traits::ResultExt;
6
7use crate::cedar::{self, Action};
8use crate::http_ext::CoreRequestExt;
9
10pub(crate) mod login;
11pub(crate) mod organization_invitations;
12pub(crate) mod organizations;
13pub(crate) mod user_session_requests;
14pub(crate) mod user_sessions;
15pub(crate) mod users;
16
17pub(crate) struct OperationDriver<'a, G: core_traits::Global> {
18    global: &'a Arc<G>,
19    conn: Option<G::Connection<'a>>,
20}
21
22impl<'a, G: core_traits::Global> OperationDriver<'a, G> {
23    const fn new(global: &'a Arc<G>) -> Self {
24        OperationDriver { global, conn: None }
25    }
26
27    pub(crate) async fn conn(&mut self) -> Result<&mut G::Connection<'a>, tonic::Status> {
28        let conn = &mut self.conn;
29        if let Some(conn) = conn {
30            return Ok(conn);
31        }
32
33        let mut db = self
34            .global
35            .db()
36            .await
37            .into_tonic_internal_err("failed to connect to database")?;
38        <G::Connection<'a> as diesel_async::AsyncConnection>::TransactionManager::begin_transaction(&mut db)
39            .await
40            .into_tonic_internal_err("failed to begin transaction")?;
41        Ok(conn.insert(db))
42    }
43
44    async fn abort(self) -> Result<(), tonic::Status> {
45        let Some(mut conn) = self.conn else {
46            return Ok(());
47        };
48
49        <G::Connection<'a> as diesel_async::AsyncConnection>::TransactionManager::rollback_transaction(&mut conn)
50            .await
51            .into_tonic_internal_err("failed to rollback transaction")
52    }
53
54    async fn commit(self) -> Result<(), tonic::Status> {
55        let Some(mut conn) = self.conn else {
56            return Ok(());
57        };
58
59        <G::Connection<'a> as diesel_async::AsyncConnection>::TransactionManager::commit_transaction(&mut conn)
60            .await
61            .into_tonic_internal_err("failed to commit transaction")
62    }
63}
64
65/// This trait defines a framework for operations.
66///
67/// The process of running an operation (calling [`run`](Self::run)) is as follows:
68/// 1. Validate the operation with [`validate`](Self::validate).
69/// 2. Start the operation driver with [`OperationDriver::start`].
70/// 3. Load the principal with [`load_principal`](Self::load_principal).
71/// 4. Load the resource with [`load_resource`](Self::load_resource).
72/// 5. Check if the principal is authorized to perform the action on the resource with Cedar.
73/// 6. Execute the operation with [`execute`](Self::execute).
74/// 7. Commit the operation with [`OperationDriver::finish`] if successful,
75///    or abort with [`OperationDriver::abort`] if an error occurred.
76pub(crate) trait Operation<G: core_traits::Global>: ext_traits::RequestExt + Sized + Send {
77    /// The cedar principal type for the operation.
78    type Principal: CedarEntity + Send + Sync;
79    /// The cedar resource type for the operation.
80    type Resource: CedarEntity + Send + Sync;
81    /// The response type for the operation.
82    type Response: Send;
83
84    /// The action that this operation represents.
85    const ACTION: Action;
86
87    /// Validates the operation request.
88    /// This is called before loading principal and resource and executing the operation.
89    /// If this returns an error, the operation will not be executed.
90    async fn validate(&mut self) -> Result<(), tonic::Status> {
91        Ok(())
92    }
93
94    fn load_principal(
95        &mut self,
96        driver: &mut OperationDriver<'_, G>,
97    ) -> impl Future<Output = Result<Self::Principal, tonic::Status>> + Send;
98
99    fn load_resource(
100        &mut self,
101        driver: &mut OperationDriver<'_, G>,
102    ) -> impl Future<Output = Result<Self::Resource, tonic::Status>> + Send;
103
104    fn execute(
105        self,
106        driver: &mut OperationDriver<'_, G>,
107        principal: Self::Principal,
108        resource: Self::Resource,
109    ) -> impl Future<Output = Result<Self::Response, tonic::Status>> + Send;
110
111    async fn run(mut self) -> Result<Self::Response, tonic::Status> {
112        self.validate().await?;
113
114        let global = self.global::<G>()?;
115        let mut driver = OperationDriver::new(&global);
116
117        let fut = async {
118            let principal = self.load_principal(&mut driver).await?;
119            let resource = self.load_resource(&mut driver).await?;
120
121            cedar::is_authorized(&global, self.session(), &principal, &Self::ACTION, &resource).await?;
122
123            self.execute(&mut driver, principal, resource).await
124        };
125
126        match fut.await {
127            Ok(resp) => {
128                driver.commit().await?;
129                Ok(resp)
130            }
131            Err(e) => {
132                driver.abort().await?;
133                Err(e)
134            }
135        }
136    }
137}