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