diff --git a/Cargo.lock b/Cargo.lock index 76804faf432..f94f8362ab9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2743,6 +2743,34 @@ dependencies = [ "rustc_version 0.1.7", ] +[[package]] +name = "nexus-authn" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "base64", + "chrono", + "cookie", + "dropshot", + "headers", + "http", + "hyper", + "lazy_static", + "newtype_derive", + "omicron-common 0.1.0", + "openssl", + "openssl-probe", + "openssl-sys", + "samael", + "serde", + "serde_urlencoded", + "slog", + "thiserror", + "tokio", + "uuid", +] + [[package]] name = "nexus-client" version = "0.1.0" @@ -3056,7 +3084,6 @@ dependencies = [ "bb8", "chrono", "clap 3.2.12", - "cookie", "criterion", "crucible-agent-client", "db-macros", @@ -3079,6 +3106,7 @@ dependencies = [ "macaddr", "mime_guess", "newtype_derive", + "nexus-authn", "nexus-test-utils", "nexus-test-utils-macros", "num-integer", @@ -3103,7 +3131,6 @@ dependencies = [ "regex", "reqwest", "ring", - "samael", "schemars", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index e8d3f637b15..8ee365dbc3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,8 +11,9 @@ members = [ "internal-dns", "internal-dns-client", "nexus", - "nexus/src/authz/authz-macros", - "nexus/src/db/db-macros", + "nexus/authn", + "nexus/authz-macros", + "nexus/db-macros", "nexus/test-utils", "nexus/test-utils-macros", "nexus-client", @@ -43,8 +44,9 @@ default-members = [ "internal-dns", "internal-dns-client", "nexus", - "nexus/src/authz/authz-macros", - "nexus/src/db/db-macros", + "nexus/authn", + "nexus/authz-macros", + "nexus/db-macros", "package", "rpaths", "sled-agent", diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index 247592e8a1c..72754426ae2 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -11,21 +11,17 @@ path = "../rpaths" anyhow = "1.0" async-bb8-diesel = { git = "https://github.com/oxidecomputer/async-bb8-diesel", rev = "ab1f49e0b3f95557aa96bf593282199fafeef4bd" } async-trait = "0.1.56" -authz-macros = { path = "src/authz/authz-macros" } base64 = "0.13.0" bb8 = "0.8.0" clap = { version = "3.2", features = ["derive"] } -cookie = "0.16" crucible-agent-client = { git = "https://github.com/oxidecomputer/crucible", rev = "2add0de8489f1d4de901bfe98fc28b0a6efcc3ea" } diesel = { version = "2.0.0-rc.0", features = ["postgres", "r2d2", "chrono", "serde_json", "network-address", "uuid"] } diesel-dtrace = { git = "https://github.com/oxidecomputer/diesel-dtrace" } fatfs = "0.3.5" futures = "0.3.21" -headers = "0.3.7" hex = "0.4.3" http = "0.2.7" hyper = "0.14" -db-macros = { path = "src/db/db-macros" } internal-dns-client = { path = "../internal-dns-client" } ipnetwork = "0.18" lazy_static = "1.4.0" @@ -48,7 +44,6 @@ rand = "0.8.5" ref-cast = "1.0" reqwest = { version = "0.11.8", features = [ "json" ] } ring = "0.16" -samael = { git = "https://github.com/njaremko/samael", features = ["xmlsec"], branch = "master" } serde_json = "1.0" serde_urlencoded = "0.7.1" serde_with = "2.0.0" @@ -60,6 +55,10 @@ toml = "0.5.9" tough = { version = "0.12", features = [ "http" ] } usdt = "0.3.1" +authz-macros = { path = "authz-macros" } +db-macros = { path = "db-macros" } +nexus-authn = { path = "authn" } + [dependencies.api_identity] path = "../api_identity" @@ -119,6 +118,7 @@ features = [ "serde", "v4" ] [dev-dependencies] criterion = { version = "0.3", features = [ "async_tokio" ] } expectorate = "1.0.5" +headers = "0.3.7" itertools = "0.10.3" nexus-test-utils-macros = { path = "test-utils-macros" } nexus-test-utils = { path = "test-utils" } diff --git a/nexus/authn/Cargo.toml b/nexus/authn/Cargo.toml new file mode 100644 index 00000000000..e509ea9344c --- /dev/null +++ b/nexus/authn/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "nexus-authn" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[dependencies] +async-trait = "0.1.56" +anyhow = "1.0" +base64 = "0.13.0" +chrono = { version = "0.4", features = ["serde"] } +cookie = "0.16" +headers = "0.3.7" +http = "0.2.7" +hyper = "0.14" +lazy_static = "1.4.0" +newtype_derive = "0.1.6" +# must match samael's crate! +openssl = "0.10" +openssl-sys = "0.9" +openssl-probe = "0.1.2" +samael = { git = "https://github.com/njaremko/samael", features = ["xmlsec"], branch = "master" } +serde = { version = "1.0", features = ["derive"] } +serde_urlencoded = "0.7.1" +slog = { version = "2.7", features = ["max_level_trace", "release_max_level_debug"] } +thiserror = "1.0" +uuid = { version = "1.1.0", features = ["serde", "v4"] } + +dropshot = { git = "https://github.com/oxidecomputer/dropshot", branch = "main", features = ["usdt-probes"] } + +omicron-common = { path = "../../common" } + +[dev-dependencies] +tokio = { version = "1.20", features = ["full"] } diff --git a/nexus/src/authn/external/cookies.rs b/nexus/authn/src/external/cookies.rs similarity index 100% rename from nexus/src/authn/external/cookies.rs rename to nexus/authn/src/external/cookies.rs diff --git a/nexus/src/authn/external/mod.rs b/nexus/authn/src/external/mod.rs similarity index 90% rename from nexus/src/authn/external/mod.rs rename to nexus/authn/src/external/mod.rs index acbe24ee355..7bc7d5fe12a 100644 --- a/nexus/src/authn/external/mod.rs +++ b/nexus/authn/src/external/mod.rs @@ -4,9 +4,12 @@ //! Authentication for requests to the external HTTP API -use crate::authn; +use crate::Context; +use crate::Error; +use crate::Kind; +use crate::Reason; +use crate::SchemeName; use async_trait::async_trait; -use authn::Reason; use uuid::Uuid; pub mod cookies; @@ -38,7 +41,7 @@ where pub async fn authn_request( &self, rqctx: &dropshot::RequestContext, - ) -> Result { + ) -> Result { let log = &rqctx.log; let request = &rqctx.request.lock().await; let ctx = rqctx.context(); @@ -53,7 +56,7 @@ where ctx: &T, log: &slog::Logger, request: &http::Request, - ) -> Result { + ) -> Result { // For debuggability, keep track of the schemes that we've tried. let mut schemes_tried = Vec::with_capacity(self.allowed_schemes.len()); for scheme_impl in &self.allowed_schemes { @@ -67,11 +70,11 @@ where // NOT that they simply didn't try), should we try the others // instead of returning the failure here? SchemeResult::Failed(reason) => { - return Err(authn::Error { reason, schemes_tried }) + return Err(Error { reason, schemes_tried }) } SchemeResult::Authenticated(details) => { - return Ok(authn::Context { - kind: authn::Kind::Authenticated(details), + return Ok(Context { + kind: Kind::Authenticated(details), schemes_tried, }) } @@ -79,7 +82,7 @@ where } } - Ok(authn::Context { kind: authn::Kind::Unauthenticated, schemes_tried }) + Ok(Context { kind: Kind::Unauthenticated, schemes_tried }) } } @@ -90,7 +93,7 @@ where T: Send + Sync + 'static, { /// Returns the (unique) name for this scheme (for observability) - fn name(&self) -> authn::SchemeName; + fn name(&self) -> SchemeName; /// Locate credentials in the HTTP request and attempt to verify them async fn authn( @@ -119,9 +122,21 @@ pub trait SiloUserSilo { async fn silo_user_silo(&self, silo_user_id: Uuid) -> Result; } +#[async_trait] +impl SiloUserSilo for std::sync::Arc +where + T: SiloUserSilo + Send + Sync, +{ + async fn silo_user_silo(&self, silo_user_id: Uuid) -> Result { + SiloUserSilo::silo_user_silo(&**self, silo_user_id).await + } +} + #[cfg(test)] mod test { use super::*; + use crate::Actor; + use crate::Details; use anyhow::anyhow; use std::sync::atomic::AtomicU8; use std::sync::atomic::Ordering; @@ -131,7 +146,7 @@ mod test { #[derive(Debug)] struct GruntScheme { /// unique name for this grunt - name: authn::SchemeName, + name: SchemeName, /// Specifies what to do with the next authn request that we get /// @@ -142,7 +157,7 @@ mod test { nattempts: Arc, /// actor to use when authenticated - actor: authn::Actor, + actor: Actor, } // Values of the "next" bool @@ -152,7 +167,7 @@ mod test { #[async_trait] impl HttpAuthnScheme<()> for GruntScheme { - fn name(&self) -> authn::SchemeName { + fn name(&self) -> SchemeName { self.name } @@ -165,9 +180,9 @@ mod test { self.nattempts.fetch_add(1, Ordering::SeqCst); match self.next.load(Ordering::SeqCst) { SKIP => SchemeResult::NotRequested, - OK => SchemeResult::Authenticated(authn::Details { - actor: self.actor, - }), + OK => { + SchemeResult::Authenticated(Details { actor: self.actor }) + } FAIL => SchemeResult::Failed(Reason::BadCredentials { actor: self.actor, source: anyhow!("grunt error"), @@ -194,8 +209,8 @@ mod test { let flag1 = Arc::new(AtomicU8::new(SKIP)); let count1 = Arc::new(AtomicU8::new(0)); let mut expected_count1 = 0; - let name1 = authn::SchemeName("grunt1"); - let actor1 = authn::Actor::UserBuiltin { + let name1 = SchemeName("grunt1"); + let actor1 = Actor::UserBuiltin { user_builtin_id: "1c91bab2-4841-669f-cc32-de80da5bbf39" .parse() .unwrap(), @@ -210,8 +225,8 @@ mod test { let flag2 = Arc::new(AtomicU8::new(SKIP)); let count2 = Arc::new(AtomicU8::new(0)); let mut expected_count2 = 0; - let name2 = authn::SchemeName("grunt2"); - let actor2 = authn::Actor::UserBuiltin { + let name2 = SchemeName("grunt2"); + let actor2 = Actor::UserBuiltin { user_builtin_id: "799684af-533a-cb66-b5ac-ab55a791d5ef" .parse() .unwrap(), diff --git a/nexus/src/authn/external/session_cookie.rs b/nexus/authn/src/external/session_cookie.rs similarity index 93% rename from nexus/src/authn/external/session_cookie.rs rename to nexus/authn/src/external/session_cookie.rs index 116f4f4ca1d..41cfc5b1fcd 100644 --- a/nexus/src/authn/external/session_cookie.rs +++ b/nexus/authn/src/external/session_cookie.rs @@ -6,8 +6,7 @@ use super::cookies::parse_cookies; use super::{HttpAuthnScheme, Reason, SchemeResult}; -use crate::authn; -use crate::authn::{Actor, Details}; +use crate::{Actor, Details, SchemeName}; use anyhow::anyhow; use async_trait::async_trait; use chrono::{DateTime, Duration, Utc}; @@ -49,10 +48,40 @@ pub trait SessionStore { fn session_absolute_timeout(&self) -> Duration; } +#[async_trait] +impl SessionStore for std::sync::Arc +where + T: SessionStore + Send + Sync, +{ + type SessionModel = T::SessionModel; + + async fn session_fetch(&self, token: String) -> Option { + SessionStore::session_fetch(&**self, token).await + } + + async fn session_update_last_used( + &self, + token: String, + ) -> Option { + SessionStore::session_update_last_used(&**self, token).await + } + + async fn session_expire(&self, token: String) -> Option<()> { + SessionStore::session_expire(&**self, token).await + } + + fn session_idle_timeout(&self) -> Duration { + SessionStore::session_idle_timeout(&**self) + } + + fn session_absolute_timeout(&self) -> Duration { + SessionStore::session_absolute_timeout(&**self) + } +} + // generic cookie name is recommended by OWASP pub const SESSION_COOKIE_COOKIE_NAME: &str = "session"; -pub const SESSION_COOKIE_SCHEME_NAME: authn::SchemeName = - authn::SchemeName("session_cookie"); +pub const SESSION_COOKIE_SCHEME_NAME: SchemeName = SchemeName("session_cookie"); /// Generate session cookie header pub fn session_cookie_header_value(token: &str, max_age: Duration) -> String { @@ -83,7 +112,7 @@ where T: Send + Sync + 'static + SessionStore, T::SessionModel: Send + Sync + 'static + Session, { - fn name(&self) -> authn::SchemeName { + fn name(&self) -> SchemeName { SESSION_COOKIE_SCHEME_NAME } diff --git a/nexus/src/authn/external/spoof.rs b/nexus/authn/src/external/spoof.rs similarity index 98% rename from nexus/src/authn/external/spoof.rs rename to nexus/authn/src/external/spoof.rs index 8a6f9674f91..7b2677da2c3 100644 --- a/nexus/src/authn/external/spoof.rs +++ b/nexus/authn/src/external/spoof.rs @@ -9,8 +9,8 @@ use super::HttpAuthnScheme; use super::Reason; use super::SchemeResult; use super::SiloUserSilo; -use crate::authn; -use crate::authn::Actor; +use crate::Actor; +use crate::SchemeName; use anyhow::anyhow; use anyhow::Context; use async_trait::async_trait; @@ -42,7 +42,7 @@ use uuid::Uuid; // they say they are. That's true of any bearer token, but this one is // particularly dangerous because the tokens are long-lived and not secret. -pub const SPOOF_SCHEME_NAME: authn::SchemeName = authn::SchemeName("spoof"); +pub const SPOOF_SCHEME_NAME: SchemeName = SchemeName("spoof"); /// Magic value to produce a "no such actor" error const SPOOF_RESERVED_BAD_ACTOR: &str = "Jack-Donaghy"; @@ -79,7 +79,7 @@ impl HttpAuthnScheme for HttpAuthnSpoof where T: SiloUserSilo + Send + Sync + 'static, { - fn name(&self) -> authn::SchemeName { + fn name(&self) -> SchemeName { SPOOF_SCHEME_NAME } diff --git a/nexus/src/authn/external/token.rs b/nexus/authn/src/external/token.rs similarity index 93% rename from nexus/src/authn/external/token.rs rename to nexus/authn/src/external/token.rs index 9145583df23..df8ba449cfe 100644 --- a/nexus/src/authn/external/token.rs +++ b/nexus/authn/src/external/token.rs @@ -9,7 +9,8 @@ use super::HttpAuthnScheme; use super::Reason; use super::SchemeResult; use super::SiloUserSilo; -use crate::authn; +use crate::Actor; +use crate::SchemeName; use async_trait::async_trait; use headers::authorization::{Authorization, Bearer}; use headers::HeaderMapExt; @@ -32,7 +33,7 @@ use headers::HeaderMapExt; // _authentication_ information. Similarly, the "Unauthorized" HTTP response // code usually describes an _authentication_ error.) -pub const TOKEN_SCHEME_NAME: authn::SchemeName = authn::SchemeName("token"); +pub const TOKEN_SCHEME_NAME: SchemeName = SchemeName("token"); /// Prefix used on the bearer token to identify this scheme // RFC 6750 expects bearer tokens to be opaque base64-encoded data. In our @@ -49,7 +50,7 @@ impl HttpAuthnScheme for HttpAuthnToken where T: SiloUserSilo + TokenContext + Send + Sync + 'static, { - fn name(&self) -> authn::SchemeName { + fn name(&self) -> SchemeName { TOKEN_SCHEME_NAME } @@ -91,7 +92,17 @@ fn parse_token( /// A context that can look up a Silo user and client ID from a token. #[async_trait] pub trait TokenContext { - async fn token_actor(&self, token: String) -> Result; + async fn token_actor(&self, token: String) -> Result; +} + +#[async_trait] +impl TokenContext for std::sync::Arc +where + T: TokenContext + Send + Sync, +{ + async fn token_actor(&self, token: String) -> Result { + TokenContext::token_actor(&**self, token).await + } } #[cfg(test)] diff --git a/nexus/authn/src/fixed_data.rs b/nexus/authn/src/fixed_data.rs new file mode 100644 index 00000000000..1e0e0433d38 --- /dev/null +++ b/nexus/authn/src/fixed_data.rs @@ -0,0 +1,101 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +//! Fixed (hardcoded) data that gets inserted into the database programmatically +//! either when the rack is set up or when Nexus starts up. + +// Here's a proposed convention for choosing uuids that we hardcode into +// Omicron. +// +// 001de000-05e4-4000-8000-000000000000 +// ^^^^^^^^ ^^^^ ^ ^ +// +-----|---|----|-------------------- prefix used for all reserved uuids +// | | | (looks a bit like "oxide") +// +---|----|-------------------- says what kind of resource it is +// (see below) +// +----|-------------------- v4 +// +-------------------- variant 1 (most common for v4) +// +// This way, the uuids stand out a bit. It's not clear if this convention will +// be very useful, but it beats a random uuid. (Is it safe to do this? Well, +// these are valid v4 uuids, and they're as unlikely to collide with a future +// uuid as any random uuid is.) +// +// The specific kinds of resources to which we've assigned uuids: +// +// UUID PREFIX RESOURCE +// 001de000-05e4 built-in users ("05e4" looks a bit like "user") +// 001de000-1334 built-in fleet ("1334" looks like the "leet" in "fleet") +// 001de000-5110 built-in silo ("5110" looks like "silo") + +pub mod silo { + lazy_static::lazy_static! { + pub static ref SILO_ID: uuid::Uuid = + "001de000-5110-4000-8000-000000000000" + .parse() + .expect("invalid uuid for builtin silo id"); + } +} + +pub mod silo_user { + lazy_static::lazy_static! { + pub static ref USER_TEST_PRIVILEGED_ID: uuid::Uuid = + // "4007" looks a bit like "root". + "001de000-05e4-4000-8000-000000004007" + .parse() + .expect("invalid uuid for user test privileged"); + + pub static ref USER_TEST_UNPRIVILEGED_ID: uuid::Uuid = + // 60001 is the decimal uid for "nobody" on Helios. + "001de000-05e4-4000-8000-000000060001" + .parse() + .expect("invalid uuid for user test unprivileged"); + } +} + +pub mod user_builtin { + lazy_static::lazy_static! { + /// Internal user used for seeding initial database data + // NOTE: This uuid is duplicated in dbinit.sql. + pub static ref USER_DB_INIT_ID: uuid::Uuid = + // "0001" is the first possible user that wouldn't be confused with + // 0, or root. + "001de000-05e4-4000-8000-000000000001" + .parse() + .expect("invalid uuid for built-in user"); + + /// Internal user for performing operations to manage the + /// provisioning of services across the fleet. + pub static ref USER_SERVICE_BALANCER_ID: uuid::Uuid = + "001de000-05e4-4000-8000-00000000bac3" + .parse() + .expect("invalid uuid for built-in user"); + + /// Internal user used by Nexus when handling internal API requests + pub static ref USER_INTERNAL_API_ID: uuid::Uuid = + "001de000-05e4-4000-8000-000000000002" + .parse() + .expect("invalid uuid for built-in user"); + + /// Internal user used by Nexus to read privileged control plane data + pub static ref USER_INTERNAL_READ_ID: uuid::Uuid = + // "4ead" looks like "read + "001de000-05e4-4000-8000-000000004ead" + .parse() + .expect("invalid uuid for built-in user"); + + /// Internal user used by Nexus when recovering sagas + pub static ref USER_SAGA_RECOVERY_ID: uuid::Uuid = + // "3a8a" looks a bit like "saga" + "001de000-05e4-4000-8000-000000003a8a" + .parse() + .expect("invalid uuid for built-in user"); + + /// Internal user used by Nexus when authenticating external requests + pub static ref USER_EXTERNAL_AUTHN_ID: uuid::Uuid = + "001de000-05e4-4000-8000-000000000003" + .parse() + .expect("invalid uuid for built-in user"); + } +} diff --git a/nexus/src/authn/mod.rs b/nexus/authn/src/lib.rs similarity index 73% rename from nexus/src/authn/mod.rs rename to nexus/authn/src/lib.rs index 8f1cfbc20a6..17934da4382 100644 --- a/nexus/src/authn/mod.rs +++ b/nexus/authn/src/lib.rs @@ -24,24 +24,26 @@ //! privileges it has. These submodules might provide different mechanisms for //! authentication, but they'd all produce the same [`Context`] struct. +#[macro_use] +extern crate newtype_derive; +#[macro_use] +extern crate slog; + pub mod external; +pub mod fixed_data; pub mod saga; pub mod silos; -pub use crate::db::fixed_data::silo_user::USER_TEST_PRIVILEGED; -pub use crate::db::fixed_data::silo_user::USER_TEST_UNPRIVILEGED; -pub use crate::db::fixed_data::user_builtin::USER_DB_INIT; -pub use crate::db::fixed_data::user_builtin::USER_EXTERNAL_AUTHN; -pub use crate::db::fixed_data::user_builtin::USER_INTERNAL_API; -pub use crate::db::fixed_data::user_builtin::USER_INTERNAL_READ; -pub use crate::db::fixed_data::user_builtin::USER_SAGA_RECOVERY; -pub use crate::db::fixed_data::user_builtin::USER_SERVICE_BALANCER; -use crate::db::model::ConsoleSession; - -use crate::authz; -use crate::db; -use crate::db::identity::Asset; -use omicron_common::api::external::LookupType; +pub use self::fixed_data::silo::SILO_ID; +pub use self::fixed_data::silo_user::USER_TEST_PRIVILEGED_ID; +pub use self::fixed_data::silo_user::USER_TEST_UNPRIVILEGED_ID; +pub use self::fixed_data::user_builtin::USER_DB_INIT_ID; +pub use self::fixed_data::user_builtin::USER_EXTERNAL_AUTHN_ID; +pub use self::fixed_data::user_builtin::USER_INTERNAL_API_ID; +pub use self::fixed_data::user_builtin::USER_INTERNAL_READ_ID; +pub use self::fixed_data::user_builtin::USER_SAGA_RECOVERY_ID; +pub use self::fixed_data::user_builtin::USER_SERVICE_BALANCER_ID; + use serde::Deserialize; use serde::Serialize; use uuid::Uuid; @@ -86,52 +88,6 @@ impl Context { } } - /// Returns the current actor's Silo if they have one or an appropriate - /// error otherwise - /// - /// This is intended for code paths that always expect a Silo to be present. - /// Built-in users have no Silo, and this function will return an - /// InternalError if the currently-authenticated user is built-in. If you - /// want to handle that case differently, see - /// [`Context::silo_or_builtin()`]. - pub fn silo_required( - &self, - ) -> Result { - self.silo_or_builtin().and_then(|maybe_silo| { - maybe_silo.ok_or_else(|| { - omicron_common::api::external::Error::internal_error( - "needed Silo for a built-in user, but \ - built-in users have no Silo", - ) - }) - }) - } - - /// Determine whether the currently authenticated actor has a Silo or is a - /// built-in user - /// - /// This function allows callers to distinguish these three cases: - /// - /// * there's an authenticated user with an associated Silo (most common) - /// * there's an authenticated built-in user who has no associated Silo - /// * there's no authenticated user (returned as an error) - /// - /// Built-in users have no Silo, and so they usually can't do anything that - /// might use a Silo. You usually want to use [`Context::silo_required()`] - /// if you don't expect to be looking at a built-in user. - pub fn silo_or_builtin( - &self, - ) -> Result, omicron_common::api::external::Error> { - self.actor_required().map(|actor| match actor { - Actor::SiloUser { silo_id, .. } => Some(authz::Silo::new( - authz::FLEET, - *silo_id, - LookupType::ById(*silo_id), - )), - Actor::UserBuiltin { .. } => None, - }) - } - /// Returns the list of schemes tried, in order /// /// This should generally *not* be exposed to clients. @@ -146,34 +102,34 @@ impl Context { /// Returns an authenticated context for handling internal API contexts pub fn internal_api() -> Context { - Context::context_for_builtin_user(USER_INTERNAL_API.id) + Context::context_for_builtin_user(*USER_INTERNAL_API_ID) } /// Returns an authenticated context for saga recovery pub fn internal_saga_recovery() -> Context { - Context::context_for_builtin_user(USER_SAGA_RECOVERY.id) + Context::context_for_builtin_user(*USER_SAGA_RECOVERY_ID) } /// Returns an authenticated context for use by internal resource allocation pub fn internal_read() -> Context { - Context::context_for_builtin_user(USER_INTERNAL_READ.id) + Context::context_for_builtin_user(*USER_INTERNAL_READ_ID) } /// Returns an authenticated context for use for authenticating external /// requests pub fn external_authn() -> Context { - Context::context_for_builtin_user(USER_EXTERNAL_AUTHN.id) + Context::context_for_builtin_user(*USER_EXTERNAL_AUTHN_ID) } /// Returns an authenticated context for Nexus-startup database /// initialization pub fn internal_db_init() -> Context { - Context::context_for_builtin_user(USER_DB_INIT.id) + Context::context_for_builtin_user(*USER_DB_INIT_ID) } /// Returns an authenticated context for Nexus-driven service balancing. pub fn internal_service_balancer() -> Context { - Context::context_for_builtin_user(USER_SERVICE_BALANCER.id) + Context::context_for_builtin_user(*USER_SERVICE_BALANCER_ID) } fn context_for_builtin_user(user_builtin_id: Uuid) -> Context { @@ -192,8 +148,8 @@ impl Context { Context { kind: Kind::Authenticated(Details { actor: Actor::SiloUser { - silo_user_id: USER_TEST_PRIVILEGED.id(), - silo_id: USER_TEST_PRIVILEGED.silo_id, + silo_user_id: *USER_TEST_PRIVILEGED_ID, + silo_id: *SILO_ID, }, }), schemes_tried: Vec::new(), @@ -202,16 +158,12 @@ impl Context { /// Returns an authenticated context for the special unprivileged user /// (for testing only) - #[cfg(test)] pub fn unprivileged_test_user() -> Context { - Self::test_silo_user( - USER_TEST_UNPRIVILEGED.silo_id, - USER_TEST_UNPRIVILEGED.id(), - ) + Self::test_silo_user(*SILO_ID, *USER_TEST_UNPRIVILEGED_ID) } - /// Returns an authenticated context for a given silo user - #[cfg(test)] + /// Returns an authenticated context for a given silo user (for testing + /// only) pub fn test_silo_user(silo_id: Uuid, silo_user_id: Uuid) -> Context { Context { kind: Kind::Authenticated(Details { @@ -225,15 +177,14 @@ impl Context { #[cfg(test)] mod test { use super::Context; - use super::USER_DB_INIT; - use super::USER_INTERNAL_API; - use super::USER_INTERNAL_READ; - use super::USER_SAGA_RECOVERY; - use super::USER_SERVICE_BALANCER; - use super::USER_TEST_PRIVILEGED; - use super::USER_TEST_UNPRIVILEGED; - use crate::db::fixed_data::user_builtin::USER_EXTERNAL_AUTHN; - use crate::db::identity::Asset; + use super::USER_DB_INIT_ID; + use super::USER_EXTERNAL_AUTHN_ID; + use super::USER_INTERNAL_API_ID; + use super::USER_INTERNAL_READ_ID; + use super::USER_SAGA_RECOVERY_ID; + use super::USER_SERVICE_BALANCER_ID; + use super::USER_TEST_PRIVILEGED_ID; + use super::USER_TEST_UNPRIVILEGED_ID; #[test] fn test_internal_users() { @@ -246,35 +197,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.actor_id(), *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.actor_id(), *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.actor_id(), *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.actor_id(), *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.actor_id(), *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.actor_id(), *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.actor_id(), *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.actor_id(), *USER_INTERNAL_API_ID); } } @@ -306,13 +257,6 @@ pub enum Actor { } impl Actor { - pub fn actor_type(&self) -> db::model::IdentityType { - match self { - Actor::UserBuiltin { .. } => db::model::IdentityType::UserBuiltin, - Actor::SiloUser { .. } => db::model::IdentityType::SiloUser, - } - } - pub fn actor_id(&self) -> Uuid { match self { Actor::UserBuiltin { user_builtin_id, .. } => *user_builtin_id, @@ -351,13 +295,6 @@ impl std::fmt::Debug for Actor { } } -/// A console session with the silo id of the authenticated user -#[derive(Clone, Debug)] -pub struct ConsoleSessionWithSiloId { - pub console_session: ConsoleSession, - pub silo_id: Uuid, -} - /// Label for a particular authentication scheme (used in log messages and /// internal error messages) #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/nexus/src/authn/saga.rs b/nexus/authn/src/saga.rs similarity index 72% rename from nexus/src/authn/saga.rs rename to nexus/authn/src/saga.rs index 7cd04d7541a..0ddfa7137dd 100644 --- a/nexus/src/authn/saga.rs +++ b/nexus/authn/src/saga.rs @@ -7,8 +7,8 @@ // id. We should think through that a little more. Should we instead preload a // bunch of roles and then serialize that, for example? -use crate::authn; -use crate::context::OpContext; +use crate::Context; +use crate::Kind; use serde::Deserialize; use serde::Serialize; @@ -17,15 +17,15 @@ use serde::Serialize; // structures, but this one has a particularly large blast radius.) #[derive(Debug, Deserialize, Serialize)] pub struct Serialized { - kind: authn::Kind, + kind: Kind, } impl Serialized { - pub fn for_opctx(opctx: &OpContext) -> Serialized { - Serialized { kind: opctx.authn.kind.clone() } + pub fn for_ctx(ctx: &Context) -> Serialized { + Serialized { kind: ctx.kind.clone() } } - pub fn to_authn(&self) -> authn::Context { - authn::Context { kind: self.kind.clone(), schemes_tried: vec![] } + pub fn to_authn(&self) -> Context { + Context { kind: self.kind.clone(), schemes_tried: vec![] } } } diff --git a/nexus/src/authn/silos.rs b/nexus/authn/src/silos.rs similarity index 79% rename from nexus/src/authn/silos.rs rename to nexus/authn/src/silos.rs index 9b605bf211c..cc6123773cc 100644 --- a/nexus/src/authn/silos.rs +++ b/nexus/authn/src/silos.rs @@ -4,25 +4,19 @@ //! Silo related authentication types and functions -use crate::authz; -use crate::context::OpContext; -use crate::db::lookup::LookupPath; -use crate::db::{model, DataStore}; -use omicron_common::api::external::LookupResult; - use anyhow::{anyhow, Result}; +use dropshot::HttpError; use samael::metadata::ContactPerson; use samael::metadata::ContactType; -use samael::metadata::EntityDescriptor; use samael::metadata::NameIdFormat; use samael::metadata::HTTP_REDIRECT_BINDING; use samael::schema::Response as SAMLResponse; use samael::service_provider::ServiceProvider; use samael::service_provider::ServiceProviderBuilder; - -use dropshot::HttpError; use serde::{Deserialize, Serialize}; +pub use samael::metadata::EntityDescriptor; + #[derive(Deserialize)] pub struct SamlIdentityProvider { pub idp_metadata_document_string: String, @@ -35,88 +29,10 @@ pub struct SamlIdentityProvider { pub private_key: Option, } -impl TryFrom for SamlIdentityProvider { - type Error = anyhow::Error; - fn try_from( - model: model::SamlIdentityProvider, - ) -> Result { - let provider = SamlIdentityProvider { - idp_metadata_document_string: model.idp_metadata_document_string, - idp_entity_id: model.idp_entity_id, - sp_client_id: model.sp_client_id, - acs_url: model.acs_url, - slo_url: model.slo_url, - technical_contact_email: model.technical_contact_email, - public_cert: model.public_cert, - private_key: model.private_key, - }; - - // check that the idp metadata document string parses into an EntityDescriptor - let _idp_metadata: EntityDescriptor = - provider.idp_metadata_document_string.parse()?; - - // check that there is a valid sign in url - let _sign_in_url = provider.sign_in_url(None)?; - - Ok(provider) - } -} - pub enum IdentityProviderType { Saml(SamlIdentityProvider), } -impl IdentityProviderType { - /// First, look up the provider type, then look in for the specific - /// provider details. - pub async fn lookup( - datastore: &DataStore, - opctx: &OpContext, - silo_name: &model::Name, - provider_name: &model::Name, - ) -> LookupResult<(authz::Silo, model::Silo, Self)> { - let (authz_silo, db_silo) = LookupPath::new(opctx, datastore) - .silo_name(silo_name) - .fetch() - .await?; - - let (.., identity_provider) = LookupPath::new(opctx, datastore) - .silo_name(silo_name) - .identity_provider_name(provider_name) - .fetch() - .await?; - - match identity_provider.provider_type { - model::IdentityProviderType::Saml => { - let (.., saml_identity_provider) = - LookupPath::new(opctx, datastore) - .silo_name(silo_name) - .saml_identity_provider_name(provider_name) - .fetch() - .await?; - - let saml_identity_provider = IdentityProviderType::Saml( - saml_identity_provider.try_into() - .map_err(|e: anyhow::Error| - // If an error is encountered converting from the - // model to the authn type here, this is a server - // error: it was validated before it went into the - // DB. - omicron_common::api::external::Error::internal_error( - &format!( - "saml_identity_provider.try_into() failed! {}", - &e.to_string() - ) - ) - )? - ); - - Ok((authz_silo, db_silo, saml_identity_provider)) - } - } - } -} - impl SamlIdentityProvider { pub fn sign_in_url(&self, relay_state: Option) -> Result { let idp_metadata: EntityDescriptor = diff --git a/nexus/src/authz/authz-macros/Cargo.toml b/nexus/authz-macros/Cargo.toml similarity index 100% rename from nexus/src/authz/authz-macros/Cargo.toml rename to nexus/authz-macros/Cargo.toml diff --git a/nexus/src/authz/authz-macros/src/lib.rs b/nexus/authz-macros/src/lib.rs similarity index 100% rename from nexus/src/authz/authz-macros/src/lib.rs rename to nexus/authz-macros/src/lib.rs diff --git a/nexus/src/db/db-macros/Cargo.toml b/nexus/db-macros/Cargo.toml similarity index 100% rename from nexus/src/db/db-macros/Cargo.toml rename to nexus/db-macros/Cargo.toml diff --git a/nexus/src/db/db-macros/src/lib.rs b/nexus/db-macros/src/lib.rs similarity index 100% rename from nexus/src/db/db-macros/src/lib.rs rename to nexus/db-macros/src/lib.rs diff --git a/nexus/src/db/db-macros/src/lookup.rs b/nexus/db-macros/src/lookup.rs similarity index 100% rename from nexus/src/db/db-macros/src/lookup.rs rename to nexus/db-macros/src/lookup.rs diff --git a/nexus/src/app/disk.rs b/nexus/src/app/disk.rs index 21964cb09df..2912248f23d 100644 --- a/nexus/src/app/disk.rs +++ b/nexus/src/app/disk.rs @@ -162,7 +162,7 @@ impl super::Nexus { } let saga_params = Arc::new(sagas::disk_create::Params { - serialized_authn: authn::saga::Serialized::for_opctx(opctx), + serialized_authn: authn::saga::Serialized::for_ctx(&opctx.authn), project_id: authz_project.id(), create_params: params.clone(), }); diff --git a/nexus/src/app/iam.rs b/nexus/src/app/iam.rs index 71cc1692d58..a213e902ad7 100644 --- a/nexus/src/app/iam.rs +++ b/nexus/src/app/iam.rs @@ -5,6 +5,7 @@ //! Built-ins and roles use crate::authz; +use crate::authz::AuthnContextExt; use crate::context::OpContext; use crate::db; use crate::db::lookup::LookupPath; diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 7cba9d5be28..b0aefe7bd65 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -86,7 +86,7 @@ impl super::Nexus { } let saga_params = Arc::new(sagas::instance_create::Params { - serialized_authn: authn::saga::Serialized::for_opctx(opctx), + serialized_authn: authn::saga::Serialized::for_ctx(&opctx.authn), organization_name: organization_name.clone().into(), project_name: project_name.clone().into(), project_id: authz_project.id(), @@ -241,7 +241,7 @@ impl super::Nexus { // Kick off the migration saga let saga_params = Arc::new(sagas::instance_migrate::Params { - serialized_authn: authn::saga::Serialized::for_opctx(opctx), + serialized_authn: authn::saga::Serialized::for_ctx(&opctx.authn), instance_id: authz_instance.id(), migrate_params: params, }); diff --git a/nexus/src/app/session.rs b/nexus/src/app/session.rs index 7ac23cf5ce3..a1f9ee01578 100644 --- a/nexus/src/app/session.rs +++ b/nexus/src/app/session.rs @@ -4,7 +4,6 @@ //! Console session management. -use crate::authn; use crate::authn::Reason; use crate::authz; use crate::context::OpContext; @@ -89,7 +88,7 @@ impl super::Nexus { &self, opctx: &OpContext, token: String, - ) -> LookupResult { + ) -> LookupResult { let (.., db_console_session) = LookupPath::new(opctx, &self.db_datastore) .console_session_token(&token) @@ -101,7 +100,7 @@ impl super::Nexus { .fetch() .await?; - Ok(authn::ConsoleSessionWithSiloId { + Ok(db::model::ConsoleSessionWithSiloId { console_session: db_console_session, silo_id: db_silo_user.silo_id, }) @@ -112,7 +111,7 @@ impl super::Nexus { &self, opctx: &OpContext, token: &str, - ) -> UpdateResult { + ) -> UpdateResult { let authz_session = authz::ConsoleSession::new( authz::FLEET, token.to_string(), diff --git a/nexus/src/authz/actor.rs b/nexus/src/authz/actor.rs index 55812ddb6fc..9eabaefec6f 100644 --- a/nexus/src/authz/actor.rs +++ b/nexus/src/authz/actor.rs @@ -76,7 +76,7 @@ impl oso::PolarClass for AuthenticatedActor { .with_equality_check() .add_constant( AuthenticatedActor { - actor_id: authn::USER_DB_INIT.id, + actor_id: *authn::USER_DB_INIT_ID, silo_id: None, roles: RoleSet::new(), }, diff --git a/nexus/src/authz/api_resources.rs b/nexus/src/authz/api_resources.rs index 89d3a947ab8..51174973db2 100644 --- a/nexus/src/authz/api_resources.rs +++ b/nexus/src/authz/api_resources.rs @@ -726,6 +726,54 @@ impl ApiResourceWithRolesType for Silo { type AllowedRoles = SiloRole; } +pub trait AuthnContextExt { + /// Returns the current actor's Silo if they have one or an appropriate + /// error otherwise + /// + /// This is intended for code paths that always expect a Silo to be present. + /// Built-in users have no Silo, and this function will return an + /// InternalError if the currently-authenticated user is built-in. If you + /// want to handle that case differently, see + /// [`AuthnContextExt::silo_or_builtin()`]. + fn silo_required(&self) -> Result; + + /// Determine whether the currently authenticated actor has a Silo or is a + /// built-in user + /// + /// This function allows callers to distinguish these three cases: + /// + /// * there's an authenticated user with an associated Silo (most common) + /// * there's an authenticated built-in user who has no associated Silo + /// * there's no authenticated user (returned as an error) + /// + /// Built-in users have no Silo, and so they usually can't do anything that + /// might use a Silo. You usually want to use [`Context::silo_required()`] + /// if you don't expect to be looking at a built-in user. + fn silo_or_builtin(&self) -> Result, Error>; +} + +impl AuthnContextExt for authn::Context { + fn silo_required(&self) -> Result { + self.silo_or_builtin().and_then(|maybe_silo| { + maybe_silo.ok_or_else(|| { + Error::internal_error( + "needed Silo for a built-in user, but \ + built-in users have no Silo", + ) + }) + }) + } + + fn silo_or_builtin(&self) -> Result, Error> { + self.actor_required().map(|actor| match actor { + authn::Actor::SiloUser { silo_id, .. } => { + Some(Silo::new(FLEET, *silo_id, LookupType::ById(*silo_id))) + } + authn::Actor::UserBuiltin { .. } => None, + }) + } +} + #[derive( Clone, Copy, diff --git a/nexus/src/authz/roles.rs b/nexus/src/authz/roles.rs index 12e75b2f0eb..af98705fd08 100644 --- a/nexus/src/authz/roles.rs +++ b/nexus/src/authz/roles.rs @@ -37,6 +37,7 @@ use super::api_resources::ApiResource; use crate::authn; use crate::context::OpContext; +use crate::db::model::IdentityType; use crate::db::DataStore; use omicron_common::api::external::Error; use omicron_common::api::external::ResourceType; @@ -146,7 +147,7 @@ pub async fn load_roles_for_resource( let roles = datastore .role_asgn_list_for( opctx, - actor.actor_type(), + IdentityType::from(actor), actor.actor_id(), resource_type, resource_id, diff --git a/nexus/src/context.rs b/nexus/src/context.rs index 28ea5caab8a..29238b13849 100644 --- a/nexus/src/context.rs +++ b/nexus/src/context.rs @@ -9,8 +9,9 @@ use super::config; use super::db; use super::Nexus; use crate::authn::external::session_cookie::{Session, SessionStore}; -use crate::authn::ConsoleSessionWithSiloId; use crate::authz::AuthorizedResource; +use crate::db::model::ConsoleSessionWithSiloId; +use crate::db::model::IdentityType; use crate::db::DataStore; use crate::saga_interface::SagaContext; use async_trait::async_trait; @@ -315,7 +316,7 @@ impl OpContext { let log = if let Some(actor) = authn.actor() { let actor_id = actor.actor_id(); - let actor_type = actor.actor_type(); + let actor_type = IdentityType::from(actor); metadata .insert(String::from("authenticated"), String::from("true")); metadata.insert( @@ -541,7 +542,7 @@ mod test { } #[async_trait] -impl authn::external::SiloUserSilo for Arc { +impl authn::external::SiloUserSilo for ServerContext { async fn silo_user_silo( &self, silo_user_id: Uuid, @@ -552,7 +553,7 @@ impl authn::external::SiloUserSilo for Arc { } #[async_trait] -impl authn::external::token::TokenContext for Arc { +impl authn::external::token::TokenContext for ServerContext { async fn token_actor( &self, token: String, @@ -563,7 +564,7 @@ impl authn::external::token::TokenContext for Arc { } #[async_trait] -impl SessionStore for Arc { +impl SessionStore for ServerContext { type SessionModel = ConsoleSessionWithSiloId; async fn session_fetch(&self, token: String) -> Option { diff --git a/nexus/src/db/datastore/console_session.rs b/nexus/src/db/datastore/console_session.rs index 07743adabac..0a2a48e3b38 100644 --- a/nexus/src/db/datastore/console_session.rs +++ b/nexus/src/db/datastore/console_session.rs @@ -5,12 +5,12 @@ //! [`DataStore`] methods related to [`ConsoleSession`]s. use super::DataStore; -use crate::authn; use crate::authz; use crate::context::OpContext; use crate::db; use crate::db::lookup::LookupPath; use crate::db::model::ConsoleSession; +use crate::db::model::ConsoleSessionWithSiloId; use crate::db::model::IdentityType; use async_bb8_diesel::AsyncRunQueryDsl; use chrono::Utc; @@ -61,7 +61,7 @@ impl DataStore { &self, opctx: &OpContext, authz_session: &authz::ConsoleSession, - ) -> UpdateResult { + ) -> UpdateResult { opctx.authorize(authz::Action::Modify, authz_session).await?; use db::schema::console_session::dsl; @@ -89,7 +89,7 @@ impl DataStore { )) })?; - Ok(authn::ConsoleSessionWithSiloId { + Ok(ConsoleSessionWithSiloId { console_session, silo_id: db_silo_user.silo_id, }) @@ -123,7 +123,7 @@ impl DataStore { // to check, and if we add another type of Actor, we'll be forced here // to consider if they should be able to have console sessions and log // out of them. - let silo_user_id = match actor.actor_type() { + let silo_user_id = match IdentityType::from(actor) { IdentityType::SiloUser => actor.actor_id(), IdentityType::UserBuiltin => { return Err(Error::invalid_request("not a Silo user")) diff --git a/nexus/src/db/datastore/mod.rs b/nexus/src/db/datastore/mod.rs index 2d11e01a759..4f820331d48 100644 --- a/nexus/src/db/datastore/mod.rs +++ b/nexus/src/db/datastore/mod.rs @@ -220,9 +220,9 @@ pub async fn datastore_test( mod test { use super::*; use crate::authn; + use crate::authn::SILO_ID; use crate::authz; use crate::db::explain::ExplainableAsync; - use crate::db::fixed_data::silo::SILO_ID; use crate::db::identity::Asset; use crate::db::identity::Resource; use crate::db::lookup::LookupPath; diff --git a/nexus/src/db/datastore/organization.rs b/nexus/src/db/datastore/organization.rs index e237aa63819..1fb4d799b16 100644 --- a/nexus/src/db/datastore/organization.rs +++ b/nexus/src/db/datastore/organization.rs @@ -6,6 +6,7 @@ use super::DataStore; use crate::authz; +use crate::authz::AuthnContextExt; use crate::context::OpContext; use crate::db; use crate::db::collection_insert::AsyncInsertError; diff --git a/nexus/src/db/datastore/silo_user.rs b/nexus/src/db/datastore/silo_user.rs index aad72987124..cba9d7f69d8 100644 --- a/nexus/src/db/datastore/silo_user.rs +++ b/nexus/src/db/datastore/silo_user.rs @@ -5,7 +5,6 @@ //! [`DataStore`] methods related to [`SiloUser`]s. use super::DataStore; -use crate::authn; use crate::authz; use crate::context::OpContext; use crate::db; @@ -128,11 +127,11 @@ impl DataStore { let builtin_users = [ // Note: "db_init" is also a builtin user, but that one by necessity // is created with the database. - &*authn::USER_SERVICE_BALANCER, - &*authn::USER_INTERNAL_API, - &*authn::USER_INTERNAL_READ, - &*authn::USER_EXTERNAL_AUTHN, - &*authn::USER_SAGA_RECOVERY, + &*db::fixed_data::user_builtin::USER_SERVICE_BALANCER, + &*db::fixed_data::user_builtin::USER_INTERNAL_API, + &*db::fixed_data::user_builtin::USER_INTERNAL_READ, + &*db::fixed_data::user_builtin::USER_EXTERNAL_AUTHN, + &*db::fixed_data::user_builtin::USER_SAGA_RECOVERY, ] .iter() .map(|u| { @@ -172,8 +171,10 @@ impl DataStore { opctx.authorize(authz::Action::Modify, &authz::DATABASE).await?; - let users = - [&*authn::USER_TEST_PRIVILEGED, &*authn::USER_TEST_UNPRIVILEGED]; + let users = [ + &*db::fixed_data::silo_user::USER_TEST_PRIVILEGED, + &*db::fixed_data::silo_user::USER_TEST_UNPRIVILEGED, + ]; debug!(opctx.log, "attempting to create silo users"); let count = diesel::insert_into(dsl::silo_user) diff --git a/nexus/src/db/fixed_data/mod.rs b/nexus/src/db/fixed_data/mod.rs index ab4e42d1e9d..e7c2ee91b87 100644 --- a/nexus/src/db/fixed_data/mod.rs +++ b/nexus/src/db/fixed_data/mod.rs @@ -4,30 +4,6 @@ //! Fixed (hardcoded) data that gets inserted into the database programmatically //! either when the rack is set up or when Nexus starts up. -// Here's a proposed convention for choosing uuids that we hardcode into -// Omicron. -// -// 001de000-05e4-4000-8000-000000000000 -// ^^^^^^^^ ^^^^ ^ ^ -// +-----|---|----|-------------------- prefix used for all reserved uuids -// | | | (looks a bit like "oxide") -// +---|----|-------------------- says what kind of resource it is -// (see below) -// +----|-------------------- v4 -// +-------------------- variant 1 (most common for v4) -// -// This way, the uuids stand out a bit. It's not clear if this convention will -// be very useful, but it beats a random uuid. (Is it safe to do this? Well, -// these are valid v4 uuids, and they're as unlikely to collide with a future -// uuid as any random uuid is.) -// -// The specific kinds of resources to which we've assigned uuids: -// -// UUID PREFIX RESOURCE -// 001de000-05e4 built-in users ("05e4" looks a bit like "user") -// 001de000-1334 built-in fleet ("1334" looks like the "leet" in "fleet") -// 001de000-5110 built-in silo ("5110" looks like "silo") - use lazy_static::lazy_static; pub mod role_assignment; @@ -37,7 +13,7 @@ pub mod silo_user; pub mod user_builtin; lazy_static! { - /* See above for where this uuid comes from. */ + /* See nexus-authn for where this uuid comes from. */ pub static ref FLEET_ID: uuid::Uuid = "001de000-1334-4000-8000-000000000000" .parse() diff --git a/nexus/src/db/fixed_data/silo.rs b/nexus/src/db/fixed_data/silo.rs index 6828fc59130..9de98e8346c 100644 --- a/nexus/src/db/fixed_data/silo.rs +++ b/nexus/src/db/fixed_data/silo.rs @@ -2,17 +2,15 @@ // 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::authn; use crate::db; use crate::external_api::{params, shared}; use lazy_static::lazy_static; use omicron_common::api::external::IdentityMetadataCreateParams; lazy_static! { - pub static ref SILO_ID: uuid::Uuid = "001de000-5110-4000-8000-000000000000" - .parse() - .expect("invalid uuid for builtin silo id"); pub static ref DEFAULT_SILO: db::model::Silo = db::model::Silo::new_with_id( - *SILO_ID, + *authn::SILO_ID, params::SiloCreate { identity: IdentityMetadataCreateParams { name: "default-silo".parse().unwrap(), diff --git a/nexus/src/db/fixed_data/silo_user.rs b/nexus/src/db/fixed_data/silo_user.rs index d985b7a414b..c0f5200ddb0 100644 --- a/nexus/src/db/fixed_data/silo_user.rs +++ b/nexus/src/db/fixed_data/silo_user.rs @@ -4,6 +4,7 @@ //! Built-in Silo Users use super::role_builtin; +use crate::authn; use crate::db; use crate::db::identity::Asset; use lazy_static::lazy_static; @@ -16,9 +17,8 @@ lazy_static! { // not automatically at Nexus startup. pub static ref USER_TEST_PRIVILEGED: db::model::SiloUser = db::model::SiloUser::new( - *db::fixed_data::silo::SILO_ID, - // "4007" looks a bit like "root". - "001de000-05e4-4000-8000-000000004007".parse().unwrap(), + *authn::SILO_ID, + *authn::USER_TEST_PRIVILEGED_ID, "privileged".into(), ); @@ -42,9 +42,8 @@ lazy_static! { // not automatically at Nexus startup. pub static ref USER_TEST_UNPRIVILEGED: db::model::SiloUser = db::model::SiloUser::new( - *db::fixed_data::silo::SILO_ID, - // 60001 is the decimal uid for "nobody" on Helios. - "001de000-05e4-4000-8000-000000060001".parse().unwrap(), + *authn::SILO_ID, + *authn::USER_TEST_UNPRIVILEGED_ID, "unprivileged".into(), ); } diff --git a/nexus/src/db/fixed_data/user_builtin.rs b/nexus/src/db/fixed_data/user_builtin.rs index 87f33fa3558..a5a873b35c4 100644 --- a/nexus/src/db/fixed_data/user_builtin.rs +++ b/nexus/src/db/fixed_data/user_builtin.rs @@ -3,6 +3,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! Built-in users +use crate::authn; use lazy_static::lazy_static; use omicron_common::api; use uuid::Uuid; @@ -15,12 +16,12 @@ pub struct UserBuiltinConfig { impl UserBuiltinConfig { fn new_static( - id: &str, + id: Uuid, name: &str, description: &'static str, ) -> UserBuiltinConfig { UserBuiltinConfig { - id: id.parse().expect("invalid uuid for builtin user id"), + id, name: name.parse().expect("invalid name for builtin user name"), description, } @@ -29,12 +30,10 @@ impl UserBuiltinConfig { lazy_static! { /// Internal user used for seeding initial database data - // NOTE: This uuid and name are duplicated in dbinit.sql. + // NOTE: This name (and the UUID from authn) are duplicated in dbinit.sql. pub static ref USER_DB_INIT: UserBuiltinConfig = UserBuiltinConfig::new_static( - // "0001" is the first possible user that wouldn't be confused with - // 0, or root. - "001de000-05e4-4000-8000-000000000001", + *authn::USER_DB_INIT_ID, "db-init", "used for seeding initial database data", ); @@ -43,7 +42,7 @@ lazy_static! { /// provisioning of services across the fleet. pub static ref USER_SERVICE_BALANCER: UserBuiltinConfig = UserBuiltinConfig::new_static( - "001de000-05e4-4000-8000-00000000bac3", + *authn::USER_SERVICE_BALANCER_ID, "service-balancer", "used for Nexus-driven service balancing", ); @@ -51,7 +50,7 @@ lazy_static! { /// Internal user used by Nexus when handling internal API requests pub static ref USER_INTERNAL_API: UserBuiltinConfig = UserBuiltinConfig::new_static( - "001de000-05e4-4000-8000-000000000002", + *authn::USER_INTERNAL_API_ID, "internal-api", "used by Nexus when handling internal API requests", ); @@ -59,8 +58,7 @@ lazy_static! { /// Internal user used by Nexus to read privileged control plane data pub static ref USER_INTERNAL_READ: UserBuiltinConfig = UserBuiltinConfig::new_static( - // "4ead" looks like "read" - "001de000-05e4-4000-8000-000000004ead", + *authn::USER_INTERNAL_READ_ID, "internal-read", "used by Nexus to read privileged control plane data", ); @@ -68,8 +66,7 @@ lazy_static! { /// Internal user used by Nexus when recovering sagas pub static ref USER_SAGA_RECOVERY: UserBuiltinConfig = UserBuiltinConfig::new_static( - // "3a8a" looks a bit like "saga". - "001de000-05e4-4000-8000-000000003a8a", + *authn::USER_SAGA_RECOVERY_ID, "saga-recovery", "used by Nexus when recovering sagas", ); @@ -77,7 +74,7 @@ lazy_static! { /// Internal user used by Nexus when authenticating external requests pub static ref USER_EXTERNAL_AUTHN: UserBuiltinConfig = UserBuiltinConfig::new_static( - "001de000-05e4-4000-8000-000000000003", + *authn::USER_EXTERNAL_AUTHN_ID, "external-authn", "used by Nexus when authenticating external requests", ); diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 97093a81be8..05aa7f4fbc1 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -8,6 +8,7 @@ use super::datastore::DataStore; use super::identity::Asset; use super::identity::Resource; use super::model; +use crate::authz::AuthnContextExt; use crate::{ authz, context::OpContext, diff --git a/nexus/src/db/model/console_session.rs b/nexus/src/db/model/console_session.rs index e22a6d06dc6..d3becd32f54 100644 --- a/nexus/src/db/model/console_session.rs +++ b/nexus/src/db/model/console_session.rs @@ -27,3 +27,10 @@ impl ConsoleSession { self.token.clone() } } + +/// A console session with the silo id of the authenticated user +#[derive(Clone, Debug)] +pub struct ConsoleSessionWithSiloId { + pub console_session: ConsoleSession, + pub silo_id: Uuid, +} diff --git a/nexus/src/db/model/identity_provider.rs b/nexus/src/db/model/identity_provider.rs index 04f5a7b02ea..4c2ad9da263 100644 --- a/nexus/src/db/model/identity_provider.rs +++ b/nexus/src/db/model/identity_provider.rs @@ -2,10 +2,16 @@ // 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::authn; +use crate::authz; +use crate::context::OpContext; +use crate::db::lookup::LookupPath; use crate::db::model::impl_enum_type; use crate::db::schema::{identity_provider, saml_identity_provider}; +use crate::db::DataStore; +use async_trait::async_trait; use db_macros::Resource; - +use omicron_common::api::external::LookupResult; use serde::{Deserialize, Serialize}; use std::io::Write; use uuid::Uuid; @@ -34,6 +40,68 @@ pub struct IdentityProvider { pub provider_type: IdentityProviderType, } +#[async_trait] +pub trait IdentityProviderLookup: Sized { + /// First, look up the provider type, then look in for the specific + /// provider details. + async fn lookup( + datastore: &DataStore, + opctx: &OpContext, + silo_name: &super::Name, + provider_name: &super::Name, + ) -> LookupResult<(authz::Silo, super::Silo, Self)>; +} + +#[async_trait] +impl IdentityProviderLookup for authn::silos::IdentityProviderType { + async fn lookup( + datastore: &DataStore, + opctx: &OpContext, + silo_name: &super::Name, + provider_name: &super::Name, + ) -> LookupResult<(authz::Silo, super::Silo, Self)> { + let (authz_silo, db_silo) = LookupPath::new(opctx, datastore) + .silo_name(silo_name) + .fetch() + .await?; + + let (.., identity_provider) = LookupPath::new(opctx, datastore) + .silo_name(silo_name) + .identity_provider_name(provider_name) + .fetch() + .await?; + + match identity_provider.provider_type { + IdentityProviderType::Saml => { + let (.., saml_identity_provider) = + LookupPath::new(opctx, datastore) + .silo_name(silo_name) + .saml_identity_provider_name(provider_name) + .fetch() + .await?; + + let saml_identity_provider = authn::silos::IdentityProviderType::Saml( + saml_identity_provider.try_into() + .map_err(|e: anyhow::Error| + // If an error is encountered converting from the + // model to the authn type here, this is a server + // error: it was validated before it went into the + // DB. + omicron_common::api::external::Error::internal_error( + &format!( + "saml_identity_provider.try_into() failed! {}", + &e.to_string() + ) + ) + )? + ); + + Ok((authz_silo, db_silo, saml_identity_provider)) + } + } + } +} + #[derive(Queryable, Insertable, Clone, Debug, Selectable, Resource)] #[diesel(table_name = saml_identity_provider)] pub struct SamlIdentityProvider { @@ -52,3 +120,28 @@ pub struct SamlIdentityProvider { pub public_cert: Option, pub private_key: Option, } + +impl TryFrom for authn::silos::SamlIdentityProvider { + type Error = anyhow::Error; + fn try_from(model: SamlIdentityProvider) -> Result { + let provider = authn::silos::SamlIdentityProvider { + idp_metadata_document_string: model.idp_metadata_document_string, + idp_entity_id: model.idp_entity_id, + sp_client_id: model.sp_client_id, + acs_url: model.acs_url, + slo_url: model.slo_url, + technical_contact_email: model.technical_contact_email, + public_cert: model.public_cert, + private_key: model.private_key, + }; + + // check that the idp metadata document string parses into an EntityDescriptor + let _idp_metadata: authn::silos::EntityDescriptor = + provider.idp_metadata_document_string.parse()?; + + // check that there is a valid sign in url + let _sign_in_url = provider.sign_in_url(None)?; + + Ok(provider) + } +} diff --git a/nexus/src/db/model/role_assignment.rs b/nexus/src/db/model/role_assignment.rs index 72b35ece417..a43ea5fff4d 100644 --- a/nexus/src/db/model/role_assignment.rs +++ b/nexus/src/db/model/role_assignment.rs @@ -3,6 +3,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use super::impl_enum_type; +use crate::authn::Actor; use crate::db::schema::role_assignment; use crate::external_api::shared; use serde::{Deserialize, Serialize}; @@ -39,6 +40,15 @@ impl From for IdentityType { } } +impl From<&'_ Actor> for IdentityType { + fn from(actor: &'_ Actor) -> Self { + match actor { + Actor::UserBuiltin { .. } => IdentityType::UserBuiltin, + Actor::SiloUser { .. } => IdentityType::SiloUser, + } + } +} + /// Describes an assignment of a built-in role for a user #[derive(Clone, Queryable, Insertable, Debug, Selectable)] #[diesel(table_name = role_assignment)] diff --git a/nexus/src/external_api/console_api.rs b/nexus/src/external_api/console_api.rs index 536a950bebc..c2fe1a0ed30 100644 --- a/nexus/src/external_api/console_api.rs +++ b/nexus/src/external_api/console_api.rs @@ -8,10 +8,11 @@ //! external API, but in order to avoid CORS issues for now, we are serving //! these routes directly from the external API. use super::views; -use crate::authn::{ - silos::IdentityProviderType, USER_TEST_PRIVILEGED, USER_TEST_UNPRIVILEGED, -}; +use crate::authn::silos::IdentityProviderType; use crate::context::OpContext; +use crate::db::fixed_data::silo_user::USER_TEST_PRIVILEGED; +use crate::db::fixed_data::silo_user::USER_TEST_UNPRIVILEGED; +use crate::db::model::IdentityProviderLookup; use crate::ServerContext; use crate::{ authn::external::{ diff --git a/nexus/src/lib.rs b/nexus/src/lib.rs index 5ab34280c74..78ff717609a 100644 --- a/nexus/src/lib.rs +++ b/nexus/src/lib.rs @@ -14,7 +14,6 @@ #![allow(clippy::style)] pub mod app; // Public for documentation examples -pub mod authn; // Public only for testing pub mod authz; // Public for documentation examples mod cidata; pub mod config; // Public for testing @@ -27,6 +26,10 @@ mod populate; mod saga_interface; pub mod updates; // public for testing +// Conceptually nexus-authn is our module, but it's built as a separate crate as +// a concession to compile times. +pub use nexus_authn as authn; // Public only for testing + pub use app::test_interfaces::TestInterfaces; pub use app::Nexus; pub use config::{Config, PackageConfig}; diff --git a/nexus/test-utils/src/http_testing.rs b/nexus/test-utils/src/http_testing.rs index ac00ddbc294..cb6c341d140 100644 --- a/nexus/test-utils/src/http_testing.rs +++ b/nexus/test-utils/src/http_testing.rs @@ -11,7 +11,6 @@ use dropshot::test_util::ClientTestContext; use dropshot::ResultsPage; use headers::authorization::Credentials; use omicron_nexus::authn::external::spoof; -use omicron_nexus::db::identity::Asset; use serde_urlencoded; use std::convert::TryInto; use std::fmt::Debug; @@ -485,9 +484,8 @@ impl<'a> NexusRequest<'a> { match mode { AuthnMode::UnprivilegedUser => { - let header_value = spoof::make_header_value( - authn::USER_TEST_UNPRIVILEGED.id(), - ); + let header_value = + spoof::make_header_value(*authn::USER_TEST_UNPRIVILEGED_ID); self.request_builder = self.request_builder.header( &http::header::AUTHORIZATION, header_value.0.encode(), @@ -495,7 +493,7 @@ impl<'a> NexusRequest<'a> { } AuthnMode::PrivilegedUser => { let header_value = - spoof::make_header_value(authn::USER_TEST_PRIVILEGED.id()); + spoof::make_header_value(*authn::USER_TEST_PRIVILEGED_ID); self.request_builder = self.request_builder.header( &http::header::AUTHORIZATION, header_value.0.encode(), diff --git a/nexus/tests/integration_tests/authn_http.rs b/nexus/tests/integration_tests/authn_http.rs index 0ae6e3fd3cf..d4e9c555c99 100644 --- a/nexus/tests/integration_tests/authn_http.rs +++ b/nexus/tests/integration_tests/authn_http.rs @@ -24,7 +24,7 @@ use omicron_nexus::authn::external::spoof::HttpAuthnSpoof; use omicron_nexus::authn::external::spoof::SPOOF_SCHEME_NAME; use omicron_nexus::authn::external::HttpAuthnScheme; use omicron_nexus::authn::external::SiloUserSilo; -use omicron_nexus::db::fixed_data::silo::SILO_ID; +use omicron_nexus::authn::SILO_ID; use std::collections::HashMap; use std::sync::{Arc, Mutex}; use uuid::Uuid; diff --git a/nexus/tests/integration_tests/console_api.rs b/nexus/tests/integration_tests/console_api.rs index 8ed49ab0f57..f137f01c023 100644 --- a/nexus/tests/integration_tests/console_api.rs +++ b/nexus/tests/integration_tests/console_api.rs @@ -16,9 +16,10 @@ use nexus_test_utils::{ }; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::IdentityMetadataCreateParams; -use omicron_nexus::authn::{USER_TEST_PRIVILEGED, USER_TEST_UNPRIVILEGED}; use omicron_nexus::authz::SiloRole; use omicron_nexus::db::fixed_data::silo::DEFAULT_SILO; +use omicron_nexus::db::fixed_data::silo_user::USER_TEST_PRIVILEGED; +use omicron_nexus::db::fixed_data::silo_user::USER_TEST_UNPRIVILEGED; use omicron_nexus::db::identity::{Asset, Resource}; use omicron_nexus::external_api::console_api::SpoofLoginBody; use omicron_nexus::external_api::params::OrganizationCreate; diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index 90f17f685a3..e6f2661fec2 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -23,8 +23,8 @@ use omicron_common::api::external::RouteTarget; use omicron_common::api::external::RouterRouteCreateParams; use omicron_common::api::external::RouterRouteUpdateParams; use omicron_common::api::external::VpcFirewallRuleUpdateParams; -use omicron_nexus::authn; use omicron_nexus::authz; +use omicron_nexus::db::fixed_data::user_builtin::USER_DB_INIT; use omicron_nexus::external_api::params; use omicron_nexus::external_api::shared; use omicron_nexus::external_api::shared::IpRange; @@ -490,7 +490,7 @@ impl AllowedMethod { lazy_static! { pub static ref URL_USERS_DB_INIT: String = - format!("/system/user/{}", authn::USER_DB_INIT.name); + format!("/system/user/{}", USER_DB_INIT.name); /// List of endpoints to be verified pub static ref VERIFY_ENDPOINTS: Vec = vec![ diff --git a/nexus/tests/integration_tests/role_assignments.rs b/nexus/tests/integration_tests/role_assignments.rs index 1c59cb91ed2..be96e798ba8 100644 --- a/nexus/tests/integration_tests/role_assignments.rs +++ b/nexus/tests/integration_tests/role_assignments.rs @@ -17,9 +17,9 @@ use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::ObjectIdentity; -use omicron_nexus::authn::USER_TEST_UNPRIVILEGED; use omicron_nexus::authz; use omicron_nexus::db::fixed_data; +use omicron_nexus::db::fixed_data::silo_user::USER_TEST_UNPRIVILEGED; use omicron_nexus::db::identity::Asset; use omicron_nexus::db::identity::Resource; use omicron_nexus::db::model::DatabaseString; diff --git a/nexus/tests/integration_tests/saml.rs b/nexus/tests/integration_tests/saml.rs index 0e3a529bfa7..cf7b613687a 100644 --- a/nexus/tests/integration_tests/saml.rs +++ b/nexus/tests/integration_tests/saml.rs @@ -7,6 +7,7 @@ use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_nexus::authn::silos::{ IdentityProviderType, SamlIdentityProvider, SamlLoginPost, }; +use omicron_nexus::db::model::IdentityProviderLookup; use omicron_nexus::external_api::console_api; use omicron_nexus::external_api::views::{self, Silo}; use omicron_nexus::external_api::{params, shared}; diff --git a/nexus/tests/integration_tests/silos.rs b/nexus/tests/integration_tests/silos.rs index 87c42177e72..bb71436d684 100644 --- a/nexus/tests/integration_tests/silos.rs +++ b/nexus/tests/integration_tests/silos.rs @@ -7,6 +7,7 @@ use omicron_common::api::external::{IdentityMetadataCreateParams, Name}; use omicron_nexus::authn::silos::{AuthenticatedSubject, IdentityProviderType}; use omicron_nexus::context::OpContext; use omicron_nexus::db::lookup::LookupPath; +use omicron_nexus::db::model::IdentityProviderLookup; use omicron_nexus::external_api::views::{ self, IdentityProvider, Organization, SamlIdentityProvider, Silo, }; @@ -28,8 +29,9 @@ use omicron_nexus::authz::{self, SiloRole}; use uuid::Uuid; use httptest::{matchers::*, responders::*, Expectation, Server}; -use omicron_nexus::authn::{USER_TEST_PRIVILEGED, USER_TEST_UNPRIVILEGED}; -use omicron_nexus::db::fixed_data::silo::SILO_ID; +use omicron_nexus::authn::SILO_ID; +use omicron_nexus::db::fixed_data::silo_user::USER_TEST_PRIVILEGED; +use omicron_nexus::db::fixed_data::silo_user::USER_TEST_UNPRIVILEGED; use omicron_nexus::db::identity::Asset; #[nexus_test] diff --git a/nexus/tests/integration_tests/users_builtin.rs b/nexus/tests/integration_tests/users_builtin.rs index baa1f6fcf67..fe990b2594e 100644 --- a/nexus/tests/integration_tests/users_builtin.rs +++ b/nexus/tests/integration_tests/users_builtin.rs @@ -5,7 +5,7 @@ use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; -use omicron_nexus::authn; +use omicron_nexus::db::fixed_data::user_builtin; use omicron_nexus::external_api::views::UserBuiltin; use std::collections::BTreeMap; @@ -25,19 +25,28 @@ async fn test_users_builtin(cptestctx: &ControlPlaneTestContext) { .map(|u| (u.identity.name.to_string(), u)) .collect::>(); - let u = users.remove(&authn::USER_DB_INIT.name.to_string()).unwrap(); - assert_eq!(u.identity.id, authn::USER_DB_INIT.id); - let u = - users.remove(&authn::USER_SERVICE_BALANCER.name.to_string()).unwrap(); - assert_eq!(u.identity.id, authn::USER_SERVICE_BALANCER.id); - let u = users.remove(&authn::USER_INTERNAL_API.name.to_string()).unwrap(); - assert_eq!(u.identity.id, authn::USER_INTERNAL_API.id); - let u = users.remove(&authn::USER_INTERNAL_READ.name.to_string()).unwrap(); - assert_eq!(u.identity.id, authn::USER_INTERNAL_READ.id); - let u = users.remove(&authn::USER_EXTERNAL_AUTHN.name.to_string()).unwrap(); - assert_eq!(u.identity.id, authn::USER_EXTERNAL_AUTHN.id); - let u = users.remove(&authn::USER_SAGA_RECOVERY.name.to_string()).unwrap(); - assert_eq!(u.identity.id, authn::USER_SAGA_RECOVERY.id); + let u = users.remove(&user_builtin::USER_DB_INIT.name.to_string()).unwrap(); + assert_eq!(u.identity.id, user_builtin::USER_DB_INIT.id); + let u = users + .remove(&user_builtin::USER_SERVICE_BALANCER.name.to_string()) + .unwrap(); + assert_eq!(u.identity.id, user_builtin::USER_SERVICE_BALANCER.id); + let u = users + .remove(&user_builtin::USER_INTERNAL_API.name.to_string()) + .unwrap(); + assert_eq!(u.identity.id, user_builtin::USER_INTERNAL_API.id); + let u = users + .remove(&user_builtin::USER_INTERNAL_READ.name.to_string()) + .unwrap(); + assert_eq!(u.identity.id, user_builtin::USER_INTERNAL_READ.id); + let u = users + .remove(&user_builtin::USER_EXTERNAL_AUTHN.name.to_string()) + .unwrap(); + assert_eq!(u.identity.id, user_builtin::USER_EXTERNAL_AUTHN.id); + let u = users + .remove(&user_builtin::USER_SAGA_RECOVERY.name.to_string()) + .unwrap(); + assert_eq!(u.identity.id, user_builtin::USER_SAGA_RECOVERY.id); assert!(users.is_empty(), "found unexpected built-in users"); // TODO-coverage add test for fetching individual users, including invalid