diff --git a/Cargo.toml b/Cargo.toml index fcfc60e0b..e8be3f648 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -176,12 +176,6 @@ opentelemetry-otlp = { version = "0.13.0", default-features = false, features = "grpc-tonic", ] } opentelemetry_sdk = "0.20.0" -opentelemetry-stdout = { version = "0.1.0", features = [ - "logs", - "metrics", - "trace", -] } -tonic = "0.10.2" tracing-opentelemetry = "0.21.0" paste = "^1.0.14" diff --git a/tools/orca/README.md b/tools/orca/README.md index 91e553398..f2889dbc6 100644 --- a/tools/orca/README.md +++ b/tools/orca/README.md @@ -1,6 +1,6 @@ # Orca - A Kanidm Load Testing Tool -Make a profile +Make a profile.toml ```shell orca setup-wizard --idm-admin-password ... \ diff --git a/tools/orca/src/error.rs b/tools/orca/src/error.rs index 88ea436e7..a2cc9697d 100644 --- a/tools/orca/src/error.rs +++ b/tools/orca/src/error.rs @@ -5,7 +5,7 @@ pub enum Error { KanidmClient, ProfileBuilder, Tokio, - Interupt, + Interrupt, Crossbeam, InvalidState, } diff --git a/tools/orca/src/generate.rs b/tools/orca/src/generate.rs index 1be6ac317..5987a6921 100644 --- a/tools/orca/src/generate.rs +++ b/tools/orca/src/generate.rs @@ -1,10 +1,11 @@ use crate::error::Error; use crate::kani::KanidmOrcaClient; +use crate::model::ActorRole; use crate::profile::Profile; -use crate::state::{Credential, Flag, Model, Person, PreflightState, State}; -use rand::distributions::{Alphanumeric, DistString}; -use rand::seq::SliceRandom; -use rand::SeedableRng; +use crate::state::{Credential, Flag, Group, Model, Person, PreflightState, State}; +use rand::distributions::{Alphanumeric, DistString, Uniform}; +use rand::seq::{index, SliceRandom}; +use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; use std::collections::BTreeSet; @@ -53,9 +54,23 @@ pub async fn populate(_client: &KanidmOrcaClient, profile: Profile) -> Result Result Result Result Result<(), Error> { + self.idm_admin_client + .idm_group_set_members(group_name, members) + .await + .map_err(|err| { + error!(?err, ?group_name, "Unable to set group members"); + Error::KanidmClient + }) + } + + pub async fn group_add_members(&self, group_name: &str, members: &[&str]) -> Result<(), Error> { + self.idm_admin_client + .idm_group_add_members(group_name, members) + .await + .map_err(|err| { + error!(?err, ?group_name, "Unable to add group members"); + Error::KanidmClient + }) + } + + pub async fn group_exists(&self, group_name: &str) -> Result { + self.idm_admin_client + .idm_group_get(group_name) + .await + .map(|e| e.is_some()) + .map_err(|err| { + error!(?err, ?group_name, "Unable to check group"); + Error::KanidmClient + }) + } + + pub async fn group_create(&self, group_name: &str) -> Result<(), Error> { + self.idm_admin_client + .idm_group_create(group_name, Some("idm_admins")) + .await + .map_err(|err| { + error!(?err, ?group_name, "Unable to create group"); + Error::KanidmClient + }) + } } diff --git a/tools/orca/src/main.rs b/tools/orca/src/main.rs index a268c6fe9..acca76c90 100644 --- a/tools/orca/src/main.rs +++ b/tools/orca/src/main.rs @@ -1,4 +1,4 @@ -// #![deny(warnings)] +#![deny(warnings)] #![warn(unused_extern_crates)] #![allow(clippy::panic)] #![deny(clippy::unreachable)] @@ -26,7 +26,7 @@ mod error; mod generate; mod kani; mod model; -mod model_basic; +mod models; mod populate; mod profile; mod run; diff --git a/tools/orca/src/model.rs b/tools/orca/src/model.rs index 2fc9ccabe..efa795b7e 100644 --- a/tools/orca/src/model.rs +++ b/tools/orca/src/model.rs @@ -3,13 +3,16 @@ use crate::run::{EventDetail, EventRecord}; use crate::state::*; use std::time::{Duration, Instant}; -use kanidm_client::KanidmClient; +use kanidm_client::{ClientError, KanidmClient}; use async_trait::async_trait; +use serde::{Deserialize, Serialize}; pub enum TransitionAction { Login, Logout, + PrivilegeReauth, + WriteAttributePersonMail, } // Is this the right way? Should transitions/delay be part of the actor model? Should @@ -31,10 +34,28 @@ pub enum TransitionResult { Ok, // We need to re-authenticate, the session expired. // AuthenticationNeeded, - // An error occured. + // An error occurred. Error, } +#[derive(Debug, Clone, Default, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] +pub enum ActorRole { + #[default] + None, + PeoplePiiReader, + PeopleSelfWriteMail, +} + +impl ActorRole { + pub fn requires_membership_to(&self) -> Option<&[&str]> { + match self { + ActorRole::None => None, + ActorRole::PeoplePiiReader => Some(&["idm_people_pii_read"]), + ActorRole::PeopleSelfWriteMail => Some(&["idm_people_self_write_mail"]), + } + } +} + #[async_trait] pub trait ActorModel { async fn transition( @@ -57,31 +78,59 @@ pub async fn login( .await } }; - let end = Instant::now(); - let duration = end.duration_since(start); + let duration = Instant::now().duration_since(start); + Ok(parse_call_result_into_transition_result_and_event_record( + result, + EventDetail::Login, + start, + duration, + )) +} - match result { - Ok(_) => Ok(( - TransitionResult::Ok, - EventRecord { - start, - duration, - details: EventDetail::Authentication, - }, - )), - Err(client_err) => { - debug!(?client_err); - Ok(( - TransitionResult::Error, - EventRecord { - start, - duration, - details: EventDetail::Error, - }, - )) - } - } +pub async fn person_set_self_mail( + client: &KanidmClient, + person: &Person, + values: &[&str], +) -> Result<(TransitionResult, EventRecord), Error> { + // Should we measure the time of each call rather than the time with multiple calls? + let person_username = person.username.as_str(); + + let start = Instant::now(); + let result = client + .idm_person_account_set_attr(person_username, "mail", values) + .await; + + let duration = Instant::now().duration_since(start); + let parsed_result = parse_call_result_into_transition_result_and_event_record( + result, + EventDetail::PersonSetSelfMail, + start, + duration, + ); + + Ok(parsed_result) +} + +pub async fn privilege_reauth( + client: &KanidmClient, + person: &Person, +) -> Result<(TransitionResult, EventRecord), Error> { + let start = Instant::now(); + + let result = match &person.credential { + Credential::Password { plain } => client.reauth_simple_password(plain.as_str()).await, + }; + + let duration = Instant::now().duration_since(start); + + let parsed_result = parse_call_result_into_transition_result_and_event_record( + result, + EventDetail::PersonReauth, + start, + duration, + ); + Ok(parsed_result) } pub async fn logout( @@ -90,29 +139,41 @@ pub async fn logout( ) -> Result<(TransitionResult, EventRecord), Error> { let start = Instant::now(); let result = client.logout().await; - let end = Instant::now(); + let duration = Instant::now().duration_since(start); - let duration = end.duration_since(start); + Ok(parse_call_result_into_transition_result_and_event_record( + result, + EventDetail::Logout, + start, + duration, + )) +} +fn parse_call_result_into_transition_result_and_event_record( + result: Result, + details: EventDetail, + start: Instant, + duration: Duration, +) -> (TransitionResult, EventRecord) { match result { - Ok(_) => Ok(( + Ok(_) => ( TransitionResult::Ok, EventRecord { start, duration, - details: EventDetail::Logout, + details, }, - )), + ), Err(client_err) => { debug!(?client_err); - Ok(( + ( TransitionResult::Error, EventRecord { start, duration, details: EventDetail::Error, }, - )) + ) } } } diff --git a/tools/orca/src/model_basic.rs b/tools/orca/src/models/auth_only.rs similarity index 69% rename from tools/orca/src/model_basic.rs rename to tools/orca/src/models/auth_only.rs index c712072a8..773c48bb9 100644 --- a/tools/orca/src/model_basic.rs +++ b/tools/orca/src/models/auth_only.rs @@ -14,20 +14,20 @@ enum State { Authenticated, } -pub struct ActorBasic { +pub struct ActorAuthOnly { state: State, } -impl ActorBasic { +impl ActorAuthOnly { pub fn new() -> Self { - ActorBasic { + ActorAuthOnly { state: State::Unauthenticated, } } } #[async_trait] -impl ActorModel for ActorBasic { +impl ActorModel for ActorAuthOnly { async fn transition( &mut self, client: &KanidmClient, @@ -43,16 +43,16 @@ impl ActorModel for ActorBasic { let (result, event) = match transition.action { TransitionAction::Login => model::login(client, person).await, TransitionAction::Logout => model::logout(client, person).await, + _ => Err(Error::InvalidState), }?; - // Given the result, make a choice about what text. - self.next_state(result); + self.next_state(transition.action, result); Ok(event) } } -impl ActorBasic { +impl ActorAuthOnly { fn next_transition(&mut self) -> Transition { match self.state { State::Unauthenticated => Transition { @@ -66,20 +66,19 @@ impl ActorBasic { } } - fn next_state(&mut self, result: TransitionResult) { - // Is this a design flaw? We probably need to know what the state was that we - // requested to move to? - match (&self.state, result) { - (State::Unauthenticated, TransitionResult::Ok) => { + fn next_state(&mut self, action: TransitionAction, result: TransitionResult) { + match (&self.state, action, result) { + (State::Unauthenticated, TransitionAction::Login, TransitionResult::Ok) => { self.state = State::Authenticated; } - (State::Unauthenticated, TransitionResult::Error) => { + (State::Authenticated, TransitionAction::Logout, TransitionResult::Ok) => { self.state = State::Unauthenticated; } - (State::Authenticated, TransitionResult::Ok) => { - self.state = State::Unauthenticated; + // Shouldn't be reachable? + (_, _, TransitionResult::Ok) => { + unreachable!(); } - (State::Authenticated, TransitionResult::Error) => { + (_, _, TransitionResult::Error) => { self.state = State::Unauthenticated; } } diff --git a/tools/orca/src/models/basic.rs b/tools/orca/src/models/basic.rs new file mode 100644 index 000000000..318fe0878 --- /dev/null +++ b/tools/orca/src/models/basic.rs @@ -0,0 +1,117 @@ +use crate::model::{self, ActorModel, ActorRole, Transition, TransitionAction, TransitionResult}; + +use crate::error::Error; +use crate::run::EventRecord; +use crate::state::*; +use kanidm_client::KanidmClient; + +use async_trait::async_trait; + +use std::collections::BTreeSet; +use std::time::Duration; + +enum State { + Unauthenticated, + Authenticated, + AuthenticatedWithReauth, +} + +pub struct ActorBasic { + state: State, +} + +impl ActorBasic { + pub fn new() -> Self { + ActorBasic { + state: State::Unauthenticated, + } + } +} + +#[async_trait] +impl ActorModel for ActorBasic { + async fn transition( + &mut self, + client: &KanidmClient, + person: &Person, + ) -> Result { + let transition = self.next_transition(&person.roles); + + if let Some(delay) = transition.delay { + tokio::time::sleep(delay).await; + } + + // Once we get to here, we want the transition to go ahead. + let (result, event) = match transition.action { + TransitionAction::Login => model::login(client, person).await, + TransitionAction::Logout => model::logout(client, person).await, + TransitionAction::PrivilegeReauth => model::privilege_reauth(client, person).await, + TransitionAction::WriteAttributePersonMail => { + let mail = format!("{}@example.com", person.username); + let values = &[mail.as_str()]; + model::person_set_self_mail(client, person, values).await + } + }?; + + self.next_state(transition.action, result); + + Ok(event) + } +} + +impl ActorBasic { + fn next_transition(&mut self, roles: &BTreeSet) -> Transition { + match self.state { + State::Unauthenticated => Transition { + delay: None, + action: TransitionAction::Login, + }, + State::Authenticated => Transition { + delay: Some(Duration::from_millis(100)), + action: TransitionAction::PrivilegeReauth, + }, + State::AuthenticatedWithReauth => { + if roles.contains(&ActorRole::PeopleSelfWriteMail) { + Transition { + delay: Some(Duration::from_millis(200)), + action: TransitionAction::WriteAttributePersonMail, + } + } else { + Transition { + delay: Some(Duration::from_secs(5)), + action: TransitionAction::Logout, + } + } + } + } + } + + fn next_state(&mut self, action: TransitionAction, result: TransitionResult) { + // Is this a design flaw? We probably need to know what the state was that we + // requested to move to? + match (&self.state, action, result) { + (State::Unauthenticated, TransitionAction::Login, TransitionResult::Ok) => { + self.state = State::Authenticated; + } + (State::Authenticated, TransitionAction::PrivilegeReauth, TransitionResult::Ok) => { + self.state = State::AuthenticatedWithReauth; + } + ( + State::AuthenticatedWithReauth, + TransitionAction::WriteAttributePersonMail, + TransitionResult::Ok, + ) => { + self.state = State::AuthenticatedWithReauth; + } + (_, TransitionAction::Logout, TransitionResult::Ok) => { + self.state = State::Unauthenticated; + } + (_, _, TransitionResult::Ok) => { + unreachable!(); + } + (_, _, TransitionResult::Error) => { + self.state = State::Unauthenticated; + } + } + } +} diff --git a/tools/orca/src/models/mod.rs b/tools/orca/src/models/mod.rs new file mode 100644 index 000000000..6ece12d7f --- /dev/null +++ b/tools/orca/src/models/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod auth_only; +pub(crate) mod basic; diff --git a/tools/orca/src/opt.rs b/tools/orca/src/opt.rs index 7e6592588..55a6743f3 100644 --- a/tools/orca/src/opt.rs +++ b/tools/orca/src/opt.rs @@ -50,7 +50,7 @@ enum OrcaOpt { /// This allows deterministic regeneration of a test state file. seed: Option, - // Todo - support the extra uris field for replicated tests. + // TODO - support the extra uris field for replicated tests. #[clap(long = "profile")] /// The configuration file path to update (or create) profile_path: PathBuf, diff --git a/tools/orca/src/populate.rs b/tools/orca/src/populate.rs index ec9b5062e..594404ca6 100644 --- a/tools/orca/src/populate.rs +++ b/tools/orca/src/populate.rs @@ -30,11 +30,40 @@ async fn preflight_person( match &person.credential { Credential::Password { plain } => { client - .person_set_pirmary_password_only(&person.username, plain) + .person_set_primary_password_only(&person.username, plain) .await?; } } + // For each role we are part of, did we have other permissions required to fufil that? + for role in &person.roles { + if let Some(need_groups) = role.requires_membership_to() { + for group_name in need_groups { + client + .group_add_members(&group_name, &[person.username.as_str()]) + .await?; + } + } + } + + Ok(()) +} + +async fn preflight_group(client: Arc, group: Group) -> Result<(), Error> { + if client.group_exists(group.name.as_str()).await? { + // Do nothing? Do we need to reset them later? + } else { + client.group_create(group.name.as_str()).await?; + } + + // We can submit all the members in one go. + + let members = group.members.iter().map(|s| s.as_str()).collect::>(); + + client + .group_set_members(group.name.as_str(), members.as_slice()) + .await?; + Ok(()) } @@ -45,8 +74,9 @@ pub async fn preflight(state: State) -> Result<(), Error> { // Apply any flags if they exist. apply_flags(client.clone(), state.preflight_flags.as_slice()).await?; - // Create persons. let mut tasks = Vec::with_capacity(state.persons.len()); + + // Create persons. for person in state.persons.into_iter() { let c = client.clone(); tasks.push(tokio::spawn(preflight_person(c, person))) @@ -62,6 +92,19 @@ pub async fn preflight(state: State) -> Result<(), Error> { } // Create groups. + let mut tasks = Vec::with_capacity(state.groups.len()); + + for group in state.groups.into_iter() { + let c = client.clone(); + tasks.push(tokio::spawn(preflight_group(c, group))) + } + + for task in tasks { + task.await.map_err(|tokio_err| { + error!(?tokio_err, "Failed to join task"); + Error::Tokio + })??; + } // Create integrations. diff --git a/tools/orca/src/run.rs b/tools/orca/src/run.rs index 43187d5fc..495a123a2 100644 --- a/tools/orca/src/run.rs +++ b/tools/orca/src/run.rs @@ -22,7 +22,7 @@ async fn actor_person( stats_queue: Arc>, mut actor_rx: broadcast::Receiver, ) -> Result<(), Error> { - let mut model = person.model.as_dyn_object(); + let mut model = person.model.as_dyn_object()?; while let Err(broadcast::error::TryRecvError::Empty) = actor_rx.try_recv() { let event = model.transition(&client, &person).await?; @@ -41,9 +41,10 @@ pub struct EventRecord { } pub enum EventDetail { - Authentication, + Login, Logout, - + PersonSetSelfMail, + PersonReauth, Error, } @@ -64,12 +65,13 @@ async fn execute_inner( // continue. } _ = control_rx.recv() => { - // Untill we add other signal types, any event is + // Until we add other signal types, any event is // either Ok(Signal::Stop) or Err(_), both of which indicate // we need to stop immediately. - return Err(Error::Interupt); + return Err(Error::Interrupt); } } + info!("warmup time passed, statistics will now be collected ..."); let start = Instant::now(); if let Err(crossbeam_err) = stat_ctrl.push(TestPhase::Start(start)) { @@ -92,10 +94,11 @@ async fn execute_inner( // continue. } _ = recv => { - // Untill we add other signal types, any event is + // Until we add other signal types, any event is // either Ok(Signal::Stop) or Err(_), both of which indicate // we need to stop immediately. - return Err(Error::Interupt); + debug!("Interrupt"); + return Err(Error::Interrupt); } } } else { @@ -106,7 +109,7 @@ async fn execute_inner( if let Err(crossbeam_err) = stat_ctrl.push(TestPhase::End(end)) { error!( ?crossbeam_err, - "Unable to signal statistics collector to start" + "Unable to signal statistics collector to end" ); return Err(Error::Crossbeam); } @@ -175,14 +178,14 @@ pub async fn execute(state: State, control_rx: broadcast::Receiver) -> R } let warmup = state.profile.warmup_time(); - let testtime = state.profile.test_time(); + let test_time = state.profile.test_time(); - // We run a seperate test inner so we don't have to worry about + // We run a separate test inner so we don't have to worry about // task spawn/join within our logic. let c_stats_ctrl = stats_ctrl.clone(); // Don't ? this, we want to stash the result so we cleanly stop all the workers // before returning the inner test result. - let test_result = execute_inner(warmup, testtime, control_rx, c_stats_ctrl).await; + let test_result = execute_inner(warmup, test_time, control_rx, c_stats_ctrl).await; info!("stopping stats"); diff --git a/tools/orca/src/state.rs b/tools/orca/src/state.rs index 6b46a0e91..5817e80d3 100644 --- a/tools/orca/src/state.rs +++ b/tools/orca/src/state.rs @@ -1,10 +1,10 @@ use crate::error::Error; -use crate::model::ActorModel; +use crate::model::{ActorModel, ActorRole}; +use crate::models; use crate::profile::Profile; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; use std::path::Path; - /// A serialisable state representing the content of a kanidm database and potential /// test content that can be created and modified. /// @@ -16,7 +16,7 @@ pub struct State { // ---------------------------- pub preflight_flags: Vec, pub persons: Vec, - // groups: Vec, + pub groups: Vec, // oauth_clients: Vec, } @@ -55,23 +55,37 @@ pub enum Flag { DisableAllPersonsMFAPolicy, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Default, Debug, Serialize, Deserialize)] pub enum PreflightState { + #[default] Present, Absent, } -#[derive(Debug, Serialize, Deserialize, Clone)] +/// A model defines *how* an actors makes it's choices. For example the choices +/// could be purely random, they could be a linear pattern, or they could have +/// some set of weights related to choices they make. +/// +/// Some models can *restrict* the set of choices that an actor may make. +/// +/// This compliments ActorRoles, which define the extended actions an Actor may +/// choose to perform. If ActorRoles are present, the model MAY choose to use +/// these roles to perform extended operations. +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub enum Model { /// This is a "hardcoded" model that just authenticates and searches + AuthOnly, + /// A simple linear executor that does actions in a loop. + #[default] Basic, } impl Model { - pub fn as_dyn_object(&self) -> Box { - match self { - Model::Basic => Box::new(crate::model_basic::ActorBasic::new()), - } + pub fn as_dyn_object(&self) -> Result, Error> { + Ok(match self { + Model::AuthOnly => Box::new(models::auth_only::ActorAuthOnly::new()), + Model::Basic => Box::new(models::basic::ActorBasic::new()), + }) } } @@ -85,7 +99,15 @@ pub struct Person { pub preflight_state: PreflightState, pub username: String, pub display_name: String, - pub member_of: BTreeSet, + pub roles: BTreeSet, pub credential: Credential, pub model: Model, } + +#[derive(Default, Debug, Serialize, Deserialize)] +pub struct Group { + pub name: String, + pub preflight_state: PreflightState, + pub role: ActorRole, + pub members: BTreeSet, +} diff --git a/tools/orca/src/stats.rs b/tools/orca/src/stats.rs index 412d1e842..45d795a76 100644 --- a/tools/orca/src/stats.rs +++ b/tools/orca/src/stats.rs @@ -47,6 +47,7 @@ impl DataCollector for BasicStatistics { break start; } Some(TestPhase::End(_)) => { + error!("invalid state"); // Invalid state. return Err(Error::InvalidState); } @@ -69,6 +70,7 @@ impl DataCollector for BasicStatistics { break end; } Some(TestPhase::StopNow) => { + warn!("requested to stop now!"); // We have been told to stop immediately. return Ok(()); } @@ -76,6 +78,8 @@ impl DataCollector for BasicStatistics { } }; + info!("start statistics processing ..."); + let mut count: usize = 0; let mut optimes = Vec::new();