mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-24 13:07:00 +01:00
224 lines
7.2 KiB
Rust
224 lines
7.2 KiB
Rust
use crate::error::Error;
|
|
use crate::kani::KanidmOrcaClient;
|
|
use crate::model::ActorRole;
|
|
use crate::profile::Profile;
|
|
use crate::state::{Credential, Flag, Group, GroupName, Person, PreflightState, State};
|
|
use hashbrown::HashMap;
|
|
use rand::distributions::{Alphanumeric, DistString, Uniform};
|
|
use rand::seq::{index, SliceRandom};
|
|
use rand::{Rng, SeedableRng};
|
|
use rand_chacha::ChaCha8Rng;
|
|
|
|
use std::collections::BTreeSet;
|
|
|
|
const PEOPLE_PREFIX: &str = "person";
|
|
|
|
// #[derive(Debug)]
|
|
// pub struct PartialGroup {
|
|
// pub name: String,
|
|
// pub members: BTreeSet<String>,
|
|
// }
|
|
|
|
fn random_name(prefix: &str, rng: &mut ChaCha8Rng) -> String {
|
|
let suffix = Alphanumeric.sample_string(rng, 8).to_lowercase();
|
|
format!("{}_{}", prefix, suffix)
|
|
}
|
|
|
|
fn random_password(rng: &mut ChaCha8Rng) -> String {
|
|
Alphanumeric.sample_string(rng, 24)
|
|
}
|
|
|
|
pub async fn populate(_client: &KanidmOrcaClient, profile: Profile) -> Result<State, Error> {
|
|
// IMPORTANT: We have to perform these steps in order so that the RNG is deterministic between
|
|
// multiple invocations.
|
|
let mut seeded_rng = ChaCha8Rng::seed_from_u64(profile.seed());
|
|
|
|
let female_given_names = std::include_str!("../names-dataset/dataset/Female_given_names.txt");
|
|
let male_given_names = std::include_str!("../names-dataset/dataset/Male_given_names.txt");
|
|
|
|
let given_names = female_given_names
|
|
.split('\n')
|
|
.chain(male_given_names.split('\n'))
|
|
.collect::<Vec<_>>();
|
|
|
|
let surnames = std::include_str!("../names-dataset/dataset/Surnames.txt");
|
|
|
|
let surnames = surnames.split('\n').collect::<Vec<_>>();
|
|
|
|
debug!(
|
|
"name pool: given: {} - family: {}",
|
|
given_names.len(),
|
|
surnames.len()
|
|
);
|
|
|
|
let thread_count = profile.thread_count();
|
|
|
|
// PHASE 0 - For now, set require MFA off and extend the privilege expiry.
|
|
let preflight_flags = vec![
|
|
Flag::DisableAllPersonsMFAPolicy,
|
|
Flag::ExtendPrivilegedAuthExpiry,
|
|
];
|
|
|
|
// PHASE 1 - generate a pool of persons that are not-yet created for future import.
|
|
|
|
// 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: GroupName::RolePeopleSelfSetPassword,
|
|
role: ActorRole::PeopleSelfSetPassword,
|
|
..Default::default()
|
|
},
|
|
Group {
|
|
name: GroupName::RolePeoplePiiReader,
|
|
role: ActorRole::PeoplePiiReader,
|
|
..Default::default()
|
|
},
|
|
Group {
|
|
name: GroupName::RolePeopleSelfMailWrite,
|
|
role: ActorRole::PeopleSelfMailWrite,
|
|
..Default::default()
|
|
},
|
|
Group {
|
|
name: GroupName::RolePeopleSelfReadProfile,
|
|
role: ActorRole::PeopleSelfReadProfile,
|
|
..Default::default()
|
|
},
|
|
Group {
|
|
name: GroupName::RolePeopleSelfReadMemberOf,
|
|
role: ActorRole::PeopleSelfReadMemberOf,
|
|
..Default::default()
|
|
},
|
|
Group {
|
|
name: GroupName::RolePeopleGroupAdmin,
|
|
role: ActorRole::PeopleGroupAdmin,
|
|
..Default::default()
|
|
},
|
|
];
|
|
|
|
// PHASE 3 - generate persons
|
|
// - assign them credentials of various types.
|
|
let mut persons = Vec::with_capacity(profile.person_count() as usize);
|
|
let mut person_usernames = BTreeSet::new();
|
|
|
|
let model = *profile.model();
|
|
|
|
for _ in 0..profile.person_count() {
|
|
let given_name = given_names
|
|
.choose(&mut seeded_rng)
|
|
.expect("name set corrupted");
|
|
let surname = surnames
|
|
.choose(&mut seeded_rng)
|
|
.expect("name set corrupted");
|
|
|
|
let display_name = format!("{} {}", given_name, surname);
|
|
|
|
let username = display_name
|
|
.chars()
|
|
.filter(|c| c.is_ascii_alphanumeric())
|
|
.collect::<String>()
|
|
.to_lowercase();
|
|
|
|
let mut username = if username.is_empty() {
|
|
random_name(PEOPLE_PREFIX, &mut seeded_rng)
|
|
} else {
|
|
username
|
|
};
|
|
|
|
while person_usernames.contains(&username) {
|
|
username = random_name(PEOPLE_PREFIX, &mut seeded_rng);
|
|
}
|
|
|
|
let password = random_password(&mut seeded_rng);
|
|
|
|
let roles = BTreeSet::new();
|
|
|
|
// Data is ready, make changes to the server. These should be idempotent if possible.
|
|
let p = Person {
|
|
preflight_state: PreflightState::Present,
|
|
username: username.clone(),
|
|
display_name,
|
|
roles,
|
|
credential: Credential::Password { plain: password },
|
|
model,
|
|
};
|
|
|
|
debug!(?p);
|
|
|
|
person_usernames.insert(username.clone());
|
|
persons.push(p);
|
|
}
|
|
|
|
// 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.
|
|
let member_count_by_group: HashMap<GroupName, u64> = profile
|
|
.get_properties_by_group()
|
|
.iter()
|
|
.filter_map(|(name, properties)| {
|
|
let group_name = GroupName::try_from(name).ok()?;
|
|
properties.member_count.map(|count| (group_name, count))
|
|
})
|
|
.collect();
|
|
|
|
for group in groups.iter_mut() {
|
|
let persons_to_choose = match member_count_by_group.get(&group.name) {
|
|
Some(person_count) => *person_count as usize,
|
|
None => {
|
|
let baseline = persons.len() / 3;
|
|
let inverse = persons.len() - baseline;
|
|
// Randomly add extra from the inverse
|
|
let extra = Uniform::new(0, inverse);
|
|
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 5 - generate excess groups with nesting. Randomly assign persons.
|
|
|
|
// PHASE 6 - generate integrations -
|
|
|
|
// PHASE 7 - given the integrations and groupings,
|
|
|
|
drop(member_count_by_group); // it looks ugly but we have to do this to reassure the borrow checker we can return profile, as we were borrowing
|
|
//the group names from it
|
|
|
|
// Return the state.
|
|
let state = State {
|
|
profile,
|
|
// ---------------
|
|
groups,
|
|
preflight_flags,
|
|
persons,
|
|
thread_count,
|
|
};
|
|
|
|
Ok(state)
|
|
}
|