Skip to content

Commit c07a3ff

Browse files
committed
prototype audit log improvements + coverage test
test for verifying audit log coverage
1 parent c51ff72 commit c07a3ff

File tree

5 files changed

+418
-219
lines changed

5 files changed

+418
-219
lines changed

nexus/src/context.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,56 @@ use omicron_uuid_kinds::GenericUuid;
2626
use omicron_uuid_kinds::SiloUserUuid;
2727
use oximeter::types::ProducerRegistry;
2828
use oximeter_instruments::http::{HttpService, LatencyTracker};
29+
use schemars::JsonSchema;
30+
use serde::Serialize;
2931
use slog::Logger;
3032
use std::env;
33+
use std::future::Future;
3134
use std::sync::Arc;
3235
use uuid::Uuid;
3336

37+
use dropshot::{
38+
HttpError, HttpResponse, HttpResponseAccepted, HttpResponseCreated,
39+
HttpResponseDeleted, HttpResponseOk, HttpResponseUpdatedNoContent,
40+
};
41+
use omicron_common::api::external::SimpleIdentity;
42+
43+
/// Trait for extracting resource ID from HTTP response types to record in
44+
/// the audit log. Implemented for response types that may contain a created
45+
/// resource.
46+
pub trait MaybeHasResourceId {
47+
fn resource_id(&self) -> Option<Uuid> {
48+
None
49+
}
50+
}
51+
52+
impl<T> MaybeHasResourceId for HttpResponseCreated<T>
53+
where
54+
T: SimpleIdentity + Serialize + JsonSchema + Send + Sync + 'static,
55+
{
56+
fn resource_id(&self) -> Option<Uuid> {
57+
Some(self.0.id())
58+
}
59+
}
60+
61+
// We only pull the ID out of HttpResponseCreated responses. For the rest of
62+
// these, keep the default impl with no resource ID because the identifier is
63+
// there in the URL. Something to think about: the identifier in the URL can
64+
// be a name, which can then be reused after the thing is deleted or renamed,
65+
// so names don't actually identify things uniquely the way IDs do. So we may
66+
// end up needing to record the ID for delete or update operations as well.
67+
68+
impl<T> MaybeHasResourceId for HttpResponseOk<T> where
69+
T: Serialize + JsonSchema + Send + Sync + 'static
70+
{
71+
}
72+
impl MaybeHasResourceId for HttpResponseDeleted {}
73+
impl MaybeHasResourceId for HttpResponseUpdatedNoContent {}
74+
impl<T> MaybeHasResourceId for HttpResponseAccepted<T> where
75+
T: Serialize + JsonSchema + Send + Sync + 'static
76+
{
77+
}
78+
3479
/// Indicates the kind of HTTP server.
3580
#[derive(Clone, Copy)]
3681
pub enum ServerKind {
@@ -332,6 +377,46 @@ impl ServerContext {
332377
omdb_config: config.pkg.omdb.clone(),
333378
}))
334379
}
380+
381+
/// Execute an external API handler with audit logging and latency tracking.
382+
///
383+
/// This helper:
384+
/// 1. Creates an OpContext via authentication
385+
/// 2. Initializes an audit log entry
386+
/// 3. Runs the handler
387+
/// 4. Completes the audit log entry with result info
388+
/// 5. Wraps everything in latency instrumentation
389+
pub async fn audit_and_time<F, Fut, R>(
390+
&self,
391+
rqctx: &dropshot::RequestContext<ApiContext>,
392+
handler: F,
393+
) -> Result<R, HttpError>
394+
where
395+
F: FnOnce(Arc<OpContext>, Arc<Nexus>) -> Fut,
396+
Fut: Future<Output = Result<R, HttpError>>,
397+
R: HttpResponse + MaybeHasResourceId,
398+
{
399+
let nexus = Arc::clone(&self.nexus);
400+
self.external_latencies
401+
.instrument_dropshot_handler(rqctx, async {
402+
let opctx = Arc::new(op_context_for_external_api(rqctx).await?);
403+
let audit =
404+
self.nexus.audit_log_entry_init(&opctx, rqctx).await?;
405+
406+
let result = handler(Arc::clone(&opctx), nexus).await;
407+
408+
// TODO: pass resource_id to audit_log_entry_complete once
409+
// the schema supports it
410+
let _resource_id =
411+
result.as_ref().ok().and_then(|r| r.resource_id());
412+
let _ = self
413+
.nexus
414+
.audit_log_entry_complete(&opctx, &audit, &result)
415+
.await;
416+
result
417+
})
418+
.await
419+
}
335420
}
336421

337422
/// Authenticates an incoming request to the external API and produces a new

0 commit comments

Comments
 (0)