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};