Skip to main content

cognee_delete/
authorized.rs

1//! ACL-enforcing wrapper around [`DeleteService`].
2//!
3//! [`AuthorizedDeleteService`] checks that the calling principal holds the
4//! `"delete"` permission on each target dataset before delegating to the
5//! inner [`DeleteService`]. The plain `DeleteService` remains available for
6//! edge/embedded deployments that do not require ACL enforcement.
7
8use std::sync::Arc;
9
10use cognee_database::{AclDb, DeleteDb};
11use uuid::Uuid;
12
13use crate::{DeleteError, DeletePreview, DeleteRequest, DeleteResult, DeleteScope, DeleteService};
14
15/// Authorization-enforcing delete service.
16///
17/// Wraps a [`DeleteService`] and an [`AclDb`] implementation. Before every
18/// `execute()` or `preview()` call the wrapper verifies that `principal_id`
19/// holds `"delete"` permission on all affected datasets.
20pub struct AuthorizedDeleteService {
21    inner: DeleteService,
22    acl_db: Arc<dyn AclDb>,
23    database: Arc<dyn DeleteDb>,
24}
25
26impl AuthorizedDeleteService {
27    /// Create a new authorized delete service.
28    ///
29    /// `database` must be the same connection used to construct the inner
30    /// `DeleteService`, so that dataset resolution is consistent.
31    pub fn new(inner: DeleteService, acl_db: Arc<dyn AclDb>, database: Arc<dyn DeleteDb>) -> Self {
32        Self {
33            inner,
34            acl_db,
35            database,
36        }
37    }
38
39    /// Preview the deletion, checking ACL first.
40    pub async fn preview(
41        &self,
42        request: &DeleteRequest,
43        principal_id: Uuid,
44    ) -> Result<DeletePreview, DeleteError> {
45        self.check_authorization(request, principal_id).await?;
46        self.inner.preview(request).await
47    }
48
49    /// Execute the deletion, checking ACL first.
50    pub async fn execute(
51        &self,
52        request: &DeleteRequest,
53        principal_id: Uuid,
54    ) -> Result<DeleteResult, DeleteError> {
55        self.check_authorization(request, principal_id).await?;
56        self.inner.execute(request).await
57    }
58
59    /// Verify that `principal_id` has "delete" permission on all datasets
60    /// that would be affected by the request.
61    async fn check_authorization(
62        &self,
63        request: &DeleteRequest,
64        principal_id: Uuid,
65    ) -> Result<(), DeleteError> {
66        match &request.scope {
67            DeleteScope::Data {
68                owner_id,
69                data_id,
70                dataset_name,
71                ..
72            } => {
73                if let Some(ds_name) = dataset_name {
74                    let dataset_id = self.resolve_dataset_id(*owner_id, ds_name).await?;
75                    self.require_delete_permission(principal_id, dataset_id)
76                        .await?;
77                } else {
78                    // When no dataset_name is given, the inner service resolves
79                    // all datasets linked to this data item that belong to the
80                    // owner. We must check that the principal has delete
81                    // permission on each of those datasets.
82                    let datasets = self
83                        .database
84                        .list_datasets_for_data(*data_id)
85                        .await
86                        .map_err(|e| {
87                            DeleteError::Runtime(format!(
88                                "Failed to list datasets for data {data_id}: {e}"
89                            ))
90                        })?;
91
92                    for ds in &datasets {
93                        if ds.owner_id == *owner_id {
94                            self.require_delete_permission(principal_id, ds.id).await?;
95                        }
96                    }
97                }
98            }
99            DeleteScope::Dataset {
100                owner_id,
101                dataset_name,
102            } => {
103                let dataset_id = self.resolve_dataset_id(*owner_id, dataset_name).await?;
104                self.require_delete_permission(principal_id, dataset_id)
105                    .await?;
106            }
107            DeleteScope::User { owner_id } => {
108                // For user-scoped deletion, verify the principal has delete
109                // on all datasets that would be affected. The inner service
110                // will list all datasets by owner. We check that the
111                // principal's authorized set covers them.
112                let authorized = self
113                    .acl_db
114                    .authorized_dataset_ids(principal_id, "delete")
115                    .await
116                    .map_err(|e| DeleteError::Runtime(format!("ACL query failed: {e}")))?;
117
118                let owner_datasets = self
119                    .database
120                    .list_datasets_by_owner(*owner_id)
121                    .await
122                    .map_err(|e| {
123                        DeleteError::Runtime(format!("Failed to list owner datasets: {e}"))
124                    })?;
125
126                for ds in &owner_datasets {
127                    if !authorized.contains(&ds.id) {
128                        return Err(DeleteError::PermissionDenied(format!(
129                            "Principal {} does not have 'delete' permission on dataset '{}'",
130                            principal_id, ds.name
131                        )));
132                    }
133                }
134            }
135            DeleteScope::All => {
136                // All-scope is an administrative operation. We check that
137                // the principal has delete permission on every dataset that
138                // exists. If any dataset lacks the permission, deny.
139                let authorized = self
140                    .acl_db
141                    .authorized_dataset_ids(principal_id, "delete")
142                    .await
143                    .map_err(|e| DeleteError::Runtime(format!("ACL query failed: {e}")))?;
144
145                let all_datasets = self.database.list_datasets().await.map_err(|e| {
146                    DeleteError::Runtime(format!("Failed to list all datasets: {e}"))
147                })?;
148
149                for ds in &all_datasets {
150                    if !authorized.contains(&ds.id) {
151                        return Err(DeleteError::PermissionDenied(format!(
152                            "Principal {} does not have 'delete' permission on dataset '{}'",
153                            principal_id, ds.name
154                        )));
155                    }
156                }
157            }
158        }
159
160        Ok(())
161    }
162
163    /// Look up a dataset by name + owner and return its ID.
164    async fn resolve_dataset_id(
165        &self,
166        owner_id: Uuid,
167        dataset_name: &str,
168    ) -> Result<Uuid, DeleteError> {
169        let dataset = self
170            .database
171            .get_dataset_by_name(dataset_name, owner_id, None)
172            .await
173            .map_err(|e| {
174                DeleteError::Runtime(format!("Failed to resolve dataset '{dataset_name}': {e}"))
175            })?
176            .ok_or_else(|| {
177                DeleteError::Validation(format!(
178                    "Dataset '{dataset_name}' was not found for owner {owner_id}"
179                ))
180            })?;
181
182        Ok(dataset.id)
183    }
184
185    /// Check that the principal has "delete" permission on the dataset.
186    async fn require_delete_permission(
187        &self,
188        principal_id: Uuid,
189        dataset_id: Uuid,
190    ) -> Result<(), DeleteError> {
191        let has_perm = self
192            .acl_db
193            .has_permission(principal_id, dataset_id, "delete")
194            .await
195            .map_err(|e| DeleteError::Runtime(format!("ACL check failed: {e}")))?;
196
197        if !has_perm {
198            return Err(DeleteError::PermissionDenied(format!(
199                "Principal {principal_id} does not have 'delete' permission on dataset {dataset_id}"
200            )));
201        }
202
203        Ok(())
204    }
205}