@@ -26,11 +26,56 @@ use omicron_uuid_kinds::GenericUuid;
2626use omicron_uuid_kinds:: SiloUserUuid ;
2727use oximeter:: types:: ProducerRegistry ;
2828use oximeter_instruments:: http:: { HttpService , LatencyTracker } ;
29+ use schemars:: JsonSchema ;
30+ use serde:: Serialize ;
2931use slog:: Logger ;
3032use std:: env;
33+ use std:: future:: Future ;
3134use std:: sync:: Arc ;
3235use 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 ) ]
3681pub 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