diff --git a/Cargo.lock b/Cargo.lock index 2f9aaf29541..1f7b2b7784c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6352,6 +6352,7 @@ dependencies = [ "nexus-types", "omicron-common", "omicron-rpaths", + "omicron-uuid-kinds", "omicron-workspace-hack", "pq-sys", "strum 0.27.2", @@ -8897,6 +8898,7 @@ dependencies = [ "hickory-resolver 0.25.2", "http", "hyper", + "omicron-uuid-kinds", "omicron-workspace-hack", "progenitor 0.10.0", "progenitor-client 0.10.0", diff --git a/clients/oxide-client/Cargo.toml b/clients/oxide-client/Cargo.toml index 8133a978023..0b85129ed1e 100644 --- a/clients/oxide-client/Cargo.toml +++ b/clients/oxide-client/Cargo.toml @@ -26,3 +26,4 @@ thiserror.workspace = true tokio = { workspace = true, features = [ "net" ] } uuid.workspace = true omicron-workspace-hack.workspace = true +omicron-uuid-kinds.workspace = true diff --git a/clients/oxide-client/src/lib.rs b/clients/oxide-client/src/lib.rs index 367c2b4adf1..d82edf79e6c 100644 --- a/clients/oxide-client/src/lib.rs +++ b/clients/oxide-client/src/lib.rs @@ -20,6 +20,12 @@ progenitor::generate_api!( spec = "../../openapi/nexus.json", interface = Builder, tags = Separate, + replace = { + TypedUuidForAlertReceiverKind = omicron_uuid_kinds::AlertReceiverUuid, + TypedUuidForAlertKind = omicron_uuid_kinds::AlertUuid, + TypedUuidForSiloUserKind = omicron_uuid_kinds::SiloUserUuid, + TypedUuidForSiloGroupKind = omicron_uuid_kinds::SiloGroupUuid, + }, ); /// Custom reqwest DNS resolver intended for use with the Oxide client diff --git a/nexus/auth/src/authn/external/mod.rs b/nexus/auth/src/authn/external/mod.rs index c1ec0b50a96..f420b690673 100644 --- a/nexus/auth/src/authn/external/mod.rs +++ b/nexus/auth/src/authn/external/mod.rs @@ -10,6 +10,7 @@ use crate::authn; use crate::probes; use async_trait::async_trait; use authn::Reason; +use omicron_uuid_kinds::SiloUserUuid; use slog::trace; use std::borrow::Borrow; use uuid::Uuid; @@ -153,7 +154,10 @@ pub enum SchemeResult { /// A context that can look up a Silo user's Silo. #[async_trait] pub trait SiloUserSilo { - async fn silo_user_silo(&self, silo_user_id: Uuid) -> Result; + async fn silo_user_silo( + &self, + silo_user_id: SiloUserUuid, + ) -> Result; } #[cfg(test)] diff --git a/nexus/auth/src/authn/external/session_cookie.rs b/nexus/auth/src/authn/external/session_cookie.rs index 94918776927..d4b3b560983 100644 --- a/nexus/auth/src/authn/external/session_cookie.rs +++ b/nexus/auth/src/authn/external/session_cookie.rs @@ -14,6 +14,7 @@ use dropshot::HttpError; use http::HeaderValue; use nexus_types::authn::cookies::parse_cookies; use omicron_uuid_kinds::ConsoleSessionUuid; +use omicron_uuid_kinds::SiloUserUuid; use slog::debug; use uuid::Uuid; @@ -22,7 +23,7 @@ use uuid::Uuid; pub trait Session { fn id(&self) -> ConsoleSessionUuid; - fn silo_user_id(&self) -> Uuid; + fn silo_user_id(&self) -> SiloUserUuid; fn silo_id(&self) -> Uuid; fn time_last_used(&self) -> DateTime; fn time_created(&self) -> DateTime; @@ -202,6 +203,7 @@ mod test { use chrono::{DateTime, Duration, Utc}; use http; use omicron_uuid_kinds::ConsoleSessionUuid; + use omicron_uuid_kinds::SiloUserUuid; use slog; use std::sync::Mutex; use uuid::Uuid; @@ -216,7 +218,7 @@ mod test { struct FakeSession { id: ConsoleSessionUuid, token: String, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, silo_id: Uuid, time_created: DateTime, time_last_used: DateTime, @@ -226,7 +228,7 @@ mod test { fn id(&self) -> ConsoleSessionUuid { self.id } - fn silo_user_id(&self) -> Uuid { + fn silo_user_id(&self) -> SiloUserUuid { self.silo_user_id } fn silo_id(&self) -> Uuid { @@ -331,7 +333,7 @@ mod test { sessions: Mutex::new(vec![FakeSession { id: ConsoleSessionUuid::new_v4(), token: "abc".to_string(), - silo_user_id: Uuid::new_v4(), + silo_user_id: SiloUserUuid::new_v4(), silo_id: Uuid::new_v4(), time_last_used: Utc::now() - Duration::hours(2), time_created: Utc::now() - Duration::hours(2), @@ -357,7 +359,7 @@ mod test { sessions: Mutex::new(vec![FakeSession { id: ConsoleSessionUuid::new_v4(), token: "abc".to_string(), - silo_user_id: Uuid::new_v4(), + silo_user_id: SiloUserUuid::new_v4(), silo_id: Uuid::new_v4(), time_last_used: Utc::now(), time_created: Utc::now() - Duration::hours(20), @@ -384,7 +386,7 @@ mod test { sessions: Mutex::new(vec![FakeSession { id: ConsoleSessionUuid::new_v4(), token: "abc".to_string(), - silo_user_id: Uuid::new_v4(), + silo_user_id: SiloUserUuid::new_v4(), silo_id: Uuid::new_v4(), 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 7aba530e63c..8e68691c266 100644 --- a/nexus/auth/src/authn/external/spoof.rs +++ b/nexus/auth/src/authn/external/spoof.rs @@ -18,8 +18,9 @@ use anyhow::anyhow; use async_trait::async_trait; use headers::HeaderMapExt; use headers::authorization::{Authorization, Bearer}; +use omicron_uuid_kinds::SiloUserUuid; use slog::debug; -use uuid::Uuid; +use std::str::FromStr; // This scheme is intended for demos, development, and testing until we have a // more automatable identity provider that can be used for those purposes. @@ -118,7 +119,7 @@ where fn authn_spoof_parse_id( raw_value: Option<&Authorization>, -) -> Result, Reason> { +) -> Result, Reason> { let token = match raw_value { None => return Ok(None), Some(bearer) => bearer.token(), @@ -142,7 +143,7 @@ fn authn_spoof_parse_id( }); } - Uuid::parse_str(str_value) + SiloUserUuid::from_str(str_value) .context("parsing header value as UUID") .map(|silo_user_id| Some(silo_user_id)) .map_err(|source| Reason::BadFormat { source }) @@ -150,7 +151,7 @@ fn authn_spoof_parse_id( /// Returns a value of the `Authorization` header for this actor that will be /// accepted using this scheme -pub fn make_header_value(id: Uuid) -> Authorization { +pub fn make_header_value(id: T) -> Authorization { make_header_value_str(&id.to_string()).unwrap() } @@ -193,6 +194,7 @@ mod test { use headers::HeaderMapExt; use headers::authorization::Bearer; use headers::authorization::Credentials; + use omicron_uuid_kinds::SiloUserUuid; use uuid::Uuid; #[test] @@ -243,14 +245,14 @@ mod test { #[test] fn test_spoof_header_valid() { let test_uuid_str = "37b56e4f-8c60-453b-a37e-99be6efe8a89"; - let test_uuid = test_uuid_str.parse::().unwrap(); + let test_uuid = test_uuid_str.parse::().unwrap(); let test_header = make_header_value(test_uuid); // Success case: the client provided a valid uuid in the header. let success_case = authn_spoof_parse_id(Some(&test_header)); match success_case { - Ok(Some(actor_id)) => { - assert_eq!(actor_id, test_uuid); + Ok(Some(silo_user_id)) => { + assert_eq!(silo_user_id, test_uuid); } _ => { assert!(false); diff --git a/nexus/auth/src/authn/mod.rs b/nexus/auth/src/authn/mod.rs index c79ea352ef7..a2ef8e968ce 100644 --- a/nexus/auth/src/authn/mod.rs +++ b/nexus/auth/src/authn/mod.rs @@ -44,6 +44,8 @@ use nexus_types::external_api::shared::FleetRole; use nexus_types::external_api::shared::SiloRole; use nexus_types::identity::Asset; use omicron_common::api::external::LookupType; +use omicron_uuid_kinds::BuiltInUserUuid; +use omicron_uuid_kinds::SiloUserUuid; use serde::Deserialize; use serde::Serialize; use std::collections::BTreeMap; @@ -197,7 +199,7 @@ impl Context { Context::context_for_builtin_user(USER_SERVICE_BALANCER.id) } - fn context_for_builtin_user(user_builtin_id: Uuid) -> Context { + fn context_for_builtin_user(user_builtin_id: BuiltInUserUuid) -> Context { Context { kind: Kind::Authenticated( Details { actor: Actor::UserBuiltin { user_builtin_id } }, @@ -239,7 +241,7 @@ impl Context { /// Returns an authenticated context for the specific Silo user. Not marked /// as #[cfg(test)] so that this is available in integration tests. pub fn for_test_user( - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, silo_id: Uuid, silo_authn_policy: SiloAuthnPolicy, ) -> Context { @@ -311,35 +313,35 @@ mod test { // The privileges are (or will be) verified in authz tests. let authn = Context::privileged_test_user(); let actor = authn.actor().unwrap(); - assert_eq!(actor.actor_id(), USER_TEST_PRIVILEGED.id()); + assert_eq!(actor.silo_user_id(), Some(USER_TEST_PRIVILEGED.id())); let authn = Context::unprivileged_test_user(); let actor = authn.actor().unwrap(); - assert_eq!(actor.actor_id(), USER_TEST_UNPRIVILEGED.id()); + assert_eq!(actor.silo_user_id(), Some(USER_TEST_UNPRIVILEGED.id())); let authn = Context::internal_read(); let actor = authn.actor().unwrap(); - assert_eq!(actor.actor_id(), USER_INTERNAL_READ.id); + assert_eq!(actor.built_in_user_id(), Some(USER_INTERNAL_READ.id)); let authn = Context::external_authn(); let actor = authn.actor().unwrap(); - assert_eq!(actor.actor_id(), USER_EXTERNAL_AUTHN.id); + assert_eq!(actor.built_in_user_id(), Some(USER_EXTERNAL_AUTHN.id)); let authn = Context::internal_db_init(); let actor = authn.actor().unwrap(); - assert_eq!(actor.actor_id(), USER_DB_INIT.id); + assert_eq!(actor.built_in_user_id(), Some(USER_DB_INIT.id)); let authn = Context::internal_service_balancer(); let actor = authn.actor().unwrap(); - assert_eq!(actor.actor_id(), USER_SERVICE_BALANCER.id); + assert_eq!(actor.built_in_user_id(), Some(USER_SERVICE_BALANCER.id)); let authn = Context::internal_saga_recovery(); let actor = authn.actor().unwrap(); - assert_eq!(actor.actor_id(), USER_SAGA_RECOVERY.id); + assert_eq!(actor.built_in_user_id(), Some(USER_SAGA_RECOVERY.id)); let authn = Context::internal_api(); let actor = authn.actor().unwrap(); - assert_eq!(actor.actor_id(), USER_INTERNAL_API.id); + assert_eq!(actor.built_in_user_id(), Some(USER_INTERNAL_API.id)); } } @@ -366,18 +368,11 @@ pub struct Details { /// Who is performing an operation #[derive(Clone, Copy, Deserialize, Eq, PartialEq, Serialize)] pub enum Actor { - UserBuiltin { user_builtin_id: Uuid }, - SiloUser { silo_user_id: Uuid, silo_id: Uuid }, + UserBuiltin { user_builtin_id: BuiltInUserUuid }, + SiloUser { silo_user_id: SiloUserUuid, silo_id: Uuid }, } impl Actor { - pub fn actor_id(&self) -> Uuid { - match self { - Actor::UserBuiltin { user_builtin_id, .. } => *user_builtin_id, - Actor::SiloUser { silo_user_id, .. } => *silo_user_id, - } - } - pub fn silo_id(&self) -> Option { match self { Actor::UserBuiltin { .. } => None, @@ -385,12 +380,19 @@ impl Actor { } } - pub fn silo_user_id(&self) -> Option { + pub fn silo_user_id(&self) -> Option { match self { Actor::UserBuiltin { .. } => None, Actor::SiloUser { silo_user_id, .. } => Some(*silo_user_id), } } + + pub fn built_in_user_id(&self) -> Option { + match self { + Actor::UserBuiltin { user_builtin_id } => Some(*user_builtin_id), + Actor::SiloUser { .. } => None, + } + } } impl From<&Actor> for nexus_db_model::IdentityType { diff --git a/nexus/auth/src/authz/actor.rs b/nexus/auth/src/authz/actor.rs index f1ce2695ac5..26f7458b3b8 100644 --- a/nexus/auth/src/authz/actor.rs +++ b/nexus/auth/src/authz/actor.rs @@ -38,8 +38,7 @@ impl oso::PolarClass for AnyActor { }) .add_attribute_getter("authn_actor", |a: &AnyActor| { a.actor.map(|actor| AuthenticatedActor { - actor_id: actor.actor_id(), - silo_id: actor.silo_id(), + actor, roles: a.roles.clone(), silo_policy: a.silo_policy.clone(), }) @@ -50,8 +49,7 @@ impl oso::PolarClass for AnyActor { /// Represents an authenticated [`authn::Context`] for Polar #[derive(Clone, Debug)] pub struct AuthenticatedActor { - actor_id: Uuid, - silo_id: Option, + actor: authn::Actor, roles: RoleSet, silo_policy: Option, } @@ -94,7 +92,7 @@ impl AuthenticatedActor { impl PartialEq for AuthenticatedActor { fn eq(&self, other: &Self) -> bool { - self.actor_id == other.actor_id + self.actor == other.actor } } @@ -106,8 +104,9 @@ impl oso::PolarClass for AuthenticatedActor { .with_equality_check() .add_constant( AuthenticatedActor { - actor_id: authn::USER_DB_INIT.id, - silo_id: None, + actor: authn::Actor::UserBuiltin { + user_builtin_id: authn::USER_DB_INIT.id, + }, roles: RoleSet::new(), silo_policy: None, }, @@ -115,21 +114,26 @@ impl oso::PolarClass for AuthenticatedActor { ) .add_constant( AuthenticatedActor { - actor_id: authn::USER_INTERNAL_API.id, - silo_id: None, + actor: authn::Actor::UserBuiltin { + user_builtin_id: authn::USER_INTERNAL_API.id, + }, roles: RoleSet::new(), silo_policy: None, }, "USER_INTERNAL_API", ) .add_attribute_getter("silo", |a: &AuthenticatedActor| { - a.silo_id.map(|silo_id| { - super::Silo::new( - super::FLEET, - silo_id, - LookupType::ById(silo_id), - ) - }) + match a.actor { + authn::Actor::SiloUser { silo_id, .. } => { + Some(super::Silo::new( + super::FLEET, + silo_id, + LookupType::ById(silo_id), + )) + } + + authn::Actor::UserBuiltin { .. } => None, + } }) .add_method( "confers_fleet_role", @@ -139,7 +143,13 @@ impl oso::PolarClass for AuthenticatedActor { ) .add_method( "equals_silo_user", - |a: &AuthenticatedActor, u: SiloUser| a.actor_id == u.id(), + |a: &AuthenticatedActor, u: SiloUser| match a.actor { + authn::Actor::SiloUser { silo_user_id, .. } => { + silo_user_id == u.id() + } + + authn::Actor::UserBuiltin { .. } => false, + }, ) } } diff --git a/nexus/auth/src/authz/api_resources.rs b/nexus/auth/src/authz/api_resources.rs index e9cdf0999b4..c660d142cd2 100644 --- a/nexus/auth/src/authz/api_resources.rs +++ b/nexus/auth/src/authz/api_resources.rs @@ -1184,7 +1184,7 @@ authz_resource! { authz_resource! { name = "UserBuiltin", parent = "Fleet", - primary_key = Uuid, + primary_key = { uuid_kind = BuiltInUserKind }, roles_allowed = false, polar_snippet = FleetChild, } @@ -1212,7 +1212,7 @@ impl ApiResourceWithRolesType for Silo { authz_resource! { name = "SiloUser", parent = "Silo", - primary_key = Uuid, + primary_key = { uuid_kind = SiloUserKind }, roles_allowed = false, polar_snippet = Custom, } @@ -1220,7 +1220,7 @@ authz_resource! { authz_resource! { name = "SiloGroup", parent = "Silo", - primary_key = Uuid, + primary_key = { uuid_kind = SiloGroupKind }, roles_allowed = false, polar_snippet = Custom, } diff --git a/nexus/auth/src/authz/roles.rs b/nexus/auth/src/authz/roles.rs index 0716e05bc71..114da7e3baf 100644 --- a/nexus/auth/src/authz/roles.rs +++ b/nexus/auth/src/authz/roles.rs @@ -39,6 +39,7 @@ use crate::authn; use crate::context::OpContext; use omicron_common::api::external::Error; use omicron_common::api::external::ResourceType; +use omicron_uuid_kinds::GenericUuid; use slog::trace; use std::collections::BTreeSet; use uuid::Uuid; @@ -160,7 +161,14 @@ async fn load_directly_attached_roles( .role_asgn_list_for( opctx, actor.into(), - actor.actor_id(), + match &actor { + authn::Actor::SiloUser { silo_user_id, .. } => { + silo_user_id.into_untyped_uuid() + } + authn::Actor::UserBuiltin { user_builtin_id, .. } => { + user_builtin_id.into_untyped_uuid() + } + }, resource_type, resource_id, ) diff --git a/nexus/auth/src/context.rs b/nexus/auth/src/context.rs index 53930a47b00..8f666cbb0e2 100644 --- a/nexus/auth/src/context.rs +++ b/nexus/auth/src/context.rs @@ -13,6 +13,7 @@ use crate::storage::Storage; use chrono::{DateTime, Utc}; use omicron_common::api::external::Error; use omicron_uuid_kinds::ConsoleSessionUuid; +use omicron_uuid_kinds::SiloUserUuid; use slog::debug; use slog::o; use slog::trace; @@ -127,14 +128,24 @@ impl OpContext { let mut metadata = BTreeMap::new(); let log = if let Some(actor) = authn.actor() { - let actor_id = actor.actor_id(); metadata .insert(String::from("authenticated"), String::from("true")); metadata.insert(String::from("actor"), format!("{:?}", actor)); - log.new( - o!("authenticated" => true, "actor_id" => actor_id.to_string()), - ) + match &actor { + authn::Actor::SiloUser { silo_user_id, silo_id } => { + log.new(o!( + "authenticated" => true, + "silo_user_id" => silo_user_id.to_string(), + "silo_id" => silo_id.to_string(), + )) + } + + authn::Actor::UserBuiltin { user_builtin_id } => log.new(o!( + "authenticated" => true, + "user_builtin_id" => user_builtin_id.to_string(), + )), + } } else { metadata .insert(String::from("authenticated"), String::from("false")); @@ -373,9 +384,8 @@ impl Session for ConsoleSessionWithSiloId { fn id(&self) -> ConsoleSessionUuid { self.console_session.id() } - - fn silo_user_id(&self) -> Uuid { - self.console_session.silo_user_id + fn silo_user_id(&self) -> SiloUserUuid { + self.console_session.silo_user_id() } fn silo_id(&self) -> Uuid { self.silo_id diff --git a/nexus/db-fixed-data/Cargo.toml b/nexus/db-fixed-data/Cargo.toml index a6ac298452c..c9d9c9c8517 100644 --- a/nexus/db-fixed-data/Cargo.toml +++ b/nexus/db-fixed-data/Cargo.toml @@ -21,3 +21,4 @@ nexus-db-model.workspace = true nexus-types.workspace = true omicron-common.workspace = true omicron-workspace-hack.workspace = true +omicron-uuid-kinds.workspace = true diff --git a/nexus/db-fixed-data/src/lib.rs b/nexus/db-fixed-data/src/lib.rs index 4a42b696968..018050ab78c 100644 --- a/nexus/db-fixed-data/src/lib.rs +++ b/nexus/db-fixed-data/src/lib.rs @@ -66,6 +66,18 @@ fn assert_valid_uuid(id: &uuid::Uuid) { }; } +#[cfg(test)] +use omicron_uuid_kinds::GenericUuid; +#[cfg(test)] +use omicron_uuid_kinds::TypedUuid; +#[cfg(test)] +use omicron_uuid_kinds::TypedUuidKind; + +#[cfg(test)] +fn assert_valid_typed_uuid(id: &TypedUuid) { + assert_valid_uuid(&id.into_untyped_uuid()); +} + #[cfg(test)] mod test { use super::FLEET_ID; diff --git a/nexus/db-fixed-data/src/role_assignment.rs b/nexus/db-fixed-data/src/role_assignment.rs index 9b676adcf98..aae06d4b7ae 100644 --- a/nexus/db-fixed-data/src/role_assignment.rs +++ b/nexus/db-fixed-data/src/role_assignment.rs @@ -5,7 +5,6 @@ use super::FLEET_ID; use super::user_builtin; -use nexus_db_model::IdentityType; use nexus_db_model::RoleAssignment; use omicron_common::api::external::ResourceType; use std::sync::LazyLock; @@ -17,8 +16,7 @@ pub static BUILTIN_ROLE_ASSIGNMENTS: LazyLock> = // This is a pretty elevated privilege. // TODO-security We should scope this down (or, really, figure out a // better internal authn/authz story). - RoleAssignment::new( - IdentityType::UserBuiltin, + RoleAssignment::new_for_builtin_user( user_builtin::USER_INTERNAL_API.id, ResourceType::Fleet, *FLEET_ID, @@ -30,8 +28,7 @@ pub static BUILTIN_ROLE_ASSIGNMENTS: LazyLock> = // This is necessary as services exist as resources implied by // "FLEET" - if they ever become more fine-grained, this scope // could also become smaller. - RoleAssignment::new( - IdentityType::UserBuiltin, + RoleAssignment::new_for_builtin_user( user_builtin::USER_SERVICE_BALANCER.id, ResourceType::Fleet, *FLEET_ID, @@ -41,8 +38,7 @@ pub static BUILTIN_ROLE_ASSIGNMENTS: LazyLock> = // Fleet. This will grant them the ability to read various control // plane data (like the list of sleds), which is in turn used to // talk to sleds or allocate resources. - RoleAssignment::new( - IdentityType::UserBuiltin, + RoleAssignment::new_for_builtin_user( user_builtin::USER_INTERNAL_READ.id, ResourceType::Fleet, *FLEET_ID, @@ -51,8 +47,7 @@ pub static BUILTIN_ROLE_ASSIGNMENTS: LazyLock> = // The "external-authenticator" user gets the // "external-authenticator" role on the sole fleet. This grants // them the ability to create sessions. - RoleAssignment::new( - IdentityType::UserBuiltin, + RoleAssignment::new_for_builtin_user( user_builtin::USER_EXTERNAL_AUTHN.id, ResourceType::Fleet, *FLEET_ID, diff --git a/nexus/db-fixed-data/src/silo_user.rs b/nexus/db-fixed-data/src/silo_user.rs index 13e5680a19c..38252826f37 100644 --- a/nexus/db-fixed-data/src/silo_user.rs +++ b/nexus/db-fixed-data/src/silo_user.rs @@ -29,15 +29,13 @@ pub static ROLE_ASSIGNMENTS_PRIVILEGED: LazyLock> = vec![ // The "test-privileged" user gets the "admin" role on the sole // Fleet as well as the default Silo. - model::RoleAssignment::new( - model::IdentityType::SiloUser, + model::RoleAssignment::new_for_silo_user( USER_TEST_PRIVILEGED.id(), ResourceType::Fleet, *crate::FLEET_ID, "admin", ), - model::RoleAssignment::new( - model::IdentityType::SiloUser, + model::RoleAssignment::new_for_silo_user( USER_TEST_PRIVILEGED.id(), ResourceType::Silo, DEFAULT_SILO_ID, @@ -62,14 +60,14 @@ pub static USER_TEST_UNPRIVILEGED: LazyLock = #[cfg(test)] mod test { - use super::super::assert_valid_uuid; + use super::super::assert_valid_typed_uuid; use super::USER_TEST_PRIVILEGED; use super::USER_TEST_UNPRIVILEGED; use nexus_types::identity::Asset; #[test] fn test_silo_user_ids_are_valid() { - assert_valid_uuid(&USER_TEST_PRIVILEGED.id()); - assert_valid_uuid(&USER_TEST_UNPRIVILEGED.id()); + assert_valid_typed_uuid(&USER_TEST_PRIVILEGED.id()); + assert_valid_typed_uuid(&USER_TEST_UNPRIVILEGED.id()); } } diff --git a/nexus/db-fixed-data/src/user_builtin.rs b/nexus/db-fixed-data/src/user_builtin.rs index 08236af5db3..1194fe23a53 100644 --- a/nexus/db-fixed-data/src/user_builtin.rs +++ b/nexus/db-fixed-data/src/user_builtin.rs @@ -4,11 +4,11 @@ //! Built-in users use omicron_common::api; +use omicron_uuid_kinds::BuiltInUserUuid; use std::sync::LazyLock; -use uuid::Uuid; pub struct UserBuiltinConfig { - pub id: Uuid, + pub id: BuiltInUserUuid, pub name: api::external::Name, pub description: &'static str, } @@ -20,7 +20,9 @@ impl UserBuiltinConfig { description: &'static str, ) -> UserBuiltinConfig { UserBuiltinConfig { - id: id.parse().expect("invalid uuid for builtin user id"), + id: id + .parse() + .expect("invalid built-in user uuid for builtin user id"), name: name.parse().expect("invalid name for builtin user name"), description, } @@ -94,7 +96,7 @@ pub static USER_EXTERNAL_AUTHN: LazyLock = #[cfg(test)] mod test { - use super::super::assert_valid_uuid; + use super::super::assert_valid_typed_uuid; use super::USER_DB_INIT; use super::USER_EXTERNAL_AUTHN; use super::USER_INTERNAL_API; @@ -104,11 +106,11 @@ mod test { #[test] fn test_builtin_user_ids_are_valid() { - assert_valid_uuid(&USER_SERVICE_BALANCER.id); - assert_valid_uuid(&USER_DB_INIT.id); - assert_valid_uuid(&USER_INTERNAL_API.id); - assert_valid_uuid(&USER_EXTERNAL_AUTHN.id); - assert_valid_uuid(&USER_INTERNAL_READ.id); - assert_valid_uuid(&USER_SAGA_RECOVERY.id); + assert_valid_typed_uuid(&USER_SERVICE_BALANCER.id); + assert_valid_typed_uuid(&USER_DB_INIT.id); + assert_valid_typed_uuid(&USER_INTERNAL_API.id); + assert_valid_typed_uuid(&USER_EXTERNAL_AUTHN.id); + assert_valid_typed_uuid(&USER_INTERNAL_READ.id); + assert_valid_typed_uuid(&USER_SAGA_RECOVERY.id); } } diff --git a/nexus/db-lookup/src/lookup.rs b/nexus/db-lookup/src/lookup.rs index b326491ca0a..3e8dc2e99f1 100644 --- a/nexus/db-lookup/src/lookup.rs +++ b/nexus/db-lookup/src/lookup.rs @@ -17,6 +17,7 @@ use async_bb8_diesel::AsyncRunQueryDsl; use db_macros::lookup_resource; use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; use ipnetwork::IpNetwork; +use nexus_auth::authn; use nexus_auth::authz; use nexus_auth::context::OpContext; use nexus_db_errors::{ErrorHandler, public_error_from_diesel}; @@ -26,17 +27,7 @@ use nexus_types::identity::Resource; use omicron_common::api::external::Error; use omicron_common::api::external::InternalContext; use omicron_common::api::external::{LookupResult, LookupType, ResourceType}; -use omicron_uuid_kinds::AccessTokenKind; -use omicron_uuid_kinds::AlertReceiverUuid; -use omicron_uuid_kinds::AlertUuid; -use omicron_uuid_kinds::ConsoleSessionUuid; -use omicron_uuid_kinds::PhysicalDiskUuid; -use omicron_uuid_kinds::SupportBundleUuid; -use omicron_uuid_kinds::TufArtifactKind; -use omicron_uuid_kinds::TufRepoKind; -use omicron_uuid_kinds::TufTrustRootUuid; -use omicron_uuid_kinds::TypedUuid; -use omicron_uuid_kinds::WebhookSecretUuid; +use omicron_uuid_kinds::*; use slog::{error, trace}; use uuid::Uuid; @@ -257,12 +248,29 @@ impl<'a> LookupPath<'a> { } /// Select a resource of type SiloUser, identified by its id - pub fn silo_user_id(self, id: Uuid) -> SiloUser<'a> { + pub fn silo_user_id(self, id: SiloUserUuid) -> SiloUser<'a> { SiloUser::PrimaryKey(Root { lookup_root: self }, id) } + /// Select a resource of type SiloUser that matches an authenticated Actor + pub fn silo_user_actor( + self, + actor: &'a authn::Actor, + ) -> Result, Error> { + match actor.silo_user_id() { + Some(silo_user_id) => Ok(SiloUser::PrimaryKey( + Root { lookup_root: self }, + silo_user_id, + )), + + None => Err(Error::non_resourcetype_not_found( + "could not find silo user", + )), + } + } + /// Select a resource of type SiloGroup, identified by its id - pub fn silo_group_id(self, id: Uuid) -> SiloGroup<'a> { + pub fn silo_group_id(self, id: SiloGroupUuid) -> SiloGroup<'a> { SiloGroup::PrimaryKey(Root { lookup_root: self }, id) } @@ -368,8 +376,8 @@ impl<'a> LookupPath<'a> { TufArtifact::PrimaryKey(Root { lookup_root: self }, id) } - /// Select a resource of type UserBuiltin, identified by its `name` - pub fn user_builtin_id<'b>(self, id: Uuid) -> UserBuiltin<'b> + /// Select a resource of type UserBuiltin, identified by its `id` + pub fn user_builtin_id<'b>(self, id: BuiltInUserUuid) -> UserBuiltin<'b> where 'a: 'b, { @@ -532,7 +540,7 @@ lookup_resource! { ancestors = [ "Silo" ], lookup_by_name = false, soft_deletes = true, - primary_key_columns = [ { column_name = "id", rust_type = Uuid } ], + primary_key_columns = [ { column_name = "id", uuid_kind = SiloUserKind } ], visible_outside_silo = true } @@ -541,7 +549,7 @@ lookup_resource! { ancestors = [ "Silo" ], lookup_by_name = false, soft_deletes = true, - primary_key_columns = [ { column_name = "id", rust_type = Uuid } ] + primary_key_columns = [ { column_name = "id", uuid_kind = SiloGroupKind } ] } lookup_resource! { @@ -584,7 +592,7 @@ lookup_resource! { lookup_resource! { name = "SshKey", - ancestors = [ "Silo", "SiloUser" ], + ancestors = [ "Silo", "SiloUser*" ], lookup_by_name = true, soft_deletes = true, primary_key_columns = [ { column_name = "id", rust_type = Uuid } ] @@ -841,7 +849,7 @@ lookup_resource! { ancestors = [], lookup_by_name = true, soft_deletes = false, - primary_key_columns = [ { column_name = "id", rust_type = Uuid } ] + primary_key_columns = [ { column_name = "id", uuid_kind = BuiltInUserKind } ] } lookup_resource! { diff --git a/nexus/db-macros/src/lookup.rs b/nexus/db-macros/src/lookup.rs index 120a81949f2..d2c4868277f 100644 --- a/nexus/db-macros/src/lookup.rs +++ b/nexus/db-macros/src/lookup.rs @@ -33,6 +33,9 @@ pub struct Input { /// ordered list of resources that are ancestors of this resource, starting /// with the top of the hierarchy /// (e.g., for an Instance, this would be `[ "Silo", "Project" ]` + /// + /// if the resource name has a * as the last character, the primary key is a + /// typed uuid. ancestors: Vec, /// whether lookup by name is supported (usually within the parent collection) lookup_by_name: bool, @@ -138,22 +141,22 @@ impl Config { fn for_input(input: Input) -> syn::Result { let resource = Resource::for_name(&input.name); + let ancestors: Vec = + input.ancestors.iter().map(|s| Resource::for_name(s)).collect(); + let mut path_types: Vec<_> = - input.ancestors.iter().map(|a| format_ident!("{}", a)).collect(); + ancestors.iter().map(|r| format_ident!("{}", r.name)).collect(); path_types.push(resource.name.clone()); - let mut path_authz_names: Vec<_> = input - .ancestors + let mut path_authz_names: Vec<_> = ancestors .iter() - .map(|a| { - format_ident!("authz_{}", heck::AsSnakeCase(&a).to_string()) - }) + .map(|r| format_ident!("{}", r.authz_name)) .collect(); path_authz_names.push(resource.authz_name.clone()); - let parent = input.ancestors.last().map(|s| Resource::for_name(s)); + let parent = ancestors.last().cloned(); let silo_restricted = !input.visible_outside_silo - && input.ancestors.iter().any(|s| s == "Silo"); + && ancestors.iter().any(|r| r.name == "Silo"); let primary_key_columns: Vec<_> = input .primary_key_columns @@ -176,6 +179,7 @@ impl Config { /// Information about a resource (either the one we're generating or an /// ancestor in its path) +#[derive(Clone)] struct Resource { /// PascalCase resource name itself (e.g., `Project`) /// @@ -185,14 +189,25 @@ struct Resource { name_as_snake: String, /// identifier for an authz object for this resource (e.g., `authz_project`) authz_name: syn::Ident, + /// the primary key uses a typed uuid + primary_key_is_typed_uuid: bool, } impl Resource { fn for_name(name: &str) -> Resource { + let (name, primary_key_is_typed_uuid) = + if name.chars().last() == Some('*') { + let name = &name[0..(name.len() - 1)]; + (name, true) + } else { + (name, false) + }; + assert!(!name.contains("*")); + let name_as_snake = heck::AsSnakeCase(&name).to_string(); let name = format_ident!("{}", name); let authz_name = format_ident!("authz_{}", name_as_snake); - Resource { name, authz_name, name_as_snake } + Resource { name, authz_name, name_as_snake, primary_key_is_typed_uuid } } } @@ -784,13 +799,33 @@ fn generate_database_functions(config: &Config) -> TokenStream { ( quote! { #parent_authz_name: &authz::#parent_resource_name, }, quote! { #parent_authz_name, }, - quote! { - let (#(#ancestors_authz_names,)* _) = - #parent_resource_name::lookup_by_id_no_authz( - opctx, datastore, &db_row.#parent_id.into() - ).await?; + // If the parent's id is a typed uuid, there will be a method + // converting the db typed uuid into an "external" typed uuid. Use + // that for `lookup_by_id_no_authz`. + if p.primary_key_is_typed_uuid { + quote! { + let (#(#ancestors_authz_names,)* _) = + #parent_resource_name::lookup_by_id_no_authz( + opctx, datastore, &db_row.#parent_id() + ).await?; + } + } else { + quote! { + let (#(#ancestors_authz_names,)* _) = + #parent_resource_name::lookup_by_id_no_authz( + opctx, datastore, &db_row.#parent_id.into() + ).await?; + } + }, + // If the parent's id is a typed uuid, then the `to_db_typed_uuid` + // method is required to convert to the db typed uuid + if p.primary_key_is_typed_uuid { + quote! { .filter(dsl::#parent_id.eq( + ::nexus_db_model::to_db_typed_uuid(#parent_authz_name.id()) + )) } + } else { + quote! { .filter(dsl::#parent_id.eq(#parent_authz_name.id())) } }, - quote! { .filter(dsl::#parent_id.eq(#parent_authz_name.id())) }, quote! { #parent_authz_name }, ) } else { diff --git a/nexus/db-model/src/audit_log.rs b/nexus/db-model/src/audit_log.rs index 52a53429f16..6541f64516e 100644 --- a/nexus/db-model/src/audit_log.rs +++ b/nexus/db-model/src/audit_log.rs @@ -13,14 +13,17 @@ use ipnetwork::IpNetwork; use nexus_db_schema::schema::{audit_log, audit_log_complete}; use nexus_types::external_api::views; use omicron_common::api::external::Error; +use omicron_uuid_kinds::BuiltInUserUuid; +use omicron_uuid_kinds::GenericUuid; +use omicron_uuid_kinds::SiloUserUuid; use serde::{Deserialize, Serialize}; use uuid::Uuid; /// Actor information for audit log initialization. Inspired by `authn::Actor` #[derive(Clone, Debug)] pub enum AuditLogActor { - UserBuiltin { user_builtin_id: Uuid }, - SiloUser { silo_user_id: Uuid, silo_id: Uuid }, + UserBuiltin { user_builtin_id: BuiltInUserUuid }, + SiloUser { silo_user_id: SiloUserUuid, silo_id: Uuid }, Unauthenticated, } @@ -126,12 +129,16 @@ impl From for AuditLogEntryInit { } = params; let (actor_id, actor_silo_id, actor_kind) = match actor { - AuditLogActor::UserBuiltin { user_builtin_id } => { - (Some(user_builtin_id), None, AuditLogActorKind::UserBuiltin) - } - AuditLogActor::SiloUser { silo_user_id, silo_id } => { - (Some(silo_user_id), Some(silo_id), AuditLogActorKind::SiloUser) - } + AuditLogActor::UserBuiltin { user_builtin_id } => ( + Some(user_builtin_id.into_untyped_uuid()), + None, + AuditLogActorKind::UserBuiltin, + ), + AuditLogActor::SiloUser { silo_user_id, silo_id } => ( + Some(silo_user_id.into_untyped_uuid()), + Some(silo_id), + AuditLogActorKind::SiloUser, + ), AuditLogActor::Unauthenticated => { (None, None, AuditLogActorKind::Unauthenticated) } @@ -274,7 +281,11 @@ impl TryFrom for views::AuditLogEntry { "UserBuiltin actor missing actor_id", ) })?; - views::AuditLogEntryActor::UserBuiltin { user_builtin_id } + views::AuditLogEntryActor::UserBuiltin { + user_builtin_id: BuiltInUserUuid::from_untyped_uuid( + user_builtin_id, + ), + } } AuditLogActorKind::SiloUser => { let silo_user_id = entry.actor_id.ok_or_else(|| { @@ -286,7 +297,9 @@ impl TryFrom for views::AuditLogEntry { ) })?; views::AuditLogEntryActor::SiloUser { - silo_user_id, + silo_user_id: SiloUserUuid::from_untyped_uuid( + silo_user_id, + ), silo_id, } } diff --git a/nexus/db-model/src/console_session.rs b/nexus/db-model/src/console_session.rs index 6332cb26638..db0ef14d7de 100644 --- a/nexus/db-model/src/console_session.rs +++ b/nexus/db-model/src/console_session.rs @@ -5,9 +5,11 @@ use chrono::{DateTime, Utc}; use nexus_db_schema::schema::console_session; use nexus_types::external_api::views; +use omicron_uuid_kinds::SiloUserKind; +use omicron_uuid_kinds::SiloUserUuid; use omicron_uuid_kinds::{ConsoleSessionKind, ConsoleSessionUuid, GenericUuid}; -use uuid::Uuid; +use crate::to_db_typed_uuid; use crate::typed_uuid::DbTypedUuid; // TODO: `struct SessionToken(String)` for session token @@ -19,24 +21,37 @@ pub struct ConsoleSession { pub token: String, pub time_created: DateTime, pub time_last_used: DateTime, - pub silo_user_id: Uuid, + silo_user_id: DbTypedUuid, } impl ConsoleSession { - pub fn new(token: String, silo_user_id: Uuid) -> Self { + pub fn new(token: String, silo_user_id: SiloUserUuid) -> Self { let now = Utc::now(); + Self::new_with_times(token, silo_user_id, now, now) + } + + pub fn new_with_times( + token: String, + silo_user_id: SiloUserUuid, + time_created: DateTime, + time_last_used: DateTime, + ) -> Self { Self { id: ConsoleSessionUuid::new_v4().into(), token, - silo_user_id, - time_last_used: now, - time_created: now, + silo_user_id: to_db_typed_uuid(silo_user_id), + time_created, + time_last_used, } } pub fn id(&self) -> ConsoleSessionUuid { self.id.0 } + + pub fn silo_user_id(&self) -> SiloUserUuid { + self.silo_user_id.into() + } } impl From for views::ConsoleSession { diff --git a/nexus/db-model/src/device_auth.rs b/nexus/db-model/src/device_auth.rs index e2d8d7ce7df..49e31535f92 100644 --- a/nexus/db-model/src/device_auth.rs +++ b/nexus/db-model/src/device_auth.rs @@ -11,7 +11,9 @@ use nexus_db_schema::schema::{device_access_token, device_auth_request}; use chrono::{DateTime, Duration, Utc}; use nexus_types::external_api::views; -use omicron_uuid_kinds::{AccessTokenKind, GenericUuid, TypedUuid}; +use omicron_uuid_kinds::{ + AccessTokenKind, GenericUuid, SiloUserKind, SiloUserUuid, TypedUuid, +}; use rand::{Rng, RngCore, SeedableRng, distributions::Slice, rngs::StdRng}; use std::num::NonZeroU32; use uuid::Uuid; @@ -134,7 +136,7 @@ pub struct DeviceAccessToken { pub token: String, pub client_id: Uuid, pub device_code: String, - pub silo_user_id: Uuid, + silo_user_id: DbTypedUuid, pub time_requested: DateTime, pub time_created: DateTime, pub time_expires: Option>, @@ -145,7 +147,7 @@ impl DeviceAccessToken { client_id: Uuid, device_code: String, time_requested: DateTime, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, time_expires: Option>, ) -> Self { let now = Utc::now(); @@ -157,7 +159,7 @@ impl DeviceAccessToken { token: generate_token(), client_id, device_code, - silo_user_id, + silo_user_id: silo_user_id.into(), time_requested, time_created: now, time_expires, @@ -172,6 +174,10 @@ impl DeviceAccessToken { self.time_expires = Some(time); self } + + pub fn silo_user_id(&self) -> SiloUserUuid { + self.silo_user_id.into() + } } impl From for views::DeviceAccessTokenGrant { diff --git a/nexus/db-model/src/role_assignment.rs b/nexus/db-model/src/role_assignment.rs index 6523e9e1f0d..3fa7f1a5a06 100644 --- a/nexus/db-model/src/role_assignment.rs +++ b/nexus/db-model/src/role_assignment.rs @@ -7,6 +7,9 @@ use anyhow::anyhow; use nexus_db_schema::schema::role_assignment; use nexus_types::external_api::shared; use omicron_common::api::external::Error; +use omicron_uuid_kinds::BuiltInUserUuid; +use omicron_uuid_kinds::GenericUuid; +use omicron_uuid_kinds::SiloUserUuid; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -81,6 +84,38 @@ impl RoleAssignment { role_name: String::from(role_name), } } + + /// Creates a new database RoleAssignment object for a silo user + pub fn new_for_silo_user( + user_id: SiloUserUuid, + resource_type: omicron_common::api::external::ResourceType, + resource_id: Uuid, + role_name: &str, + ) -> Self { + Self::new( + IdentityType::SiloUser, + user_id.into_untyped_uuid(), + resource_type, + resource_id, + role_name, + ) + } + + /// Creates a new database RoleAssignment object for a built-in user + pub fn new_for_builtin_user( + user_id: BuiltInUserUuid, + resource_type: omicron_common::api::external::ResourceType, + resource_id: Uuid, + role_name: &str, + ) -> Self { + Self::new( + IdentityType::UserBuiltin, + user_id.into_untyped_uuid(), + resource_type, + resource_id, + role_name, + ) + } } impl TryFrom diff --git a/nexus/db-model/src/silo_group.rs b/nexus/db-model/src/silo_group.rs index 1f0ec68552d..384b74edf53 100644 --- a/nexus/db-model/src/silo_group.rs +++ b/nexus/db-model/src/silo_group.rs @@ -2,15 +2,22 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +use crate::DbTypedUuid; +use crate::to_db_typed_uuid; use db_macros::Asset; use nexus_db_schema::schema::{silo_group, silo_group_membership}; use nexus_types::external_api::views; use nexus_types::identity::Asset; +use omicron_uuid_kinds::SiloGroupKind; +use omicron_uuid_kinds::SiloGroupUuid; +use omicron_uuid_kinds::SiloUserKind; +use omicron_uuid_kinds::SiloUserUuid; use uuid::Uuid; /// Describes a silo group within the database. #[derive(Asset, Queryable, Insertable, Debug, Selectable)] #[diesel(table_name = silo_group)] +#[asset(uuid_kind = SiloGroupKind)] pub struct SiloGroup { #[diesel(embed)] identity: SiloGroupIdentity, @@ -22,7 +29,7 @@ pub struct SiloGroup { } impl SiloGroup { - pub fn new(id: Uuid, silo_id: Uuid, external_id: String) -> Self { + pub fn new(id: SiloGroupUuid, silo_id: Uuid, external_id: String) -> Self { Self { identity: SiloGroupIdentity::new(id), silo_id, external_id } } } @@ -31,13 +38,19 @@ impl SiloGroup { #[derive(Queryable, Insertable, Debug, Selectable)] #[diesel(table_name = silo_group_membership)] pub struct SiloGroupMembership { - pub silo_group_id: Uuid, - pub silo_user_id: Uuid, + pub silo_group_id: DbTypedUuid, + pub silo_user_id: DbTypedUuid, } impl SiloGroupMembership { - pub fn new(silo_group_id: Uuid, silo_user_id: Uuid) -> Self { - Self { silo_group_id, silo_user_id } + pub fn new( + silo_group_id: SiloGroupUuid, + silo_user_id: SiloUserUuid, + ) -> Self { + Self { + silo_group_id: to_db_typed_uuid(silo_group_id), + silo_user_id: to_db_typed_uuid(silo_user_id), + } } } diff --git a/nexus/db-model/src/silo_user.rs b/nexus/db-model/src/silo_user.rs index b00ec9a3f53..bcf31aeff48 100644 --- a/nexus/db-model/src/silo_user.rs +++ b/nexus/db-model/src/silo_user.rs @@ -6,11 +6,13 @@ use db_macros::Asset; use nexus_db_schema::schema::silo_user; use nexus_types::external_api::views; use nexus_types::identity::Asset; +use omicron_uuid_kinds::SiloUserUuid; use uuid::Uuid; /// Describes a silo user within the database. #[derive(Asset, Queryable, Insertable, Debug, Selectable)] #[diesel(table_name = silo_user)] +#[asset(uuid_kind = SiloUserKind)] pub struct SiloUser { #[diesel(embed)] identity: SiloUserIdentity, @@ -23,7 +25,11 @@ pub struct SiloUser { } impl SiloUser { - pub fn new(silo_id: Uuid, user_id: Uuid, external_id: String) -> Self { + pub fn new( + silo_id: Uuid, + user_id: SiloUserUuid, + external_id: String, + ) -> Self { Self { identity: SiloUserIdentity::new(user_id), time_deleted: None, diff --git a/nexus/db-model/src/silo_user_password_hash.rs b/nexus/db-model/src/silo_user_password_hash.rs index 70edad3deeb..0d6b65fc632 100644 --- a/nexus/db-model/src/silo_user_password_hash.rs +++ b/nexus/db-model/src/silo_user_password_hash.rs @@ -2,14 +2,17 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +use crate::DbTypedUuid; +use crate::to_db_typed_uuid; use diesel::backend::Backend; use diesel::deserialize::{self, FromSql}; use diesel::serialize::{self, ToSql}; use diesel::sql_types; use nexus_db_schema::schema::silo_user_password_hash; +use omicron_uuid_kinds::SiloUserKind; +use omicron_uuid_kinds::SiloUserUuid; use parse_display::Display; use ref_cast::RefCast; -use uuid::Uuid; /// Newtype wrapper around [`omicron_passwords::PasswordHashString`]. #[derive( @@ -58,14 +61,22 @@ where #[derive(Queryable, Insertable, Debug, Selectable)] #[diesel(table_name = silo_user_password_hash)] pub struct SiloUserPasswordHash { - pub silo_user_id: Uuid, + silo_user_id: DbTypedUuid, pub hash: PasswordHashString, pub time_created: chrono::DateTime, } impl SiloUserPasswordHash { - pub fn new(silo_user_id: Uuid, hash: PasswordHashString) -> Self { - Self { silo_user_id, hash, time_created: chrono::Utc::now() } + pub fn new(silo_user_id: SiloUserUuid, hash: PasswordHashString) -> Self { + Self { + silo_user_id: to_db_typed_uuid(silo_user_id), + hash, + time_created: chrono::Utc::now(), + } + } + + pub fn silo_user_id(&self) -> SiloUserUuid { + self.silo_user_id.into() } } diff --git a/nexus/db-model/src/ssh_key.rs b/nexus/db-model/src/ssh_key.rs index 44c228898e7..869d21fb8ec 100644 --- a/nexus/db-model/src/ssh_key.rs +++ b/nexus/db-model/src/ssh_key.rs @@ -8,8 +8,13 @@ use nexus_db_schema::schema::ssh_key; use nexus_types::external_api::params; use nexus_types::external_api::views; use nexus_types::identity::Resource; +use omicron_uuid_kinds::SiloUserKind; +use omicron_uuid_kinds::SiloUserUuid; use uuid::Uuid; +use crate::DbTypedUuid; +use crate::to_db_typed_uuid; + /// Describes a user's public SSH key within the database. #[derive(Clone, Debug, Insertable, Queryable, Resource, Selectable)] #[diesel(table_name = ssh_key)] @@ -17,33 +22,40 @@ pub struct SshKey { #[diesel(embed)] identity: SshKeyIdentity, - pub silo_user_id: Uuid, + silo_user_id: DbTypedUuid, pub public_key: String, } impl SshKey { - pub fn new(silo_user_id: Uuid, params: params::SshKeyCreate) -> Self { + pub fn new( + silo_user_id: SiloUserUuid, + params: params::SshKeyCreate, + ) -> Self { Self::new_with_id(Uuid::new_v4(), silo_user_id, params) } pub fn new_with_id( id: Uuid, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, params: params::SshKeyCreate, ) -> Self { Self { identity: SshKeyIdentity::new(id, params.identity), - silo_user_id, + silo_user_id: to_db_typed_uuid(silo_user_id), public_key: params.public_key, } } + + pub fn silo_user_id(&self) -> SiloUserUuid { + self.silo_user_id.into() + } } impl From for views::SshKey { fn from(ssh_key: SshKey) -> Self { Self { identity: ssh_key.identity(), - silo_user_id: ssh_key.silo_user_id, + silo_user_id: ssh_key.silo_user_id(), public_key: ssh_key.public_key, } } diff --git a/nexus/db-model/src/user_builtin.rs b/nexus/db-model/src/user_builtin.rs index f9c386f4dac..fc1e266719b 100644 --- a/nexus/db-model/src/user_builtin.rs +++ b/nexus/db-model/src/user_builtin.rs @@ -7,11 +7,12 @@ use nexus_db_schema::schema::user_builtin; use nexus_types::external_api::params; use nexus_types::external_api::views; use nexus_types::identity::Resource; -use uuid::Uuid; +use omicron_uuid_kinds::BuiltInUserUuid; /// Describes a built-in user, as stored in the database #[derive(Queryable, Insertable, Debug, Resource, Selectable)] #[diesel(table_name = user_builtin)] +#[resource(uuid_kind = BuiltInUserKind)] pub struct UserBuiltin { #[diesel(embed)] pub identity: UserBuiltinIdentity, @@ -19,7 +20,7 @@ pub struct UserBuiltin { impl UserBuiltin { /// Creates a new database UserBuiltin object. - pub fn new(id: Uuid, params: params::UserBuiltinCreate) -> Self { + pub fn new(id: BuiltInUserUuid, params: params::UserBuiltinCreate) -> Self { Self { identity: UserBuiltinIdentity::new(id, params.identity) } } } diff --git a/nexus/db-queries/src/db/datastore/console_session.rs b/nexus/db-queries/src/db/datastore/console_session.rs index 24598ab2140..99cd9d93b24 100644 --- a/nexus/db-queries/src/db/datastore/console_session.rs +++ b/nexus/db-queries/src/db/datastore/console_session.rs @@ -9,6 +9,7 @@ use crate::authn; use crate::authz; use crate::context::OpContext; use crate::db::model::ConsoleSession; +use crate::db::model::to_db_typed_uuid; use crate::db::pagination::paginated; use async_bb8_diesel::AsyncRunQueryDsl; use chrono::TimeDelta; @@ -125,7 +126,7 @@ impl DataStore { })?; let (.., db_silo_user) = LookupPath::new(opctx, self) - .silo_user_id(console_session.silo_user_id) + .silo_user_id(console_session.silo_user_id()) .fetch() .await .map_err(|e| { @@ -186,7 +187,7 @@ impl DataStore { use nexus_db_schema::schema::console_session::dsl; paginated(dsl::console_session, dsl::id, &pagparams) - .filter(dsl::silo_user_id.eq(user_id)) + .filter(dsl::silo_user_id.eq(to_db_typed_uuid(user_id))) // session is not expired according to abs timeout .filter(dsl::time_created.ge(now - abs_ttl)) // session is also not expired according to idle timeout @@ -211,7 +212,7 @@ impl DataStore { use nexus_db_schema::schema::console_session; diesel::delete(console_session::table) - .filter(console_session::silo_user_id.eq(user_id)) + .filter(console_session::silo_user_id.eq(to_db_typed_uuid(user_id))) .execute_async(&*self.pool_connection_authorized(opctx).await?) .await .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) diff --git a/nexus/db-queries/src/db/datastore/device_auth.rs b/nexus/db-queries/src/db/datastore/device_auth.rs index 120f2891dc3..c49042c7f81 100644 --- a/nexus/db-queries/src/db/datastore/device_auth.rs +++ b/nexus/db-queries/src/db/datastore/device_auth.rs @@ -9,6 +9,7 @@ use crate::authz; use crate::context::OpContext; use crate::db::model::DeviceAccessToken; use crate::db::model::DeviceAuthRequest; +use crate::db::model::to_db_typed_uuid; use crate::db::pagination::paginated; use async_bb8_diesel::AsyncRunQueryDsl; use chrono::Utc; @@ -92,7 +93,7 @@ impl DataStore { authz_user: &authz::SiloUser, access_token: DeviceAccessToken, ) -> CreateResult { - assert_eq!(authz_user.id(), access_token.silo_user_id); + assert_eq!(authz_user.id(), access_token.silo_user_id()); opctx.authorize(authz::Action::Delete, authz_request).await?; opctx.authorize(authz::Action::CreateChild, authz_user).await?; @@ -198,9 +199,18 @@ impl DataStore { .actor_required() .internal_context("listing current user's tokens")?; + let silo_user_id = match actor.silo_user_id() { + Some(silo_user_id) => silo_user_id, + None => { + return Err(Error::non_resourcetype_not_found( + "could not find silo user", + ))?; + } + }; + use nexus_db_schema::schema::device_access_token::dsl; paginated(dsl::device_access_token, dsl::id, &pagparams) - .filter(dsl::silo_user_id.eq(actor.actor_id())) + .filter(dsl::silo_user_id.eq(to_db_typed_uuid(silo_user_id))) // we don't have time_deleted on tokens. unfortunately this is not // indexed well. maybe it can be! .filter( @@ -227,7 +237,7 @@ impl DataStore { use nexus_db_schema::schema::device_access_token::dsl; paginated(dsl::device_access_token, dsl::id, &pagparams) - .filter(dsl::silo_user_id.eq(silo_user_id)) + .filter(dsl::silo_user_id.eq(to_db_typed_uuid(silo_user_id))) // we don't have time_deleted on tokens. unfortunately this is not // indexed well. maybe it can be! .filter( @@ -251,9 +261,18 @@ impl DataStore { .actor_required() .internal_context("deleting current user's token")?; + let silo_user_id = match actor.silo_user_id() { + Some(silo_user_id) => silo_user_id, + None => { + return Err(Error::non_resourcetype_not_found( + "could not find silo user", + ))?; + } + }; + use nexus_db_schema::schema::device_access_token::dsl; let num_deleted = diesel::delete(dsl::device_access_token) - .filter(dsl::silo_user_id.eq(actor.actor_id())) + .filter(dsl::silo_user_id.eq(to_db_typed_uuid(silo_user_id))) .filter(dsl::id.eq(token_id)) .execute_async(&*self.pool_connection_authorized(opctx).await?) .await @@ -279,11 +298,13 @@ impl DataStore { // target user's own silo in particular opctx.authorize(authz::Action::Modify, authz_token_list).await?; + let silo_user_id = authz_token_list.silo_user().id(); + use nexus_db_schema::schema::device_access_token; diesel::delete(device_access_token::table) .filter( device_access_token::silo_user_id - .eq(authz_token_list.silo_user().id()), + .eq(to_db_typed_uuid(silo_user_id)), ) .execute_async(&*self.pool_connection_authorized(opctx).await?) .await diff --git a/nexus/db-queries/src/db/datastore/identity_provider.rs b/nexus/db-queries/src/db/datastore/identity_provider.rs index cb5268f22f2..c87371eafb4 100644 --- a/nexus/db-queries/src/db/datastore/identity_provider.rs +++ b/nexus/db-queries/src/db/datastore/identity_provider.rs @@ -104,37 +104,41 @@ impl DataStore { &self, opctx: &OpContext, authz_idp_list: &authz::SiloIdentityProviderList, - provider: db::model::SamlIdentityProvider, + saml_provider: db::model::SamlIdentityProvider, ) -> CreateResult { opctx.authorize(authz::Action::CreateChild, authz_idp_list).await?; - assert_eq!(provider.silo_id, authz_idp_list.silo().id()); + assert_eq!(saml_provider.silo_id, authz_idp_list.silo().id()); - let name = provider.identity().name.to_string(); + let name = saml_provider.identity().name.to_string(); let conn = self.pool_connection_authorized(opctx).await?; + // Identity providers have two records, one generic, and one + // specialized. Create the generic one from the specialized one here. + let provider = db::model::IdentityProvider { + identity: db::model::IdentityProviderIdentity { + id: saml_provider.identity.id, + name: saml_provider.identity.name.clone(), + description: saml_provider.identity.description.clone(), + time_created: saml_provider.identity.time_created, + time_modified: saml_provider.identity.time_modified, + time_deleted: saml_provider.identity.time_deleted, + }, + silo_id: saml_provider.silo_id, + provider_type: db::model::IdentityProviderType::Saml, + }; + self.transaction_retry_wrapper("saml_identity_provider_create") .transaction(&conn, |conn| { + let saml_provider = saml_provider.clone(); let provider = provider.clone(); + async move { // insert silo identity provider record with type Saml - use nexus_db_schema::schema::identity_provider::dsl as idp_dsl; + use nexus_db_schema::schema::identity_provider::dsl as + idp_dsl; + diesel::insert_into(idp_dsl::identity_provider) - .values(db::model::IdentityProvider { - identity: db::model::IdentityProviderIdentity { - id: provider.identity.id, - name: provider.identity.name.clone(), - description: provider - .identity - .description - .clone(), - time_created: provider.identity.time_created, - time_modified: provider.identity.time_modified, - time_deleted: provider.identity.time_deleted, - }, - silo_id: provider.silo_id, - provider_type: - db::model::IdentityProviderType::Saml, - }) + .values(provider) .execute_async(&conn) .await?; @@ -142,7 +146,7 @@ impl DataStore { use nexus_db_schema::schema::saml_identity_provider::dsl; let result = diesel::insert_into(dsl::saml_identity_provider) - .values(provider) + .values(saml_provider) .returning( db::model::SamlIdentityProvider::as_returning(), ) diff --git a/nexus/db-queries/src/db/datastore/mod.rs b/nexus/db-queries/src/db/datastore/mod.rs index 7f4e3dd7627..d4ca4830bc7 100644 --- a/nexus/db-queries/src/db/datastore/mod.rs +++ b/nexus/db-queries/src/db/datastore/mod.rs @@ -489,12 +489,13 @@ mod test { ByteCount, Error, IdentityMetadataCreateParams, LookupType, Name, }; use omicron_test_utils::dev; + use omicron_uuid_kinds::CollectionUuid; use omicron_uuid_kinds::DatasetUuid; use omicron_uuid_kinds::GenericUuid; use omicron_uuid_kinds::PhysicalDiskUuid; + use omicron_uuid_kinds::SiloUserUuid; use omicron_uuid_kinds::SledUuid; use omicron_uuid_kinds::VolumeUuid; - use omicron_uuid_kinds::{CollectionUuid, TypedUuid}; use std::collections::HashMap; use std::collections::HashSet; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV6}; @@ -579,15 +580,16 @@ mod test { ); let token = "a_token".to_string(); - let silo_user_id = Uuid::new_v4(); + let silo_user_id = SiloUserUuid::new_v4(); - let session = ConsoleSession { - id: TypedUuid::new_v4().into(), - token: token.clone(), - time_created: Utc::now() - Duration::minutes(5), - time_last_used: Utc::now() - Duration::minutes(5), + let both_times = Utc::now() - Duration::minutes(5); + + let session = ConsoleSession::new_with_times( + token.clone(), silo_user_id, - }; + both_times, + both_times, + ); let _ = datastore .session_create(&authn_opctx, session.clone()) @@ -613,7 +615,7 @@ mod test { .unwrap(); let (.., db_silo_user) = LookupPath::new(&opctx, datastore) - .silo_user_id(session.silo_user_id) + .silo_user_id(session.silo_user_id()) .fetch() .await .unwrap(); @@ -624,7 +626,7 @@ mod test { .session_lookup_by_token(&authn_opctx, token.clone()) .await .unwrap(); - assert_eq!(session.silo_user_id, fetched.silo_user_id); + assert_eq!(session.silo_user_id(), fetched.silo_user_id()); assert_eq!(session.id, fetched.id); // also try looking it up by ID @@ -633,7 +635,7 @@ mod test { .fetch() .await .unwrap(); - assert_eq!(session.silo_user_id, fetched.silo_user_id); + assert_eq!(session.silo_user_id(), fetched.silo_user_id()); assert_eq!(session.token, fetched.token); // trying to insert the same one again fails @@ -1738,7 +1740,7 @@ mod test { DEFAULT_SILO_ID, LookupType::ById(DEFAULT_SILO_ID), ); - let silo_user_id = Uuid::new_v4(); + let silo_user_id = SiloUserUuid::new_v4(); datastore .silo_user_create( &authz_silo, @@ -1775,7 +1777,7 @@ mod test { .ssh_key_create(&opctx, &authz_user, ssh_key.clone()) .await .unwrap(); - assert_eq!(created.silo_user_id, ssh_key.silo_user_id); + assert_eq!(created.silo_user_id(), ssh_key.silo_user_id()); assert_eq!(created.public_key, ssh_key.public_key); // Lookup the key we just created. @@ -1788,7 +1790,7 @@ mod test { .unwrap(); assert_eq!(authz_silo.id(), DEFAULT_SILO_ID); assert_eq!(authz_silo_user.id(), silo_user_id); - assert_eq!(found.silo_user_id, ssh_key.silo_user_id); + assert_eq!(found.silo_user_id(), ssh_key.silo_user_id()); assert_eq!(found.public_key, ssh_key.public_key); // Trying to insert the same one again fails. diff --git a/nexus/db-queries/src/db/datastore/rack.rs b/nexus/db-queries/src/db/datastore/rack.rs index 816b132aedd..bf3dba93161 100644 --- a/nexus/db-queries/src/db/datastore/rack.rs +++ b/nexus/db-queries/src/db/datastore/rack.rs @@ -51,7 +51,6 @@ use nexus_types::deployment::OmicronZoneExternalIp; use nexus_types::deployment::blueprint_zone_type; use nexus_types::external_api::params as external_params; use nexus_types::external_api::shared; -use nexus_types::external_api::shared::IdentityType; use nexus_types::external_api::shared::IpRange; use nexus_types::external_api::shared::SiloRole; use nexus_types::identity::Resource; @@ -67,6 +66,7 @@ use omicron_common::api::external::UpdateResult; use omicron_common::api::external::UserId; use omicron_common::bail_unless; use omicron_uuid_kinds::GenericUuid; +use omicron_uuid_kinds::SiloUserUuid; use omicron_uuid_kinds::SledUuid; use slog_error_chain::InlineErrorChain; use std::sync::{Arc, OnceLock}; @@ -449,7 +449,7 @@ impl DataStore { info!(log, "Created recovery silo"); // Create the first user in the initial Recovery Silo - let silo_user_id = Uuid::new_v4(); + let silo_user_id = SiloUserUuid::new_v4(); let silo_user = SiloUser::new( db_silo.id(), silo_user_id, @@ -492,11 +492,10 @@ impl DataStore { let (q1, q2) = Self::role_assignment_replace_visible_queries( opctx, &authz_silo, - &[shared::RoleAssignment { - identity_type: IdentityType::SiloUser, - identity_id: silo_user_id, - role_name: SiloRole::Admin, - }], + &[shared::RoleAssignment::for_silo_user( + silo_user_id, + SiloRole::Admin, + )], ) .await .map_err(RackInitError::RoleAssignment)?; @@ -1210,7 +1209,7 @@ mod test { let authz_silo_user = authz::SiloUser::new( authz_silo, silo_users[0].id(), - LookupType::ById(silo_users[0].id()), + LookupType::by_id(silo_users[0].id()), ); let hash = datastore .silo_user_password_hash_fetch(&opctx, &authz_silo_user) diff --git a/nexus/db-queries/src/db/datastore/silo.rs b/nexus/db-queries/src/db/datastore/silo.rs index 327848a3c4c..a1babdd62f3 100644 --- a/nexus/db-queries/src/db/datastore/silo.rs +++ b/nexus/db-queries/src/db/datastore/silo.rs @@ -43,6 +43,7 @@ use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupType; use omicron_common::api::external::ResourceType; use omicron_common::api::external::http_pagination::PaginatedBy; +use omicron_uuid_kinds::SiloGroupUuid; use ref_cast::RefCast; use uuid::Uuid; @@ -177,7 +178,7 @@ impl DataStore { dns_update: DnsVersionUpdateBuilder, ) -> Result> { let silo_id = Uuid::new_v4(); - let silo_group_id = Uuid::new_v4(); + let silo_group_id = SiloGroupUuid::new_v4(); let silo_create_query = Self::silo_create_query( opctx, @@ -208,30 +209,31 @@ impl DataStore { None }; - let silo_admin_group_role_assignment_queries = - if new_silo_params.admin_group_name.is_some() { - // Grant silo admin role for members of the admin group. - let policy = shared::Policy { - role_assignments: vec![shared::RoleAssignment { - identity_type: shared::IdentityType::SiloGroup, - identity_id: silo_group_id, - role_name: SiloRole::Admin, - }], - }; - - let silo_admin_group_role_assignment_queries = - DataStore::role_assignment_replace_visible_queries( - opctx, - &authz_silo, - &policy.role_assignments, - ) - .await?; - - Some(silo_admin_group_role_assignment_queries) - } else { - None + let silo_admin_group_role_assignment_queries = if new_silo_params + .admin_group_name + .is_some() + { + // Grant silo admin role for members of the admin group. + let policy = shared::Policy { + role_assignments: vec![shared::RoleAssignment::for_silo_group( + silo_group_id, + SiloRole::Admin, + )], }; + let silo_admin_group_role_assignment_queries = + DataStore::role_assignment_replace_visible_queries( + opctx, + &authz_silo, + &policy.role_assignments, + ) + .await?; + + Some(silo_admin_group_role_assignment_queries) + } else { + None + }; + // This method uses nested transactions, which are not supported // with retryable transactions. let silo = self diff --git a/nexus/db-queries/src/db/datastore/silo_group.rs b/nexus/db-queries/src/db/datastore/silo_group.rs index e6cf139bb23..eb8f2438d35 100644 --- a/nexus/db-queries/src/db/datastore/silo_group.rs +++ b/nexus/db-queries/src/db/datastore/silo_group.rs @@ -12,6 +12,7 @@ use crate::db::IncompleteOnConflictExt; use crate::db::datastore::RunnableQueryNoReturn; use crate::db::model::SiloGroup; use crate::db::model::SiloGroupMembership; +use crate::db::model::to_db_typed_uuid; use crate::db::pagination::paginated; use async_bb8_diesel::AsyncRunQueryDsl; use chrono::Utc; @@ -27,6 +28,8 @@ use omicron_common::api::external::InternalContext; use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; use omicron_common::api::external::UpdateResult; +use omicron_uuid_kinds::SiloGroupUuid; +use omicron_uuid_kinds::SiloUserUuid; use uuid::Uuid; impl DataStore { @@ -90,13 +93,13 @@ impl DataStore { &self, opctx: &OpContext, authz_silo: &authz::Silo, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, ) -> ListResultVec { opctx.authorize(authz::Action::ListChildren, authz_silo).await?; use nexus_db_schema::schema::silo_group_membership::dsl; dsl::silo_group_membership - .filter(dsl::silo_user_id.eq(silo_user_id)) + .filter(dsl::silo_user_id.eq(to_db_typed_uuid(silo_user_id))) .select(SiloGroupMembership::as_returning()) .get_results_async(&*self.pool_connection_authorized(opctx).await?) .await @@ -116,12 +119,21 @@ impl DataStore { .actor_required() .internal_context("fetching current user's group memberships")?; + let silo_user_id = match actor.silo_user_id() { + Some(silo_user_id) => silo_user_id, + None => { + return Err(Error::non_resourcetype_not_found( + "could not find silo user", + ))?; + } + }; + use nexus_db_schema::schema::{ silo_group as sg, silo_group_membership as sgm, }; paginated(sg::dsl::silo_group, sg::id, pagparams) .inner_join(sgm::table.on(sgm::silo_group_id.eq(sg::id))) - .filter(sgm::silo_user_id.eq(actor.actor_id())) + .filter(sgm::silo_user_id.eq(to_db_typed_uuid(silo_user_id))) .filter(sg::time_deleted.is_null()) .select(SiloGroup::as_returning()) .get_results_async(&*self.pool_connection_authorized(opctx).await?) @@ -142,7 +154,7 @@ impl DataStore { &self, opctx: &OpContext, authz_silo_user: &authz::SiloUser, - silo_group_ids: Vec, + silo_group_ids: Vec, ) -> UpdateResult<()> { opctx.authorize(authz::Action::Modify, authz_silo_user).await?; @@ -157,7 +169,10 @@ impl DataStore { // Delete existing memberships for user let silo_user_id = authz_silo_user.id(); diesel::delete(dsl::silo_group_membership) - .filter(dsl::silo_user_id.eq(silo_user_id)) + .filter( + dsl::silo_user_id + .eq(to_db_typed_uuid(silo_user_id)), + ) .execute_async(&conn) .await?; @@ -167,8 +182,8 @@ impl DataStore { > = silo_group_ids .iter() .map(|group_id| db::model::SiloGroupMembership { - silo_group_id: *group_id, - silo_user_id, + silo_group_id: to_db_typed_uuid(*group_id), + silo_user_id: to_db_typed_uuid(silo_user_id), }) .collect(); @@ -194,7 +209,7 @@ impl DataStore { #[derive(Debug, thiserror::Error)] enum SiloDeleteError { #[error("group {0} still has memberships")] - GroupStillHasMemberships(Uuid), + GroupStillHasMemberships(SiloGroupUuid), } type TxnError = TransactionError; @@ -211,7 +226,7 @@ impl DataStore { silo_group_membership::dsl::silo_group_membership .filter( silo_group_membership::dsl::silo_group_id - .eq(group_id), + .eq(to_db_typed_uuid(group_id)), ) .select(SiloGroupMembership::as_returning()) .limit(1) @@ -227,7 +242,7 @@ impl DataStore { // Delete silo group use nexus_db_schema::schema::silo_group::dsl; diesel::update(dsl::silo_group) - .filter(dsl::id.eq(group_id)) + .filter(dsl::id.eq(to_db_typed_uuid(group_id))) .filter(dsl::time_deleted.is_null()) .set(dsl::time_deleted.eq(Utc::now())) .execute_async(&conn) diff --git a/nexus/db-queries/src/db/datastore/silo_user.rs b/nexus/db-queries/src/db/datastore/silo_user.rs index 8b19c1886f8..0eb374af0bb 100644 --- a/nexus/db-queries/src/db/datastore/silo_user.rs +++ b/nexus/db-queries/src/db/datastore/silo_user.rs @@ -16,6 +16,7 @@ use crate::db::model::SiloUserPasswordHash; use crate::db::model::SiloUserPasswordUpdate; use crate::db::model::UserBuiltin; use crate::db::model::UserProvisionType; +use crate::db::model::to_db_typed_uuid; use crate::db::pagination::paginated; use crate::db::update_and_check::UpdateAndCheck; use async_bb8_diesel::AsyncRunQueryDsl; @@ -36,6 +37,7 @@ use omicron_common::api::external::LookupType; use omicron_common::api::external::ResourceType; use omicron_common::api::external::UpdateResult; use omicron_common::bail_unless; +use omicron_uuid_kinds::GenericUuid; use uuid::Uuid; impl DataStore { @@ -71,7 +73,7 @@ impl DataStore { let authz_silo_user = authz::SiloUser::new( authz_silo.clone(), silo_user_id, - LookupType::ById(silo_user_id), + LookupType::by_id(silo_user_id), ); (authz_silo_user, db_silo_user) }) @@ -98,10 +100,14 @@ impl DataStore { { use nexus_db_schema::schema::silo_user::dsl; diesel::update(dsl::silo_user) - .filter(dsl::id.eq(authz_silo_user_id)) + .filter( + dsl::id.eq(to_db_typed_uuid(authz_silo_user_id)), + ) .filter(dsl::time_deleted.is_null()) .set(dsl::time_deleted.eq(Utc::now())) - .check_if_exists::(authz_silo_user_id) + .check_if_exists::( + authz_silo_user_id.into_untyped_uuid(), + ) .execute_and_check(&conn) .await?; } @@ -110,7 +116,10 @@ impl DataStore { { use nexus_db_schema::schema::console_session::dsl; diesel::delete(dsl::console_session) - .filter(dsl::silo_user_id.eq(authz_silo_user_id)) + .filter( + dsl::silo_user_id + .eq(to_db_typed_uuid(authz_silo_user_id)), + ) .execute_async(&conn) .await?; } @@ -119,7 +128,10 @@ impl DataStore { { use nexus_db_schema::schema::device_access_token::dsl; diesel::delete(dsl::device_access_token) - .filter(dsl::silo_user_id.eq(authz_silo_user_id)) + .filter( + dsl::silo_user_id + .eq(to_db_typed_uuid(authz_silo_user_id)), + ) .execute_async(&conn) .await?; } @@ -128,7 +140,10 @@ impl DataStore { { use nexus_db_schema::schema::silo_group_membership::dsl; diesel::delete(dsl::silo_group_membership) - .filter(dsl::silo_user_id.eq(authz_silo_user_id)) + .filter( + dsl::silo_user_id + .eq(to_db_typed_uuid(authz_silo_user_id)), + ) .execute_async(&conn) .await?; } @@ -137,7 +152,10 @@ impl DataStore { { use nexus_db_schema::schema::ssh_key::dsl; diesel::update(dsl::ssh_key) - .filter(dsl::silo_user_id.eq(authz_silo_user_id)) + .filter( + dsl::silo_user_id + .eq(to_db_typed_uuid(authz_silo_user_id)), + ) .filter(dsl::time_deleted.is_null()) .set(dsl::time_deleted.eq(Utc::now())) .execute_async(&conn) @@ -233,11 +251,14 @@ impl DataStore { paginated(user::table, user::id, pagparams) .filter(user::silo_id.eq(authz_silo_user_list.silo().id())) .filter(user::time_deleted.is_null()) - .inner_join(user_to_group::table.on( - user_to_group::silo_user_id.eq(user::id).and( - user_to_group::silo_group_id.eq(authz_silo_group.id()), - ), - )) + .inner_join( + user_to_group::table.on(user_to_group::silo_user_id + .eq(user::id) + .and( + user_to_group::silo_group_id + .eq(to_db_typed_uuid(authz_silo_group.id())), + )), + ) .select(SiloUser::as_select()) .load_async::( &*self.pool_connection_authorized(opctx).await?, @@ -267,7 +288,7 @@ impl DataStore { bail_unless!(db_silo_user.id() == authz_silo_user.id()); if let Some(db_silo_user_password_hash) = &db_silo_user_password_hash { bail_unless!( - db_silo_user_password_hash.silo_user_id == db_silo_user.id() + db_silo_user_password_hash.silo_user_id() == db_silo_user.id() ); } @@ -291,7 +312,10 @@ impl DataStore { })?; } else { diesel::delete(dsl::silo_user_password_hash) - .filter(dsl::silo_user_id.eq(authz_silo_user.id())) + .filter( + dsl::silo_user_id + .eq(to_db_typed_uuid(authz_silo_user.id())), + ) .execute_async(&*self.pool_connection_authorized(opctx).await?) .await .map_err(|e| { @@ -324,7 +348,9 @@ impl DataStore { use nexus_db_schema::schema::silo_user_password_hash::dsl; Ok(dsl::silo_user_password_hash - .filter(dsl::silo_user_id.eq(authz_silo_user.id())) + .filter( + dsl::silo_user_id.eq(to_db_typed_uuid(authz_silo_user.id())), + ) .select(SiloUserPasswordHash::as_select()) .load_async::( &*self.pool_connection_authorized(opctx).await?, diff --git a/nexus/db-queries/src/db/datastore/ssh_key.rs b/nexus/db-queries/src/db/datastore/ssh_key.rs index 37e2f939ff6..0eb9c988880 100644 --- a/nexus/db-queries/src/db/datastore/ssh_key.rs +++ b/nexus/db-queries/src/db/datastore/ssh_key.rs @@ -11,6 +11,7 @@ use crate::db; use crate::db::identity::Resource; use crate::db::model::Name; use crate::db::model::SshKey; +use crate::db::model::to_db_typed_uuid; use crate::db::pagination::paginated; use crate::db::update_and_check::UpdateAndCheck; use async_bb8_diesel::AsyncRunQueryDsl; @@ -56,7 +57,7 @@ impl DataStore { use nexus_db_schema::schema::ssh_key::dsl; let result: Vec<(Uuid, Name)> = dsl::ssh_key .filter(dsl::id.eq_any(ids).or(dsl::name.eq_any(names))) - .filter(dsl::silo_user_id.eq(authz_user.id())) + .filter(dsl::silo_user_id.eq(to_db_typed_uuid(authz_user.id()))) .filter(dsl::time_deleted.is_null()) .select((dsl::id, dsl::name)) .get_results_async(&*self.pool_connection_authorized(opctx).await?) @@ -107,7 +108,7 @@ impl DataStore { use nexus_db_schema::schema::ssh_key::dsl; dsl::ssh_key .filter(dsl::id.eq_any(keys.to_owned())) - .filter(dsl::silo_user_id.eq(authz_user.id())) + .filter(dsl::silo_user_id.eq(to_db_typed_uuid(authz_user.id()))) .filter(dsl::time_deleted.is_null()) .select(SshKey::as_select()) .get_results_async(&*self.pool_connection_authorized(opctx).await?) @@ -132,7 +133,9 @@ impl DataStore { None => { use nexus_db_schema::schema::ssh_key::dsl; dsl::ssh_key - .filter(dsl::silo_user_id.eq(authz_user.id())) + .filter( + dsl::silo_user_id.eq(to_db_typed_uuid(authz_user.id())), + ) .filter(dsl::time_deleted.is_null()) .select(dsl::id) .get_results_async( @@ -235,7 +238,7 @@ impl DataStore { &pagparams.map_name(|n| Name::ref_cast(n)), ), } - .filter(dsl::silo_user_id.eq(authz_user.id())) + .filter(dsl::silo_user_id.eq(to_db_typed_uuid(authz_user.id()))) .filter(dsl::time_deleted.is_null()) .select(SshKey::as_select()) .load_async(&*self.pool_connection_authorized(opctx).await?) @@ -250,7 +253,7 @@ impl DataStore { authz_user: &authz::SiloUser, ssh_key: SshKey, ) -> CreateResult { - assert_eq!(authz_user.id(), ssh_key.silo_user_id); + assert_eq!(authz_user.id(), ssh_key.silo_user_id()); opctx.authorize(authz::Action::CreateChild, authz_user).await?; let name = ssh_key.name().to_string(); diff --git a/nexus/db-queries/src/policy_test/mod.rs b/nexus/db-queries/src/policy_test/mod.rs index 0b445606315..93524c7da43 100644 --- a/nexus/db-queries/src/policy_test/mod.rs +++ b/nexus/db-queries/src/policy_test/mod.rs @@ -77,11 +77,10 @@ async fn test_iam_roles_behavior() { .role_assignment_replace_visible( &opctx, &main_silo, - &[shared::RoleAssignment { - identity_type: shared::IdentityType::SiloUser, - identity_id: USER_TEST_PRIVILEGED.id(), - role_name: SiloRole::Admin, - }], + &[shared::RoleAssignment::for_silo_user( + USER_TEST_PRIVILEGED.id(), + SiloRole::Admin, + )], ) .await .unwrap(); @@ -343,11 +342,10 @@ async fn test_conferred_roles() { .role_assignment_replace_visible( &opctx, &main_silo, - &[shared::RoleAssignment { - identity_type: shared::IdentityType::SiloUser, - identity_id: USER_TEST_PRIVILEGED.id(), - role_name: SiloRole::Admin, - }], + &[shared::RoleAssignment::for_silo_user( + USER_TEST_PRIVILEGED.id(), + SiloRole::Admin, + )], ) .await .unwrap(); diff --git a/nexus/db-queries/src/policy_test/resource_builder.rs b/nexus/db-queries/src/policy_test/resource_builder.rs index 06f56780509..5620396c226 100644 --- a/nexus/db-queries/src/policy_test/resource_builder.rs +++ b/nexus/db-queries/src/policy_test/resource_builder.rs @@ -18,6 +18,7 @@ use nexus_db_model::DatabaseString; use nexus_types::external_api::shared; use omicron_common::api::external::Error; use omicron_common::api::external::LookupType; +use omicron_uuid_kinds::SiloUserUuid; use std::sync::Arc; use strum::IntoEnumIterator; use uuid::Uuid; @@ -39,7 +40,7 @@ pub struct ResourceBuilder<'a> { /// list of resources created so far resources: Vec>, /// list of users created so far - users: Vec<(String, Uuid)>, + users: Vec<(String, SiloUserUuid)>, } impl<'a> ResourceBuilder<'a> { @@ -102,7 +103,7 @@ impl<'a> ResourceBuilder<'a> { for role in T::AllowedRoles::iter() { let role_name = role.to_database_string(); let username = format!("{}-{}", resource_name, role_name); - let user_id = Uuid::new_v4(); + let user_id = SiloUserUuid::new_v4(); println!("creating user: {}", &username); self.users.push((username.clone(), user_id)); @@ -125,11 +126,9 @@ impl<'a> ResourceBuilder<'a> { let new_role_assignments = old_role_assignments .into_iter() .map(|r| r.try_into().unwrap()) - .chain(std::iter::once(shared::RoleAssignment { - identity_type: shared::IdentityType::SiloUser, - identity_id: user_id, - role_name: role, - })) + .chain(std::iter::once(shared::RoleAssignment::for_silo_user( + user_id, role, + ))) .collect::>(); datastore .role_assignment_replace_visible( @@ -152,7 +151,7 @@ impl<'a> ResourceBuilder<'a> { /// were created with specific roles on those resources pub struct ResourceSet { resources: Vec>, - users: Vec<(String, Uuid)>, + users: Vec<(String, SiloUserUuid)>, } impl ResourceSet { @@ -167,7 +166,7 @@ impl ResourceSet { /// Iterate the users that were created as `(username, user_id)` pairs pub fn users( &self, - ) -> impl std::iter::Iterator + '_ { + ) -> impl std::iter::Iterator + '_ { self.users.iter() } } diff --git a/nexus/db-queries/src/policy_test/resources.rs b/nexus/db-queries/src/policy_test/resources.rs index c3096630531..a10e67c27dc 100644 --- a/nexus/db-queries/src/policy_test/resources.rs +++ b/nexus/db-queries/src/policy_test/resources.rs @@ -11,6 +11,8 @@ use omicron_common::api::external::LookupType; use omicron_uuid_kinds::AccessTokenKind; use omicron_uuid_kinds::GenericUuid; use omicron_uuid_kinds::PhysicalDiskUuid; +use omicron_uuid_kinds::SiloGroupUuid; +use omicron_uuid_kinds::SiloUserUuid; use omicron_uuid_kinds::SupportBundleUuid; use omicron_uuid_kinds::TypedUuid; use oso::PolarClass; @@ -257,7 +259,7 @@ async fn make_silo( )); builder.new_resource(authz::SiloUserList::new(silo.clone())); - let silo_user_id = Uuid::new_v4(); + let silo_user_id = SiloUserUuid::new_v4(); let silo_user = authz::SiloUser::new( silo.clone(), silo_user_id, @@ -270,7 +272,7 @@ async fn make_silo( ssh_key_id, LookupType::ByName(format!("{}-user-ssh-key", silo_name)), )); - let silo_group_id = Uuid::new_v4(); + let silo_group_id = SiloGroupUuid::new_v4(); builder.new_resource(authz::SiloGroup::new( silo.clone(), silo_group_id, diff --git a/nexus/src/app/device_auth.rs b/nexus/src/app/device_auth.rs index 668e363572c..0a97c21cc07 100644 --- a/nexus/src/app/device_auth.rs +++ b/nexus/src/app/device_auth.rs @@ -51,6 +51,7 @@ use nexus_db_queries::authn::{Actor, Reason}; use nexus_db_queries::authz; use nexus_db_queries::context::OpContext; use nexus_db_queries::db::model::{DeviceAccessToken, DeviceAuthRequest}; +use omicron_uuid_kinds::SiloUserUuid; use anyhow::anyhow; use nexus_types::external_api::params; @@ -99,7 +100,7 @@ impl super::Nexus { &self, opctx: &OpContext, user_code: String, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, ) -> CreateResult { let (.., authz_request, db_request) = LookupPath::new(opctx, &self.db_datastore) @@ -208,7 +209,7 @@ impl super::Nexus { e => Reason::UnknownError { source: e }, })?; - let silo_user_id = db_access_token.silo_user_id; + let silo_user_id = db_access_token.silo_user_id(); let (.., db_silo_user) = LookupPath::new(opctx, &self.db_datastore) .silo_user_id(silo_user_id) .fetch() diff --git a/nexus/src/app/iam.rs b/nexus/src/app/iam.rs index d95cd5ec1a3..d29c86fcb0b 100644 --- a/nexus/src/app/iam.rs +++ b/nexus/src/app/iam.rs @@ -20,6 +20,9 @@ use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; use omicron_common::api::external::NameOrId; use omicron_common::api::external::UpdateResult; +use omicron_uuid_kinds::BuiltInUserUuid; +use omicron_uuid_kinds::GenericUuid; +use omicron_uuid_kinds::SiloGroupUuid; use ref_cast::RefCast; use uuid::Uuid; @@ -84,7 +87,7 @@ impl super::Nexus { &self, opctx: &OpContext, pagparams: &DataPageParams<'_, Uuid>, - group_id: &Uuid, + group_id: &SiloGroupUuid, ) -> ListResultVec { let authz_silo = opctx .authn @@ -118,7 +121,7 @@ impl super::Nexus { .actor_required() .internal_context("loading current user")?; let (.., db_silo_user) = LookupPath::new(opctx, &self.db_datastore) - .silo_user_id(actor.actor_id()) + .silo_user_actor(&actor)? .fetch() .await?; Ok(db_silo_user) @@ -166,7 +169,8 @@ impl super::Nexus { let lookup_path = LookupPath::new(opctx, &self.db_datastore); let user = match user_selector { params::UserBuiltinSelector { user: NameOrId::Id(id) } => { - lookup_path.user_builtin_id(*id) + lookup_path + .user_builtin_id(BuiltInUserUuid::from_untyped_uuid(*id)) } params::UserBuiltinSelector { user: NameOrId::Name(name) } => { lookup_path.user_builtin_name(Name::ref_cast(name)) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index c2dbf6269da..62a3c015a04 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -273,7 +273,7 @@ async fn normalize_ssh_keys( .actor_required() .internal_context("loading current user's ssh keys for new Instance")?; let (.., authz_user) = LookupPath::new(opctx, datastore) - .silo_user_id(actor.actor_id()) + .silo_user_actor(&actor)? .lookup_for(authz::Action::ListChildren) .await?; diff --git a/nexus/src/app/sagas/instance_create.rs b/nexus/src/app/sagas/instance_create.rs index 7242379ee8a..0dff1a62272 100644 --- a/nexus/src/app/sagas/instance_create.rs +++ b/nexus/src/app/sagas/instance_create.rs @@ -391,7 +391,8 @@ async fn sic_associate_ssh_keys( .map_err(ActionError::action_failed)?; let (.., authz_user) = LookupPath::new(&opctx, datastore) - .silo_user_id(actor.actor_id()) + .silo_user_actor(&actor) + .map_err(ActionError::action_failed)? .lookup_for(authz::Action::ListChildren) .await .map_err(ActionError::action_failed)?; diff --git a/nexus/src/app/session.rs b/nexus/src/app/session.rs index 36bedf393ac..1febf60eb90 100644 --- a/nexus/src/app/session.rs +++ b/nexus/src/app/session.rs @@ -20,6 +20,7 @@ use omicron_common::api::external::LookupType; use omicron_common::api::external::UpdateResult; use omicron_uuid_kinds::ConsoleSessionUuid; use omicron_uuid_kinds::GenericUuid; +use omicron_uuid_kinds::SiloUserUuid; use rand::{RngCore, SeedableRng, rngs::StdRng}; use uuid::Uuid; @@ -56,7 +57,7 @@ impl super::Nexus { self.db_datastore.session_lookup_by_token(&opctx, token).await?; let (.., db_silo_user) = LookupPath::new(opctx, &self.db_datastore) - .silo_user_id(db_session.silo_user_id) + .silo_user_id(db_session.silo_user_id()) .fetch() .await?; @@ -91,7 +92,7 @@ impl super::Nexus { pub(crate) async fn lookup_silo_for_authn( &self, opctx: &OpContext, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, ) -> Result { let (.., db_silo_user) = LookupPath::new(opctx, &self.db_datastore) .silo_user_id(silo_user_id) diff --git a/nexus/src/app/silo.rs b/nexus/src/app/silo.rs index 2ae6393cfd9..411ae7b613d 100644 --- a/nexus/src/app/silo.rs +++ b/nexus/src/app/silo.rs @@ -31,6 +31,8 @@ use omicron_common::api::external::{DataPageParams, ResourceType}; use omicron_common::api::external::{DeleteResult, NameOrId}; use omicron_common::api::external::{Error, InternalContext}; use omicron_common::bail_unless; +use omicron_uuid_kinds::SiloGroupUuid; +use omicron_uuid_kinds::SiloUserUuid; use std::net::IpAddr; use std::str::FromStr; use uuid::Uuid; @@ -266,7 +268,7 @@ impl super::Nexus { &self, opctx: &OpContext, authz_silo: &authz::Silo, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, action: authz::Action, ) -> LookupResult<(authz::SiloUser, db::model::SiloUser)> { let (_, authz_silo_user, db_silo_user) = @@ -300,7 +302,7 @@ impl super::Nexus { &self, opctx: &OpContext, silo_lookup: &lookup::Silo<'_>, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, ) -> LookupResult { let (authz_silo,) = silo_lookup.lookup_for(authz::Action::Read).await?; let (_, db_silo_user) = self @@ -318,7 +320,7 @@ impl super::Nexus { pub(crate) async fn current_silo_user_logout( &self, opctx: &OpContext, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, ) -> UpdateResult<()> { let (_, authz_silo_user, _) = LookupPath::new(opctx, self.datastore()) .silo_user_id(silo_user_id) @@ -354,7 +356,7 @@ impl super::Nexus { pub(crate) async fn current_silo_user_lookup( &self, opctx: &OpContext, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, ) -> LookupResult<(authz::SiloUser, db::model::SiloUser)> { let (_, authz_silo_user, db_silo_user) = LookupPath::new(opctx, self.datastore()) @@ -369,7 +371,7 @@ impl super::Nexus { pub(crate) async fn silo_user_token_list( &self, opctx: &OpContext, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, pagparams: &DataPageParams<'_, Uuid>, ) -> ListResultVec { let (_, authz_silo_user, _db_silo_user) = @@ -389,7 +391,7 @@ impl super::Nexus { pub(crate) async fn silo_user_session_list( &self, opctx: &OpContext, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, pagparams: &DataPageParams<'_, Uuid>, // TODO: https://github.com/oxidecomputer/omicron/issues/8625 idle_ttl: TimeDelta, @@ -452,7 +454,7 @@ impl super::Nexus { .await?; let silo_user = db::model::SiloUser::new( authz_silo.id(), - Uuid::new_v4(), + SiloUserUuid::new_v4(), new_user_params.external_id.as_ref().to_owned(), ); // TODO These two steps should happen in a transaction. @@ -461,7 +463,7 @@ impl super::Nexus { let authz_silo_user = authz::SiloUser::new( authz_silo.clone(), db_silo_user.id(), - LookupType::ById(db_silo_user.id()), + LookupType::by_id(db_silo_user.id()), ); self.silo_user_password_set_internal( opctx, @@ -479,7 +481,7 @@ impl super::Nexus { &self, opctx: &OpContext, silo_lookup: &lookup::Silo<'_>, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, ) -> DeleteResult { let (authz_silo, _) = self.local_idp_fetch_silo(silo_lookup).await?; @@ -537,7 +539,7 @@ impl super::Nexus { db::model::UserProvisionType::Jit => { let silo_user = db::model::SiloUser::new( authz_silo.id(), - Uuid::new_v4(), + SiloUserUuid::new_v4(), authenticated_subject.external_id.clone(), ); @@ -552,7 +554,7 @@ impl super::Nexus { // IdP sent us. Also, if the silo user provision type is Jit, create // silo groups if new groups from the IdP are seen. - let mut silo_user_group_ids: Vec = + let mut silo_user_group_ids: Vec = Vec::with_capacity(authenticated_subject.groups.len()); for group in &authenticated_subject.groups { @@ -609,7 +611,7 @@ impl super::Nexus { &self, opctx: &OpContext, silo_lookup: &lookup::Silo<'_>, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, password_value: params::UserPassword, ) -> UpdateResult<()> { let (authz_silo, db_silo) = @@ -781,7 +783,7 @@ impl super::Nexus { opctx, authz_silo, db::model::SiloGroup::new( - Uuid::new_v4(), + SiloGroupUuid::new_v4(), authz_silo.id(), external_id.clone(), ), @@ -1031,7 +1033,7 @@ impl super::Nexus { pub fn silo_group_lookup<'a>( &'a self, opctx: &'a OpContext, - group_id: &'a Uuid, + group_id: &'a SiloGroupUuid, ) -> lookup::SiloGroup<'a> { LookupPath::new(opctx, &self.db_datastore).silo_group_id(*group_id) } diff --git a/nexus/src/app/ssh_key.rs b/nexus/src/app/ssh_key.rs index a0b77da5713..c4e6fb06136 100644 --- a/nexus/src/app/ssh_key.rs +++ b/nexus/src/app/ssh_key.rs @@ -12,8 +12,8 @@ use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; use omicron_common::api::external::NameOrId; use omicron_common::api::external::http_pagination::PaginatedBy; +use omicron_uuid_kinds::SiloUserUuid; use ref_cast::RefCast; -use uuid::Uuid; impl super::Nexus { // SSH Keys @@ -46,7 +46,7 @@ impl super::Nexus { pub(crate) async fn ssh_key_create( &self, opctx: &OpContext, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, params: params::SshKeyCreate, ) -> CreateResult { let ssh_key = db::model::SshKey::new(silo_user_id, params); @@ -61,7 +61,7 @@ impl super::Nexus { pub(crate) async fn ssh_keys_list( &self, opctx: &OpContext, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, page_params: &PaginatedBy<'_>, ) -> ListResultVec { let (.., authz_user) = LookupPath::new(opctx, self.datastore()) @@ -88,7 +88,7 @@ impl super::Nexus { pub(crate) async fn ssh_key_delete( &self, opctx: &OpContext, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, ssh_key_lookup: &lookup::SshKey<'_>, ) -> DeleteResult { let (.., authz_silo_user, authz_ssh_key) = diff --git a/nexus/src/context.rs b/nexus/src/context.rs index d170e7e79d0..91f0db6c31e 100644 --- a/nexus/src/context.rs +++ b/nexus/src/context.rs @@ -21,6 +21,7 @@ use nexus_db_queries::{authn, authz, db}; use omicron_common::address::{AZ_PREFIX, Ipv6Subnet}; use omicron_uuid_kinds::ConsoleSessionUuid; use omicron_uuid_kinds::GenericUuid; +use omicron_uuid_kinds::SiloUserUuid; use oximeter::types::ProducerRegistry; use oximeter_instruments::http::{HttpService, LatencyTracker}; use slog::Logger; @@ -442,7 +443,7 @@ impl authn::external::AuthenticatorContext for ServerContext { impl authn::external::SiloUserSilo for ServerContext { async fn silo_user_silo( &self, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, ) -> Result { let opctx = self.nexus.opctx_external_authn(); self.nexus.lookup_silo_for_authn(opctx, silo_user_id).await diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 7e465a38044..7cd1459fad7 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -661,7 +661,7 @@ impl NexusExternalApi for NexusExternalApiImpl { Ok(HttpResponseOk(ScanById::results_page( &query, users, - &|_, user: &User| user.id, + &|_, user: &User| user.id.into_untyped_uuid(), )?)) }; apictx @@ -6861,7 +6861,7 @@ impl NexusExternalApi for NexusExternalApiImpl { Ok(HttpResponseOk(ScanById::results_page( &query, users.into_iter().map(|i| i.into()).collect(), - &|_, user: &User| user.id, + &|_, user: &User| user.id.into_untyped_uuid(), )?)) }; apictx @@ -7008,7 +7008,7 @@ impl NexusExternalApi for NexusExternalApiImpl { Ok(HttpResponseOk(ScanById::results_page( &query, groups, - &|_, group: &Group| group.id, + &|_, group: &Group| group.id.into_untyped_uuid(), )?)) }; apictx @@ -7141,7 +7141,7 @@ impl NexusExternalApi for NexusExternalApiImpl { Ok(HttpResponseOk(ScanById::results_page( &query, groups, - &|_, group: &views::Group| group.id, + &|_, group: &views::Group| group.id.into_untyped_uuid(), )?)) }; apictx @@ -7168,12 +7168,23 @@ impl NexusExternalApi for NexusExternalApiImpl { .authn .actor_required() .internal_context("listing current user's ssh keys")?; + + let silo_user_id = match actor.silo_user_id() { + Some(silo_user_id) => silo_user_id, + None => { + return Err(Error::non_resourcetype_not_found( + "could not find silo user", + ))?; + } + }; + let ssh_keys = nexus - .ssh_keys_list(&opctx, actor.actor_id(), &paginated_by) + .ssh_keys_list(&opctx, silo_user_id, &paginated_by) .await? .into_iter() .map(SshKey::from) .collect::>(); + Ok(HttpResponseOk(ScanByNameOrId::results_page( &query, ssh_keys, @@ -7200,9 +7211,20 @@ impl NexusExternalApi for NexusExternalApiImpl { .authn .actor_required() .internal_context("creating ssh key for current user")?; + + let silo_user_id = match actor.silo_user_id() { + Some(silo_user_id) => silo_user_id, + None => { + return Err(Error::non_resourcetype_not_found( + "could not find silo user", + ))?; + } + }; + let ssh_key = nexus - .ssh_key_create(&opctx, actor.actor_id(), new_key.into_inner()) + .ssh_key_create(&opctx, silo_user_id, new_key.into_inner()) .await?; + Ok(HttpResponseCreated(ssh_key.into())) }; apictx @@ -7226,15 +7248,24 @@ impl NexusExternalApi for NexusExternalApiImpl { .authn .actor_required() .internal_context("fetching one of current user's ssh keys")?; - let ssh_key_selector = params::SshKeySelector { - silo_user_id: actor.actor_id(), - ssh_key: path.ssh_key, + + let silo_user_id = match actor.silo_user_id() { + Some(silo_user_id) => silo_user_id, + None => { + return Err(Error::non_resourcetype_not_found( + "could not find silo user", + ))?; + } }; + + let ssh_key_selector = + params::SshKeySelector { silo_user_id, ssh_key: path.ssh_key }; + let ssh_key_lookup = nexus.ssh_key_lookup(&opctx, &ssh_key_selector)?; - let (.., silo_user, _, ssh_key) = ssh_key_lookup.fetch().await?; - // Ensure the SSH key exists in the current silo - assert_eq!(silo_user.id(), actor.actor_id()); + + let (.., ssh_key) = ssh_key_lookup.fetch().await?; + Ok(HttpResponseOk(ssh_key.into())) }; apictx @@ -7258,15 +7289,24 @@ impl NexusExternalApi for NexusExternalApiImpl { .authn .actor_required() .internal_context("deleting one of current user's ssh keys")?; - let ssh_key_selector = params::SshKeySelector { - silo_user_id: actor.actor_id(), - ssh_key: path.ssh_key, + + let silo_user_id = match actor.silo_user_id() { + Some(silo_user_id) => silo_user_id, + None => { + return Err(Error::non_resourcetype_not_found( + "could not find silo user", + ))?; + } }; + + let ssh_key_selector = + params::SshKeySelector { silo_user_id, ssh_key: path.ssh_key }; + let ssh_key_lookup = nexus.ssh_key_lookup(&opctx, &ssh_key_selector)?; - nexus - .ssh_key_delete(&opctx, actor.actor_id(), &ssh_key_lookup) - .await?; + + nexus.ssh_key_delete(&opctx, silo_user_id, &ssh_key_lookup).await?; + Ok(HttpResponseDeleted()) }; apictx @@ -8215,7 +8255,9 @@ impl NexusExternalApi for NexusExternalApiImpl { let handler = async { let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let nexus = &apictx.context.nexus; + // This is an authenticated request, so we know who the user // is. In that respect it's more like a regular resource create // operation and not like the true login endpoints `login_local` @@ -8227,13 +8269,24 @@ impl NexusExternalApi for NexusExternalApiImpl { let &actor = opctx.authn.actor_required().internal_context( "creating new device auth session for current user", )?; + + let silo_user_id = match actor.silo_user_id() { + Some(silo_user_id) => silo_user_id, + None => { + return Err(Error::non_resourcetype_not_found( + "could not find silo user", + ))?; + } + }; + let _token = nexus .device_auth_request_verify( &opctx, params.user_code, - actor.actor_id(), + silo_user_id, ) .await?; + Ok(HttpResponseUpdatedNoContent()) } .await; diff --git a/nexus/test-utils/src/http_testing.rs b/nexus/test-utils/src/http_testing.rs index 9b63525a48d..ab4905267e6 100644 --- a/nexus/test-utils/src/http_testing.rs +++ b/nexus/test-utils/src/http_testing.rs @@ -15,6 +15,7 @@ use headers::authorization::Credentials; use http_body_util::BodyExt; use nexus_db_queries::authn::external::spoof; use nexus_db_queries::db::identity::Asset; +use omicron_uuid_kinds::SiloUserUuid; use serde_urlencoded; use std::convert::TryInto; use std::fmt::Debug; @@ -546,7 +547,7 @@ impl TestResponse { pub enum AuthnMode { UnprivilegedUser, PrivilegedUser, - SiloUser(uuid::Uuid), + SiloUser(SiloUserUuid), Session(String), } diff --git a/nexus/test-utils/src/resource_helpers.rs b/nexus/test-utils/src/resource_helpers.rs index 97d96e1fceb..133ee9a20fa 100644 --- a/nexus/test-utils/src/resource_helpers.rs +++ b/nexus/test-utils/src/resource_helpers.rs @@ -20,7 +20,6 @@ use nexus_types::deployment::Blueprint; use nexus_types::external_api::params; use nexus_types::external_api::shared; use nexus_types::external_api::shared::Baseboard; -use nexus_types::external_api::shared::IdentityType; use nexus_types::external_api::shared::IpRange; use nexus_types::external_api::views; use nexus_types::external_api::views::AffinityGroup; @@ -66,6 +65,7 @@ use omicron_test_utils::dev::poll::wait_for_condition; use omicron_uuid_kinds::DatasetUuid; use omicron_uuid_kinds::GenericUuid; use omicron_uuid_kinds::PhysicalDiskUuid; +use omicron_uuid_kinds::SiloUserUuid; use omicron_uuid_kinds::SledUuid; use omicron_uuid_kinds::ZpoolUuid; use oxnet::Ipv4Net; @@ -1099,7 +1099,7 @@ pub async fn grant_iam( client: &ClientTestContext, grant_resource_url: &str, grant_role: T, - grant_user: Uuid, + grant_user: SiloUserUuid, run_as: AuthnMode, ) where T: serde::Serialize + serde::de::DeserializeOwned, @@ -1113,11 +1113,8 @@ pub async fn grant_iam( .expect("failed to fetch policy") .parsed_body() .expect("failed to parse policy"); - let new_role_assignment = shared::RoleAssignment { - identity_type: IdentityType::SiloUser, - identity_id: grant_user, - role_name: grant_role, - }; + let new_role_assignment = + shared::RoleAssignment::for_silo_user(grant_user, grant_role); let new_role_assignments = existing_policy .role_assignments .into_iter() diff --git a/nexus/tests/integration_tests/authn_http.rs b/nexus/tests/integration_tests/authn_http.rs index 59275e881dd..7a50c137f61 100644 --- a/nexus/tests/integration_tests/authn_http.rs +++ b/nexus/tests/integration_tests/authn_http.rs @@ -19,6 +19,7 @@ use dropshot::test_util::LogContext; use dropshot::test_util::TestContext; use headers::authorization::Credentials; use http::header::HeaderValue; +use nexus_db_queries::authn::Actor; use nexus_db_queries::authn::external::AuthenticatorContext; use nexus_db_queries::authn::external::HttpAuthnScheme; use nexus_db_queries::authn::external::SiloUserSilo; @@ -28,6 +29,7 @@ use nexus_db_queries::authn::external::spoof::HttpAuthnSpoof; use nexus_db_queries::authn::external::spoof::SPOOF_SCHEME_NAME; use nexus_types::silo::DEFAULT_SILO_ID; use omicron_uuid_kinds::ConsoleSessionUuid; +use omicron_uuid_kinds::SiloUserUuid; use std::sync::Mutex; use uuid::Uuid; @@ -62,8 +64,7 @@ async fn test_authn_spoof_allowed() { // Successful authentication let valid_uuid = "7f927c86-3371-4295-c34a-e3246a4b9c02"; - let header = - spoof::make_header_value(valid_uuid.parse().unwrap()).0.encode(); + let header = spoof::make_header_value(valid_uuid).0.encode(); assert_eq!( whoami_request(Some(header), None, &testctx).await.unwrap(), WhoamiResponse { @@ -107,7 +108,7 @@ async fn test_authn_session_cookie() { let valid_session = FakeSession { id: ConsoleSessionUuid::new_v4(), token: "valid".to_string(), - silo_user_id: Uuid::new_v4(), + silo_user_id: SiloUserUuid::new_v4(), silo_id: Uuid::new_v4(), time_last_used: Utc::now() - Duration::seconds(5), time_created: Utc::now() - Duration::seconds(5), @@ -115,7 +116,7 @@ async fn test_authn_session_cookie() { let idle_expired_session = FakeSession { id: ConsoleSessionUuid::new_v4(), token: "idle_expired".to_string(), - silo_user_id: Uuid::new_v4(), + silo_user_id: SiloUserUuid::new_v4(), silo_id: Uuid::new_v4(), time_last_used: Utc::now() - Duration::hours(2), time_created: Utc::now() - Duration::hours(3), @@ -123,7 +124,7 @@ async fn test_authn_session_cookie() { let abs_expired_session = FakeSession { id: ConsoleSessionUuid::new_v4(), token: "abs_expired".to_string(), - silo_user_id: Uuid::new_v4(), + silo_user_id: SiloUserUuid::new_v4(), silo_id: Uuid::new_v4(), time_last_used: Utc::now(), time_created: Utc::now() - Duration::hours(10), @@ -196,11 +197,9 @@ async fn test_authn_spoof_unconfigured() { let values = [ None, Some( - spoof::make_header_value( - "7f927c86-3371-4295-c34a-e3246a4b9c02".parse().unwrap(), - ) - .0 - .encode(), + spoof::make_header_value("7f927c86-3371-4295-c34a-e3246a4b9c02") + .0 + .encode(), ), Some(spoof::make_header_value_raw(b"not-a-uuid").unwrap()), Some(spoof::SPOOF_HEADER_BAD_ACTOR.0.encode()), @@ -334,7 +333,7 @@ impl AuthenticatorContext for WhoamiServerState { impl SiloUserSilo for WhoamiServerState { async fn silo_user_silo( &self, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, ) -> Result { assert_eq!( silo_user_id.to_string(), @@ -348,7 +347,7 @@ impl SiloUserSilo for WhoamiServerState { struct FakeSession { id: ConsoleSessionUuid, token: String, - silo_user_id: Uuid, + silo_user_id: SiloUserUuid, silo_id: Uuid, time_created: DateTime, time_last_used: DateTime, @@ -358,7 +357,7 @@ impl session_cookie::Session for FakeSession { fn id(&self) -> ConsoleSessionUuid { self.id } - fn silo_user_id(&self) -> Uuid { + fn silo_user_id(&self) -> SiloUserUuid { self.silo_user_id } fn silo_id(&self) -> Uuid { @@ -435,7 +434,11 @@ async fn whoami_get( ) -> Result, dropshot::HttpError> { let whoami_state = rqctx.context(); let authn = whoami_state.authn.authn_request(&rqctx).await?; - let actor = authn.actor().map(|a| a.actor_id().to_string()); + 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(), + }); let authenticated = actor.is_some(); let schemes_tried = authn.schemes_tried().iter().map(|s| s.to_string()).collect(); diff --git a/nexus/tests/integration_tests/authz.rs b/nexus/tests/integration_tests/authz.rs index 84b97969f9b..fda3f5de0e6 100644 --- a/nexus/tests/integration_tests/authz.rs +++ b/nexus/tests/integration_tests/authz.rs @@ -16,7 +16,7 @@ use omicron_common::api::external::IdentityMetadataCreateParams; use dropshot::ResultsPage; use nexus_test_utils::resource_helpers::{create_local_user, create_silo}; -use uuid::Uuid; +use omicron_uuid_kinds::SiloUserUuid; type ControlPlaneTestContext = nexus_test_utils::ControlPlaneTestContext; @@ -178,7 +178,8 @@ async fn test_list_silo_users_for_unpriv(cptestctx: &ControlPlaneTestContext) { .unwrap(); // And only show the first silo's user - let user_ids: Vec = users.items.iter().map(|x| x.id).collect(); + let user_ids: Vec = + users.items.iter().map(|x| x.id).collect(); assert_eq!(user_ids, vec![new_silo_user_id]); } diff --git a/nexus/tests/integration_tests/certificates.rs b/nexus/tests/integration_tests/certificates.rs index 57d90e27006..46cf686ec2a 100644 --- a/nexus/tests/integration_tests/certificates.rs +++ b/nexus/tests/integration_tests/certificates.rs @@ -25,6 +25,8 @@ use omicron_common::api::internal::nexus::Certificate as InternalCertificate; use omicron_test_utils::certificates::CertificateChain; use omicron_test_utils::dev::poll::CondCheckError; use omicron_test_utils::dev::poll::wait_for_condition; +use omicron_uuid_kinds::GenericUuid; +use omicron_uuid_kinds::SiloUserUuid; use oxide_client::ClientCurrentUserExt; use oxide_client::ClientSilosExt; use oxide_client::ClientSystemSilosExt; @@ -409,7 +411,7 @@ async fn test_silo_certificates() { .expect("failed to create Silo"); // Create a local user in that Silo. - let silo2_user = silo1_client + let silo2_user: SiloUserUuid = silo1_client .local_idp_user_create() .silo(silo2.silo_name.clone()) .body( @@ -433,7 +435,7 @@ async fn test_silo_certificates() { .into_inner(); silo2_policy.role_assignments.push( oxide_client::types::SiloRoleRoleAssignment::builder() - .identity_id(silo2_user) + .identity_id(silo2_user.into_untyped_uuid()) .identity_type(oxide_client::types::IdentityType::SiloUser) .role_name(oxide_client::types::SiloRole::Admin) .try_into() @@ -472,7 +474,7 @@ async fn test_silo_certificates() { .send() .await .expect("failed to create Silo"); - let silo3_user = silo1_client + let silo3_user: SiloUserUuid = silo1_client .local_idp_user_create() .silo(silo3.silo_name.clone()) .body( @@ -496,7 +498,7 @@ async fn test_silo_certificates() { .into_inner(); silo3_policy.role_assignments.push( oxide_client::types::SiloRoleRoleAssignment::builder() - .identity_id(silo3_user) + .identity_id(silo3_user.into_untyped_uuid()) .identity_type(oxide_client::types::IdentityType::SiloUser) .role_name(oxide_client::types::SiloRole::Admin) .try_into() diff --git a/nexus/tests/integration_tests/device_auth.rs b/nexus/tests/integration_tests/device_auth.rs index 60532eff4e8..8963da78b61 100644 --- a/nexus/tests/integration_tests/device_auth.rs +++ b/nexus/tests/integration_tests/device_auth.rs @@ -29,6 +29,7 @@ use nexus_types::external_api::{ DeviceAccessTokenGrant, DeviceAccessTokenType, DeviceAuthResponse, }, }; +use omicron_uuid_kinds::SiloUserUuid; use http::{StatusCode, header, method::Method}; use omicron_sled_agent::sim; @@ -846,7 +847,7 @@ async fn get_tokens_priv( async fn list_user_tokens( testctx: &ClientTestContext, - user_id: Uuid, + user_id: SiloUserUuid, ) -> Vec { NexusRequest::object_get(testctx, "/v1/me/access-tokens") .authn_as(AuthnMode::SiloUser(user_id)) @@ -857,7 +858,7 @@ async fn list_user_tokens( async fn list_user_sessions( testctx: &ClientTestContext, - user_id: Uuid, + user_id: SiloUserUuid, ) -> Vec { let url = format!("/v1/users/{}/sessions", user_id); NexusRequest::object_get(testctx, &url) diff --git a/nexus/tests/integration_tests/role_assignments.rs b/nexus/tests/integration_tests/role_assignments.rs index c11c0e1fb34..40cefc8e662 100644 --- a/nexus/tests/integration_tests/role_assignments.rs +++ b/nexus/tests/integration_tests/role_assignments.rs @@ -424,11 +424,10 @@ async fn run_test( // resource. This is a little ugly, but we don't have a way of creating // silo users yet and it's worth testing this. let mut new_policy = initial_policy.clone(); - let role_assignment = shared::RoleAssignment { - identity_type: shared::IdentityType::SiloUser, - identity_id: USER_TEST_UNPRIVILEGED.id(), - role_name: T::ROLE, - }; + let role_assignment = shared::RoleAssignment::for_silo_user( + USER_TEST_UNPRIVILEGED.id(), + T::ROLE, + ); new_policy.role_assignments.push(role_assignment.clone()); // Make sure the unprivileged user can't grant themselves access! diff --git a/nexus/tests/integration_tests/saml.rs b/nexus/tests/integration_tests/saml.rs index 4b1795a67bc..0995a193009 100644 --- a/nexus/tests/integration_tests/saml.rs +++ b/nexus/tests/integration_tests/saml.rs @@ -14,13 +14,13 @@ use nexus_types::external_api::views::{self, Silo}; use nexus_types::external_api::{params, shared}; use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_nexus::TestInterfaces; +use omicron_uuid_kinds::SiloGroupUuid; use base64::Engine; use dropshot::ResultsPage; use http::StatusCode; use http::method::Method; use httptest::{Expectation, Server, matchers::*, responders::*}; -use uuid::Uuid; type ControlPlaneTestContext = nexus_test_utils::ControlPlaneTestContext; @@ -1227,7 +1227,8 @@ async fn test_post_saml_response(cptestctx: &ControlPlaneTestContext) { let silo_group_names: Vec<&str> = groups.items.iter().map(|g| g.display_name.as_str()).collect(); - let silo_group_ids: Vec = groups.items.iter().map(|g| g.id).collect(); + let silo_group_ids: Vec = + groups.items.iter().map(|g| g.id).collect(); assert_same_items(silo_group_names, vec!["SRE", "Admins"]); diff --git a/nexus/tests/integration_tests/silo_users.rs b/nexus/tests/integration_tests/silo_users.rs index 025f81ea238..d1c11f83f74 100644 --- a/nexus/tests/integration_tests/silo_users.rs +++ b/nexus/tests/integration_tests/silo_users.rs @@ -84,7 +84,7 @@ async fn test_silo_group_users(cptestctx: &ControlPlaneTestContext) { let authz_silo_user = authz::SiloUser::new( authz_silo, USER_TEST_UNPRIVILEGED.id(), - LookupType::ById(USER_TEST_UNPRIVILEGED.id()), + LookupType::by_id(USER_TEST_UNPRIVILEGED.id()), ); // Now add unprivileged user to the group, and we should see only that user diff --git a/nexus/tests/integration_tests/silos.rs b/nexus/tests/integration_tests/silos.rs index 0cdd73f9d0f..8616cca3a6c 100644 --- a/nexus/tests/integration_tests/silos.rs +++ b/nexus/tests/integration_tests/silos.rs @@ -32,6 +32,7 @@ use omicron_common::api::external::{ use omicron_common::api::external::{ObjectIdentity, UserId}; use omicron_test_utils::certificates::CertificateChain; use omicron_test_utils::dev::poll::{CondCheckError, wait_for_condition}; +use omicron_uuid_kinds::SiloUserUuid; use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::fmt::Write; @@ -47,7 +48,6 @@ use nexus_types::external_api::shared::{FleetRole, SiloRole}; use std::convert::Infallible; use std::net::Ipv4Addr; use std::time::Duration; -use uuid::Uuid; type ControlPlaneTestContext = nexus_test_utils::ControlPlaneTestContext; @@ -1080,7 +1080,7 @@ async fn test_silo_groups_jit(cptestctx: &ControlPlaneTestContext) { for group_membership in &group_memberships { let (.., db_group) = LookupPath::new(&authn_opctx, nexus.datastore()) - .silo_group_id(group_membership.silo_group_id) + .silo_group_id(group_membership.silo_group_id.into()) .fetch() .await .unwrap(); @@ -1209,7 +1209,7 @@ async fn test_silo_groups_remove_from_one_group( for group_membership in &group_memberships { let (.., db_group) = LookupPath::new(&authn_opctx, nexus.datastore()) - .silo_group_id(group_membership.silo_group_id) + .silo_group_id(group_membership.silo_group_id.into()) .fetch() .await .unwrap(); @@ -1250,7 +1250,7 @@ async fn test_silo_groups_remove_from_one_group( for group_membership in &group_memberships { let (.., db_group) = LookupPath::new(&authn_opctx, nexus.datastore()) - .silo_group_id(group_membership.silo_group_id) + .silo_group_id(group_membership.silo_group_id.into()) .fetch() .await .unwrap(); @@ -1320,7 +1320,7 @@ async fn test_silo_groups_remove_from_both_groups( for group_membership in &group_memberships { let (.., db_group) = LookupPath::new(&authn_opctx, nexus.datastore()) - .silo_group_id(group_membership.silo_group_id) + .silo_group_id(group_membership.silo_group_id.into()) .fetch() .await .unwrap(); @@ -1361,7 +1361,7 @@ async fn test_silo_groups_remove_from_both_groups( for group_membership in &group_memberships { let (.., db_group) = LookupPath::new(&authn_opctx, nexus.datastore()) - .silo_group_id(group_membership.silo_group_id) + .silo_group_id(group_membership.silo_group_id.into()) .fetch() .await .unwrap(); @@ -1562,7 +1562,8 @@ async fn test_silo_user_views(cptestctx: &ControlPlaneTestContext) { silo2_expected_users.sort_by_key(|u| u.id); let users_by_id = { - let mut users_by_id: BTreeMap = BTreeMap::new(); + let mut users_by_id: BTreeMap = + BTreeMap::new(); assert_eq!(users_by_id.insert(silo1_user1_id, &silo1_user1), None); assert_eq!(users_by_id.insert(silo1_user2_id, &silo1_user2), None); assert_eq!(users_by_id.insert(silo2_user1_id, &silo2_user1), None); @@ -1719,7 +1720,7 @@ async fn create_jit_user( ) -> views::User { assert_eq!(silo.identity_mode, shared::SiloIdentityMode::SamlJit); let silo_id = silo.identity.id; - let silo_user_id = Uuid::new_v4(); + let silo_user_id = SiloUserUuid::new_v4(); let authz_silo = authz::Silo::new(authz::FLEET, silo_id, LookupType::ById(silo_id)); let silo_user = diff --git a/nexus/tests/integration_tests/users_builtin.rs b/nexus/tests/integration_tests/users_builtin.rs index 3df709c7f39..50642e10424 100644 --- a/nexus/tests/integration_tests/users_builtin.rs +++ b/nexus/tests/integration_tests/users_builtin.rs @@ -6,6 +6,7 @@ use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::views::UserBuiltin; +use omicron_uuid_kinds::GenericUuid; use std::collections::BTreeMap; type ControlPlaneTestContext = @@ -29,18 +30,24 @@ async fn test_users_builtin(cptestctx: &ControlPlaneTestContext) { .collect::>(); let u = users.remove(&authn::USER_DB_INIT.name.to_string()).unwrap(); - assert_eq!(u.identity.id, authn::USER_DB_INIT.id); + assert_eq!(u.identity.id, authn::USER_DB_INIT.id.into_untyped_uuid()); let u = users.remove(&authn::USER_SERVICE_BALANCER.name.to_string()).unwrap(); - assert_eq!(u.identity.id, authn::USER_SERVICE_BALANCER.id); + assert_eq!( + u.identity.id, + authn::USER_SERVICE_BALANCER.id.into_untyped_uuid() + ); let u = users.remove(&authn::USER_INTERNAL_API.name.to_string()).unwrap(); - assert_eq!(u.identity.id, authn::USER_INTERNAL_API.id); + assert_eq!(u.identity.id, authn::USER_INTERNAL_API.id.into_untyped_uuid()); let u = users.remove(&authn::USER_INTERNAL_READ.name.to_string()).unwrap(); - assert_eq!(u.identity.id, authn::USER_INTERNAL_READ.id); + assert_eq!(u.identity.id, authn::USER_INTERNAL_READ.id.into_untyped_uuid()); let u = users.remove(&authn::USER_EXTERNAL_AUTHN.name.to_string()).unwrap(); - assert_eq!(u.identity.id, authn::USER_EXTERNAL_AUTHN.id); + assert_eq!( + u.identity.id, + authn::USER_EXTERNAL_AUTHN.id.into_untyped_uuid() + ); let u = users.remove(&authn::USER_SAGA_RECOVERY.name.to_string()).unwrap(); - assert_eq!(u.identity.id, authn::USER_SAGA_RECOVERY.id); + assert_eq!(u.identity.id, authn::USER_SAGA_RECOVERY.id.into_untyped_uuid()); assert!(users.is_empty(), "found unexpected built-in users"); // TODO-coverage add test for fetching individual users, including invalid diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 6004682b158..236e4c53138 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -17,6 +17,8 @@ use omicron_common::api::external::{ RouteDestination, RouteTarget, UserId, }; use omicron_common::disk::DiskVariant; +use omicron_uuid_kinds::SiloGroupUuid; +use omicron_uuid_kinds::SiloUserUuid; use oxnet::{IpNet, Ipv4Net, Ipv6Net}; use parse_display::Display; use schemars::JsonSchema; @@ -45,11 +47,15 @@ macro_rules! path_param { macro_rules! id_path_param { ($struct:ident, $param:ident, $name:tt) => { + id_path_param!($struct, $param, $name, Uuid); + }; + + ($struct:ident, $param:ident, $name:tt, $uuid_type:ident) => { #[derive(Serialize, Deserialize, JsonSchema)] pub struct $struct { #[doc = "ID of the "] #[doc = $name] - pub $param: Uuid, + pub $param: $uuid_type, } }; } @@ -95,8 +101,8 @@ path_param!(ProbePath, probe, "probe"); path_param!(CertificatePath, certificate, "certificate"); id_path_param!(SupportBundlePath, bundle_id, "support bundle"); -id_path_param!(GroupPath, group_id, "group"); -id_path_param!(UserPath, user_id, "user"); +id_path_param!(GroupPath, group_id, "group", SiloGroupUuid); +id_path_param!(UserPath, user_id, "user", SiloUserUuid); id_path_param!(TokenPath, token_id, "token"); id_path_param!(TufTrustRootPath, trust_root_id, "trust root"); @@ -181,7 +187,7 @@ pub struct OptionalSiloSelector { #[derive(Deserialize, JsonSchema)] pub struct UserParam { /// The user's internal ID - pub user_id: Uuid, + pub user_id: SiloUserUuid, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] @@ -198,7 +204,7 @@ pub struct SamlIdentityProviderSelector { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct SshKeySelector { /// ID of the silo user - pub silo_user_id: Uuid, + pub silo_user_id: SiloUserUuid, /// Name or ID of the SSH key pub ssh_key: NameOrId, } @@ -2291,7 +2297,7 @@ pub struct SnapshotCreate { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct OptionalGroupSelector { - pub group: Option, + pub group: Option, } // BUILT-IN USERS diff --git a/nexus/types/src/external_api/shared.rs b/nexus/types/src/external_api/shared.rs index 2bb4beb6f12..6ba42be5bcc 100644 --- a/nexus/types/src/external_api/shared.rs +++ b/nexus/types/src/external_api/shared.rs @@ -12,6 +12,9 @@ use chrono::DateTime; use chrono::Utc; use omicron_common::api::external::Name; use omicron_common::api::internal::shared::NetworkInterface; +use omicron_uuid_kinds::GenericUuid; +use omicron_uuid_kinds::SiloGroupUuid; +use omicron_uuid_kinds::SiloUserUuid; use omicron_uuid_kinds::SupportBundleUuid; use parse_display::FromStr; use schemars::JsonSchema; @@ -88,6 +91,30 @@ pub struct RoleAssignment { pub role_name: AllowedRoles, } +impl RoleAssignment { + pub fn for_silo_user( + silo_user_id: SiloUserUuid, + role_name: AllowedRoles, + ) -> Self { + Self { + identity_type: IdentityType::SiloUser, + identity_id: silo_user_id.into_untyped_uuid(), + role_name, + } + } + + pub fn for_silo_group( + silo_group_id: SiloGroupUuid, + role_name: AllowedRoles, + ) -> Self { + Self { + identity_type: IdentityType::SiloGroup, + identity_id: silo_group_id.into_untyped_uuid(), + role_name, + } + } +} + #[derive( Clone, Copy, diff --git a/nexus/types/src/external_api/views.rs b/nexus/types/src/external_api/views.rs index 2b8f8282307..6c451eda083 100644 --- a/nexus/types/src/external_api/views.rs +++ b/nexus/types/src/external_api/views.rs @@ -18,7 +18,11 @@ use omicron_common::api::external::{ Digest, Error, FailureDomain, IdentityMetadata, InstanceState, Name, ObjectIdentity, SimpleIdentity, SimpleIdentityOrName, }; -use omicron_uuid_kinds::{AlertReceiverUuid, AlertUuid}; +use omicron_uuid_kinds::AlertReceiverUuid; +use omicron_uuid_kinds::AlertUuid; +use omicron_uuid_kinds::BuiltInUserUuid; +use omicron_uuid_kinds::SiloGroupUuid; +use omicron_uuid_kinds::SiloUserUuid; use oxnet::{Ipv4Net, Ipv6Net}; use schemars::JsonSchema; use semver::Version; @@ -939,7 +943,8 @@ impl fmt::Display for PhysicalDiskState { /// View of a User #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, JsonSchema)] pub struct User { - pub id: Uuid, + pub id: SiloUserUuid, + /** Human-readable name that can identify the user */ pub display_name: String, @@ -965,7 +970,7 @@ pub struct CurrentUser { /// View of a Group #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, JsonSchema)] pub struct Group { - pub id: Uuid, + pub id: SiloGroupUuid, /// Human-readable name that can identify the group pub display_name: String, @@ -999,7 +1004,7 @@ pub struct SshKey { pub identity: IdentityMetadata, /// The user to whom this key belongs - pub silo_user_id: Uuid, + pub silo_user_id: SiloUserUuid, /// SSH public key, e.g., `"ssh-ed25519 AAAAC3NzaC..."` pub public_key: String, @@ -1579,8 +1584,8 @@ mod test { #[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum AuditLogEntryActor { - UserBuiltin { user_builtin_id: Uuid }, - SiloUser { silo_user_id: Uuid, silo_id: Uuid }, + UserBuiltin { user_builtin_id: BuiltInUserUuid }, + SiloUser { silo_user_id: SiloUserUuid, silo_id: Uuid }, Unauthenticated, } diff --git a/openapi/nexus.json b/openapi/nexus.json index 8534901038c..2adaff218ec 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -3228,8 +3228,7 @@ "description": "ID of the group", "required": true, "schema": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/TypedUuidForSiloGroupKind" } } ], @@ -8068,8 +8067,7 @@ "description": "The user's internal ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/TypedUuidForSiloUserKind" } }, { @@ -8110,8 +8108,7 @@ "description": "The user's internal ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/TypedUuidForSiloUserKind" } }, { @@ -11266,8 +11263,7 @@ "description": "The user's internal ID", "required": true, "schema": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/TypedUuidForSiloUserKind" } }, { @@ -11554,9 +11550,7 @@ "in": "query", "name": "group", "schema": { - "nullable": true, - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/TypedUuidForSiloGroupKind" } }, { @@ -11624,8 +11618,7 @@ "description": "ID of the user", "required": true, "schema": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/TypedUuidForSiloUserKind" } } ], @@ -11663,8 +11656,7 @@ "description": "ID of the user", "required": true, "schema": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/TypedUuidForSiloUserKind" } }, { @@ -11733,8 +11725,7 @@ "description": "ID of the user", "required": true, "schema": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/TypedUuidForSiloUserKind" } } ], @@ -11765,8 +11756,7 @@ "description": "ID of the user", "required": true, "schema": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/TypedUuidForSiloUserKind" } }, { @@ -14641,8 +14631,7 @@ ] }, "user_builtin_id": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/TypedUuidForBuiltInUserKind" } }, "required": [ @@ -14664,8 +14653,7 @@ "format": "uuid" }, "silo_user_id": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/TypedUuidForSiloUserKind" } }, "required": [ @@ -16759,8 +16747,7 @@ "type": "string" }, "id": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/TypedUuidForSiloUserKind" }, "silo_id": { "description": "Uuid of the silo to which this user belongs", @@ -18824,8 +18811,7 @@ "type": "string" }, "id": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/TypedUuidForSiloGroupKind" }, "silo_id": { "description": "Uuid of the silo to which this group belongs", @@ -24405,8 +24391,11 @@ }, "silo_user_id": { "description": "The user to whom this key belongs", - "type": "string", - "format": "uuid" + "allOf": [ + { + "$ref": "#/components/schemas/TypedUuidForSiloUserKind" + } + ] }, "time_created": { "description": "timestamp when this resource was created", @@ -25917,10 +25906,22 @@ "type": "string", "format": "uuid" }, + "TypedUuidForBuiltInUserKind": { + "type": "string", + "format": "uuid" + }, "TypedUuidForInstanceKind": { "type": "string", "format": "uuid" }, + "TypedUuidForSiloGroupKind": { + "type": "string", + "format": "uuid" + }, + "TypedUuidForSiloUserKind": { + "type": "string", + "format": "uuid" + }, "TypedUuidForSupportBundleKind": { "type": "string", "format": "uuid" @@ -26071,8 +26072,7 @@ "type": "string" }, "id": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/TypedUuidForSiloUserKind" }, "silo_id": { "description": "Uuid of the silo to which this user belongs", diff --git a/uuid-kinds/src/lib.rs b/uuid-kinds/src/lib.rs index 00e2793a982..c2bbc054ce2 100644 --- a/uuid-kinds/src/lib.rs +++ b/uuid-kinds/src/lib.rs @@ -57,6 +57,7 @@ impl_typed_uuid_kind! { AlertReceiver => "alert_receiver", AntiAffinityGroup => "anti_affinity_group", Blueprint => "blueprint", + BuiltInUser => "built_in_user", Collection => "collection", ConsoleSession => "console_session", Dataset => "dataset", @@ -84,6 +85,8 @@ impl_typed_uuid_kind! { RackReset => "rack_reset", ReconfiguratorSim => "reconfigurator_sim", Region => "region", + SiloGroup => "silo_group", + SiloUser => "silo_user", Sled => "sled", SpUpdate => "sp_update", SupportBundle => "support_bundle",