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//!     db: &modkit_db::secure::SecureConn,
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(db)
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//!     db: &modkit_db::secure::SecureConn,
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(db)
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//!     db: &modkit_db::secure::SecureConn,
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(db)
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//!     db: &modkit_db::secure::SecureConn,
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(db)
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//!     db: &modkit_db::secure::SecureConn,
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(db)
209//!         .await?;
210//!     
211//!     Ok(config)
212//! }
213//! ```
214//!
215//! ### Example 6: Advanced composition (no raw escape hatch)
216//!
217//! If you need more advanced query composition, prefer extending the secure wrappers in `modkit-db`
218//! (or using higher-level helpers like `OData` pagination). Module code should not unwrap raw `SeaORM`
219//! builders.
220//!
221//! ## Integration with Repository Pattern
222//!
223//! A typical repository would look like:
224//!
225//! ```rust,ignore
226//! use modkit_db::secure::{AccessScope, SecureConn, SecureEntityExt, ScopeError};
227//! use uuid::Uuid;
228//!
229//! pub struct UserRepository {
230//!     conn: SecureConn,
231//! }
232//!
233//! impl UserRepository {
234//!     pub async fn list_for_scope(
235//!         &self,
236//!         scope: &AccessScope,
237//!     ) -> Result<Vec<user::Model>, ScopeError> {
238//!         user::Entity::find()
239//!             .secure()
240//!             .scope_with(scope)?
241//!             .all(&self.conn)
242//!             .await
243//!     }
244//!     
245//!     pub async fn find_by_id(
246//!         &self,
247//!         tenant_id: Uuid,
248//!         user_id: Uuid,
249//!     ) -> Result<Option<user::Model>, ScopeError> {
250//!         let scope = AccessScope::both(vec![tenant_id], vec![user_id]);
251//!         
252//!         user::Entity::find()
253//!             .secure()
254//!             .scope_with(&scope)?
255//!             .one(&self.conn)
256//!             .await
257//!     }
258//! }
259//! ```
260//!
261//! ## Security Guarantees
262//!
263//! 1. **No unscoped execution**: Queries cannot be executed without calling `.scope_with()`
264//! 2. **Explicit deny-all**: Empty scopes are denied rather than returning all data
265//! 3. **Tenant isolation**: When `tenant_ids` are provided, they're always enforced
266//! 4. **Type safety**: Typestates prevent misuse at compile time
267//! 5. **No runtime overhead**: All checks happen at compile time or query build time
268//!
269//! ## Phase 2: Planned Enhancements
270//!
271//! Future versions will include:
272//!
273//! - `#[derive(Scopable)]` macro to auto-implement `ScopableEntity`
274//! - Support for scoped UPDATE and DELETE operations
275//! - Row-level security helpers for `PostgreSQL`
276//! - Audit logging integration
277//! - Policy composition (e.g., role-based filters)
278//!
279//! ## Error Handling
280//!
281//! The layer uses [`ScopeError`](crate::secure::ScopeError) for all errors:
282//!
283//! ```rust,ignore
284//! match user::Entity::find().secure().scope_with(&scope) {
285//!     Ok(scoped) => {
286//!         // Execute query
287//!     }
288//!     Err(ScopeError::Db(msg)) => {
289//!         // Handle database error
290//!     }
291//! }
292//! ```
293
294#[cfg(doc)]
295use crate::secure::{AccessScope, ScopableEntity, SecureSelect};