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