mirror of
https://github.com/stoatchat/stoatchat.git
synced 2026-08-30 23:07:27 +00:00
fix: select specific audit log entries to fix nondeterministic sorting of audited events submitted too quickly Signed-off-by: Assisting <erik@eriklabine.com>
237 lines
7.3 KiB
Rust
237 lines
7.3 KiB
Rust
use revolt_database::{
|
|
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
|
AuditLogEntry, AuditLogQuery, Database, User,
|
|
};
|
|
use revolt_models::v0;
|
|
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
|
|
use revolt_result::{create_error, Result};
|
|
use rocket::{serde::json::Json, State};
|
|
use validator::Validate;
|
|
|
|
/// # Audit Log Query
|
|
///
|
|
/// Queries a server's audit logs.
|
|
#[openapi(tag = "Audit Logs")]
|
|
#[get("/<target>/audit_logs?<options..>")]
|
|
pub async fn query(
|
|
db: &State<Database>,
|
|
user: User,
|
|
target: Reference<'_>,
|
|
options: v0::OptionsAuditLogQuery,
|
|
) -> Result<Json<v0::AuditLogQueryResponse>> {
|
|
options.validate().map_err(|error| {
|
|
create_error!(FailedValidation {
|
|
error: error.to_string()
|
|
})
|
|
})?;
|
|
|
|
let server = target.as_server(db).await?;
|
|
|
|
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
|
|
calculate_server_permissions(&mut query)
|
|
.await
|
|
.throw_if_lacking_channel_permission(ChannelPermission::ViewAuditLogs)?;
|
|
|
|
let v0::OptionsAuditLogQuery {
|
|
user: user_filter,
|
|
target,
|
|
r#type,
|
|
before,
|
|
after,
|
|
limit,
|
|
} = options;
|
|
|
|
let audit_logs = db
|
|
.get_server_audit_logs(
|
|
&server.id,
|
|
AuditLogQuery {
|
|
user: user_filter,
|
|
target,
|
|
r#type,
|
|
before,
|
|
after,
|
|
limit: limit.unwrap_or(50),
|
|
},
|
|
)
|
|
.await?;
|
|
|
|
let (users, members) = AuditLogEntry::with_users(db, &server.id, &user, &audit_logs).await?;
|
|
|
|
Ok(Json(v0::AuditLogQueryResponse {
|
|
audit_logs: audit_logs.into_iter().map(Into::into).collect(),
|
|
users,
|
|
members,
|
|
}))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use revolt_database::{Member, Server};
|
|
use revolt_models::v0;
|
|
use rocket::http::{Header, Status};
|
|
|
|
use crate::util::test::TestHarness;
|
|
|
|
#[rocket::async_test]
|
|
async fn audit_log_query() {
|
|
let harness = TestHarness::new().await;
|
|
let (_, session, user) = harness.new_user().await;
|
|
let (server, channels) = Server::create(
|
|
&harness.db,
|
|
v0::DataCreateServer {
|
|
name: "Test Server".to_string(),
|
|
..Default::default()
|
|
},
|
|
&user,
|
|
true,
|
|
)
|
|
.await
|
|
.expect("Failed to create test server.");
|
|
Member::create(&harness.db, &server, &user, None).await.unwrap();
|
|
|
|
let channel = &channels[0];
|
|
|
|
let status = harness
|
|
.client
|
|
.patch(format!("/channels/{}", channel.id()))
|
|
.header(Header::new("X-Audit-Log-Reason", "Test Reason 1"))
|
|
.header(Header::new("x-session-token", session.token.clone()))
|
|
.json(&v0::DataEditChannel {
|
|
description: Some("General chat channel.".to_string()),
|
|
name: None,
|
|
owner: None,
|
|
icon: None,
|
|
nsfw: None,
|
|
archived: None,
|
|
voice: None,
|
|
slowmode: None,
|
|
remove: Vec::new(),
|
|
})
|
|
.dispatch()
|
|
.await
|
|
.status();
|
|
|
|
assert_eq!(status, Status::Ok);
|
|
|
|
let status = harness
|
|
.client
|
|
.patch(format!("/channels/{}", channel.id()))
|
|
.header(Header::new("X-Audit-Log-Reason", "Test Reason 2"))
|
|
.header(Header::new("x-session-token", session.token.clone()))
|
|
.json(&v0::DataEditChannel {
|
|
description: Some("New description.".to_string()),
|
|
name: None,
|
|
owner: None,
|
|
icon: None,
|
|
nsfw: None,
|
|
archived: None,
|
|
voice: None,
|
|
slowmode: None,
|
|
remove: Vec::new(),
|
|
})
|
|
.dispatch()
|
|
.await
|
|
.status();
|
|
|
|
assert_eq!(status, Status::Ok);
|
|
|
|
let status = harness
|
|
.client
|
|
.delete(format!("/channels/{}", channel.id()))
|
|
.header(Header::new("X-Audit-Log-Reason", "Test Reason 3"))
|
|
.header(Header::new("x-session-token", session.token.clone()))
|
|
.dispatch()
|
|
.await
|
|
.status();
|
|
|
|
assert_eq!(status, Status::NoContent);
|
|
|
|
let response = harness
|
|
.client
|
|
.get(format!(
|
|
"/servers/{}/audit_logs?include_users=true",
|
|
&server.id
|
|
))
|
|
.header(Header::new("x-session-token", session.token.clone()))
|
|
.dispatch()
|
|
.await
|
|
.into_json::<v0::AuditLogQueryResponse>()
|
|
.await
|
|
.expect("Failed to deserialise audit_logs response");
|
|
|
|
let v0::AuditLogQueryResponse {
|
|
audit_logs: entries,
|
|
users,
|
|
members,
|
|
} = response;
|
|
|
|
assert_eq!(entries.len(), 3);
|
|
assert_eq!(users.len(), 1);
|
|
assert_eq!(members.len(), 1);
|
|
|
|
assert_eq!(&users[0].id, &user.id);
|
|
|
|
let entry = entries
|
|
.iter()
|
|
.find(|entry| entry.reason.as_deref() == Some("Test Reason 3"))
|
|
.expect("Missing channel deletion audit record");
|
|
|
|
assert_eq!(entry.reason.as_deref(), Some("Test Reason 3"));
|
|
assert_eq!(&entry.server, &server.id);
|
|
assert_eq!(&entry.user, &user.id);
|
|
assert_eq!(
|
|
&entry.action,
|
|
&v0::AuditLogEntryAction::ChannelDelete {
|
|
channel: channel.id().to_string(),
|
|
name: "General".to_string()
|
|
}
|
|
);
|
|
|
|
let entry = entries
|
|
.iter()
|
|
.find(|entry| entry.reason.as_deref() == Some("Test Reason 2"))
|
|
.expect("Missing channel description edit audit record");
|
|
|
|
assert_eq!(entry.reason.as_deref(), Some("Test Reason 2"));
|
|
assert_eq!(&entry.server, &server.id);
|
|
assert_eq!(&entry.user, &user.id);
|
|
assert_eq!(
|
|
&entry.action,
|
|
&v0::AuditLogEntryAction::ChannelEdit {
|
|
channel: channel.id().to_string(),
|
|
before: v0::PartialChannel {
|
|
description: Some("General chat channel.".to_string()),
|
|
..Default::default()
|
|
},
|
|
after: v0::PartialChannel {
|
|
description: Some("New description.".to_string()),
|
|
..Default::default()
|
|
}
|
|
}
|
|
);
|
|
|
|
let entry = entries
|
|
.iter()
|
|
.find(|entry| entry.reason.as_deref() == Some("Test Reason 1"))
|
|
.expect("Missing channel edit audit record");
|
|
|
|
assert_eq!(entry.reason.as_deref(), Some("Test Reason 1"));
|
|
assert_eq!(&entry.server, &server.id);
|
|
assert_eq!(&entry.user, &user.id);
|
|
assert_eq!(
|
|
&entry.action,
|
|
&v0::AuditLogEntryAction::ChannelEdit {
|
|
channel: channel.id().to_string(),
|
|
before: v0::PartialChannel {
|
|
description: None,
|
|
..Default::default()
|
|
},
|
|
after: v0::PartialChannel {
|
|
description: Some("General chat channel.".to_string()),
|
|
..Default::default()
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|