Skip to content

Commit

Permalink
Merge pull request #4829 from systeminit/nick/eng-2751
Browse files Browse the repository at this point in the history
Add custom extractor for filtering audit logs
  • Loading branch information
nickgerace authored Oct 22, 2024
2 parents cc4d07f + b7b2611 commit 1bdd992
Show file tree
Hide file tree
Showing 11 changed files with 889 additions and 759 deletions.
286 changes: 151 additions & 135 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ serde = { version = "1.0.197", features = ["derive", "rc"] }
serde-aux = "4.5.0"
serde_json = { version = "=1.0.125", features = ["preserve_order"] }
serde_path_to_error = { version = "0.1.16" }
serde_qs = "0.13.0"
serde_url_params = "0.2.1"
serde_with = "3.7.0"
serde_yaml = "0.9.33" # NOTE(nick): this has been archived upstream
Expand Down
2 changes: 2 additions & 0 deletions lib/dal/src/audit_log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ pub fn filter_and_paginate(
filtered_audit_logs.reverse();
}

// Count the number of audit logs after filtering, but before pagination. We need this so that
// the frontend can know how many pages exists when paginating data.
let total = filtered_audit_logs.len();

// Finally, paginate and return.
Expand Down
1 change: 1 addition & 0 deletions lib/sdf-server/BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ rust_library(
"//third-party/rust:reqwest",
"//third-party/rust:serde",
"//third-party/rust:serde_json",
"//third-party/rust:serde_qs",
"//third-party/rust:serde_with",
"//third-party/rust:sodiumoxide",
"//third-party/rust:strum",
Expand Down
47 changes: 25 additions & 22 deletions lib/sdf-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,49 +9,52 @@ rust-version.workspace = true
publish.workspace = true

[dependencies]
buck2-resources = { path = "../../lib/buck2-resources" }
dal = { path = "../../lib/dal" }
module-index-client = { path = "../../lib/module-index-client" }
nats-multiplexer = { path = "../../lib/nats-multiplexer" }
nats-multiplexer-client = { path = "../../lib/nats-multiplexer-client" }
nats-subscriber = { path = "../../lib/nats-subscriber" }
permissions = { path = "../../lib/permissions" }
rebaser-client = { path = "../../lib/rebaser-client" }
si-crypto = { path = "../../lib/si-crypto" }
si-data-nats = { path = "../../lib/si-data-nats" }
si-data-pg = { path = "../../lib/si-data-pg" }
si-data-spicedb = { path = "../../lib/si-data-spicedb" }
si-events = { path = "../../lib/si-events-rs" }
si-frontend-types = { path = "../../lib/si-frontend-types-rs" }
si-layer-cache = { path = "../../lib/si-layer-cache" }
si-pkg = { path = "../../lib/si-pkg" }
si-posthog = { path = "../../lib/si-posthog-rs" }
si-settings = { path = "../../lib/si-settings" }
si-std = { path = "../../lib/si-std" }
telemetry = { path = "../../lib/telemetry-rs" }
telemetry-http = { path = "../../lib/telemetry-http-rs" }
veritech-client = { path = "../../lib/veritech-client" }

async-trait = { workspace = true }
axum = { workspace = true }
base64 = { workspace = true }
buck2-resources = { path = "../../lib/buck2-resources" }
chrono = { workspace = true }
clap = { workspace = true }
convert_case = { workspace = true }
dal = { path = "../../lib/dal" }
derive_builder = { workspace = true }
futures = { workspace = true }
futures-lite = { workspace = true }
hyper = { workspace = true }
module-index-client = { path = "../../lib/module-index-client" }
names = { workspace = true }
nats-multiplexer = { path = "../../lib/nats-multiplexer" }
nats-multiplexer-client = { path = "../../lib/nats-multiplexer-client" }
nats-subscriber = { path = "../../lib/nats-subscriber" }
nix = { workspace = true }
once_cell = { workspace = true }
pathdiff = { workspace = true }
permissions = { path = "../../lib/permissions" }
rand = { workspace = true }
rebaser-client = { path = "../../lib/rebaser-client" }
remain = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_qs = { workspace = true }
serde_with = { workspace = true }
si-crypto = { path = "../../lib/si-crypto" }
si-data-nats = { path = "../../lib/si-data-nats" }
si-data-pg = { path = "../../lib/si-data-pg" }
si-data-spicedb = { path = "../../lib/si-data-spicedb" }
si-events = { path = "../../lib/si-events-rs" }
si-frontend-types = { path = "../../lib/si-frontend-types-rs" }
si-layer-cache = { path = "../../lib/si-layer-cache" }
si-pkg = { path = "../../lib/si-pkg" }
si-posthog = { path = "../../lib/si-posthog-rs" }
si-settings = { path = "../../lib/si-settings" }
si-std = { path = "../../lib/si-std" }
sodiumoxide = { workspace = true }
strum = { workspace = true }
telemetry = { path = "../../lib/telemetry-rs" }
telemetry-http = { path = "../../lib/telemetry-http-rs" }
thiserror = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
Expand All @@ -62,11 +65,11 @@ tower-http = { workspace = true }
tracing-tunnel = { workspace = true }
ulid = { workspace = true }
url = { workspace = true }
veritech-client = { path = "../../lib/veritech-client" }
y-sync = { workspace = true }

[dev-dependencies]
dal-test = { path = "../../lib/dal-test" }

pretty_assertions_sorted = { workspace = true }
serde_url_params = { workspace = true }
tokio-util = { workspace = true }
Expand Down
58 changes: 58 additions & 0 deletions lib/sdf-server/src/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,36 @@ async fn tenancy_from_claim(
Ok(Tenancy(dal::Tenancy::new(claim.workspace_pk)))
}

/// Use instead of [`axum::extract::Query`] when the query contains array params using "[]"
/// notation.
///
/// Inspiration : https://dev.to/pongsakornsemsuwan/rust-axum-extracting-query-param-of-vec-4pdm
pub struct QueryWithVecParams<T>(pub T);

#[async_trait]
impl<T> FromRequestParts<AppState> for QueryWithVecParams<T>
where
T: serde::de::DeserializeOwned,
{
type Rejection = (StatusCode, Json<serde_json::Value>);

async fn from_request_parts(
parts: &mut Parts,
_state: &AppState,
) -> Result<Self, Self::Rejection> {
let query = parts
.uri
.query()
.ok_or(not_found_error("no query string found in uri"))?;
let deserialized = serde_qs::from_str(query).map_err(|err| {
unprocessable_entity_error(&format!(
"could not deserialize query string: {query} (error: {err})"
))
})?;
Ok(Self(deserialized))
}
}

fn internal_error(message: impl fmt::Display) -> (StatusCode, Json<serde_json::Value>) {
let status_code = StatusCode::INTERNAL_SERVER_ERROR;
(
Expand Down Expand Up @@ -298,3 +328,31 @@ fn unauthorized_error() -> (StatusCode, Json<serde_json::Value>) {
})),
)
}

fn not_found_error(message: &str) -> (StatusCode, Json<serde_json::Value>) {
let status_code = StatusCode::NOT_FOUND;
(
status_code,
Json(serde_json::json!({
"error": {
"message": message,
"statusCode": status_code.as_u16(),
"code": 42,
},
})),
)
}

fn unprocessable_entity_error(message: &str) -> (StatusCode, Json<serde_json::Value>) {
let status_code = StatusCode::UNPROCESSABLE_ENTITY;
(
status_code,
Json(serde_json::json!({
"error": {
"message": message,
"statusCode": status_code.as_u16(),
"code": 42,
},
})),
)
}
7 changes: 4 additions & 3 deletions lib/sdf-server/src/service/v2/audit_log/list_audit_logs.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::HashSet;

use axum::{
extract::{OriginalUri, Path, Query},
extract::{OriginalUri, Path},
Json,
};
use dal::{ChangeSetId, WorkspacePk};
Expand All @@ -13,7 +13,7 @@ use si_events::{
use si_frontend_types as frontend_types;

use super::AuditLogResult;
use crate::extract::{AccessBuilder, HandlerContext, PosthogClient};
use crate::extract::{AccessBuilder, HandlerContext, PosthogClient, QueryWithVecParams};

#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -41,7 +41,7 @@ pub async fn list_audit_logs(
PosthogClient(_posthog_client): PosthogClient,
OriginalUri(_original_uri): OriginalUri,
Path((_workspace_pk, change_set_id)): Path<(WorkspacePk, ChangeSetId)>,
Query(request): Query<ListAuditLogsRequest>,
QueryWithVecParams(request): QueryWithVecParams<ListAuditLogsRequest>,
) -> AuditLogResult<Json<ListAuditLogsResponse>> {
let ctx = builder
.build(access_builder.build(change_set_id.into()))
Expand Down Expand Up @@ -76,6 +76,7 @@ pub async fn list_audit_logs(
None => HashSet::new(),
},
)?;

Ok(Json(ListAuditLogsResponse {
logs: filtered_and_paginated_audit_logs,
total,
Expand Down
Loading

0 comments on commit 1bdd992

Please sign in to comment.