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