Skip to main content

modkit_db/secure/
docs.rs

1//! # Secure ORM Layer Documentation
2//!
3//! The secure ORM layer provides type-safe, scoped access to database entities using `SeaORM`.
4//! It enforces an implicit security policy that prevents unscoped queries from executing.
5//!
6//! ## Core Concepts
7//!
8//! ### 1. `AccessScope`
9//!
10//! The [`AccessScope`](crate::secure::AccessScope) struct defines the security boundary:
11//!
12//! ```rust
13//! use modkit_db::secure::{AccessScope, ScopeConstraint, ScopeFilter, pep_properties};
14//! use uuid::Uuid;
15//!
16//! let tenant_id = Uuid::new_v4();
17//! let resource_id = Uuid::new_v4();
18//!
19//! // Scope to specific tenants
20//! let scope = AccessScope::for_tenants(vec![tenant_id]);
21//!
22//! // Scope to specific resources
23//! let scope = AccessScope::for_resources(vec![resource_id]);
24//!
25//! // Scope to both (AND relationship – single constraint with two filters)
26//! let scope = AccessScope::single(ScopeConstraint::new(vec![
27//!     ScopeFilter::in_uuids(pep_properties::OWNER_TENANT_ID, vec![tenant_id]),
28//!     ScopeFilter::in_uuids(pep_properties::RESOURCE_ID, vec![resource_id]),
29//! ]));
30//!
31//! // Empty scope (will deny all)
32//! let scope = AccessScope::default();
33//! ```
34//!
35//! ### 2. `ScopableEntity`
36//!
37//! Entities must implement [`ScopableEntity`](crate::secure::ScopableEntity) to declare
38//! which columns are used for scoping:
39//!
40//! ```rust,ignore
41//! use modkit_db::secure::ScopableEntity;
42//!
43//! impl ScopableEntity for user::Entity {
44//!     fn tenant_col() -> Option<Self::Column> {
45//!         Some(user::Column::TenantId)  // Multi-tenant entity
46//!     }
47//!     fn resource_col() -> Option<Self::Column> {
48//!         Some(user::Column::Id)
49//!     }
50//!     fn owner_col() -> Option<Self::Column> {
51//!         None
52//!     }
53//!     fn type_col() -> Option<Self::Column> {
54//!         None
55//!     }
56//! }
57//!
58//! // Global entity (no tenant scoping)
59//! impl ScopableEntity for system_config::Entity {
60//!     fn tenant_col() -> Option<Self::Column> {
61//!         None  // Global entity
62//!     }
63//!     fn resource_col() -> Option<Self::Column> {
64//!         Some(system_config::Column::Id)
65//!     }
66//!     fn owner_col() -> Option<Self::Column> {
67//!         None
68//!     }
69//!     fn type_col() -> Option<Self::Column> {
70//!         None
71//!     }
72//! }
73//! ```
74//!
75//! ### 3. Typestate-Based Queries
76//!
77//! The [`SecureSelect`](crate::secure::SecureSelect) wrapper uses typestates to prevent
78//! executing unscoped queries at compile time:
79//!
80//! ```rust,ignore
81//! use modkit_db::secure::{AccessScope, SecureEntityExt};
82//!
83//! // This works ✓
84//! let users = user::Entity::find()
85//!     .secure()              // Returns SecureSelect<E, Unscoped>
86//!     .scope_with(&scope)?   // Returns SecureSelect<E, Scoped>
87//!     .all(conn)             // Now can execute
88//!     .await?;
89//!
90//! // This won't compile ✗
91//! let users = user::Entity::find()
92//!     .secure()
93//!     .all(conn);  // ERROR: method not found in `SecureSelect<E, Unscoped>`
94//! ```
95//!
96//! ## Implicit Security Policy
97//!
98//! The layer enforces these rules automatically:
99//!
100//! | Scope Condition | SQL Result |
101//! |----------------|------------|
102//! | Empty (no tenant, no resource) | `WHERE 1=0` (deny all) |
103//! | Tenants only | `WHERE tenant_id IN (...)` |
104//! | Tenants only + entity has no `tenant_col` | `WHERE 1=0` (deny all) |
105//! | Resources only | `WHERE resource_col IN (...)` |
106//! | Both tenants and resources | `WHERE tenant_col IN (...) AND resource_col IN (...)` |
107//!
108//! ## Usage Examples
109//!
110//! ### Example 1: List users for a tenant
111//!
112//! ```rust,ignore
113//! use modkit_db::secure::{AccessScope, SecureEntityExt};
114//!
115//! pub async fn list_tenant_users(
116//!     db: &modkit_db::secure::SecureConn,
117//!     tenant_id: Uuid,
118//! ) -> Result<Vec<user::Model>, anyhow::Error> {
119//!     let scope = AccessScope::for_tenants(vec![tenant_id]);
120//!     
121//!     let users = user::Entity::find()
122//!         .secure()
123//!         .scope_with(&scope)?
124//!         .all(db)
125//!         .await?;
126//!     
127//!     Ok(users)
128//! }
129//! ```
130//!
131//! ### Example 2: Get specific user by ID (with tenant check)
132//!
133//! ```rust,ignore
134//! use modkit_db::secure::{AccessScope, SecureEntityExt};
135//!
136//! pub async fn get_user(
137//!     db: &modkit_db::secure::SecureConn,
138//!     tenant_id: Uuid,
139//!     user_id: Uuid,
140//! ) -> Result<Option<user::Model>, anyhow::Error> {
141//!     // This ensures the user belongs to the tenant (implicit AND)
142//!     let scope = AccessScope::single(ScopeConstraint::new(vec![
143//!         ScopeFilter::in_uuids(pep_properties::OWNER_TENANT_ID, vec![tenant_id]),
144//!         ScopeFilter::in_uuids(pep_properties::RESOURCE_ID, vec![user_id]),
145//!     ]));
146//!     
147//!     let user = user::Entity::find()
148//!         .secure()
149//!         .scope_with(&scope)?
150//!         .one(db)
151//!         .await?;
152//!     
153//!     Ok(user)
154//! }
155//! ```
156//!
157//! ### Example 3: List specific resources regardless of tenant
158//!
159//! ```rust,ignore
160//! // Useful for admin operations or cross-tenant reports
161//! pub async fn get_users_by_ids(
162//!     db: &modkit_db::secure::SecureConn,
163//!     user_ids: Vec<Uuid>,
164//! ) -> Result<Vec<user::Model>, anyhow::Error> {
165//!     let scope = AccessScope::for_resources(user_ids);
166//!     
167//!     let users = user::Entity::find()
168//!         .secure()
169//!         .scope_with(&scope)?
170//!         .all(db)
171//!         .await?;
172//!     
173//!     Ok(users)
174//! }
175//! ```
176//!
177//! ### Example 4: Additional filtering after scoping
178//!
179//! ```rust,ignore
180//! use sea_orm::{ColumnTrait, QueryFilter};
181//!
182//! pub async fn list_active_users(
183//!     db: &modkit_db::secure::SecureConn,
184//!     tenant_id: Uuid,
185//! ) -> Result<Vec<user::Model>, anyhow::Error> {
186//!     let scope = AccessScope::for_tenants(vec![tenant_id]);
187//!     
188//!     let users = user::Entity::find()
189//!         .secure()
190//!         .scope_with(&scope)?
191//!         .filter(user::Column::IsActive.eq(true))  // Additional filter
192//!         .order_by(user::Column::Email, Order::Asc)
193//!         .limit(100)
194//!         .all(db)
195//!         .await?;
196//!     
197//!     Ok(users)
198//! }
199//! ```
200//!
201//! ### Example 5: Working with global entities
202//!
203//! ```rust,ignore
204//! // Global entities (no tenant column) work with resource IDs only
205//! pub async fn get_system_config(
206//!     db: &modkit_db::secure::SecureConn,
207//!     config_id: Uuid,
208//! ) -> Result<Option<system_config::Model>, anyhow::Error> {
209//!     let scope = AccessScope::for_resources(vec![config_id]);
210//!     
211//!     let config = system_config::Entity::find()
212//!         .secure()
213//!         .scope_with(&scope)?
214//!         .one(db)
215//!         .await?;
216//!     
217//!     Ok(config)
218//! }
219//! ```
220//!
221//! ### Example 6: Advanced composition (no raw escape hatch)
222//!
223//! If you need more advanced query composition, prefer extending the secure wrappers in `modkit-db`
224//! (or using higher-level helpers like `OData` pagination). Module code should not unwrap raw `SeaORM`
225//! builders.
226//!
227//! ## Integration with Repository Pattern
228//!
229//! A typical repository would look like:
230//!
231//! ```rust,ignore
232//! use modkit_db::secure::{AccessScope, SecureConn, SecureEntityExt, ScopeError};
233//! use uuid::Uuid;
234//!
235//! pub struct UserRepository {
236//!     conn: SecureConn,
237//! }
238//!
239//! impl UserRepository {
240//!     pub async fn list_for_scope(
241//!         &self,
242//!         scope: &AccessScope,
243//!     ) -> Result<Vec<user::Model>, ScopeError> {
244//!         user::Entity::find()
245//!             .secure()
246//!             .scope_with(scope)?
247//!             .all(&self.conn)
248//!             .await
249//!     }
250//!     
251//!     pub async fn find_by_id(
252//!         &self,
253//!         tenant_id: Uuid,
254//!         user_id: Uuid,
255//!     ) -> Result<Option<user::Model>, ScopeError> {
256//!         let scope = AccessScope::single(ScopeConstraint::new(vec![
257//!             ScopeFilter::in_uuids(pep_properties::OWNER_TENANT_ID, vec![tenant_id]),
258//!             ScopeFilter::in_uuids(pep_properties::RESOURCE_ID, vec![user_id]),
259//!         ]));
260//!         
261//!         user::Entity::find()
262//!             .secure()
263//!             .scope_with(&scope)?
264//!             .one(&self.conn)
265//!             .await
266//!     }
267//! }
268//! ```
269//!
270//! ## Security Guarantees
271//!
272//! 1. **No unscoped execution**: Queries cannot be executed without calling `.scope_with()`
273//! 2. **Explicit deny-all**: Empty scopes are denied rather than returning all data
274//! 3. **Tenant isolation**: When `tenant_ids` are provided, they're always enforced
275//! 4. **Type safety**: Typestates prevent misuse at compile time
276//! 5. **No runtime overhead**: All checks happen at compile time or query build time
277//!
278//! ## Phase 2: Planned Enhancements
279//!
280//! Future versions will include:
281//!
282//! - `#[derive(Scopable)]` macro to auto-implement `ScopableEntity`
283//! - Support for scoped UPDATE and DELETE operations
284//! - Row-level security helpers for `PostgreSQL`
285//! - Audit logging integration
286//! - Policy composition (e.g., role-based filters)
287//!
288//! ## Error Handling
289//!
290//! The layer uses [`ScopeError`](crate::secure::ScopeError) for all errors:
291//!
292//! ```rust,ignore
293//! match user::Entity::find().secure().scope_with(&scope) {
294//!     Ok(scoped) => {
295//!         // Execute query
296//!     }
297//!     Err(ScopeError::Db(msg)) => {
298//!         // Handle database error
299//!     }
300//! }
301//! ```
302
303#[cfg(doc)]
304use crate::secure::{AccessScope, ScopableEntity, SecureSelect};