From 79c0228befeb1982ddb9e7f93b6d60cf2e4ede3a Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 25 Nov 2025 20:23:07 -0500 Subject: [PATCH] prototype audit log improvements + coverage test test for verifying audit log coverage --- nexus/src/context.rs | 84 ++++ nexus/src/external_api/http_entrypoints.rs | 376 +++++------------- nexus/tests/integration_tests/audit_log.rs | 127 ++++++ .../output/uncovered-audit-log-endpoints.txt | 123 ++++++ nexus/types/src/external_api/views.rs | 6 + 5 files changed, 438 insertions(+), 278 deletions(-) create mode 100644 nexus/tests/output/uncovered-audit-log-endpoints.txt diff --git a/nexus/src/context.rs b/nexus/src/context.rs index 9a38dc52a67..3d7d5ec9a4c 100644 --- a/nexus/src/context.rs +++ b/nexus/src/context.rs @@ -26,11 +26,56 @@ use omicron_uuid_kinds::GenericUuid; use omicron_uuid_kinds::SiloUserUuid; use oximeter::types::ProducerRegistry; use oximeter_instruments::http::{HttpService, LatencyTracker}; +use schemars::JsonSchema; +use serde::Serialize; use slog::Logger; use std::env; +use std::future::Future; use std::sync::Arc; use uuid::Uuid; +use dropshot::{ + HttpError, HttpResponse, HttpResponseAccepted, HttpResponseCreated, + HttpResponseDeleted, HttpResponseOk, HttpResponseUpdatedNoContent, +}; +use omicron_common::api::external::SimpleIdentity; + +/// Trait for extracting resource ID from HTTP response types to record in +/// the audit log. Implemented for response types that may contain a created +/// resource. +pub trait MaybeHasResourceId { + fn resource_id(&self) -> Option { + None + } +} + +impl MaybeHasResourceId for HttpResponseCreated +where + T: SimpleIdentity + Serialize + JsonSchema + Send + Sync + 'static, +{ + fn resource_id(&self) -> Option { + Some(self.0.id()) + } +} + +// We only pull the ID out of HttpResponseCreated responses. For the rest of +// these, keep the default impl with no resource ID because the identifier is +// there in the URL. Something to think about: the identifier in the URL can +// be a name, which can then be reused after the thing is deleted or renamed, +// so names don't actually identify things uniquely the way IDs do. So we may +// end up needing to record the ID for delete or update operations as well. + +impl MaybeHasResourceId for HttpResponseOk where + T: Serialize + JsonSchema + Send + Sync + 'static +{ +} +impl MaybeHasResourceId for HttpResponseDeleted {} +impl MaybeHasResourceId for HttpResponseUpdatedNoContent {} +impl MaybeHasResourceId for HttpResponseAccepted where + T: Serialize + JsonSchema + Send + Sync + 'static +{ +} + /// Indicates the kind of HTTP server. #[derive(Clone, Copy)] pub enum ServerKind { @@ -334,6 +379,45 @@ impl ServerContext { } } +/// Execute an external API handler with audit logging and latency tracking. +/// +/// This helper: +/// 1. Creates an OpContext via authentication +/// 2. Initializes an audit log entry +/// 3. Runs the handler +/// 4. Completes the audit log entry with result info +/// 5. Wraps everything in latency instrumentation +pub async fn audit_and_time( + rqctx: &dropshot::RequestContext, + handler: F, +) -> Result +where + F: FnOnce(Arc, Arc) -> Fut, + Fut: Future>, + R: HttpResponse + MaybeHasResourceId, +{ + let apictx = rqctx.context(); + let nexus = Arc::clone(&apictx.context.nexus); + apictx + .context + .external_latencies + .instrument_dropshot_handler(rqctx, async { + let opctx = Arc::new(op_context_for_external_api(rqctx).await?); + let audit = nexus.audit_log_entry_init(&opctx, rqctx).await?; + + let result = handler(Arc::clone(&opctx), Arc::clone(&nexus)).await; + + // TODO: pass resource_id to audit_log_entry_complete once + // the schema supports it + let _resource_id = + result.as_ref().ok().and_then(|r| r.resource_id()); + let _ = + nexus.audit_log_entry_complete(&opctx, &audit, &result).await; + result + }) + .await +} + /// Authenticates an incoming request to the external API and produces a new /// operation context for it pub(crate) async fn op_context_for_external_api( diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 2c08c457fb0..76e1cdd93be 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -15,7 +15,7 @@ use super::{ }; use crate::app::external_endpoints::authority_for_request; use crate::app::support_bundles::SupportBundleQueryType; -use crate::context::ApiContext; +use crate::context::{ApiContext, audit_and_time}; use crate::external_api::shared; use dropshot::Body; use dropshot::EmptyScanParams; @@ -426,28 +426,16 @@ impl NexusExternalApi for NexusExternalApiImpl { path_params: Path, new_quota: TypedBody, ) -> Result, HttpError> { - let apictx = rqctx.context(); - let handler = async { - let nexus = &apictx.context.nexus; - - let opctx = - crate::context::op_context_for_external_api(&rqctx).await?; - let silo_lookup = - nexus.silo_lookup(&opctx, path_params.into_inner().silo)?; + let path = path_params.into_inner(); + let new_quota = new_quota.into_inner(); + audit_and_time(&rqctx, |opctx, nexus| async move { + let silo_lookup = nexus.silo_lookup(&opctx, path.silo)?; let quota = nexus - .silo_update_quota( - &opctx, - &silo_lookup, - &new_quota.into_inner(), - ) + .silo_update_quota(&opctx, &silo_lookup, &new_quota) .await?; Ok(HttpResponseOk(quota.into())) - }; - apictx - .context - .external_latencies - .instrument_dropshot_handler(&rqctx, handler) - .await + }) + .await } async fn silo_list( @@ -889,33 +877,14 @@ impl NexusExternalApi for NexusExternalApiImpl { query_params: Query, ) -> Result>, HttpError> { - let apictx = rqctx.context(); - let handler = async { - let opctx = - crate::context::op_context_for_external_api(&rqctx).await?; - let nexus = &apictx.context.nexus; - let audit = nexus.audit_log_entry_init(&opctx, &rqctx).await?; - - let result = async { - let query = query_params.into_inner(); - let silo_lookup = nexus.silo_lookup(&opctx, query.silo)?; - - let tokens = - nexus.scim_idp_get_tokens(&opctx, &silo_lookup).await?; - - Ok(HttpResponseOk(tokens)) - } - .await; - - let _ = - nexus.audit_log_entry_complete(&opctx, &audit, &result).await; - result - }; - apictx - .context - .external_latencies - .instrument_dropshot_handler(&rqctx, handler) - .await + let query = query_params.into_inner(); + audit_and_time(&rqctx, |opctx, nexus| async move { + let silo_lookup = nexus.silo_lookup(&opctx, query.silo)?; + let tokens = + nexus.scim_idp_get_tokens(&opctx, &silo_lookup).await?; + Ok(HttpResponseOk(tokens)) + }) + .await } async fn scim_token_create( @@ -923,33 +892,14 @@ impl NexusExternalApi for NexusExternalApiImpl { query_params: Query, ) -> Result, HttpError> { - let apictx = rqctx.context(); - let handler = async { - let opctx = - crate::context::op_context_for_external_api(&rqctx).await?; - let nexus = &apictx.context.nexus; - let audit = nexus.audit_log_entry_init(&opctx, &rqctx).await?; - - let result = async { - let query = query_params.into_inner(); - let silo_lookup = nexus.silo_lookup(&opctx, query.silo)?; - - let token = - nexus.scim_idp_create_token(&opctx, &silo_lookup).await?; - - Ok(HttpResponseCreated(token)) - } - .await; - - let _ = - nexus.audit_log_entry_complete(&opctx, &audit, &result).await; - result - }; - apictx - .context - .external_latencies - .instrument_dropshot_handler(&rqctx, handler) - .await + let query = query_params.into_inner(); + audit_and_time(&rqctx, |opctx, nexus| async move { + let silo_lookup = nexus.silo_lookup(&opctx, query.silo)?; + let token = + nexus.scim_idp_create_token(&opctx, &silo_lookup).await?; + Ok(HttpResponseCreated(token)) + }) + .await } async fn scim_token_view( @@ -957,39 +907,16 @@ impl NexusExternalApi for NexusExternalApiImpl { path_params: Path, query_params: Query, ) -> Result, HttpError> { - let apictx = rqctx.context(); - let handler = async { - let opctx = - crate::context::op_context_for_external_api(&rqctx).await?; - let nexus = &apictx.context.nexus; - let audit = nexus.audit_log_entry_init(&opctx, &rqctx).await?; - - let result = async { - let query = query_params.into_inner(); - let path_params = path_params.into_inner(); - let silo_lookup = nexus.silo_lookup(&opctx, query.silo)?; - - let token = nexus - .scim_idp_get_token_by_id( - &opctx, - &silo_lookup, - path_params.token_id, - ) - .await?; - - Ok(HttpResponseOk(token)) - } - .await; - - let _ = - nexus.audit_log_entry_complete(&opctx, &audit, &result).await; - result - }; - apictx - .context - .external_latencies - .instrument_dropshot_handler(&rqctx, handler) - .await + let query = query_params.into_inner(); + let path = path_params.into_inner(); + audit_and_time(&rqctx, |opctx, nexus| async move { + let silo_lookup = nexus.silo_lookup(&opctx, query.silo)?; + let token = nexus + .scim_idp_get_token_by_id(&opctx, &silo_lookup, path.token_id) + .await?; + Ok(HttpResponseOk(token)) + }) + .await } async fn scim_token_delete( @@ -997,39 +924,20 @@ impl NexusExternalApi for NexusExternalApiImpl { path_params: Path, query_params: Query, ) -> Result { - let apictx = rqctx.context(); - let handler = async { - let opctx = - crate::context::op_context_for_external_api(&rqctx).await?; - let nexus = &apictx.context.nexus; - let audit = nexus.audit_log_entry_init(&opctx, &rqctx).await?; - - let result = async { - let query = query_params.into_inner(); - let path_params = path_params.into_inner(); - let silo_lookup = nexus.silo_lookup(&opctx, query.silo)?; - - nexus - .scim_idp_delete_token_by_id( - &opctx, - &silo_lookup, - path_params.token_id, - ) - .await?; - - Ok(HttpResponseDeleted()) - } - .await; - - let _ = - nexus.audit_log_entry_complete(&opctx, &audit, &result).await; - result - }; - apictx - .context - .external_latencies - .instrument_dropshot_handler(&rqctx, handler) - .await + let query = query_params.into_inner(); + let path = path_params.into_inner(); + audit_and_time(&rqctx, |opctx, nexus| async move { + let silo_lookup = nexus.silo_lookup(&opctx, query.silo)?; + nexus + .scim_idp_delete_token_by_id( + &opctx, + &silo_lookup, + path.token_id, + ) + .await?; + Ok(HttpResponseDeleted()) + }) + .await } async fn scim_v2_list_users( @@ -1451,30 +1359,12 @@ impl NexusExternalApi for NexusExternalApiImpl { rqctx: RequestContext, new_project: TypedBody, ) -> Result, HttpError> { - let apictx = rqctx.context(); - let handler = async { - let opctx = - crate::context::op_context_for_external_api(&rqctx).await?; - let nexus = &apictx.context.nexus; - let audit = nexus.audit_log_entry_init(&opctx, &rqctx).await?; - - let result = async { - let project = nexus - .project_create(&opctx, &new_project.into_inner()) - .await?; - Ok(HttpResponseCreated(project.into())) - } - .await; - - let _ = - nexus.audit_log_entry_complete(&opctx, &audit, &result).await; - result - }; - apictx - .context - .external_latencies - .instrument_dropshot_handler(&rqctx, handler) - .await + let new_project = new_project.into_inner(); + audit_and_time(&rqctx, |opctx, nexus| async move { + let project = nexus.project_create(&opctx, &new_project).await?; + Ok(HttpResponseCreated(project.into())) + }) + .await } async fn project_view( @@ -1504,33 +1394,16 @@ impl NexusExternalApi for NexusExternalApiImpl { rqctx: RequestContext, path_params: Path, ) -> Result { - let apictx = rqctx.context(); - let handler = async { - let opctx = - crate::context::op_context_for_external_api(&rqctx).await?; - let nexus = &apictx.context.nexus; - let audit = nexus.audit_log_entry_init(&opctx, &rqctx).await?; - - let result = async { - let path = path_params.into_inner(); - let project_selector = - params::ProjectSelector { project: path.project }; - let project_lookup = - nexus.project_lookup(&opctx, project_selector)?; - nexus.project_delete(&opctx, &project_lookup).await?; - Ok(HttpResponseDeleted()) - } - .await; - - let _ = - nexus.audit_log_entry_complete(&opctx, &audit, &result).await; - result - }; - apictx - .context - .external_latencies - .instrument_dropshot_handler(&rqctx, handler) - .await + let path = path_params.into_inner(); + audit_and_time(&rqctx, |opctx, nexus| async move { + let project_selector = + params::ProjectSelector { project: path.project }; + let project_lookup = + nexus.project_lookup(&opctx, project_selector)?; + nexus.project_delete(&opctx, &project_lookup).await?; + Ok(HttpResponseDeleted()) + }) + .await } // TODO-correctness: Is it valid for PUT to accept application/json that's a @@ -2044,22 +1917,14 @@ impl NexusExternalApi for NexusExternalApiImpl { path_params: Path, range_params: TypedBody, ) -> Result { - let apictx = &rqctx.context(); - let handler = async { - let opctx = - crate::context::op_context_for_external_api(&rqctx).await?; - let nexus = &apictx.context.nexus; - let path = path_params.into_inner(); - let range = range_params.into_inner(); + let path = path_params.into_inner(); + let range = range_params.into_inner(); + audit_and_time(&rqctx, |opctx, nexus| async move { let pool_lookup = nexus.ip_pool_lookup(&opctx, &path.pool)?; nexus.ip_pool_delete_range(&opctx, &pool_lookup, &range).await?; Ok(HttpResponseUpdatedNoContent()) - }; - apictx - .context - .external_latencies - .instrument_dropshot_handler(&rqctx, handler) - .await + }) + .await } async fn ip_pool_service_range_list( @@ -2300,31 +2165,20 @@ impl NexusExternalApi for NexusExternalApiImpl { query_params: Query, target: TypedBody, ) -> Result, HttpError> { - let apictx = rqctx.context(); - let handler = async { - let opctx = - crate::context::op_context_for_external_api(&rqctx).await?; - let nexus = &apictx.context.nexus; - let path = path_params.into_inner(); - let query = query_params.into_inner(); + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let target = target.into_inner(); + audit_and_time(&rqctx, |opctx, nexus| async move { let floating_ip_selector = params::FloatingIpSelector { floating_ip: path.floating_ip, project: query.project, }; let ip = nexus - .floating_ip_attach( - &opctx, - floating_ip_selector, - target.into_inner(), - ) + .floating_ip_attach(&opctx, floating_ip_selector, target) .await?; Ok(HttpResponseAccepted(ip)) - }; - apictx - .context - .external_latencies - .instrument_dropshot_handler(&rqctx, handler) - .await + }) + .await } async fn floating_ip_detach( @@ -2708,33 +2562,16 @@ impl NexusExternalApi for NexusExternalApiImpl { query_params: Query, new_disk: TypedBody, ) -> Result, HttpError> { - let apictx = rqctx.context(); - let handler = async { - let opctx = - crate::context::op_context_for_external_api(&rqctx).await?; - let nexus = &apictx.context.nexus; - let audit = nexus.audit_log_entry_init(&opctx, &rqctx).await?; - - let result = async { - let query = query_params.into_inner(); - let params = new_disk.into_inner(); - let project_lookup = nexus.project_lookup(&opctx, query)?; - let disk = nexus - .project_create_disk(&opctx, &project_lookup, ¶ms) - .await?; - Ok(HttpResponseCreated(disk.into())) - } - .await; - - let _ = - nexus.audit_log_entry_complete(&opctx, &audit, &result).await; - result - }; - apictx - .context - .external_latencies - .instrument_dropshot_handler(&rqctx, handler) - .await + let query = query_params.into_inner(); + let params = new_disk.into_inner(); + audit_and_time(&rqctx, |opctx, nexus| async move { + let project_lookup = nexus.project_lookup(&opctx, query)?; + let disk = nexus + .project_create_disk(&opctx, &project_lookup, ¶ms) + .await?; + Ok(HttpResponseCreated(disk.into())) + }) + .await } async fn disk_view( @@ -2768,35 +2605,18 @@ impl NexusExternalApi for NexusExternalApiImpl { path_params: Path, query_params: Query, ) -> Result { - let apictx = rqctx.context(); - let handler = async { - let opctx = - crate::context::op_context_for_external_api(&rqctx).await?; - let nexus = &apictx.context.nexus; - let audit = nexus.audit_log_entry_init(&opctx, &rqctx).await?; - - let result = async { - let path = path_params.into_inner(); - let query = query_params.into_inner(); - let disk_selector = params::DiskSelector { - disk: path.disk, - project: query.project, - }; - let disk_lookup = nexus.disk_lookup(&opctx, disk_selector)?; - nexus.project_delete_disk(&opctx, &disk_lookup).await?; - Ok(HttpResponseDeleted()) - } - .await; - - let _ = - nexus.audit_log_entry_complete(&opctx, &audit, &result).await; - result - }; - apictx - .context - .external_latencies - .instrument_dropshot_handler(&rqctx, handler) - .await + let path = path_params.into_inner(); + let query = query_params.into_inner(); + audit_and_time(&rqctx, |opctx, nexus| async move { + let disk_selector = params::DiskSelector { + disk: path.disk, + project: query.project, + }; + let disk_lookup = nexus.disk_lookup(&opctx, disk_selector)?; + nexus.project_delete_disk(&opctx, &disk_lookup).await?; + Ok(HttpResponseDeleted()) + }) + .await } async fn disk_bulk_write_import_start( diff --git a/nexus/tests/integration_tests/audit_log.rs b/nexus/tests/integration_tests/audit_log.rs index 72ed115cf1a..49404f1cccf 100644 --- a/nexus/tests/integration_tests/audit_log.rs +++ b/nexus/tests/integration_tests/audit_log.rs @@ -370,6 +370,133 @@ async fn test_audit_log_create_delete_ops(ctx: &ControlPlaneTestContext) { verify_entry(&items[5], "project_delete", project_del_url, 204, t2, t3); } +/// Test that mutating endpoints in VERIFY_ENDPOINTS create audit log entries. +/// This is a coverage test to catch endpoints that forget to add audit logging. +/// The snapshot file lists endpoints that are known to not have audit logging. +/// As audit logging is added to endpoints, they should be removed from the file. +#[nexus_test] +async fn test_audit_log_coverage(ctx: &ControlPlaneTestContext) { + use super::endpoints::{AllowedMethod, VERIFY_ENDPOINTS}; + use expectorate::assert_contents; + use nexus_test_utils::http_testing::{AuthnMode, NexusRequest}; + use openapiv3::OpenAPI; + use std::collections::BTreeMap; + + let client = &ctx.external_client; + + // Load the OpenAPI schema to get operation IDs + let schema_path = "../openapi/nexus.json"; + let schema_contents = std::fs::read_to_string(schema_path) + .expect("failed to read Nexus OpenAPI spec"); + let spec: OpenAPI = serde_json::from_str(&schema_contents) + .expect("Nexus OpenAPI spec was not valid OpenAPI"); + + // Build a map from (method, url_regex) to (operation_id, path_template) + let spec_operations: BTreeMap<(String, String), (String, String)> = spec + .operations() + .map(|(path, method, op)| { + // Convert path template to regex pattern + let re = regex::Regex::new("/\\{[^}]+\\}").unwrap(); + let regex_path = re.replace_all(path, "/[^/]+"); + let regex = format!("^{}$", regex_path); + let label = op + .operation_id + .clone() + .unwrap_or_else(|| String::from("unknown")); + ((method.to_uppercase(), regex), (label, path.to_string())) + }) + .collect(); + + // Set up resources needed by many endpoints + DiskTest::new(&ctx).await; + create_default_ip_pool(client).await; + let _project = create_project(client, "demo-project").await; + + let t_start = Utc::now(); + + let mut tested = 0; + let mut missing_audit: BTreeMap = BTreeMap::new(); + + for endpoint in &*VERIFY_ENDPOINTS { + for method in &endpoint.allowed_methods { + // Only test mutating methods + let is_mutating = match method { + AllowedMethod::Post(_) + | AllowedMethod::Put(_) + | AllowedMethod::Delete => true, + AllowedMethod::Get + | AllowedMethod::GetNonexistent + | AllowedMethod::GetUnimplemented + | AllowedMethod::GetVolatile + | AllowedMethod::GetWebsocket => false, + }; + if !is_mutating { + continue; + } + + let before = fetch_log(client, t_start, None).await.items.len(); + + // Make authenticated request as unprivileged user. This will fail + // authz but should still create an audit log entry if the endpoint + // has audit logging. Using unprivileged avoids actually modifying + // resources (e.g., removing our own permissions via fleet policy). + let http_method = method.http_method().clone(); + let body = method.body().cloned(); + let result = NexusRequest::new( + RequestBuilder::new(client, http_method.clone(), endpoint.url) + .body(body.as_ref()) + .expect_status(None), // accept any status + ) + .authn_as(AuthnMode::UnprivilegedUser) + .execute() + .await; + + if result.is_err() { + // Request itself failed (connection error, etc), skip + continue; + } + + let after = fetch_log(client, t_start, None).await.items.len(); + + if after <= before { + // Find the operation info from the OpenAPI spec + let method_str = http_method.to_string().to_uppercase(); + let url_path = endpoint.url.split('?').next().unwrap(); + + let (op_id, path_template) = spec_operations + .iter() + .find(|((m, regex), _)| { + *m == method_str + && regex::Regex::new(regex) + .unwrap() + .is_match(url_path) + }) + .map(|(_, (op_id, path))| (op_id.clone(), path.clone())) + .unwrap_or_else(|| { + (String::from("unknown"), url_path.to_string()) + }); + + missing_audit + .insert(op_id, (method_str.to_lowercase(), path_template)); + } + tested += 1; + } + } + + println!("Tested {} mutating endpoints", tested); + + let mut output = + String::from("Mutating endpoints without audit logging:\n"); + for (op_id, (method, path)) in &missing_audit { + output.push_str(&format!("{:44} ({:6} {:?})\n", op_id, method, path)); + } + + // If you're adding audit logging to an endpoint, remove it from this file. + // If you're adding a new endpoint, add audit logging or add it to this file + // with justification. + assert_contents("tests/output/uncovered-audit-log-endpoints.txt", &output); +} + fn verify_entry( entry: &views::AuditLogEntry, operation_id: &str, diff --git a/nexus/tests/output/uncovered-audit-log-endpoints.txt b/nexus/tests/output/uncovered-audit-log-endpoints.txt new file mode 100644 index 00000000000..2395cc053bc --- /dev/null +++ b/nexus/tests/output/uncovered-audit-log-endpoints.txt @@ -0,0 +1,123 @@ +Mutating endpoints without audit logging: +affinity_group_create (post "/v1/affinity-groups") +affinity_group_delete (delete "/v1/affinity-groups/{affinity_group}") +affinity_group_member_instance_add (post "/v1/affinity-groups/{affinity_group}/members/instance/{instance}") +affinity_group_member_instance_delete (delete "/v1/affinity-groups/{affinity_group}/members/instance/{instance}") +affinity_group_update (put "/v1/affinity-groups/{affinity_group}") +alert_receiver_delete (delete "/v1/alert-receivers/{receiver}") +alert_receiver_probe (post "/v1/alert-receivers/{receiver}/probe") +alert_receiver_subscription_add (post "/v1/alert-receivers/{receiver}/subscriptions") +alert_receiver_subscription_remove (delete "/v1/alert-receivers/{receiver}/subscriptions/{subscription}") +anti_affinity_group_create (post "/v1/anti-affinity-groups") +anti_affinity_group_delete (delete "/v1/anti-affinity-groups/{anti_affinity_group}") +anti_affinity_group_member_instance_add (post "/v1/anti-affinity-groups/{anti_affinity_group}/members/instance/{instance}") +anti_affinity_group_member_instance_delete (delete "/v1/anti-affinity-groups/{anti_affinity_group}/members/instance/{instance}") +anti_affinity_group_update (put "/v1/anti-affinity-groups/{anti_affinity_group}") +auth_settings_update (put "/v1/auth-settings") +certificate_create (post "/v1/certificates") +certificate_delete (delete "/v1/certificates/{certificate}") +current_user_ssh_key_create (post "/v1/me/ssh-keys") +current_user_ssh_key_delete (delete "/v1/me/ssh-keys/{ssh_key}") +disk_bulk_write_import (post "/v1/disks/{disk}/bulk-write") +disk_bulk_write_import_start (post "/v1/disks/{disk}/bulk-write-start") +disk_bulk_write_import_stop (post "/v1/disks/{disk}/bulk-write-stop") +disk_finalize_import (post "/v1/disks/{disk}/finalize") +floating_ip_create (post "/v1/floating-ips") +floating_ip_delete (delete "/v1/floating-ips/{floating_ip}") +floating_ip_detach (post "/v1/floating-ips/{floating_ip}/detach") +floating_ip_update (put "/v1/floating-ips/{floating_ip}") +image_create (post "/v1/images") +image_delete (delete "/v1/images/{image}") +image_demote (post "/v1/images/{image}/demote") +image_promote (post "/v1/images/{image}/promote") +instance_disk_attach (post "/v1/instances/{instance}/disks/attach") +instance_disk_detach (post "/v1/instances/{instance}/disks/detach") +instance_ephemeral_ip_attach (post "/v1/instances/{instance}/external-ips/ephemeral") +instance_ephemeral_ip_detach (delete "/v1/instances/{instance}/external-ips/ephemeral") +instance_multicast_group_join (put "/v1/instances/{instance}/multicast-groups/{multicast_group}") +instance_multicast_group_leave (delete "/v1/instances/{instance}/multicast-groups/{multicast_group}") +instance_network_interface_create (post "/v1/network-interfaces") +instance_network_interface_delete (delete "/v1/network-interfaces/{interface}") +instance_network_interface_update (put "/v1/network-interfaces/{interface}") +instance_reboot (post "/v1/instances/{instance}/reboot") +instance_start (post "/v1/instances/{instance}/start") +instance_stop (post "/v1/instances/{instance}/stop") +instance_update (put "/v1/instances/{instance}") +internet_gateway_create (post "/v1/internet-gateways") +internet_gateway_delete (delete "/v1/internet-gateways/{gateway}") +internet_gateway_ip_address_create (post "/v1/internet-gateway-ip-addresses") +internet_gateway_ip_address_delete (delete "/v1/internet-gateway-ip-addresses/{address}") +internet_gateway_ip_pool_create (post "/v1/internet-gateway-ip-pools") +internet_gateway_ip_pool_delete (delete "/v1/internet-gateway-ip-pools/{pool}") +ip_pool_create (post "/v1/system/ip-pools") +ip_pool_delete (delete "/v1/system/ip-pools/{pool}") +ip_pool_range_add (post "/v1/system/ip-pools/{pool}/ranges/add") +ip_pool_service_range_add (post "/v1/system/ip-pools-service/ranges/add") +ip_pool_service_range_remove (post "/v1/system/ip-pools-service/ranges/remove") +ip_pool_silo_link (post "/v1/system/ip-pools/{pool}/silos") +ip_pool_silo_unlink (delete "/v1/system/ip-pools/{pool}/silos/{silo}") +ip_pool_silo_update (put "/v1/system/ip-pools/{pool}/silos/{silo}") +ip_pool_update (put "/v1/system/ip-pools/{pool}") +local_idp_user_create (post "/v1/system/identity-providers/local/users") +local_idp_user_delete (delete "/v1/system/identity-providers/local/users/{user_id}") +local_idp_user_set_password (post "/v1/system/identity-providers/local/users/{user_id}/set-password") +multicast_group_create (post "/v1/multicast-groups") +multicast_group_delete (delete "/v1/multicast-groups/{multicast_group}") +multicast_group_member_add (post "/v1/multicast-groups/{multicast_group}/members") +multicast_group_member_remove (delete "/v1/multicast-groups/{multicast_group}/members/{instance}") +multicast_group_update (put "/v1/multicast-groups/{multicast_group}") +networking_address_lot_create (post "/v1/system/networking/address-lot") +networking_address_lot_delete (delete "/v1/system/networking/address-lot/{address_lot}") +networking_allow_list_update (put "/v1/system/networking/allow-list") +networking_bfd_disable (post "/v1/system/networking/bfd-disable") +networking_bfd_enable (post "/v1/system/networking/bfd-enable") +networking_bgp_announce_set_delete (delete "/v1/system/networking/bgp-announce-set/{announce_set}") +networking_bgp_announce_set_update (put "/v1/system/networking/bgp-announce-set") +networking_bgp_config_create (post "/v1/system/networking/bgp") +networking_bgp_config_delete (delete "/v1/system/networking/bgp") +networking_inbound_icmp_update (put "/v1/system/networking/inbound-icmp") +networking_loopback_address_create (post "/v1/system/networking/loopback-address") +networking_loopback_address_delete (delete "/v1/system/networking/loopback-address/{rack_id}/{switch_location}/{address}/{subnet_mask}") +networking_switch_port_apply_settings (post "/v1/system/hardware/switch-port/{port}/settings") +networking_switch_port_clear_settings (delete "/v1/system/hardware/switch-port/{port}/settings") +networking_switch_port_settings_create (post "/v1/system/networking/switch-port-settings") +networking_switch_port_settings_delete (delete "/v1/system/networking/switch-port-settings") +policy_update (put "/v1/policy") +project_policy_update (put "/v1/projects/{project}/policy") +project_update (put "/v1/projects/{project}") +saml_identity_provider_create (post "/v1/system/identity-providers/saml") +silo_create (post "/v1/system/silos") +silo_delete (delete "/v1/system/silos/{silo}") +silo_policy_update (put "/v1/system/silos/{silo}/policy") +sled_add (post "/v1/system/hardware/sleds") +sled_set_provision_policy (put "/v1/system/hardware/sleds/{sled_id}/provision-policy") +snapshot_create (post "/v1/snapshots") +snapshot_delete (delete "/v1/snapshots/{snapshot}") +support_bundle_create (post "/experimental/v1/system/support-bundles") +support_bundle_delete (delete "/experimental/v1/system/support-bundles/{bundle_id}") +support_bundle_update (put "/experimental/v1/system/support-bundles/{bundle_id}") +system_policy_update (put "/v1/system/policy") +system_timeseries_query (post "/v1/system/timeseries/query") +system_update_repository_upload (put "/v1/system/update/repositories") +system_update_trust_root_create (post "/v1/system/update/trust-roots") +system_update_trust_root_delete (delete "/v1/system/update/trust-roots/{trust_root_id}") +target_release_update (put "/v1/system/update/target-release") +timeseries_query (post "/v1/timeseries/query") +user_logout (post "/v1/users/{user_id}/logout") +vpc_create (post "/v1/vpcs") +vpc_delete (delete "/v1/vpcs/{vpc}") +vpc_firewall_rules_update (put "/v1/vpc-firewall-rules") +vpc_router_create (post "/v1/vpc-routers") +vpc_router_delete (delete "/v1/vpc-routers/{router}") +vpc_router_route_create (post "/v1/vpc-router-routes") +vpc_router_route_delete (delete "/v1/vpc-router-routes/{route}") +vpc_router_route_update (put "/v1/vpc-router-routes/{route}") +vpc_router_update (put "/v1/vpc-routers/{router}") +vpc_subnet_create (post "/v1/vpc-subnets") +vpc_subnet_delete (delete "/v1/vpc-subnets/{subnet}") +vpc_subnet_update (put "/v1/vpc-subnets/{subnet}") +vpc_update (put "/v1/vpcs/{vpc}") +webhook_receiver_create (post "/v1/webhook-receivers") +webhook_receiver_update (put "/v1/webhook-receivers/{receiver}") +webhook_secrets_add (post "/v1/webhook-secrets") +webhook_secrets_delete (delete "/v1/webhook-secrets/{secret_id}") diff --git a/nexus/types/src/external_api/views.rs b/nexus/types/src/external_api/views.rs index 7f91e98d286..84d9b861dc5 100644 --- a/nexus/types/src/external_api/views.rs +++ b/nexus/types/src/external_api/views.rs @@ -1716,6 +1716,12 @@ pub struct ScimClientBearerTokenValue { pub bearer_token: String, } +impl SimpleIdentity for ScimClientBearerTokenValue { + fn id(&self) -> Uuid { + self.id + } +} + #[derive(Deserialize, Serialize, JsonSchema)] pub struct ScimClientBearerToken { pub id: Uuid,