refactor: harden audit filtering and management UI
This commit is contained in:
1 parent
a969f9c571
commit
3d14061795
38 files changed
+507
-440
No files matched your search
@@ -56,6 +56,7 @@ pub struct AuditQuery {
|
||||
pub actor: Option<String>,
|
||||
pub action: Option<String>,
|
||||
pub resource_type: Option<String>,
|
||||
pub resource_id: Option<String>,
|
||||
pub severity: Option<String>,
|
||||
pub since: Option<String>,
|
||||
pub until: Option<String>,
|
||||
@@ -80,6 +81,7 @@ pub async fn list_audit(
|
||||
actor_user_id: query.actor,
|
||||
action: query.action,
|
||||
resource_type: query.resource_type,
|
||||
resource_id: query.resource_id,
|
||||
severity: query.severity,
|
||||
since: query.since,
|
||||
until: query.until,
|
||||
@@ -124,6 +126,7 @@ pub async fn export_audit(
|
||||
actor_user_id: query.actor,
|
||||
action: query.action,
|
||||
resource_type: query.resource_type,
|
||||
resource_id: query.resource_id,
|
||||
severity: query.severity,
|
||||
since: query.since,
|
||||
until: query.until,
|
||||
|
||||
+10
-10
@@ -52,16 +52,6 @@ fn is_static_asset(path: &str) -> bool {
|
||||
|
||||
/// Serve a static file from the UI dist dir, or SPA fallback for app routes.
|
||||
pub async fn serve_ui(uri: Uri) -> Response {
|
||||
let dist = ui_dist_dir();
|
||||
if !dist.exists() {
|
||||
return missing_ui_page().into_response();
|
||||
}
|
||||
|
||||
let path = uri.path().trim_start_matches('/');
|
||||
if path.starts_with("api/") || path == "health" || path == "version" {
|
||||
return StatusCode::NOT_FOUND.into_response();
|
||||
}
|
||||
|
||||
// Security Hardening: Reject & sanitize any GET request containing credentials in query string.
|
||||
if let Some(query) = uri.query() {
|
||||
let q_lower = query.to_ascii_lowercase();
|
||||
@@ -84,6 +74,16 @@ pub async fn serve_ui(uri: Uri) -> Response {
|
||||
}
|
||||
}
|
||||
|
||||
let dist = ui_dist_dir();
|
||||
if !dist.exists() {
|
||||
return missing_ui_page().into_response();
|
||||
}
|
||||
|
||||
let path = uri.path().trim_start_matches('/');
|
||||
if path.starts_with("api/") || path == "health" || path == "version" {
|
||||
return StatusCode::NOT_FOUND.into_response();
|
||||
}
|
||||
|
||||
// Normalize and reject path traversal
|
||||
if path.contains("..") {
|
||||
return StatusCode::BAD_REQUEST.into_response();
|
||||
|
||||
@@ -44,6 +44,7 @@ pub struct AuditFilter {
|
||||
pub actor_user_id: Option<String>,
|
||||
pub action: Option<String>,
|
||||
pub resource_type: Option<String>,
|
||||
pub resource_id: Option<String>,
|
||||
pub severity: Option<String>,
|
||||
pub since: Option<String>,
|
||||
pub until: Option<String>,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::db::models::{AuditFilter, AuditLog};
|
||||
use crate::db::repository::traits::AuditRepository;
|
||||
use async_trait::async_trait;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::db::models::{AuditFilter, AuditLog};
|
||||
|
||||
pub struct PostgresAuditRepository {
|
||||
pub pool: PgPool,
|
||||
}
|
||||
@@ -31,29 +30,13 @@ impl AuditRepository for PostgresAuditRepository {
|
||||
user_agent: Option<&str>,
|
||||
metadata_json: Option<&str>,
|
||||
) -> Result<AuditLog, sqlx::Error> {
|
||||
sqlx::query_as::<_, AuditLog>(
|
||||
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 ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.bind(actor_user_id)
|
||||
.bind(target_user_id)
|
||||
.bind(action)
|
||||
.bind(resource_type)
|
||||
.bind(resource_id)
|
||||
.bind(severity)
|
||||
.bind(ip_address)
|
||||
.bind(user_agent)
|
||||
.bind(metadata_json)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
sqlx::query_as::<_, AuditLog>(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 ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *
|
||||
"#)
|
||||
.bind(id).bind(actor_user_id).bind(target_user_id).bind(action).bind(resource_type)
|
||||
.bind(resource_id).bind(severity).bind(ip_address).bind(user_agent).bind(metadata_json)
|
||||
.fetch_one(&self.pool).await
|
||||
}
|
||||
|
||||
async fn list_recent(&self, limit: i64) -> Result<Vec<AuditLog>, sqlx::Error> {
|
||||
@@ -64,91 +47,71 @@ impl AuditRepository for PostgresAuditRepository {
|
||||
}
|
||||
|
||||
async fn list_filtered(&self, filter: &AuditFilter) -> Result<Vec<AuditLog>, sqlx::Error> {
|
||||
let search_like = filter
|
||||
.search
|
||||
.as_ref()
|
||||
.map(|s| format!("%{}%", s.replace('%', "\\%")));
|
||||
|
||||
sqlx::query_as::<_, AuditLog>(
|
||||
r#"
|
||||
SELECT * FROM audit_logs
|
||||
WHERE ($1::text IS NULL OR actor_user_id = $1)
|
||||
AND ($2::text IS NULL OR action = $2)
|
||||
AND ($3::text IS NULL OR resource_type = $3)
|
||||
AND ($4::text IS NULL OR severity = $4)
|
||||
AND ($5::text IS NULL OR created_at >= $5)
|
||||
AND ($6::text IS NULL OR created_at <= $6)
|
||||
AND (
|
||||
$7::text IS NULL
|
||||
OR action LIKE $7 ESCAPE '\'
|
||||
OR resource_type LIKE $7 ESCAPE '\'
|
||||
OR resource_id LIKE $7 ESCAPE '\'
|
||||
OR ip_address LIKE $7 ESCAPE '\'
|
||||
OR metadata_json LIKE $7 ESCAPE '\'
|
||||
)
|
||||
AND (
|
||||
$8::boolean IS NULL
|
||||
OR ($8::boolean = TRUE AND action NOT LIKE '%fail%' AND action NOT LIKE '%denied%' AND severity != 'critical')
|
||||
OR ($8::boolean = FALSE AND (action LIKE '%fail%' OR action LIKE '%denied%' OR severity = 'critical'))
|
||||
)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $9 OFFSET $10
|
||||
"#,
|
||||
)
|
||||
.bind(filter.actor_user_id.as_deref())
|
||||
.bind(filter.action.as_deref())
|
||||
.bind(filter.resource_type.as_deref())
|
||||
.bind(filter.severity.as_deref())
|
||||
.bind(filter.since.as_deref())
|
||||
.bind(filter.until.as_deref())
|
||||
.bind(search_like.as_deref())
|
||||
.bind(filter.success)
|
||||
.bind(filter.limit)
|
||||
.bind(filter.offset)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
let search_like = search_like(filter);
|
||||
sqlx::query_as::<_, AuditLog>(FILTER_LIST_SQL)
|
||||
.bind(filter.actor_user_id.as_deref())
|
||||
.bind(filter.action.as_deref())
|
||||
.bind(filter.resource_type.as_deref())
|
||||
.bind(filter.resource_id.as_deref())
|
||||
.bind(filter.severity.as_deref())
|
||||
.bind(filter.since.as_deref())
|
||||
.bind(filter.until.as_deref())
|
||||
.bind(search_like.as_deref())
|
||||
.bind(filter.success)
|
||||
.bind(filter.limit)
|
||||
.bind(filter.offset)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn count_filtered(&self, filter: &AuditFilter) -> Result<i64, sqlx::Error> {
|
||||
let search_like = filter
|
||||
.search
|
||||
.as_ref()
|
||||
.map(|s| format!("%{}%", s.replace('%', "\\%")));
|
||||
|
||||
let row: (i64,) = sqlx::query_as(
|
||||
r#"
|
||||
SELECT COUNT(*) FROM audit_logs
|
||||
WHERE ($1::text IS NULL OR actor_user_id = $1)
|
||||
AND ($2::text IS NULL OR action = $2)
|
||||
AND ($3::text IS NULL OR resource_type = $3)
|
||||
AND ($4::text IS NULL OR severity = $4)
|
||||
AND ($5::text IS NULL OR created_at >= $5)
|
||||
AND ($6::text IS NULL OR created_at <= $6)
|
||||
AND (
|
||||
$7::text IS NULL
|
||||
OR action LIKE $7 ESCAPE '\'
|
||||
OR resource_type LIKE $7 ESCAPE '\'
|
||||
OR resource_id LIKE $7 ESCAPE '\'
|
||||
OR ip_address LIKE $7 ESCAPE '\'
|
||||
OR metadata_json LIKE $7 ESCAPE '\'
|
||||
)
|
||||
AND (
|
||||
$8::boolean IS NULL
|
||||
OR ($8::boolean = TRUE AND action NOT LIKE '%fail%' AND action NOT LIKE '%denied%' AND severity != 'critical')
|
||||
OR ($8::boolean = FALSE AND (action LIKE '%fail%' OR action LIKE '%denied%' OR severity = 'critical'))
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(filter.actor_user_id.as_deref())
|
||||
.bind(filter.action.as_deref())
|
||||
.bind(filter.resource_type.as_deref())
|
||||
.bind(filter.severity.as_deref())
|
||||
.bind(filter.since.as_deref())
|
||||
.bind(filter.until.as_deref())
|
||||
.bind(search_like.as_deref())
|
||||
.bind(filter.success)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
let search_like = search_like(filter);
|
||||
let row: (i64,) = sqlx::query_as(FILTER_COUNT_SQL)
|
||||
.bind(filter.actor_user_id.as_deref())
|
||||
.bind(filter.action.as_deref())
|
||||
.bind(filter.resource_type.as_deref())
|
||||
.bind(filter.resource_id.as_deref())
|
||||
.bind(filter.severity.as_deref())
|
||||
.bind(filter.since.as_deref())
|
||||
.bind(filter.until.as_deref())
|
||||
.bind(search_like.as_deref())
|
||||
.bind(filter.success)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
Ok(row.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn search_like(filter: &AuditFilter) -> Option<String> {
|
||||
filter
|
||||
.search
|
||||
.as_ref()
|
||||
.map(|s| format!("%{}%", s.replace('%', "\\%")))
|
||||
}
|
||||
|
||||
const FILTER_LIST_SQL: &str = r#"
|
||||
SELECT * FROM audit_logs
|
||||
WHERE ($1::text IS NULL OR actor_user_id = $1)
|
||||
AND ($2::text IS NULL OR action = $2)
|
||||
AND ($3::text IS NULL OR resource_type = $3)
|
||||
AND ($4::text IS NULL OR resource_id = $4)
|
||||
AND ($5::text IS NULL OR severity = $5)
|
||||
AND ($6::text IS NULL OR created_at >= $6)
|
||||
AND ($7::text IS NULL OR created_at <= $7)
|
||||
AND ($8::text IS NULL OR action LIKE $8 ESCAPE '\' OR resource_type LIKE $8 ESCAPE '\' OR resource_id LIKE $8 ESCAPE '\' OR ip_address LIKE $8 ESCAPE '\' OR metadata_json LIKE $8 ESCAPE '\')
|
||||
AND ($9::boolean IS NULL OR ($9::boolean = TRUE AND action NOT LIKE '%fail%' AND action NOT LIKE '%denied%' AND severity != 'critical') OR ($9::boolean = FALSE AND (action LIKE '%fail%' OR action LIKE '%denied%' OR severity = 'critical')))
|
||||
ORDER BY created_at DESC LIMIT $10 OFFSET $11
|
||||
"#;
|
||||
|
||||
const FILTER_COUNT_SQL: &str = r#"
|
||||
SELECT COUNT(*) FROM audit_logs
|
||||
WHERE ($1::text IS NULL OR actor_user_id = $1)
|
||||
AND ($2::text IS NULL OR action = $2)
|
||||
AND ($3::text IS NULL OR resource_type = $3)
|
||||
AND ($4::text IS NULL OR resource_id = $4)
|
||||
AND ($5::text IS NULL OR severity = $5)
|
||||
AND ($6::text IS NULL OR created_at >= $6)
|
||||
AND ($7::text IS NULL OR created_at <= $7)
|
||||
AND ($8::text IS NULL OR action LIKE $8 ESCAPE '\' OR resource_type LIKE $8 ESCAPE '\' OR resource_id LIKE $8 ESCAPE '\' OR ip_address LIKE $8 ESCAPE '\' OR metadata_json LIKE $8 ESCAPE '\')
|
||||
AND ($9::boolean IS NULL OR ($9::boolean = TRUE AND action NOT LIKE '%fail%' AND action NOT LIKE '%denied%' AND severity != 'critical') OR ($9::boolean = FALSE AND (action LIKE '%fail%' OR action LIKE '%denied%' OR severity = 'critical')))
|
||||
"#;
|
||||
@@ -98,17 +98,6 @@ impl UsersRepository for PostgresUsersRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_user_tenant(&self, id: &str, tenant_id: &str) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"UPDATE users SET tenant_id = $1, updated_at = to_char(clock_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') WHERE id = $2",
|
||||
)
|
||||
.bind(tenant_id)
|
||||
.bind(id)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn reassign_user_tenant_with_audit(
|
||||
&self,
|
||||
user_id: &str,
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
use crate::db::models::{AuditFilter, AuditLog};
|
||||
use crate::db::repository::traits::AuditRepository;
|
||||
use async_trait::async_trait;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::db::models::{AuditFilter, AuditLog};
|
||||
|
||||
pub struct SqliteAuditRepository {
|
||||
pub pool: SqlitePool,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AuditRepository for SqliteAuditRepository {
|
||||
/// Count all audit log entries.
|
||||
async fn count(&self) -> Result<i64, sqlx::Error> {
|
||||
let row: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM audit_logs")
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
Ok(row.0)
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn insert(
|
||||
&self,
|
||||
@@ -32,29 +30,13 @@ impl AuditRepository for SqliteAuditRepository {
|
||||
user_agent: Option<&str>,
|
||||
metadata_json: Option<&str>,
|
||||
) -> Result<AuditLog, sqlx::Error> {
|
||||
sqlx::query_as::<_, AuditLog>(
|
||||
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.bind(actor_user_id)
|
||||
.bind(target_user_id)
|
||||
.bind(action)
|
||||
.bind(resource_type)
|
||||
.bind(resource_id)
|
||||
.bind(severity)
|
||||
.bind(ip_address)
|
||||
.bind(user_agent)
|
||||
.bind(metadata_json)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
sqlx::query_as::<_, AuditLog>(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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *
|
||||
"#)
|
||||
.bind(id).bind(actor_user_id).bind(target_user_id).bind(action).bind(resource_type)
|
||||
.bind(resource_id).bind(severity).bind(ip_address).bind(user_agent).bind(metadata_json)
|
||||
.fetch_one(&self.pool).await
|
||||
}
|
||||
|
||||
async fn list_recent(&self, limit: i64) -> Result<Vec<AuditLog>, sqlx::Error> {
|
||||
@@ -65,41 +47,46 @@ impl AuditRepository for SqliteAuditRepository {
|
||||
}
|
||||
|
||||
async fn list_filtered(&self, filter: &AuditFilter) -> Result<Vec<AuditLog>, sqlx::Error> {
|
||||
let search_like = filter
|
||||
.search
|
||||
.as_ref()
|
||||
.map(|s| format!("%{}%", s.replace('%', "\\%")));
|
||||
let success_val = filter.success.map(|b| if b { 1i32 } else { 0i32 });
|
||||
list_filtered(&self.pool, filter).await
|
||||
}
|
||||
|
||||
sqlx::query_as::<_, AuditLog>(
|
||||
r#"
|
||||
SELECT * FROM audit_logs
|
||||
WHERE (?1 IS NULL OR actor_user_id = ?1)
|
||||
AND (?2 IS NULL OR action = ?2)
|
||||
AND (?3 IS NULL OR resource_type = ?3)
|
||||
AND (?4 IS NULL OR severity = ?4)
|
||||
AND (?5 IS NULL OR created_at >= ?5)
|
||||
AND (?6 IS NULL OR created_at <= ?6)
|
||||
AND (
|
||||
?7 IS NULL
|
||||
OR action LIKE ?7 ESCAPE '\'
|
||||
OR resource_type LIKE ?7 ESCAPE '\'
|
||||
OR resource_id LIKE ?7 ESCAPE '\'
|
||||
OR ip_address LIKE ?7 ESCAPE '\'
|
||||
OR metadata_json LIKE ?7 ESCAPE '\'
|
||||
)
|
||||
AND (
|
||||
?8 IS NULL
|
||||
OR (?8 = 1 AND action NOT LIKE '%fail%' AND action NOT LIKE '%denied%' AND severity != 'critical')
|
||||
OR (?8 = 0 AND (action LIKE '%fail%' OR action LIKE '%denied%' OR severity = 'critical'))
|
||||
)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?9 OFFSET ?10
|
||||
"#,
|
||||
)
|
||||
async fn count_filtered(&self, filter: &AuditFilter) -> Result<i64, sqlx::Error> {
|
||||
let search_like = search_like(filter);
|
||||
let success_val = filter.success.map(|b| if b { 1i32 } else { 0i32 });
|
||||
let row: (i64,) = sqlx::query_as(FILTER_COUNT_SQL)
|
||||
.bind(filter.actor_user_id.as_deref())
|
||||
.bind(filter.action.as_deref())
|
||||
.bind(filter.resource_type.as_deref())
|
||||
.bind(filter.resource_id.as_deref())
|
||||
.bind(filter.severity.as_deref())
|
||||
.bind(filter.since.as_deref())
|
||||
.bind(filter.until.as_deref())
|
||||
.bind(search_like.as_deref())
|
||||
.bind(success_val)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
Ok(row.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn search_like(filter: &AuditFilter) -> Option<String> {
|
||||
filter
|
||||
.search
|
||||
.as_ref()
|
||||
.map(|s| format!("%{}%", s.replace('%', "\\%")))
|
||||
}
|
||||
|
||||
async fn list_filtered(
|
||||
pool: &SqlitePool,
|
||||
filter: &AuditFilter,
|
||||
) -> Result<Vec<AuditLog>, sqlx::Error> {
|
||||
let search_like = search_like(filter);
|
||||
let success_val = filter.success.map(|b| if b { 1i32 } else { 0i32 });
|
||||
sqlx::query_as::<_, AuditLog>(FILTER_LIST_SQL)
|
||||
.bind(filter.actor_user_id.as_deref())
|
||||
.bind(filter.action.as_deref())
|
||||
.bind(filter.resource_type.as_deref())
|
||||
.bind(filter.resource_id.as_deref())
|
||||
.bind(filter.severity.as_deref())
|
||||
.bind(filter.since.as_deref())
|
||||
.bind(filter.until.as_deref())
|
||||
@@ -107,51 +94,33 @@ impl AuditRepository for SqliteAuditRepository {
|
||||
.bind(success_val)
|
||||
.bind(filter.limit)
|
||||
.bind(filter.offset)
|
||||
.fetch_all(&self.pool)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn count_filtered(&self, filter: &AuditFilter) -> Result<i64, sqlx::Error> {
|
||||
let search_like = filter
|
||||
.search
|
||||
.as_ref()
|
||||
.map(|s| format!("%{}%", s.replace('%', "\\%")));
|
||||
let success_val = filter.success.map(|b| if b { 1i32 } else { 0i32 });
|
||||
|
||||
let row: (i64,) = sqlx::query_as(
|
||||
r#"
|
||||
SELECT COUNT(*) FROM audit_logs
|
||||
WHERE (?1 IS NULL OR actor_user_id = ?1)
|
||||
AND (?2 IS NULL OR action = ?2)
|
||||
AND (?3 IS NULL OR resource_type = ?3)
|
||||
AND (?4 IS NULL OR severity = ?4)
|
||||
AND (?5 IS NULL OR created_at >= ?5)
|
||||
AND (?6 IS NULL OR created_at <= ?6)
|
||||
AND (
|
||||
?7 IS NULL
|
||||
OR action LIKE ?7 ESCAPE '\'
|
||||
OR resource_type LIKE ?7 ESCAPE '\'
|
||||
OR resource_id LIKE ?7 ESCAPE '\'
|
||||
OR ip_address LIKE ?7 ESCAPE '\'
|
||||
OR metadata_json LIKE ?7 ESCAPE '\'
|
||||
)
|
||||
AND (
|
||||
?8 IS NULL
|
||||
OR (?8 = 1 AND action NOT LIKE '%fail%' AND action NOT LIKE '%denied%' AND severity != 'critical')
|
||||
OR (?8 = 0 AND (action LIKE '%fail%' OR action LIKE '%denied%' OR severity = 'critical'))
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(filter.actor_user_id.as_deref())
|
||||
.bind(filter.action.as_deref())
|
||||
.bind(filter.resource_type.as_deref())
|
||||
.bind(filter.severity.as_deref())
|
||||
.bind(filter.since.as_deref())
|
||||
.bind(filter.until.as_deref())
|
||||
.bind(search_like.as_deref())
|
||||
.bind(success_val)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
Ok(row.0)
|
||||
}
|
||||
}
|
||||
|
||||
const FILTER_LIST_SQL: &str = r#"
|
||||
SELECT * FROM audit_logs
|
||||
WHERE (?1 IS NULL OR actor_user_id = ?1)
|
||||
AND (?2 IS NULL OR action = ?2)
|
||||
AND (?3 IS NULL OR resource_type = ?3)
|
||||
AND (?4 IS NULL OR resource_id = ?4)
|
||||
AND (?5 IS NULL OR severity = ?5)
|
||||
AND (?6 IS NULL OR created_at >= ?6)
|
||||
AND (?7 IS NULL OR created_at <= ?7)
|
||||
AND (?8 IS NULL OR action LIKE ?8 ESCAPE '\' OR resource_type LIKE ?8 ESCAPE '\' OR resource_id LIKE ?8 ESCAPE '\' OR ip_address LIKE ?8 ESCAPE '\' OR metadata_json LIKE ?8 ESCAPE '\')
|
||||
AND (?9 IS NULL OR (?9 = 1 AND action NOT LIKE '%fail%' AND action NOT LIKE '%denied%' AND severity != 'critical') OR (?9 = 0 AND (action LIKE '%fail%' OR action LIKE '%denied%' OR severity = 'critical')))
|
||||
ORDER BY created_at DESC LIMIT ?10 OFFSET ?11
|
||||
"#;
|
||||
|
||||
const FILTER_COUNT_SQL: &str = r#"
|
||||
SELECT COUNT(*) FROM audit_logs
|
||||
WHERE (?1 IS NULL OR actor_user_id = ?1)
|
||||
AND (?2 IS NULL OR action = ?2)
|
||||
AND (?3 IS NULL OR resource_type = ?3)
|
||||
AND (?4 IS NULL OR resource_id = ?4)
|
||||
AND (?5 IS NULL OR severity = ?5)
|
||||
AND (?6 IS NULL OR created_at >= ?6)
|
||||
AND (?7 IS NULL OR created_at <= ?7)
|
||||
AND (?8 IS NULL OR action LIKE ?8 ESCAPE '\' OR resource_type LIKE ?8 ESCAPE '\' OR resource_id LIKE ?8 ESCAPE '\' OR ip_address LIKE ?8 ESCAPE '\' OR metadata_json LIKE ?8 ESCAPE '\')
|
||||
AND (?9 IS NULL OR (?9 = 1 AND action NOT LIKE '%fail%' AND action NOT LIKE '%denied%' AND severity != 'critical') OR (?9 = 0 AND (action LIKE '%fail%' OR action LIKE '%denied%' OR severity = 'critical')))
|
||||
"#;
|
||||
@@ -100,17 +100,6 @@ impl UsersRepository for SqliteUsersRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_user_tenant(&self, id: &str, tenant_id: &str) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"UPDATE users SET tenant_id = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?",
|
||||
)
|
||||
.bind(tenant_id)
|
||||
.bind(id)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn reassign_user_tenant_with_audit(
|
||||
&self,
|
||||
user_id: &str,
|
||||
|
||||
@@ -21,7 +21,6 @@ pub trait UsersRepository: Send + Sync {
|
||||
password_hash: &str,
|
||||
) -> Result<User, sqlx::Error>;
|
||||
async fn update_status(&self, id: &str, status: i32) -> Result<(), sqlx::Error>;
|
||||
async fn update_user_tenant(&self, id: &str, tenant_id: &str) -> Result<(), sqlx::Error>;
|
||||
async fn reassign_user_tenant_with_audit(
|
||||
&self,
|
||||
user_id: &str,
|
||||
|
||||
Reference in new issue
Block a user