feat: harden runtime lifecycle and application credentials

- enforce deterministic runtime lifecycle state transitions
- add live graceful-to-forced shutdown escalation
- align HTTP draining and worker shutdown with global deadline
- guarantee deterministic shutdown hook ordering
- add secure application client IDs and one-time client secrets
- hash application secrets with BLAKE3 and constant-time verification
- make credential creation and rotation transactionally auditable
- enforce strict client_id authentication and redirect URI validation
- add SQLite and PostgreSQL credential migrations
- add application credential and runtime lifecycle acceptance tests
- update Dioxus application management workflows
- update security and architecture documentation
This commit is contained in:
thakares committed 2026-07-23 15:17:30 +05:30
1 parent 4c697e9adf
commit dc5417334b
26 files changed
+2477 -183

No files matched your search

+135 -17
View File
@@ -10,37 +10,87 @@ pub struct SqliteApplicationsRepository {
#[async_trait]
impl ApplicationsRepository for SqliteApplicationsRepository {
async fn create(
async fn create_with_audit(
&self,
id: &str,
tenant_id: &str,
name: &str,
slug: &str,
client_id: &str,
client_secret_hash: Option<&str>,
description: Option<&str>,
redirect_uris: Option<&str>,
scopes: Option<&str>,
audit_event: Option<crate::audit::AuditEvent<'_>>,
) -> Result<Application, sqlx::Error> {
sqlx::query_as::<_, Application>(
let mut tx = self.pool.begin().await?;
let app = sqlx::query_as::<_, Application>(
r#"
INSERT INTO applications (id, tenant_id, name, slug)
VALUES (?, ?, ?, ?)
RETURNING id, tenant_id, name, slug, enabled, created_at, updated_at, NULL as description, NULL as client_secret_hash, NULL as redirect_uris
INSERT INTO applications (id, tenant_id, name, slug, client_id, client_secret_hash, description, redirect_uris, scopes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id, tenant_id, name, slug, client_id, description, enabled, client_secret_hash, redirect_uris, scopes, created_at, updated_at
"#,
)
.bind(id)
.bind(tenant_id)
.bind(name)
.bind(slug)
.fetch_one(&self.pool)
.await
.bind(client_id)
.bind(client_secret_hash)
.bind(description)
.bind(redirect_uris)
.bind(scopes)
.fetch_one(&mut *tx)
.await?;
if let Some(event) = audit_event {
let audit_id = uuid::Uuid::new_v4().to_string();
let severity_str = event.severity.to_string();
sqlx::query(
r#"
INSERT INTO audit_logs (
id, actor_user_id, target_user_id,
action, resource_type, resource_id,
severity, ip_address, user_agent, metadata_json
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"#,
)
.bind(&audit_id)
.bind(event.actor_id)
.bind(event.target_id)
.bind(event.action)
.bind(event.resource_type)
.bind(event.resource_id)
.bind(&severity_str)
.bind(event.ip)
.bind(event.ua)
.bind(event.metadata)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(app)
}
async fn find_by_slug(&self, slug: &str) -> Result<Option<Application>, sqlx::Error> {
sqlx::query_as::<_, Application>("SELECT id, tenant_id, name, slug, enabled, created_at, updated_at, NULL as description, NULL as client_secret_hash, NULL as redirect_uris FROM applications WHERE slug = ?")
sqlx::query_as::<_, Application>("SELECT id, tenant_id, name, slug, client_id, description, enabled, client_secret_hash, redirect_uris, scopes, created_at, updated_at FROM applications WHERE slug = ?")
.bind(slug)
.fetch_optional(&self.pool)
.await
}
async fn find_by_client_id(&self, client_id: &str) -> Result<Option<Application>, sqlx::Error> {
sqlx::query_as::<_, Application>("SELECT id, tenant_id, name, slug, client_id, description, enabled, client_secret_hash, redirect_uris, scopes, created_at, updated_at FROM applications WHERE client_id = ?")
.bind(client_id)
.fetch_optional(&self.pool)
.await
}
async fn find_by_id(&self, id: &str) -> Result<Option<Application>, sqlx::Error> {
sqlx::query_as::<_, Application>("SELECT id, tenant_id, name, slug, enabled, created_at, updated_at, NULL as description, NULL as client_secret_hash, NULL as redirect_uris FROM applications WHERE id = ?")
sqlx::query_as::<_, Application>("SELECT id, tenant_id, name, slug, client_id, description, enabled, client_secret_hash, redirect_uris, scopes, created_at, updated_at FROM applications WHERE id = ?")
.bind(id)
.fetch_optional(&self.pool)
.await
@@ -48,7 +98,7 @@ impl ApplicationsRepository for SqliteApplicationsRepository {
async fn list(&self, tenant_id: &str) -> Result<Vec<Application>, sqlx::Error> {
sqlx::query_as::<_, Application>(
"SELECT id, tenant_id, name, slug, enabled, created_at, updated_at, NULL as description, NULL as client_secret_hash, NULL as redirect_uris FROM applications WHERE tenant_id = ? ORDER BY name",
"SELECT id, tenant_id, name, slug, client_id, description, enabled, client_secret_hash, redirect_uris, scopes, created_at, updated_at FROM applications WHERE tenant_id = ? ORDER BY name",
)
.bind(tenant_id)
.fetch_all(&self.pool)
@@ -57,12 +107,74 @@ impl ApplicationsRepository for SqliteApplicationsRepository {
async fn set_enabled(&self, id: &str, enabled: bool) -> Result<(), sqlx::Error> {
sqlx::query(
"UPDATE applications SET enabled = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?",
)
.bind(enabled)
.bind(id)
.execute(&self.pool)
.await?;
"UPDATE applications SET enabled = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?",
)
.bind(enabled)
.bind(id)
.execute(&self.pool)
.await?;
Ok(())
}
async fn update_secret_hash(&self, id: &str, secret_hash: &str) -> Result<(), sqlx::Error> {
sqlx::query(
"UPDATE applications SET client_secret_hash = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?",
)
.bind(secret_hash)
.bind(id)
.execute(&self.pool)
.await?;
Ok(())
}
async fn rotate_secret_with_audit(
&self,
id: &str,
secret_hash: &str,
audit_event: Option<crate::audit::AuditEvent<'_>>,
) -> Result<(), sqlx::Error> {
let mut tx = self.pool.begin().await?;
let res = sqlx::query(
"UPDATE applications SET client_secret_hash = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?",
)
.bind(secret_hash)
.bind(id)
.execute(&mut *tx)
.await?;
if res.rows_affected() == 0 {
return Err(sqlx::Error::RowNotFound);
}
if let Some(event) = audit_event {
let audit_id = uuid::Uuid::new_v4().to_string();
let severity_str = event.severity.to_string();
sqlx::query(
r#"
INSERT INTO audit_logs (
id, actor_user_id, target_user_id,
action, resource_type, resource_id,
severity, ip_address, user_agent, metadata_json
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"#,
)
.bind(&audit_id)
.bind(event.actor_id)
.bind(event.target_id)
.bind(event.action)
.bind(event.resource_type)
.bind(event.resource_id)
.bind(&severity_str)
.bind(event.ip)
.bind(event.ua)
.bind(event.metadata)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(())
}
@@ -71,18 +183,24 @@ impl ApplicationsRepository for SqliteApplicationsRepository {
id: &str,
name: &str,
slug: &str,
description: Option<&str>,
redirect_uris: Option<&str>,
scopes: Option<&str>,
enabled: bool,
) -> Result<(), sqlx::Error> {
sqlx::query(
r#"
UPDATE applications
SET name = ?, slug = ?, enabled = ?,
SET name = ?, slug = ?, description = ?, redirect_uris = ?, scopes = ?, enabled = ?,
updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
WHERE id = ?
"#,
)
.bind(name)
.bind(slug)
.bind(description)
.bind(redirect_uris)
.bind(scopes)
.bind(enabled)
.bind(id)
.execute(&self.pool)