118 lines
4.9 KiB
Rust
118 lines
4.9 KiB
Rust
use crate::db::models::{AuditFilter, AuditLog};
|
|
use crate::db::repository::traits::AuditRepository;
|
|
use async_trait::async_trait;
|
|
use sqlx::PgPool;
|
|
|
|
pub struct PostgresAuditRepository {
|
|
pub pool: PgPool,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl AuditRepository for PostgresAuditRepository {
|
|
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)]
|
|
async fn insert(
|
|
&self,
|
|
id: &str,
|
|
actor_user_id: Option<&str>,
|
|
target_user_id: Option<&str>,
|
|
action: &str,
|
|
resource_type: &str,
|
|
resource_id: Option<&str>,
|
|
severity: &str,
|
|
ip_address: Option<&str>,
|
|
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
|
|
}
|
|
|
|
async fn list_recent(&self, limit: i64) -> Result<Vec<AuditLog>, sqlx::Error> {
|
|
sqlx::query_as::<_, AuditLog>("SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT $1")
|
|
.bind(limit)
|
|
.fetch_all(&self.pool)
|
|
.await
|
|
}
|
|
|
|
async fn list_filtered(&self, filter: &AuditFilter) -> Result<Vec<AuditLog>, sqlx::Error> {
|
|
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 = 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')))
|
|
"#;
|