mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
20240409 rework orca markov (#2699)
Improve the models and what can be performed in the orca benchmarks. --------- Co-authored-by: Sebastiano Tocci <seba.tocci@gmail.com> Co-authored-by: Sebastiano Tocci <sebastiano.tocci@proton.me>
This commit is contained in:
parent
d7834b52e6
commit
62bbd7e3ea
|
@ -176,12 +176,6 @@ opentelemetry-otlp = { version = "0.13.0", default-features = false, features =
|
||||||
"grpc-tonic",
|
"grpc-tonic",
|
||||||
] }
|
] }
|
||||||
opentelemetry_sdk = "0.20.0"
|
opentelemetry_sdk = "0.20.0"
|
||||||
opentelemetry-stdout = { version = "0.1.0", features = [
|
|
||||||
"logs",
|
|
||||||
"metrics",
|
|
||||||
"trace",
|
|
||||||
] }
|
|
||||||
tonic = "0.10.2"
|
|
||||||
tracing-opentelemetry = "0.21.0"
|
tracing-opentelemetry = "0.21.0"
|
||||||
|
|
||||||
paste = "^1.0.14"
|
paste = "^1.0.14"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Orca - A Kanidm Load Testing Tool
|
# Orca - A Kanidm Load Testing Tool
|
||||||
|
|
||||||
Make a profile
|
Make a profile.toml
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
orca setup-wizard --idm-admin-password ... \
|
orca setup-wizard --idm-admin-password ... \
|
||||||
|
|
|
@ -5,7 +5,7 @@ pub enum Error {
|
||||||
KanidmClient,
|
KanidmClient,
|
||||||
ProfileBuilder,
|
ProfileBuilder,
|
||||||
Tokio,
|
Tokio,
|
||||||
Interupt,
|
Interrupt,
|
||||||
Crossbeam,
|
Crossbeam,
|
||||||
InvalidState,
|
InvalidState,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::kani::KanidmOrcaClient;
|
use crate::kani::KanidmOrcaClient;
|
||||||
|
use crate::model::ActorRole;
|
||||||
use crate::profile::Profile;
|
use crate::profile::Profile;
|
||||||
use crate::state::{Credential, Flag, Model, Person, PreflightState, State};
|
use crate::state::{Credential, Flag, Group, Model, Person, PreflightState, State};
|
||||||
use rand::distributions::{Alphanumeric, DistString};
|
use rand::distributions::{Alphanumeric, DistString, Uniform};
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::{index, SliceRandom};
|
||||||
use rand::SeedableRng;
|
use rand::{Rng, SeedableRng};
|
||||||
use rand_chacha::ChaCha8Rng;
|
use rand_chacha::ChaCha8Rng;
|
||||||
|
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
@ -53,9 +54,23 @@ pub async fn populate(_client: &KanidmOrcaClient, profile: Profile) -> Result<St
|
||||||
let preflight_flags = vec![Flag::DisableAllPersonsMFAPolicy];
|
let preflight_flags = vec![Flag::DisableAllPersonsMFAPolicy];
|
||||||
|
|
||||||
// PHASE 1 - generate a pool of persons that are not-yet created for future import.
|
// PHASE 1 - generate a pool of persons that are not-yet created for future import.
|
||||||
// todo! may need a random username vec for later stuff
|
|
||||||
|
|
||||||
// PHASE 2 - generate persons
|
// PHASE 2 - generate groups for integration access, assign roles to groups.
|
||||||
|
// These decide what each person is supposed to do with their life.
|
||||||
|
let mut groups = vec![
|
||||||
|
Group {
|
||||||
|
name: "role_people_pii_reader".to_string(),
|
||||||
|
role: ActorRole::PeoplePiiReader,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Group {
|
||||||
|
name: "role_people_self_write_mail".to_string(),
|
||||||
|
role: ActorRole::PeopleSelfWriteMail,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// PHASE 3 - generate persons
|
||||||
// - assign them credentials of various types.
|
// - assign them credentials of various types.
|
||||||
let mut persons = Vec::with_capacity(profile.person_count() as usize);
|
let mut persons = Vec::with_capacity(profile.person_count() as usize);
|
||||||
let mut person_usernames = BTreeSet::new();
|
let mut person_usernames = BTreeSet::new();
|
||||||
|
@ -88,17 +103,17 @@ pub async fn populate(_client: &KanidmOrcaClient, profile: Profile) -> Result<St
|
||||||
|
|
||||||
let password = random_password(&mut seeded_rng);
|
let password = random_password(&mut seeded_rng);
|
||||||
|
|
||||||
// TODO: Add more and different "models" to each person for their actions.
|
let roles = BTreeSet::new();
|
||||||
|
|
||||||
let model = Model::Basic;
|
let model = Model::Basic;
|
||||||
|
|
||||||
// =======
|
// =======
|
||||||
// Data is ready, make changes to the server. These should be idempotent if possible.
|
// Data is ready, make changes to the server. These should be idempotent if possible.
|
||||||
|
|
||||||
let p = Person {
|
let p = Person {
|
||||||
preflight_state: PreflightState::Present,
|
preflight_state: PreflightState::Present,
|
||||||
username: username.clone(),
|
username: username.clone(),
|
||||||
display_name,
|
display_name,
|
||||||
member_of: BTreeSet::default(),
|
roles,
|
||||||
credential: Credential::Password { plain: password },
|
credential: Credential::Password { plain: password },
|
||||||
model,
|
model,
|
||||||
};
|
};
|
||||||
|
@ -109,7 +124,43 @@ pub async fn populate(_client: &KanidmOrcaClient, profile: Profile) -> Result<St
|
||||||
persons.push(p);
|
persons.push(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PHASE 3 - generate groups for integration access, assign persons.
|
// Now, assign persons to roles.
|
||||||
|
//
|
||||||
|
// We do this by iterating through our roles, and then assigning
|
||||||
|
// them a baseline of required accounts with some variation. This
|
||||||
|
// way in each test it's guaranteed that *at least* one person
|
||||||
|
// to each role always will exist and be operational.
|
||||||
|
|
||||||
|
for group in groups.iter_mut() {
|
||||||
|
// For now, our baseline is 50%. We can adjust this in future per
|
||||||
|
// role for example.
|
||||||
|
let baseline = persons.len() / 2;
|
||||||
|
let inverse = persons.len() - baseline;
|
||||||
|
// Randomly add extra from the inverse
|
||||||
|
let extra = Uniform::new(0, inverse);
|
||||||
|
let persons_to_choose = baseline + seeded_rng.sample(extra);
|
||||||
|
|
||||||
|
assert!(persons_to_choose <= persons.len());
|
||||||
|
|
||||||
|
debug!(?persons_to_choose);
|
||||||
|
|
||||||
|
let person_index = index::sample(&mut seeded_rng, persons.len(), persons_to_choose);
|
||||||
|
|
||||||
|
// Order doesn't matter, lets optimise for linear lookup.
|
||||||
|
let mut person_index = person_index.into_vec();
|
||||||
|
person_index.sort_unstable();
|
||||||
|
|
||||||
|
for p_idx in person_index {
|
||||||
|
let person = persons.get_mut(p_idx).unwrap();
|
||||||
|
|
||||||
|
// Add the person to the group.
|
||||||
|
group.members.insert(person.username.clone());
|
||||||
|
|
||||||
|
// Add the reverse links, this allows the person in the test
|
||||||
|
// to know their roles
|
||||||
|
person.roles.insert(group.role.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PHASE 4 - generate groups for user modification rights
|
// PHASE 4 - generate groups for user modification rights
|
||||||
|
|
||||||
|
@ -117,13 +168,14 @@ pub async fn populate(_client: &KanidmOrcaClient, profile: Profile) -> Result<St
|
||||||
|
|
||||||
// PHASE 6 - generate integrations -
|
// PHASE 6 - generate integrations -
|
||||||
|
|
||||||
// PHASE 7 - given the intergariotns and groupings,
|
// PHASE 7 - given the integrations and groupings,
|
||||||
|
|
||||||
// Return the state.
|
// Return the state.
|
||||||
|
|
||||||
let state = State {
|
let state = State {
|
||||||
profile,
|
profile,
|
||||||
// ---------------
|
// ---------------
|
||||||
|
groups,
|
||||||
preflight_flags,
|
preflight_flags,
|
||||||
persons,
|
persons,
|
||||||
};
|
};
|
||||||
|
|
|
@ -84,7 +84,7 @@ impl KanidmOrcaClient {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn person_set_pirmary_password_only(
|
pub async fn person_set_primary_password_only(
|
||||||
&self,
|
&self,
|
||||||
username: &str,
|
username: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
|
@ -97,4 +97,45 @@ impl KanidmOrcaClient {
|
||||||
Error::KanidmClient
|
Error::KanidmClient
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn group_set_members(&self, group_name: &str, members: &[&str]) -> 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<bool, Error> {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// #![deny(warnings)]
|
#![deny(warnings)]
|
||||||
#![warn(unused_extern_crates)]
|
#![warn(unused_extern_crates)]
|
||||||
#![allow(clippy::panic)]
|
#![allow(clippy::panic)]
|
||||||
#![deny(clippy::unreachable)]
|
#![deny(clippy::unreachable)]
|
||||||
|
@ -26,7 +26,7 @@ mod error;
|
||||||
mod generate;
|
mod generate;
|
||||||
mod kani;
|
mod kani;
|
||||||
mod model;
|
mod model;
|
||||||
mod model_basic;
|
mod models;
|
||||||
mod populate;
|
mod populate;
|
||||||
mod profile;
|
mod profile;
|
||||||
mod run;
|
mod run;
|
||||||
|
|
|
@ -3,13 +3,16 @@ use crate::run::{EventDetail, EventRecord};
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use kanidm_client::KanidmClient;
|
use kanidm_client::{ClientError, KanidmClient};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub enum TransitionAction {
|
pub enum TransitionAction {
|
||||||
Login,
|
Login,
|
||||||
Logout,
|
Logout,
|
||||||
|
PrivilegeReauth,
|
||||||
|
WriteAttributePersonMail,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is this the right way? Should transitions/delay be part of the actor model? Should
|
// Is this the right way? Should transitions/delay be part of the actor model? Should
|
||||||
|
@ -31,10 +34,28 @@ pub enum TransitionResult {
|
||||||
Ok,
|
Ok,
|
||||||
// We need to re-authenticate, the session expired.
|
// We need to re-authenticate, the session expired.
|
||||||
// AuthenticationNeeded,
|
// AuthenticationNeeded,
|
||||||
// An error occured.
|
// An error occurred.
|
||||||
Error,
|
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]
|
#[async_trait]
|
||||||
pub trait ActorModel {
|
pub trait ActorModel {
|
||||||
async fn transition(
|
async fn transition(
|
||||||
|
@ -57,31 +78,59 @@ pub async fn login(
|
||||||
.await
|
.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 {
|
pub async fn person_set_self_mail(
|
||||||
Ok(_) => Ok((
|
client: &KanidmClient,
|
||||||
TransitionResult::Ok,
|
person: &Person,
|
||||||
EventRecord {
|
values: &[&str],
|
||||||
start,
|
) -> Result<(TransitionResult, EventRecord), Error> {
|
||||||
duration,
|
// Should we measure the time of each call rather than the time with multiple calls?
|
||||||
details: EventDetail::Authentication,
|
let person_username = person.username.as_str();
|
||||||
},
|
|
||||||
)),
|
let start = Instant::now();
|
||||||
Err(client_err) => {
|
let result = client
|
||||||
debug!(?client_err);
|
.idm_person_account_set_attr(person_username, "mail", values)
|
||||||
Ok((
|
.await;
|
||||||
TransitionResult::Error,
|
|
||||||
EventRecord {
|
let duration = Instant::now().duration_since(start);
|
||||||
start,
|
let parsed_result = parse_call_result_into_transition_result_and_event_record(
|
||||||
duration,
|
result,
|
||||||
details: EventDetail::Error,
|
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(
|
pub async fn logout(
|
||||||
|
@ -90,29 +139,41 @@ pub async fn logout(
|
||||||
) -> Result<(TransitionResult, EventRecord), Error> {
|
) -> Result<(TransitionResult, EventRecord), Error> {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let result = client.logout().await;
|
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<T>(
|
||||||
|
result: Result<T, ClientError>,
|
||||||
|
details: EventDetail,
|
||||||
|
start: Instant,
|
||||||
|
duration: Duration,
|
||||||
|
) -> (TransitionResult, EventRecord) {
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => Ok((
|
Ok(_) => (
|
||||||
TransitionResult::Ok,
|
TransitionResult::Ok,
|
||||||
EventRecord {
|
EventRecord {
|
||||||
start,
|
start,
|
||||||
duration,
|
duration,
|
||||||
details: EventDetail::Logout,
|
details,
|
||||||
},
|
},
|
||||||
)),
|
),
|
||||||
Err(client_err) => {
|
Err(client_err) => {
|
||||||
debug!(?client_err);
|
debug!(?client_err);
|
||||||
Ok((
|
(
|
||||||
TransitionResult::Error,
|
TransitionResult::Error,
|
||||||
EventRecord {
|
EventRecord {
|
||||||
start,
|
start,
|
||||||
duration,
|
duration,
|
||||||
details: EventDetail::Error,
|
details: EventDetail::Error,
|
||||||
},
|
},
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,20 +14,20 @@ enum State {
|
||||||
Authenticated,
|
Authenticated,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ActorBasic {
|
pub struct ActorAuthOnly {
|
||||||
state: State,
|
state: State,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActorBasic {
|
impl ActorAuthOnly {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
ActorBasic {
|
ActorAuthOnly {
|
||||||
state: State::Unauthenticated,
|
state: State::Unauthenticated,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ActorModel for ActorBasic {
|
impl ActorModel for ActorAuthOnly {
|
||||||
async fn transition(
|
async fn transition(
|
||||||
&mut self,
|
&mut self,
|
||||||
client: &KanidmClient,
|
client: &KanidmClient,
|
||||||
|
@ -43,16 +43,16 @@ impl ActorModel for ActorBasic {
|
||||||
let (result, event) = match transition.action {
|
let (result, event) = match transition.action {
|
||||||
TransitionAction::Login => model::login(client, person).await,
|
TransitionAction::Login => model::login(client, person).await,
|
||||||
TransitionAction::Logout => model::logout(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(transition.action, result);
|
||||||
self.next_state(result);
|
|
||||||
|
|
||||||
Ok(event)
|
Ok(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActorBasic {
|
impl ActorAuthOnly {
|
||||||
fn next_transition(&mut self) -> Transition {
|
fn next_transition(&mut self) -> Transition {
|
||||||
match self.state {
|
match self.state {
|
||||||
State::Unauthenticated => Transition {
|
State::Unauthenticated => Transition {
|
||||||
|
@ -66,20 +66,19 @@ impl ActorBasic {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_state(&mut self, result: TransitionResult) {
|
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
|
match (&self.state, action, result) {
|
||||||
// requested to move to?
|
(State::Unauthenticated, TransitionAction::Login, TransitionResult::Ok) => {
|
||||||
match (&self.state, result) {
|
|
||||||
(State::Unauthenticated, TransitionResult::Ok) => {
|
|
||||||
self.state = State::Authenticated;
|
self.state = State::Authenticated;
|
||||||
}
|
}
|
||||||
(State::Unauthenticated, TransitionResult::Error) => {
|
(State::Authenticated, TransitionAction::Logout, TransitionResult::Ok) => {
|
||||||
self.state = State::Unauthenticated;
|
self.state = State::Unauthenticated;
|
||||||
}
|
}
|
||||||
(State::Authenticated, TransitionResult::Ok) => {
|
// Shouldn't be reachable?
|
||||||
self.state = State::Unauthenticated;
|
(_, _, TransitionResult::Ok) => {
|
||||||
|
unreachable!();
|
||||||
}
|
}
|
||||||
(State::Authenticated, TransitionResult::Error) => {
|
(_, _, TransitionResult::Error) => {
|
||||||
self.state = State::Unauthenticated;
|
self.state = State::Unauthenticated;
|
||||||
}
|
}
|
||||||
}
|
}
|
117
tools/orca/src/models/basic.rs
Normal file
117
tools/orca/src/models/basic.rs
Normal file
|
@ -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<EventRecord, Error> {
|
||||||
|
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<ActorRole>) -> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
tools/orca/src/models/mod.rs
Normal file
2
tools/orca/src/models/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub(crate) mod auth_only;
|
||||||
|
pub(crate) mod basic;
|
|
@ -50,7 +50,7 @@ enum OrcaOpt {
|
||||||
/// This allows deterministic regeneration of a test state file.
|
/// This allows deterministic regeneration of a test state file.
|
||||||
seed: Option<i64>,
|
seed: Option<i64>,
|
||||||
|
|
||||||
// Todo - support the extra uris field for replicated tests.
|
// TODO - support the extra uris field for replicated tests.
|
||||||
#[clap(long = "profile")]
|
#[clap(long = "profile")]
|
||||||
/// The configuration file path to update (or create)
|
/// The configuration file path to update (or create)
|
||||||
profile_path: PathBuf,
|
profile_path: PathBuf,
|
||||||
|
|
|
@ -30,11 +30,40 @@ async fn preflight_person(
|
||||||
match &person.credential {
|
match &person.credential {
|
||||||
Credential::Password { plain } => {
|
Credential::Password { plain } => {
|
||||||
client
|
client
|
||||||
.person_set_pirmary_password_only(&person.username, plain)
|
.person_set_primary_password_only(&person.username, plain)
|
||||||
.await?;
|
.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<kani::KanidmOrcaClient>, 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::<Vec<_>>();
|
||||||
|
|
||||||
|
client
|
||||||
|
.group_set_members(group.name.as_str(), members.as_slice())
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,8 +74,9 @@ pub async fn preflight(state: State) -> Result<(), Error> {
|
||||||
// Apply any flags if they exist.
|
// Apply any flags if they exist.
|
||||||
apply_flags(client.clone(), state.preflight_flags.as_slice()).await?;
|
apply_flags(client.clone(), state.preflight_flags.as_slice()).await?;
|
||||||
|
|
||||||
// Create persons.
|
|
||||||
let mut tasks = Vec::with_capacity(state.persons.len());
|
let mut tasks = Vec::with_capacity(state.persons.len());
|
||||||
|
|
||||||
|
// Create persons.
|
||||||
for person in state.persons.into_iter() {
|
for person in state.persons.into_iter() {
|
||||||
let c = client.clone();
|
let c = client.clone();
|
||||||
tasks.push(tokio::spawn(preflight_person(c, person)))
|
tasks.push(tokio::spawn(preflight_person(c, person)))
|
||||||
|
@ -62,6 +92,19 @@ pub async fn preflight(state: State) -> Result<(), Error> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create groups.
|
// 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.
|
// Create integrations.
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ async fn actor_person(
|
||||||
stats_queue: Arc<SegQueue<EventRecord>>,
|
stats_queue: Arc<SegQueue<EventRecord>>,
|
||||||
mut actor_rx: broadcast::Receiver<Signal>,
|
mut actor_rx: broadcast::Receiver<Signal>,
|
||||||
) -> Result<(), Error> {
|
) -> 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() {
|
while let Err(broadcast::error::TryRecvError::Empty) = actor_rx.try_recv() {
|
||||||
let event = model.transition(&client, &person).await?;
|
let event = model.transition(&client, &person).await?;
|
||||||
|
@ -41,9 +41,10 @@ pub struct EventRecord {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum EventDetail {
|
pub enum EventDetail {
|
||||||
Authentication,
|
Login,
|
||||||
Logout,
|
Logout,
|
||||||
|
PersonSetSelfMail,
|
||||||
|
PersonReauth,
|
||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,12 +65,13 @@ async fn execute_inner(
|
||||||
// continue.
|
// continue.
|
||||||
}
|
}
|
||||||
_ = control_rx.recv() => {
|
_ = 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
|
// either Ok(Signal::Stop) or Err(_), both of which indicate
|
||||||
// we need to stop immediately.
|
// 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();
|
let start = Instant::now();
|
||||||
if let Err(crossbeam_err) = stat_ctrl.push(TestPhase::Start(start)) {
|
if let Err(crossbeam_err) = stat_ctrl.push(TestPhase::Start(start)) {
|
||||||
|
@ -92,10 +94,11 @@ async fn execute_inner(
|
||||||
// continue.
|
// continue.
|
||||||
}
|
}
|
||||||
_ = recv => {
|
_ = 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
|
// either Ok(Signal::Stop) or Err(_), both of which indicate
|
||||||
// we need to stop immediately.
|
// we need to stop immediately.
|
||||||
return Err(Error::Interupt);
|
debug!("Interrupt");
|
||||||
|
return Err(Error::Interrupt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -106,7 +109,7 @@ async fn execute_inner(
|
||||||
if let Err(crossbeam_err) = stat_ctrl.push(TestPhase::End(end)) {
|
if let Err(crossbeam_err) = stat_ctrl.push(TestPhase::End(end)) {
|
||||||
error!(
|
error!(
|
||||||
?crossbeam_err,
|
?crossbeam_err,
|
||||||
"Unable to signal statistics collector to start"
|
"Unable to signal statistics collector to end"
|
||||||
);
|
);
|
||||||
return Err(Error::Crossbeam);
|
return Err(Error::Crossbeam);
|
||||||
}
|
}
|
||||||
|
@ -175,14 +178,14 @@ pub async fn execute(state: State, control_rx: broadcast::Receiver<Signal>) -> R
|
||||||
}
|
}
|
||||||
|
|
||||||
let warmup = state.profile.warmup_time();
|
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.
|
// task spawn/join within our logic.
|
||||||
let c_stats_ctrl = stats_ctrl.clone();
|
let c_stats_ctrl = stats_ctrl.clone();
|
||||||
// Don't ? this, we want to stash the result so we cleanly stop all the workers
|
// Don't ? this, we want to stash the result so we cleanly stop all the workers
|
||||||
// before returning the inner test result.
|
// 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");
|
info!("stopping stats");
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::model::ActorModel;
|
use crate::model::{ActorModel, ActorRole};
|
||||||
|
use crate::models;
|
||||||
use crate::profile::Profile;
|
use crate::profile::Profile;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
/// A serialisable state representing the content of a kanidm database and potential
|
/// A serialisable state representing the content of a kanidm database and potential
|
||||||
/// test content that can be created and modified.
|
/// test content that can be created and modified.
|
||||||
///
|
///
|
||||||
|
@ -16,7 +16,7 @@ pub struct State {
|
||||||
// ----------------------------
|
// ----------------------------
|
||||||
pub preflight_flags: Vec<Flag>,
|
pub preflight_flags: Vec<Flag>,
|
||||||
pub persons: Vec<Person>,
|
pub persons: Vec<Person>,
|
||||||
// groups: Vec<Group>,
|
pub groups: Vec<Group>,
|
||||||
// oauth_clients: Vec<Oauth2Clients>,
|
// oauth_clients: Vec<Oauth2Clients>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,23 +55,37 @@ pub enum Flag {
|
||||||
DisableAllPersonsMFAPolicy,
|
DisableAllPersonsMFAPolicy,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||||
pub enum PreflightState {
|
pub enum PreflightState {
|
||||||
|
#[default]
|
||||||
Present,
|
Present,
|
||||||
Absent,
|
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 {
|
pub enum Model {
|
||||||
/// This is a "hardcoded" model that just authenticates and searches
|
/// This is a "hardcoded" model that just authenticates and searches
|
||||||
|
AuthOnly,
|
||||||
|
/// A simple linear executor that does actions in a loop.
|
||||||
|
#[default]
|
||||||
Basic,
|
Basic,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
pub fn as_dyn_object(&self) -> Box<dyn ActorModel + Send> {
|
pub fn as_dyn_object(&self) -> Result<Box<dyn ActorModel + Send>, Error> {
|
||||||
match self {
|
Ok(match self {
|
||||||
Model::Basic => Box::new(crate::model_basic::ActorBasic::new()),
|
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 preflight_state: PreflightState,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub member_of: BTreeSet<String>,
|
pub roles: BTreeSet<ActorRole>,
|
||||||
pub credential: Credential,
|
pub credential: Credential,
|
||||||
pub model: Model,
|
pub model: Model,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Group {
|
||||||
|
pub name: String,
|
||||||
|
pub preflight_state: PreflightState,
|
||||||
|
pub role: ActorRole,
|
||||||
|
pub members: BTreeSet<String>,
|
||||||
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ impl DataCollector for BasicStatistics {
|
||||||
break start;
|
break start;
|
||||||
}
|
}
|
||||||
Some(TestPhase::End(_)) => {
|
Some(TestPhase::End(_)) => {
|
||||||
|
error!("invalid state");
|
||||||
// Invalid state.
|
// Invalid state.
|
||||||
return Err(Error::InvalidState);
|
return Err(Error::InvalidState);
|
||||||
}
|
}
|
||||||
|
@ -69,6 +70,7 @@ impl DataCollector for BasicStatistics {
|
||||||
break end;
|
break end;
|
||||||
}
|
}
|
||||||
Some(TestPhase::StopNow) => {
|
Some(TestPhase::StopNow) => {
|
||||||
|
warn!("requested to stop now!");
|
||||||
// We have been told to stop immediately.
|
// We have been told to stop immediately.
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
@ -76,6 +78,8 @@ impl DataCollector for BasicStatistics {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
info!("start statistics processing ...");
|
||||||
|
|
||||||
let mut count: usize = 0;
|
let mut count: usize = 0;
|
||||||
let mut optimes = Vec::new();
|
let mut optimes = Vec::new();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue