diff --git a/Cargo.lock b/Cargo.lock index 663fd0a10f8..713641a76e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4695,7 +4695,7 @@ dependencies = [ "displaydoc", "yoke", "zerofrom", - "zerovec", + "zerovec 0.10.4", ] [[package]] @@ -4706,9 +4706,9 @@ checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", "litemap", - "tinystr", + "tinystr 0.7.6", "writeable", - "zerovec", + "zerovec 0.10.4", ] [[package]] @@ -4721,8 +4721,8 @@ dependencies = [ "icu_locid", "icu_locid_transform_data", "icu_provider", - "tinystr", - "zerovec", + "tinystr 0.7.6", + "zerovec 0.10.4", ] [[package]] @@ -4746,7 +4746,7 @@ dependencies = [ "utf16_iter", "utf8_iter", "write16", - "zerovec", + "zerovec 0.10.4", ] [[package]] @@ -4766,8 +4766,8 @@ dependencies = [ "icu_locid_transform", "icu_properties_data", "icu_provider", - "tinystr", - "zerovec", + "tinystr 0.7.6", + "zerovec 0.10.4", ] [[package]] @@ -4786,11 +4786,11 @@ dependencies = [ "icu_locid", "icu_provider_macros", "stable_deref_trait", - "tinystr", + "tinystr 0.7.6", "writeable", "yoke", "zerofrom", - "zerovec", + "zerovec 0.10.4", ] [[package]] @@ -6281,6 +6281,7 @@ dependencies = [ "slog", "strum 0.27.2", "thiserror 2.0.12", + "tinystr 0.8.1", "tokio", "usdt", "uuid", @@ -7997,6 +7998,7 @@ dependencies = [ "tempfile", "term", "thiserror 2.0.12", + "tinystr 0.8.1", "tokio", "tokio-postgres", "tokio-util", @@ -13563,7 +13565,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", - "zerovec", + "zerovec 0.10.4", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "serde", + "zerovec 0.11.4", ] [[package]] @@ -16105,6 +16118,15 @@ dependencies = [ "zerovec-derive", ] +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "zerofrom", +] + [[package]] name = "zerovec-derive" version = "0.10.3" diff --git a/Cargo.toml b/Cargo.toml index 62bcd680559..640c33ed598 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -742,6 +742,7 @@ termtree = "0.5.1" textwrap = { version = "0.16.2", features = [ "terminal_size" ] } test-strategy = "0.4.3" thiserror = "2.0" +tinystr = { version = "0.8.1", features = ["serde"] } tofino = { git = "https://github.com/oxidecomputer/tofino", branch = "main" } tokio = "1.47.0" tokio-postgres = { version = "0.7", features = [ "with-chrono-0_4", "with-uuid-1" ] } diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index 05fc6fcb792..7fe8eb94a46 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -106,6 +106,7 @@ slog-term.workspace = true steno.workspace = true tempfile.workspace = true thiserror.workspace = true +tinystr.workspace = true tokio = { workspace = true, features = ["full"] } tokio-postgres = { workspace = true, features = ["with-serde_json-1"] } tokio-util = { workspace = true, features = ["codec", "rt"] } diff --git a/nexus/auth/Cargo.toml b/nexus/auth/Cargo.toml index ceef298ae9c..3258a174b48 100644 --- a/nexus/auth/Cargo.toml +++ b/nexus/auth/Cargo.toml @@ -43,6 +43,7 @@ nexus-types.workspace = true omicron-common.workspace = true omicron-uuid-kinds.workspace = true omicron-workspace-hack.workspace = true +tinystr.workspace = true [dev-dependencies] omicron-test-utils.workspace = true diff --git a/nexus/auth/src/authn/external/mod.rs b/nexus/auth/src/authn/external/mod.rs index f420b690673..8a7945f65aa 100644 --- a/nexus/auth/src/authn/external/mod.rs +++ b/nexus/auth/src/authn/external/mod.rs @@ -267,6 +267,7 @@ mod test { user_builtin_id: "1c91bab2-4841-669f-cc32-de80da5bbf39" .parse() .unwrap(), + user_name: "actor1", }; let grunt1 = Box::new(GruntScheme { name: name1, @@ -283,6 +284,7 @@ mod test { user_builtin_id: "799684af-533a-cb66-b5ac-ab55a791d5ef" .parse() .unwrap(), + user_name: "actor2", }; let grunt2 = Box::new(GruntScheme { name: name2, diff --git a/nexus/auth/src/authn/external/session_cookie.rs b/nexus/auth/src/authn/external/session_cookie.rs index d4b3b560983..09bc9fee37f 100644 --- a/nexus/auth/src/authn/external/session_cookie.rs +++ b/nexus/auth/src/authn/external/session_cookie.rs @@ -25,6 +25,7 @@ pub trait Session { fn id(&self) -> ConsoleSessionUuid; fn silo_user_id(&self) -> SiloUserUuid; fn silo_id(&self) -> Uuid; + fn silo_name(&self) -> &str; fn time_last_used(&self) -> DateTime; fn time_created(&self) -> DateTime; } @@ -128,6 +129,7 @@ where let actor = Actor::SiloUser { silo_user_id: session.silo_user_id(), silo_id: session.silo_id(), + silo_name: session.silo_name().parse().unwrap(), }; // if the session has gone unused for longer than idle_timeout, it is @@ -220,6 +222,7 @@ mod test { token: String, silo_user_id: SiloUserUuid, silo_id: Uuid, + silo_name: &'static str, time_created: DateTime, time_last_used: DateTime, } @@ -234,6 +237,9 @@ mod test { fn silo_id(&self) -> Uuid { self.silo_id } + fn silo_name(&self) -> &str { + self.silo_name + } fn time_created(&self) -> DateTime { self.time_created } @@ -335,6 +341,7 @@ mod test { token: "abc".to_string(), silo_user_id: SiloUserUuid::new_v4(), silo_id: Uuid::new_v4(), + silo_name: "test-silo", time_last_used: Utc::now() - Duration::hours(2), time_created: Utc::now() - Duration::hours(2), }]), @@ -361,6 +368,7 @@ mod test { token: "abc".to_string(), silo_user_id: SiloUserUuid::new_v4(), silo_id: Uuid::new_v4(), + silo_name: "test-silo", time_last_used: Utc::now(), time_created: Utc::now() - Duration::hours(20), }]), @@ -388,6 +396,7 @@ mod test { token: "abc".to_string(), silo_user_id: SiloUserUuid::new_v4(), silo_id: Uuid::new_v4(), + silo_name: "test-silo", time_last_used, time_created: Utc::now(), }]), diff --git a/nexus/auth/src/authn/external/spoof.rs b/nexus/auth/src/authn/external/spoof.rs index 8e68691c266..bc60fc3479a 100644 --- a/nexus/auth/src/authn/external/spoof.rs +++ b/nexus/auth/src/authn/external/spoof.rs @@ -63,6 +63,7 @@ static SPOOF_RESERVED_BAD_CREDS_ACTOR: LazyLock = user_builtin_id: "22222222-2222-2222-2222-222222222222" .parse() .unwrap(), + user_name: "spoof-user".parse().unwrap(), }); /// Complete HTTP header value to trigger the "bad actor" error @@ -108,7 +109,7 @@ where match ctx.silo_user_silo(silo_user_id).await { Err(error) => SchemeResult::Failed(error), Ok(silo_id) => { - let actor = Actor::SiloUser { silo_id, silo_user_id }; + let actor = Actor::SiloUser { silo_id, silo_user_id, silo_name: "spoof-user".parse().unwrap()}; SchemeResult::Authenticated(Details { actor }) } } diff --git a/nexus/auth/src/authn/mod.rs b/nexus/auth/src/authn/mod.rs index a2ef8e968ce..55a463b2567 100644 --- a/nexus/auth/src/authn/mod.rs +++ b/nexus/auth/src/authn/mod.rs @@ -50,6 +50,7 @@ use serde::Deserialize; use serde::Serialize; use std::collections::BTreeMap; use std::collections::BTreeSet; +use tinystr::TinyAsciiStr; use uuid::Uuid; /// Describes how the actor performing the current operation is authenticated @@ -169,40 +170,40 @@ impl Context { /// Returns an authenticated context for handling internal API contexts pub fn internal_api() -> Context { - Context::context_for_builtin_user(USER_INTERNAL_API.id) + Context::context_for_builtin_user(USER_INTERNAL_API.id, USER_INTERNAL_API.name.as_str()) } /// Returns an authenticated context for saga recovery pub fn internal_saga_recovery() -> Context { - Context::context_for_builtin_user(USER_SAGA_RECOVERY.id) + Context::context_for_builtin_user(USER_SAGA_RECOVERY.id, USER_SAGA_RECOVERY.name.as_str()) } /// Returns an authenticated context for use by internal resource allocation pub fn internal_read() -> Context { - Context::context_for_builtin_user(USER_INTERNAL_READ.id) + Context::context_for_builtin_user(USER_INTERNAL_READ.id, USER_INTERNAL_READ.name.as_str()) } /// Returns an authenticated context for use for authenticating external /// requests pub fn external_authn() -> Context { - Context::context_for_builtin_user(USER_EXTERNAL_AUTHN.id) + Context::context_for_builtin_user(USER_EXTERNAL_AUTHN.id, USER_EXTERNAL_AUTHN.name.as_str()) } /// Returns an authenticated context for Nexus-startup database /// initialization pub fn internal_db_init() -> Context { - Context::context_for_builtin_user(USER_DB_INIT.id) + Context::context_for_builtin_user(USER_DB_INIT.id, USER_DB_INIT.name.as_str()) } /// Returns an authenticated context for Nexus-driven service balancing. pub fn internal_service_balancer() -> Context { - Context::context_for_builtin_user(USER_SERVICE_BALANCER.id) + Context::context_for_builtin_user(USER_SERVICE_BALANCER.id, USER_SERVICE_BALANCER.name.as_str()) } - fn context_for_builtin_user(user_builtin_id: BuiltInUserUuid) -> Context { + fn context_for_builtin_user(user_builtin_id: BuiltInUserUuid, user_name: &'static str) -> Context { Context { kind: Kind::Authenticated( - Details { actor: Actor::UserBuiltin { user_builtin_id } }, + Details { actor: Actor::UserBuiltin { user_builtin_id, user_name: user_name.parse().unwrap() } }, None, ), schemes_tried: Vec::new(), @@ -219,6 +220,7 @@ impl Context { actor: Actor::SiloUser { silo_user_id: USER_TEST_PRIVILEGED.id(), silo_id: USER_TEST_PRIVILEGED.silo_id, + silo_name: "test-privileged".parse().unwrap(), }, }, Some(SiloAuthnPolicy::try_from(&*DEFAULT_SILO).unwrap()), @@ -234,6 +236,7 @@ impl Context { Context::for_test_user( USER_TEST_UNPRIVILEGED.id(), USER_TEST_UNPRIVILEGED.silo_id, + "test-unprivileged", SiloAuthnPolicy::try_from(&*DEFAULT_SILO).unwrap(), ) } @@ -243,11 +246,12 @@ impl Context { pub fn for_test_user( silo_user_id: SiloUserUuid, silo_id: Uuid, + silo_name: &'static str, silo_authn_policy: SiloAuthnPolicy, ) -> Context { Context { kind: Kind::Authenticated( - Details { actor: Actor::SiloUser { silo_user_id, silo_id } }, + Details { actor: Actor::SiloUser { silo_user_id, silo_id, silo_name: silo_name.parse().unwrap() } }, Some(silo_authn_policy), ), schemes_tried: Vec::new(), @@ -368,8 +372,15 @@ pub struct Details { /// Who is performing an operation #[derive(Clone, Copy, Deserialize, Eq, PartialEq, Serialize)] pub enum Actor { - UserBuiltin { user_builtin_id: BuiltInUserUuid }, - SiloUser { silo_user_id: SiloUserUuid, silo_id: Uuid }, + UserBuiltin { + user_builtin_id: BuiltInUserUuid, + user_name: TinyAsciiStr<32>, + }, + SiloUser { + silo_user_id: SiloUserUuid, + silo_id: Uuid, + silo_name: TinyAsciiStr<32>, + }, } impl Actor { @@ -389,7 +400,7 @@ impl Actor { pub fn built_in_user_id(&self) -> Option { match self { - Actor::UserBuiltin { user_builtin_id } => Some(*user_builtin_id), + Actor::UserBuiltin { user_builtin_id, .. } => Some(*user_builtin_id), Actor::SiloUser { .. } => None, } } @@ -416,14 +427,16 @@ impl std::fmt::Debug for Actor { // Do NOT include sensitive fields (e.g., private key or a bearer // token) in this output! match self { - Actor::UserBuiltin { user_builtin_id } => f + Actor::UserBuiltin { user_builtin_id, user_name } => f .debug_struct("Actor::UserBuiltin") .field("user_builtin_id", &user_builtin_id) + .field("user_name", &user_name) .finish_non_exhaustive(), - Actor::SiloUser { silo_user_id, silo_id } => f + Actor::SiloUser { silo_user_id, silo_id, silo_name } => f .debug_struct("Actor::SiloUser") .field("silo_user_id", &silo_user_id) .field("silo_id", &silo_id) + .field("silo_name", &silo_name) .finish_non_exhaustive(), } } @@ -434,6 +447,7 @@ impl std::fmt::Debug for Actor { pub struct ConsoleSessionWithSiloId { pub console_session: nexus_db_model::ConsoleSession, pub silo_id: Uuid, + pub silo_name: TinyAsciiStr<32>, } /// Label for a particular authentication scheme (used in log messages and diff --git a/nexus/auth/src/authz/actor.rs b/nexus/auth/src/authz/actor.rs index 26f7458b3b8..e791f3ddd2b 100644 --- a/nexus/auth/src/authz/actor.rs +++ b/nexus/auth/src/authz/actor.rs @@ -106,6 +106,7 @@ impl oso::PolarClass for AuthenticatedActor { AuthenticatedActor { actor: authn::Actor::UserBuiltin { user_builtin_id: authn::USER_DB_INIT.id, + user_name: authn::USER_DB_INIT.name.as_str().parse().unwrap(), }, roles: RoleSet::new(), silo_policy: None, @@ -116,6 +117,7 @@ impl oso::PolarClass for AuthenticatedActor { AuthenticatedActor { actor: authn::Actor::UserBuiltin { user_builtin_id: authn::USER_INTERNAL_API.id, + user_name: authn::USER_INTERNAL_API.name.as_str().parse().unwrap(), }, roles: RoleSet::new(), silo_policy: None, diff --git a/nexus/auth/src/context.rs b/nexus/auth/src/context.rs index 8f666cbb0e2..038c7cd74ca 100644 --- a/nexus/auth/src/context.rs +++ b/nexus/auth/src/context.rs @@ -133,17 +133,19 @@ impl OpContext { metadata.insert(String::from("actor"), format!("{:?}", actor)); match &actor { - authn::Actor::SiloUser { silo_user_id, silo_id } => { + authn::Actor::SiloUser { silo_user_id, silo_id, silo_name } => { log.new(o!( "authenticated" => true, "silo_user_id" => silo_user_id.to_string(), "silo_id" => silo_id.to_string(), + "silo_name" => silo_name.to_string(), )) } - authn::Actor::UserBuiltin { user_builtin_id } => log.new(o!( + authn::Actor::UserBuiltin { user_builtin_id, user_name } => log.new(o!( "authenticated" => true, "user_builtin_id" => user_builtin_id.to_string(), + "user_name" => user_name.to_string(), )), } } else { @@ -390,6 +392,9 @@ impl Session for ConsoleSessionWithSiloId { fn silo_id(&self) -> Uuid { self.silo_id } + fn silo_name(&self) -> &str { + self.silo_name.as_str() + } fn time_last_used(&self) -> DateTime { self.console_session.time_last_used } diff --git a/nexus/db-model/src/audit_log.rs b/nexus/db-model/src/audit_log.rs index 6541f64516e..abaa95ee8b5 100644 --- a/nexus/db-model/src/audit_log.rs +++ b/nexus/db-model/src/audit_log.rs @@ -22,8 +22,15 @@ use uuid::Uuid; /// Actor information for audit log initialization. Inspired by `authn::Actor` #[derive(Clone, Debug)] pub enum AuditLogActor { - UserBuiltin { user_builtin_id: BuiltInUserUuid }, - SiloUser { silo_user_id: SiloUserUuid, silo_id: Uuid }, + UserBuiltin { + user_builtin_id: BuiltInUserUuid, + user_name: String, + }, + SiloUser { + silo_user_id: SiloUserUuid, + silo_id: Uuid, + silo_name: String, + }, Unauthenticated, } @@ -109,6 +116,7 @@ pub struct AuditLogEntryInit { /// Actor kind indicating builtin user, silo user, or unauthenticated pub actor_kind: AuditLogActorKind, pub actor_id: Option, + pub actor_silo_name: Option, pub actor_silo_id: Option, /// API token or session cookie. Optional because it will not be defined @@ -128,19 +136,21 @@ impl From for AuditLogEntryInit { auth_method, } = params; - let (actor_id, actor_silo_id, actor_kind) = match actor { - AuditLogActor::UserBuiltin { user_builtin_id } => ( + let (actor_id, actor_silo_id, actor_silo_name, actor_kind) = match actor { + AuditLogActor::UserBuiltin { user_builtin_id, user_name } => ( Some(user_builtin_id.into_untyped_uuid()), None, + Some(user_name), AuditLogActorKind::UserBuiltin, ), - AuditLogActor::SiloUser { silo_user_id, silo_id } => ( + AuditLogActor::SiloUser { silo_user_id, silo_id, silo_name } => ( Some(silo_user_id.into_untyped_uuid()), Some(silo_id), + Some(silo_name), AuditLogActorKind::SiloUser, ), AuditLogActor::Unauthenticated => { - (None, None, AuditLogActorKind::Unauthenticated) + (None, None, None, AuditLogActorKind::Unauthenticated) } }; @@ -151,6 +161,7 @@ impl From for AuditLogEntryInit { request_uri, operation_id, actor_id, + actor_silo_name, actor_silo_id, actor_kind, source_ip: source_ip.into(), @@ -173,6 +184,7 @@ pub struct AuditLogEntry { pub source_ip: IpNetwork, pub user_agent: Option, pub actor_id: Option, + pub actor_silo_name: Option, pub actor_silo_id: Option, /// Actor kind indicating builtin user, silo user, or unauthenticated pub actor_kind: AuditLogActorKind, @@ -191,6 +203,8 @@ pub struct AuditLogEntry { pub error_code: Option, /// Always present if result is an error pub error_message: Option, + /// Optional because not present for all operations + pub resource_id: Option, } /// Struct that we can use as a kind of constructor arg for our actual audit @@ -200,6 +214,7 @@ pub struct AuditLogEntry { pub enum AuditLogCompletion { Success { http_status_code: u16, + resource_id: Option, }, Error { http_status_code: u16, @@ -225,18 +240,20 @@ pub struct AuditLogCompletionUpdate { pub http_status_code: Option, pub error_code: Option, pub error_message: Option, + pub resource_id: Option, } impl From for AuditLogCompletionUpdate { fn from(completion: AuditLogCompletion) -> Self { let time_completed = Utc::now(); match completion { - AuditLogCompletion::Success { http_status_code } => Self { + AuditLogCompletion::Success { http_status_code, resource_id, } => Self { time_completed, result_kind: AuditLogResultKind::Success, http_status_code: Some(SqlU16(http_status_code)), error_code: None, error_message: None, + resource_id, }, AuditLogCompletion::Error { http_status_code, @@ -248,6 +265,7 @@ impl From for AuditLogCompletionUpdate { http_status_code: Some(SqlU16(http_status_code)), error_code, error_message: Some(error_message), + resource_id: None, }, AuditLogCompletion::Timeout => Self { time_completed, @@ -255,6 +273,7 @@ impl From for AuditLogCompletionUpdate { http_status_code: None, error_code: None, error_message: None, + resource_id: None, }, } } @@ -274,6 +293,7 @@ impl TryFrom for views::AuditLogEntry { operation_id: entry.operation_id, source_ip: entry.source_ip.ip(), user_agent: entry.user_agent, + resource_id: entry.resource_id, actor: match entry.actor_kind { AuditLogActorKind::UserBuiltin => { let user_builtin_id = entry.actor_id.ok_or_else(|| { @@ -281,10 +301,16 @@ impl TryFrom for views::AuditLogEntry { "UserBuiltin actor missing actor_id", ) })?; + let user_name = entry.actor_silo_name.ok_or_else(|| { + Error::internal_error( + "UserBuiltin actor missing actor_silo_name", + ) + })?; views::AuditLogEntryActor::UserBuiltin { user_builtin_id: BuiltInUserUuid::from_untyped_uuid( user_builtin_id, ), + user_name, } } AuditLogActorKind::SiloUser => { @@ -296,11 +322,17 @@ impl TryFrom for views::AuditLogEntry { "SiloUser actor missing actor_silo_id", ) })?; + let silo_name = entry.actor_silo_name.ok_or_else(|| { + Error::internal_error( + "SiloUser actor missing actor_silo_name", + ) + })?; views::AuditLogEntryActor::SiloUser { silo_user_id: SiloUserUuid::from_untyped_uuid( silo_user_id, ), silo_id, + silo_name, } } AuditLogActorKind::Unauthenticated => { diff --git a/nexus/db-queries/src/db/datastore/audit_log.rs b/nexus/db-queries/src/db/datastore/audit_log.rs index 6f78a312cbf..f5fb8454131 100644 --- a/nexus/db-queries/src/db/datastore/audit_log.rs +++ b/nexus/db-queries/src/db/datastore/audit_log.rs @@ -189,7 +189,7 @@ mod tests { let t1 = Utc::now(); - let completion = AuditLogCompletion::Success { http_status_code: 201 }; + let completion = AuditLogCompletion::Success { http_status_code: 201, resource_id: None }; datastore .audit_log_entry_complete(opctx, &entry1, completion.into()) .await @@ -309,6 +309,7 @@ mod tests { let completion = AuditLogCompletionUpdate::from(AuditLogCompletion::Success { http_status_code: 201, + resource_id: None, }); let id1 = "1710a22e-b29b-4cfc-9e79-e8c93be187d7"; diff --git a/nexus/db-queries/src/db/datastore/console_session.rs b/nexus/db-queries/src/db/datastore/console_session.rs index 99cd9d93b24..8be7d985b31 100644 --- a/nexus/db-queries/src/db/datastore/console_session.rs +++ b/nexus/db-queries/src/db/datastore/console_session.rs @@ -139,6 +139,7 @@ impl DataStore { Ok(authn::ConsoleSessionWithSiloId { console_session, silo_id: db_silo_user.silo_id, + silo_name: "default".parse().unwrap(), }) } diff --git a/nexus/db-schema/src/schema.rs b/nexus/db-schema/src/schema.rs index 23d4aa089e5..9f0b6023fbf 100644 --- a/nexus/db-schema/src/schema.rs +++ b/nexus/db-schema/src/schema.rs @@ -2734,11 +2734,13 @@ table! { source_ip -> Inet, user_agent -> Nullable, actor_id -> Nullable, + actor_silo_name -> Nullable, actor_silo_id -> Nullable, actor_kind -> crate::enums::AuditLogActorKindEnum, auth_method -> Nullable, time_completed -> Nullable, http_status_code -> Nullable, // SqlU16 + resource_id -> Nullable, error_code -> Nullable, error_message -> Nullable, result_kind -> Nullable, @@ -2755,11 +2757,13 @@ table! { source_ip -> Inet, user_agent -> Nullable, actor_id -> Nullable, + actor_silo_name -> Nullable, actor_silo_id -> Nullable, actor_kind -> crate::enums::AuditLogActorKindEnum, auth_method -> Nullable, time_completed -> Timestamptz, http_status_code -> Nullable, // SqlU16 + resource_id -> Nullable, error_code -> Nullable, error_message -> Nullable, result_kind -> crate::enums::AuditLogResultKindEnum, diff --git a/nexus/src/app/audit_log.rs b/nexus/src/app/audit_log.rs index 654feade95b..c6e39fb7f86 100644 --- a/nexus/src/app/audit_log.rs +++ b/nexus/src/app/audit_log.rs @@ -54,15 +54,22 @@ impl super::Nexus { ) -> CreateResult { // for now, this conversion is pretty much 1-1 let actor = match opctx.authn.actor() { - Some(nexus_auth::authn::Actor::UserBuiltin { user_builtin_id }) => { - AuditLogActor::UserBuiltin { user_builtin_id: *user_builtin_id } + Some(nexus_auth::authn::Actor::UserBuiltin { + user_builtin_id, user_name + }) => { + AuditLogActor::UserBuiltin { + user_builtin_id: *user_builtin_id, + user_name: user_name.to_string(), + } } Some(nexus_auth::authn::Actor::SiloUser { silo_user_id, silo_id, + silo_name, }) => AuditLogActor::SiloUser { silo_user_id: *silo_user_id, silo_id: *silo_id, + silo_name: silo_name.to_string(), }, None => AuditLogActor::Unauthenticated, }; @@ -151,10 +158,14 @@ impl super::Nexus { opctx: &OpContext, entry: &AuditLogEntryInit, result: &Result, + resource_id: Option, ) -> UpdateResult<()> { let completion = match result { - Ok(response) => AuditLogCompletion::Success { - http_status_code: response.status_code().as_u16(), + Ok(response) => { + AuditLogCompletion::Success { + http_status_code: response.status_code().as_u16(), + resource_id: resource_id, + } }, Err(error) => AuditLogCompletion::Error { http_status_code: error.status_code.as_status().as_u16(), diff --git a/nexus/src/app/device_auth.rs b/nexus/src/app/device_auth.rs index 0a97c21cc07..b6a5c75ffea 100644 --- a/nexus/src/app/device_auth.rs +++ b/nexus/src/app/device_auth.rs @@ -226,7 +226,7 @@ impl super::Nexus { let now = Utc::now(); if time_expires < now { return Err(Reason::BadCredentials { - actor: Actor::SiloUser { silo_user_id, silo_id }, + actor: Actor::SiloUser { silo_user_id, silo_id, silo_name: "unknown".parse().unwrap() }, source: anyhow!( "token expired at {} (current time: {})", time_expires, @@ -236,7 +236,7 @@ impl super::Nexus { } } - Ok(Actor::SiloUser { silo_user_id, silo_id }) + Ok(Actor::SiloUser { silo_user_id, silo_id, silo_name: "unknown".parse().unwrap() }) } pub(crate) async fn device_access_token( diff --git a/nexus/src/app/session.rs b/nexus/src/app/session.rs index 53a6204b536..4d4833a562a 100644 --- a/nexus/src/app/session.rs +++ b/nexus/src/app/session.rs @@ -64,6 +64,7 @@ impl super::Nexus { Ok(authn::ConsoleSessionWithSiloId { console_session: db_session, silo_id: db_silo_user.silo_id, + silo_name: "default".parse().unwrap(), }) } diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index c0768d1a8d5..8fb0c9bf142 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -928,16 +928,21 @@ impl NexusExternalApi for NexusExternalApiImpl { let nexus = &apictx.context.nexus; let audit = nexus.audit_log_entry_init(&opctx, &rqctx).await?; - let result = async { + let result: Result, HttpError> = async { let project = nexus .project_create(&opctx, &new_project.into_inner()) .await?; - Ok(HttpResponseCreated(project.into())) + Ok(HttpResponseCreated::(project.into())) } .await; + let project_id: Option = match result.as_ref() { + Ok(&HttpResponseCreated(ref resource)) => Some(resource.identity.id.to_string()), + _ => None, + }; + let _ = - nexus.audit_log_entry_complete(&opctx, &audit, &result).await; + nexus.audit_log_entry_complete(&opctx, &audit, &result, project_id).await; result }; apictx @@ -993,7 +998,7 @@ impl NexusExternalApi for NexusExternalApiImpl { .await; let _ = - nexus.audit_log_entry_complete(&opctx, &audit, &result).await; + nexus.audit_log_entry_complete(&opctx, &audit, &result, None).await; result }; apictx @@ -1881,12 +1886,17 @@ impl NexusExternalApi for NexusExternalApiImpl { let disk = nexus .project_create_disk(&opctx, &project_lookup, ¶ms) .await?; - Ok(HttpResponseCreated(disk.into())) + Ok(HttpResponseCreated::(disk.into())) } .await; + let disk_id: Option = match result.as_ref() { + Ok(&HttpResponseCreated(ref resource)) => Some(resource.identity.id.to_string()), + _ => None, + }; + let _ = - nexus.audit_log_entry_complete(&opctx, &audit, &result).await; + nexus.audit_log_entry_complete(&opctx, &audit, &result, disk_id).await; result }; apictx @@ -1949,7 +1959,7 @@ impl NexusExternalApi for NexusExternalApiImpl { .await; let _ = - nexus.audit_log_entry_complete(&opctx, &audit, &result).await; + nexus.audit_log_entry_complete(&opctx, &audit, &result, None).await; result }; apictx @@ -2142,12 +2152,17 @@ impl NexusExternalApi for NexusExternalApiImpl { &new_instance_params, ) .await?; - Ok(HttpResponseCreated(instance.into())) + Ok(HttpResponseCreated::(instance.into())) } .await; + let instance_id: Option = match result.as_ref() { + Ok(&HttpResponseCreated(ref resource)) => Some(resource.identity.id.to_string()), + _ => None, + }; + let _ = - nexus.audit_log_entry_complete(&opctx, &audit, &result).await; + nexus.audit_log_entry_complete(&opctx, &audit, &result, instance_id).await; result }; apictx @@ -2219,7 +2234,7 @@ impl NexusExternalApi for NexusExternalApiImpl { .await; let _ = - nexus.audit_log_entry_complete(&opctx, &audit, &result).await; + nexus.audit_log_entry_complete(&opctx, &audit, &result, None).await; result }; apictx @@ -7981,6 +7996,8 @@ impl NexusExternalApi for NexusExternalApiImpl { let audit = nexus.audit_log_entry_init_unauthed(&opctx, &rqctx).await?; + + let mut session_id: Option = None; let result = async { let path_params = path_params.into_inner(); @@ -7992,6 +8009,8 @@ impl NexusExternalApi for NexusExternalApiImpl { &path_params.provider_name.into(), ) .await?; + + session_id = Some(session.id.to_string()); let mut response = http_response_see_other(next_url)?; { @@ -8011,7 +8030,7 @@ impl NexusExternalApi for NexusExternalApiImpl { .await; let _ = - nexus.audit_log_entry_complete(&opctx, &audit, &result).await; + nexus.audit_log_entry_complete(&opctx, &audit, &result, session_id).await; result }; @@ -8051,6 +8070,8 @@ impl NexusExternalApi for NexusExternalApiImpl { let opctx = nexus.opctx_external_authn(); let audit = nexus.audit_log_entry_init_unauthed(&opctx, &rqctx).await?; + + let mut session_id: Option = None; let result = async { let path = path_params.into_inner(); @@ -8063,6 +8084,7 @@ impl NexusExternalApi for NexusExternalApiImpl { .await?; let session = nexus.session_create(opctx, &user).await?; + session_id = Some(session.id.to_string()); let mut response = HttpResponseHeaders::new_unnamed( HttpResponseUpdatedNoContent(), ); @@ -8083,7 +8105,7 @@ impl NexusExternalApi for NexusExternalApiImpl { } .await; let _ = - nexus.audit_log_entry_complete(&opctx, &audit, &result).await; + nexus.audit_log_entry_complete(&opctx, &audit, &result, session_id).await; result }; apictx @@ -8356,7 +8378,7 @@ impl NexusExternalApi for NexusExternalApiImpl { .await; let _ = - nexus.audit_log_entry_complete(&opctx, &audit, &result).await; + nexus.audit_log_entry_complete(&opctx, &audit, &result, None).await; result }; apictx diff --git a/nexus/tests/integration_tests/audit_log.rs b/nexus/tests/integration_tests/audit_log.rs index dd828a072b3..ba57e2d4456 100644 --- a/nexus/tests/integration_tests/audit_log.rs +++ b/nexus/tests/integration_tests/audit_log.rs @@ -107,6 +107,7 @@ async fn test_audit_log_list(ctx: &ControlPlaneTestContext) { views::AuditLogEntryActor::SiloUser { silo_user_id: USER_TEST_PRIVILEGED.id(), silo_id: DEFAULT_SILO_ID, + silo_name: "default".to_string(), } ); @@ -151,6 +152,7 @@ async fn test_audit_log_list(ctx: &ControlPlaneTestContext) { views::AuditLogEntryActor::SiloUser { silo_user_id: me.user.id, silo_id: me.user.silo_id, + silo_name: "default".to_string(), } ); @@ -389,6 +391,7 @@ fn verify_entry( views::AuditLogEntryActor::SiloUser { silo_user_id: USER_TEST_PRIVILEGED.id(), silo_id: DEFAULT_SILO_ID, + silo_name: "default".to_string(), } ); assert_eq!(entry.source_ip.to_string(), "127.0.0.1"); diff --git a/nexus/tests/integration_tests/authn_http.rs b/nexus/tests/integration_tests/authn_http.rs index 7a50c137f61..9596b8e9d0b 100644 --- a/nexus/tests/integration_tests/authn_http.rs +++ b/nexus/tests/integration_tests/authn_http.rs @@ -110,6 +110,7 @@ async fn test_authn_session_cookie() { token: "valid".to_string(), silo_user_id: SiloUserUuid::new_v4(), silo_id: Uuid::new_v4(), + silo_name: "test-silo-valid", time_last_used: Utc::now() - Duration::seconds(5), time_created: Utc::now() - Duration::seconds(5), }; @@ -118,6 +119,7 @@ async fn test_authn_session_cookie() { token: "idle_expired".to_string(), silo_user_id: SiloUserUuid::new_v4(), silo_id: Uuid::new_v4(), + silo_name: "test-silo-idle-expired", time_last_used: Utc::now() - Duration::hours(2), time_created: Utc::now() - Duration::hours(3), }; @@ -126,6 +128,7 @@ async fn test_authn_session_cookie() { token: "abs_expired".to_string(), silo_user_id: SiloUserUuid::new_v4(), silo_id: Uuid::new_v4(), + silo_name: "test-silo-abs-expired", time_last_used: Utc::now(), time_created: Utc::now() - Duration::hours(10), }; @@ -349,6 +352,7 @@ struct FakeSession { token: String, silo_user_id: SiloUserUuid, silo_id: Uuid, + silo_name: &'static str, time_created: DateTime, time_last_used: DateTime, } @@ -363,6 +367,9 @@ impl session_cookie::Session for FakeSession { fn silo_id(&self) -> Uuid { self.silo_id } + fn silo_name(&self) -> &'static str { + self.silo_name + } fn time_created(&self) -> DateTime { self.time_created } @@ -437,7 +444,7 @@ async fn whoami_get( let actor = authn.actor().map(|actor| match actor { Actor::SiloUser { silo_user_id, .. } => silo_user_id.to_string(), - Actor::UserBuiltin { user_builtin_id } => user_builtin_id.to_string(), + Actor::UserBuiltin { user_builtin_id, user_name } => user_builtin_id.to_string(), }); let authenticated = actor.is_some(); let schemes_tried = diff --git a/nexus/tests/integration_tests/instances.rs b/nexus/tests/integration_tests/instances.rs index 9160dd6b4cd..fd2dd4fb182 100644 --- a/nexus/tests/integration_tests/instances.rs +++ b/nexus/tests/integration_tests/instances.rs @@ -6702,6 +6702,7 @@ async fn test_instance_create_in_silo(cptestctx: &ControlPlaneTestContext) { nexus_db_queries::authn::Context::for_test_user( user_id, silo.identity.id, + "default", nexus_db_queries::authn::SiloAuthnPolicy::try_from(&*DEFAULT_SILO) .unwrap(), ), diff --git a/nexus/types/src/external_api/views.rs b/nexus/types/src/external_api/views.rs index 24d61eac90b..d84b666ee97 100644 --- a/nexus/types/src/external_api/views.rs +++ b/nexus/types/src/external_api/views.rs @@ -1596,6 +1596,7 @@ pub enum AuditLogEntryActor { UserBuiltin { #[schemars(with = "Uuid")] user_builtin_id: BuiltInUserUuid, + user_name: String, }, SiloUser { @@ -1603,6 +1604,7 @@ pub enum AuditLogEntryActor { silo_user_id: SiloUserUuid, silo_id: Uuid, + silo_name: String, }, Unauthenticated, @@ -1660,6 +1662,9 @@ pub struct AuditLogEntry { pub actor: AuditLogEntryActor, + /// The ID of the created resource, if any + pub resource_id: Option, + /// How the user authenticated the request. Possible values are /// "session_cookie" and "access_token". Optional because it will not be /// defined on unauthenticated requests like login attempts. diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index 552d3cf3d0e..8acca264156 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -5853,6 +5853,7 @@ CREATE TABLE IF NOT EXISTS omicron.public.audit_log ( -- these are all null if the request is unauthenticated. actor_id can -- be present while silo ID is null if the user is built in (non-silo). actor_id UUID, + actor_silo_name STRING(63), actor_silo_id UUID, -- actor kind indicating builtin user, silo user, or unauthenticated actor_kind omicron.public.audit_log_actor_kind NOT NULL, @@ -5864,6 +5865,10 @@ CREATE TABLE IF NOT EXISTS omicron.public.audit_log ( time_completed TIMESTAMPTZ, http_status_code INT4, + -- the ID of the created resource, if any + -- TODO: This could be just 'id of affected resource' + resource_id STRING, + -- only present on errors error_code STRING, error_message STRING, @@ -5936,11 +5941,13 @@ SELECT source_ip, user_agent, actor_id, + actor_silo_name, actor_silo_id, actor_kind, auth_method, time_completed, http_status_code, + resource_id, error_code, error_message, result_kind