pub struct SecureConn { /* private fields */ }Expand description
Secure database connection wrapper.
This is the primary interface for module developers to access the database.
All operations require a SecurityCtx parameter for per-request access control.
§Usage
Module services receive a &SecureConn and provide SecurityCtx per-request:
pub struct MyService<'a> {
db: &'a SecureConn,
}
impl<'a> MyService<'a> {
pub async fn get_user(&self, scope: &AccessScope, id: Uuid) -> Result<Option<User>> {
self.db.find_by_id::<user::Entity>(ctx, id)?
.one(self.db.conn())
.await
}
}§Security Guarantees
- All queries require
SecurityCtxfrom the request - Queries are scoped by tenant/resource from the context
- Empty scopes result in deny-all (no data returned)
- Type system prevents unscoped queries from compiling
- Cannot bypass security without
insecure-escapefeature
Implementations§
Source§impl SecureConn
impl SecureConn
Sourcepub fn new(conn: DatabaseConnection) -> Self
pub fn new(conn: DatabaseConnection) -> Self
Create a new secure database connection wrapper.
Typically created via DbHandle::sea_secure() rather than directly.
Sourcepub fn conn(&self) -> &DatabaseConnection
pub fn conn(&self) -> &DatabaseConnection
Get a reference to the underlying database connection.
§Safety
Use with caution. Direct connection access bypasses automatic scoping.
Prefer the high-level methods (find, update_many, etc.) whenever possible.
Valid use cases:
- Executing already-scoped queries (
.one(),.all(),.exec()) - Complex joins that need custom
SeaORMbuilding - Internal infrastructure code (not module business logic)
Sourcepub fn db_engine(&self) -> &'static str
pub fn db_engine(&self) -> &'static str
Return database engine identifier for tracing / logging.
Sourcepub fn find<E>(&self, scope: &AccessScope) -> SecureSelect<E, Scoped>
pub fn find<E>(&self, scope: &AccessScope) -> SecureSelect<E, Scoped>
Create a scoped select query for the given entity.
Returns a SecureSelect<E, Scoped> that automatically applies
tenant/resource filtering based on the provided security context.
§Example
let ctx = SecurityCtx::for_tenants(vec![tenant_id], user_id);
let users = db.find::<user::Entity>(&ctx)?
.filter(user::Column::Status.eq("active"))
.order_by_asc(user::Column::Email)
.all(db.conn())
.await?;§Errors
Sourcepub fn find_by_id<E>(
&self,
scope: &AccessScope,
id: Uuid,
) -> Result<SecureSelect<E, Scoped>, ScopeError>
pub fn find_by_id<E>( &self, scope: &AccessScope, id: Uuid, ) -> Result<SecureSelect<E, Scoped>, ScopeError>
Create a scoped select query filtered by a specific resource ID.
This is a convenience method that combines find() with .and_id().
§Example
let ctx = SecurityCtx::for_tenants(vec![tenant_id], user_id);
let user = db.find_by_id::<user::Entity>(&ctx, user_id)?
.one(db.conn())
.await?;§Errors
Returns ScopeError if the entity doesn’t have a resource column or scoping fails.
Sourcepub fn update_many<E>(&self, scope: &AccessScope) -> SecureUpdateMany<E, Scoped>
pub fn update_many<E>(&self, scope: &AccessScope) -> SecureUpdateMany<E, Scoped>
Create a scoped update query for the given entity.
Returns a SecureUpdateMany<E, Scoped> that automatically applies
tenant/resource filtering. Use .col_expr() or other SeaORM methods
to specify what to update.
§Example
let ctx = SecurityCtx::for_tenants(vec![tenant_id], user_id);
let result = db.update_many::<user::Entity>(&ctx)?
.col_expr(user::Column::Status, Expr::value("active"))
.col_expr(user::Column::UpdatedAt, Expr::value(Utc::now()))
.exec(db.conn())
.await?;
println!("Updated {} rows", result.rows_affected);Sourcepub fn delete_many<E>(&self, scope: &AccessScope) -> SecureDeleteMany<E, Scoped>
pub fn delete_many<E>(&self, scope: &AccessScope) -> SecureDeleteMany<E, Scoped>
Create a scoped delete query for the given entity.
Returns a SecureDeleteMany<E, Scoped> that automatically applies
tenant/resource filtering.
§Example
let ctx = SecurityCtx::for_tenants(vec![tenant_id], user_id);
let result = db.delete_many::<user::Entity>(&ctx)?
.exec(db.conn())
.await?;
println!("Deleted {} rows", result.rows_affected);Sourcepub async fn insert<E>(
&self,
scope: &AccessScope,
am: E::ActiveModel,
) -> Result<E::Model, ScopeError>where
E: ScopableEntity + EntityTrait,
E::Column: ColumnTrait + Copy,
E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
E::Model: IntoActiveModel<E::ActiveModel>,
pub async fn insert<E>(
&self,
scope: &AccessScope,
am: E::ActiveModel,
) -> Result<E::Model, ScopeError>where
E: ScopableEntity + EntityTrait,
E::Column: ColumnTrait + Copy,
E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
E::Model: IntoActiveModel<E::ActiveModel>,
Insert a new entity with automatic tenant validation.
This is a convenience wrapper around secure_insert() that uses
the provided security context.
§Example
let ctx = SecurityCtx::for_tenants(vec![tenant_id], user_id);
let am = user::ActiveModel {
id: Set(Uuid::new_v4()),
tenant_id: Set(tenant_id),
owner_id: Set(ctx.subject_id),
email: Set("user@example.com".to_string()),
..Default::default()
};
let user = db.insert::<user::Entity>(&ctx, am).await?;§Errors
ScopeError::Invalidif entity requires tenant but scope has noneScopeError::Dbif database insert fails
Sourcepub async fn update_one<E>(
&self,
am: E::ActiveModel,
) -> Result<E::Model, ScopeError>where
E: EntityTrait,
E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
E::Model: IntoActiveModel<E::ActiveModel>,
pub async fn update_one<E>(
&self,
am: E::ActiveModel,
) -> Result<E::Model, ScopeError>where
E: EntityTrait,
E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
E::Model: IntoActiveModel<E::ActiveModel>,
Update a single entity by ID (unscoped).
Warning: This method does NOT validate security scope.
Use update_with_ctx() for scope-validated updates.
This is a convenience method for the common pattern of updating one record when you’ve already validated access separately.
§Example
let mut user: user::ActiveModel = db.find_by_id::<user::Entity>(id)?
.one(db.conn())
.await?
.ok_or(NotFound)?
.into();
user.email = Set("newemail@example.com".to_string());
user.updated_at = Set(Utc::now());
let updated = db.update_one(user).await?;§Errors
Returns ScopeError::Db if the database update fails.
Sourcepub async fn update_with_ctx<E>(
&self,
scope: &AccessScope,
id: Uuid,
am: E::ActiveModel,
) -> Result<E::Model, ScopeError>where
E: ScopableEntity + EntityTrait,
E::Column: ColumnTrait + Copy,
E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
E::Model: IntoActiveModel<E::ActiveModel>,
pub async fn update_with_ctx<E>(
&self,
scope: &AccessScope,
id: Uuid,
am: E::ActiveModel,
) -> Result<E::Model, ScopeError>where
E: ScopableEntity + EntityTrait,
E::Column: ColumnTrait + Copy,
E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
E::Model: IntoActiveModel<E::ActiveModel>,
Update a single entity with security scope validation.
This method ensures the entity being updated is within the security scope before performing the update. It validates that the record is accessible based on tenant/resource constraints.
§Security
- Validates the entity exists and is accessible in the security scope
- Returns
ScopeError::Deniedif the entity is not in scope - Ensures updates cannot affect entities outside the security boundary
§Example
let ctx = SecurityCtx::for_tenant(tenant_id, user_id);
// Load and modify
let user_model = db.find_by_id::<user::Entity>(&ctx, id)?
.one(db.conn())
.await?
.ok_or(NotFound)?;
let mut user: user::ActiveModel = user_model.into();
user.email = Set("newemail@example.com".to_string());
user.updated_at = Set(Utc::now());
// Update with scope validation (pass ID separately)
let updated = db.update_with_ctx::<user::Entity>(&ctx, id, user).await?;§Errors
ScopeError::Deniedif the entity is not accessible in the current scopeScopeError::Dbif the database operation fails
Sourcepub async fn delete_by_id<E>(
&self,
scope: &AccessScope,
id: Uuid,
) -> Result<bool, ScopeError>
pub async fn delete_by_id<E>( &self, scope: &AccessScope, id: Uuid, ) -> Result<bool, ScopeError>
Delete a single entity by ID (scoped).
This validates the entity exists in scope before deleting.
§Example
let ctx = SecurityCtx::for_tenants(vec![tenant_id], user_id);
db.delete_by_id::<user::Entity>(&ctx, user_id).await?;§Returns
Ok(true)if entity was deletedOk(false)if entity not found in scope
§Errors
Returns ScopeError::Invalid if the entity does not have a resource_col defined.
Sourcepub async fn transaction<T, F>(&self, f: F) -> Result<T>
pub async fn transaction<T, F>(&self, f: F) -> Result<T>
Execute a closure inside a database transaction.
This method starts a SeaORM transaction, provides the transaction handle
to the closure as &dyn ConnectionTrait, and handles commit/rollback.
§Return Type
Returns anyhow::Result<Result<T, E>> where:
- Outer
Err: Database/infrastructure error (transaction rolls back) - Inner
Ok(T): Success (transaction commits) - Inner
Err(E): Domain/validation error (transaction still commits)
This design ensures domain validation errors don’t cause rollback.
§Architecture Note
Transaction boundaries should be managed by application/domain services, not by REST handlers. REST handlers should call service methods that internally decide when to open transactions.
§Example
use modkit_db::secure::SecureConn;
// In a domain service:
pub async fn create_user(
db: &SecureConn,
repo: &UsersRepo,
user: User,
) -> Result<User, DomainError> {
let result = db.transaction(|conn| async move {
// Check email uniqueness
if repo.email_exists(conn, &user.email).await? {
return Ok(Err(DomainError::EmailExists));
}
// Create user
let created = repo.create(conn, user).await?;
Ok(Ok(created))
}).await?;
result
}§Errors
Returns Err(anyhow::Error) if:
- The transaction cannot be started
- A database operation fails (transaction is rolled back)
- The commit fails
Sourcepub async fn transaction_with_config<T, F>(
&self,
cfg: TxConfig,
f: F,
) -> Result<T>
pub async fn transaction_with_config<T, F>( &self, cfg: TxConfig, f: F, ) -> Result<T>
Execute a closure inside a database transaction with custom configuration.
This method is similar to transaction, but allows
specifying the isolation level and access mode.
§Configuration
Use TxConfig to specify transaction settings without importing SeaORM types:
use modkit_db::secure::{TxConfig, TxIsolationLevel, TxAccessMode};
let cfg = TxConfig {
isolation: Some(TxIsolationLevel::Serializable),
access_mode: Some(TxAccessMode::ReadWrite),
};§Example
use modkit_db::secure::{SecureConn, TxConfig, TxIsolationLevel};
// In a domain service requiring serializable isolation:
pub async fn reconcile_accounts(
db: &SecureConn,
repo: &AccountsRepo,
) -> anyhow::Result<Result<ReconciliationResult, DomainError>> {
let cfg = TxConfig::serializable();
db.transaction_with_config(cfg, |conn| async move {
let accounts = repo.find_all_pending(conn).await?;
for account in accounts {
repo.reconcile(conn, &account).await?;
}
Ok(Ok(ReconciliationResult { processed: accounts.len() }))
}).await
}§Backend Notes
PostgreSQL: Full support for all isolation levels and access modes.- MySQL/InnoDB: Full support for all isolation levels and access modes.
SQLite: Only supportsSerializableisolation. Other levels are mapped toSerializable. Read-only mode is a hint only.
§Errors
Returns Err(anyhow::Error) if:
- The transaction cannot be started with the specified configuration
- A database operation fails (transaction is rolled back)
- The commit fails
Sourcepub async fn in_transaction<T, E, F>(&self, f: F) -> Result<T, TxError<E>>
pub async fn in_transaction<T, E, F>(&self, f: F) -> Result<T, TxError<E>>
Execute a closure inside a typed domain transaction.
This method returns TxError<E> which distinguishes domain errors from
infrastructure errors, allowing callers to handle them appropriately.
§Error Handling
- Domain errors returned from the closure are wrapped in
TxError::Domain(e) - Database infrastructure errors are wrapped in
TxError::Infra(InfraError)
Use TxError::into_domain to convert the result into your domain error type.
§Example
use modkit_db::secure::SecureConn;
async fn create_user(db: &SecureConn, repo: &UsersRepo, user: User) -> Result<User, DomainError> {
db.in_transaction(move |tx| Box::pin(async move {
if repo.exists(tx, user.id).await? {
return Err(DomainError::already_exists(user.id));
}
repo.create(tx, user).await
}))
.await
.map_err(|e| e.into_domain(DomainError::database_infra))
}§Errors
Returns Err(TxError<E>) if:
- The callback returns a domain error (
TxError::Domain(E)). - The transaction fails due to a database/infrastructure error (
TxError::Infra(InfraError)).
Sourcepub async fn in_transaction_mapped<T, E, F, M>(
&self,
map_infra: M,
f: F,
) -> Result<T, E>
pub async fn in_transaction_mapped<T, E, F, M>( &self, map_infra: M, f: F, ) -> Result<T, E>
Execute a typed domain transaction with automatic infrastructure error mapping.
This is a convenience wrapper around in_transaction that
automatically converts TxError into the domain error type using the provided
mapping function for infrastructure errors.
§Example
use modkit_db::secure::SecureConn;
async fn create_user(db: &SecureConn, repo: &UsersRepo, user: User) -> Result<User, DomainError> {
db.in_transaction_mapped(DomainError::database_infra, move |tx| Box::pin(async move {
if repo.exists(tx, user.id).await? {
return Err(DomainError::already_exists(user.id));
}
repo.create(tx, user).await
})).await
}§Errors
Returns Err(E) if:
- The callback returns a domain error (
E). - The transaction fails due to a database/infrastructure error, mapped via
map_infra.
Trait Implementations§
Source§impl Clone for SecureConn
impl Clone for SecureConn
Source§fn clone(&self) -> SecureConn
fn clone(&self) -> SecureConn
1.0.0 · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreAuto Trait Implementations§
impl Freeze for SecureConn
impl RefUnwindSafe for SecureConn
impl Send for SecureConn
impl Sync for SecureConn
impl Unpin for SecureConn
impl UnsafeUnpin for SecureConn
impl UnwindSafe for SecureConn
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> Paint for Twhere
T: ?Sized,
impl<T> Paint for Twhere
T: ?Sized,
Source§fn fg(&self, value: Color) -> Painted<&T>
fn fg(&self, value: Color) -> Painted<&T>
Returns a styled value derived from self with the foreground set to
value.
This method should be used rarely. Instead, prefer to use color-specific
builder methods like red() and
green(), which have the same functionality but are
pithier.
§Example
Set foreground color to white using fg():
use yansi::{Paint, Color};
painted.fg(Color::White);Set foreground color to white using white().
use yansi::Paint;
painted.white();Source§fn bright_black(&self) -> Painted<&T>
fn bright_black(&self) -> Painted<&T>
Source§fn bright_red(&self) -> Painted<&T>
fn bright_red(&self) -> Painted<&T>
Source§fn bright_green(&self) -> Painted<&T>
fn bright_green(&self) -> Painted<&T>
Source§fn bright_yellow(&self) -> Painted<&T>
fn bright_yellow(&self) -> Painted<&T>
Source§fn bright_blue(&self) -> Painted<&T>
fn bright_blue(&self) -> Painted<&T>
Source§fn bright_magenta(&self) -> Painted<&T>
fn bright_magenta(&self) -> Painted<&T>
Source§fn bright_cyan(&self) -> Painted<&T>
fn bright_cyan(&self) -> Painted<&T>
Source§fn bright_white(&self) -> Painted<&T>
fn bright_white(&self) -> Painted<&T>
Source§fn bg(&self, value: Color) -> Painted<&T>
fn bg(&self, value: Color) -> Painted<&T>
Returns a styled value derived from self with the background set to
value.
This method should be used rarely. Instead, prefer to use color-specific
builder methods like on_red() and
on_green(), which have the same functionality but
are pithier.
§Example
Set background color to red using fg():
use yansi::{Paint, Color};
painted.bg(Color::Red);Set background color to red using on_red().
use yansi::Paint;
painted.on_red();Source§fn on_primary(&self) -> Painted<&T>
fn on_primary(&self) -> Painted<&T>
Source§fn on_magenta(&self) -> Painted<&T>
fn on_magenta(&self) -> Painted<&T>
Source§fn on_bright_black(&self) -> Painted<&T>
fn on_bright_black(&self) -> Painted<&T>
Source§fn on_bright_red(&self) -> Painted<&T>
fn on_bright_red(&self) -> Painted<&T>
Source§fn on_bright_green(&self) -> Painted<&T>
fn on_bright_green(&self) -> Painted<&T>
Source§fn on_bright_yellow(&self) -> Painted<&T>
fn on_bright_yellow(&self) -> Painted<&T>
Source§fn on_bright_blue(&self) -> Painted<&T>
fn on_bright_blue(&self) -> Painted<&T>
Source§fn on_bright_magenta(&self) -> Painted<&T>
fn on_bright_magenta(&self) -> Painted<&T>
Source§fn on_bright_cyan(&self) -> Painted<&T>
fn on_bright_cyan(&self) -> Painted<&T>
Source§fn on_bright_white(&self) -> Painted<&T>
fn on_bright_white(&self) -> Painted<&T>
Source§fn attr(&self, value: Attribute) -> Painted<&T>
fn attr(&self, value: Attribute) -> Painted<&T>
Enables the styling Attribute value.
This method should be used rarely. Instead, prefer to use
attribute-specific builder methods like bold() and
underline(), which have the same functionality
but are pithier.
§Example
Make text bold using attr():
use yansi::{Paint, Attribute};
painted.attr(Attribute::Bold);Make text bold using using bold().
use yansi::Paint;
painted.bold();Source§fn rapid_blink(&self) -> Painted<&T>
fn rapid_blink(&self) -> Painted<&T>
Source§fn quirk(&self, value: Quirk) -> Painted<&T>
fn quirk(&self, value: Quirk) -> Painted<&T>
Enables the yansi Quirk value.
This method should be used rarely. Instead, prefer to use quirk-specific
builder methods like mask() and
wrap(), which have the same functionality but are
pithier.
§Example
Enable wrapping using .quirk():
use yansi::{Paint, Quirk};
painted.quirk(Quirk::Wrap);Enable wrapping using wrap().
use yansi::Paint;
painted.wrap();Source§fn clear(&self) -> Painted<&T>
👎Deprecated since 1.0.1: renamed to resetting() due to conflicts with Vec::clear().
The clear() method will be removed in a future release.
fn clear(&self) -> Painted<&T>
resetting() due to conflicts with Vec::clear().
The clear() method will be removed in a future release.Source§fn whenever(&self, value: Condition) -> Painted<&T>
fn whenever(&self, value: Condition) -> Painted<&T>
Conditionally enable styling based on whether the Condition value
applies. Replaces any previous condition.
See the crate level docs for more details.
§Example
Enable styling painted only when both stdout and stderr are TTYs:
use yansi::{Paint, Condition};
painted.red().on_yellow().whenever(Condition::STDOUTERR_ARE_TTY);