orca/
generate.rs

1use crate::error::Error;
2use crate::kani::KanidmOrcaClient;
3use crate::model::ActorRole;
4use crate::profile::Profile;
5use crate::state::{Credential, Flag, Group, GroupName, Person, PreflightState, State};
6use hashbrown::HashMap;
7use rand::distr::{Alphanumeric, SampleString, Uniform};
8use rand::seq::{index, IndexedRandom};
9
10use rand::{Rng, SeedableRng};
11use rand_chacha::ChaCha8Rng;
12
13use std::collections::BTreeSet;
14
15const PEOPLE_PREFIX: &str = "person";
16
17// #[derive(Debug)]
18// pub struct PartialGroup {
19//     pub name: String,
20//     pub members: BTreeSet<String>,
21// }
22
23fn random_name(prefix: &str, rng: &mut ChaCha8Rng) -> String {
24    let suffix = Alphanumeric.sample_string(rng, 8).to_lowercase();
25    format!("{}_{}", prefix, suffix)
26}
27
28fn random_password(rng: &mut ChaCha8Rng) -> String {
29    Alphanumeric.sample_string(rng, 24)
30}
31
32pub async fn populate(_client: &KanidmOrcaClient, profile: Profile) -> Result<State, Error> {
33    // IMPORTANT: We have to perform these steps in order so that the RNG is deterministic between
34    // multiple invocations.
35    let mut seeded_rng = ChaCha8Rng::seed_from_u64(profile.seed());
36
37    let female_given_names = std::include_str!("../names-dataset/dataset/Female_given_names.txt");
38    let male_given_names = std::include_str!("../names-dataset/dataset/Male_given_names.txt");
39
40    let given_names = female_given_names
41        .split('\n')
42        .chain(male_given_names.split('\n'))
43        .collect::<Vec<_>>();
44
45    let surnames = std::include_str!("../names-dataset/dataset/Surnames.txt");
46
47    let surnames = surnames.split('\n').collect::<Vec<_>>();
48
49    debug!(
50        "name pool: given: {} - family: {}",
51        given_names.len(),
52        surnames.len()
53    );
54
55    let thread_count = profile.thread_count();
56
57    // PHASE 0 - For now, set require MFA off and extend the privilege expiry.
58    let preflight_flags = vec![
59        Flag::DisableAllPersonsMFAPolicy,
60        Flag::ExtendPrivilegedAuthExpiry,
61    ];
62
63    // PHASE 1 - generate a pool of persons that are not-yet created for future import.
64
65    // PHASE 2 - generate groups for integration access, assign roles to groups.
66    // These decide what each person is supposed to do with their life.
67    let mut groups = vec![
68        Group {
69            name: GroupName::RolePeopleSelfSetPassword,
70            role: ActorRole::PeopleSelfSetPassword,
71            ..Default::default()
72        },
73        Group {
74            name: GroupName::RolePeoplePiiReader,
75            role: ActorRole::PeoplePiiReader,
76            ..Default::default()
77        },
78        Group {
79            name: GroupName::RolePeopleSelfMailWrite,
80            role: ActorRole::PeopleSelfMailWrite,
81            ..Default::default()
82        },
83        Group {
84            name: GroupName::RolePeopleSelfReadProfile,
85            role: ActorRole::PeopleSelfReadProfile,
86            ..Default::default()
87        },
88        Group {
89            name: GroupName::RolePeopleSelfReadMemberOf,
90            role: ActorRole::PeopleSelfReadMemberOf,
91            ..Default::default()
92        },
93        Group {
94            name: GroupName::RolePeopleGroupAdmin,
95            role: ActorRole::PeopleGroupAdmin,
96            ..Default::default()
97        },
98    ];
99
100    // PHASE 3 - generate persons
101    //         - assign them credentials of various types.
102    let mut persons = Vec::with_capacity(profile.person_count() as usize);
103    let mut person_usernames = BTreeSet::new();
104
105    let model = *profile.model();
106
107    for _ in 0..profile.person_count() {
108        let given_name = given_names
109            .choose(&mut seeded_rng)
110            .expect("name set corrupted");
111        let surname = surnames
112            .choose(&mut seeded_rng)
113            .expect("name set corrupted");
114
115        let display_name = format!("{} {}", given_name, surname);
116
117        let username = display_name
118            .chars()
119            .filter(|c| c.is_ascii_alphanumeric())
120            .collect::<String>()
121            .to_lowercase();
122
123        let mut username = if username.is_empty() {
124            random_name(PEOPLE_PREFIX, &mut seeded_rng)
125        } else {
126            username
127        };
128
129        while person_usernames.contains(&username) {
130            username = random_name(PEOPLE_PREFIX, &mut seeded_rng);
131        }
132
133        let password = random_password(&mut seeded_rng);
134
135        let roles = BTreeSet::new();
136
137        // Data is ready, make changes to the server. These should be idempotent if possible.
138        let p = Person {
139            preflight_state: PreflightState::Present,
140            username: username.clone(),
141            display_name,
142            roles,
143            credential: Credential::Password { plain: password },
144            model,
145        };
146
147        debug!(?p);
148
149        person_usernames.insert(username.clone());
150        persons.push(p);
151    }
152
153    // Now, assign persons to roles.
154    //
155    // We do this by iterating through our roles, and then assigning
156    // them a baseline of required accounts with some variation. This
157    // way in each test it's guaranteed that *at least* one person
158    // to each role always will exist and be operational.
159    let member_count_by_group: HashMap<GroupName, u64> = profile
160        .get_properties_by_group()
161        .iter()
162        .filter_map(|(name, properties)| {
163            let group_name = GroupName::try_from(name).ok()?;
164            properties.member_count.map(|count| (group_name, count))
165        })
166        .collect();
167
168    for group in groups.iter_mut() {
169        let persons_to_choose = match member_count_by_group.get(&group.name) {
170            Some(person_count) => *person_count as usize,
171            None => {
172                let baseline = persons.len() / 3;
173                let inverse = persons.len() - baseline;
174                // Randomly add extra from the inverse
175                let extra =
176                    Uniform::new(0, inverse).map_err(|err| Error::RandomNumber(err.to_string()))?;
177                baseline + seeded_rng.sample(extra)
178            }
179        };
180
181        assert!(persons_to_choose <= persons.len());
182
183        debug!(?persons_to_choose);
184
185        let person_index = index::sample(&mut seeded_rng, persons.len(), persons_to_choose);
186
187        // Order doesn't matter, lets optimise for linear lookup.
188        let mut person_index = person_index.into_vec();
189        person_index.sort_unstable();
190
191        for p_idx in person_index {
192            let person = persons.get_mut(p_idx).unwrap();
193
194            // Add the person to the group.
195            group.members.insert(person.username.clone());
196
197            // Add the reverse links, this allows the person in the test
198            // to know their roles
199            person.roles.insert(group.role.clone());
200        }
201    }
202
203    // PHASE 4 - generate groups for user modification rights
204
205    // PHASE 5 - generate excess groups with nesting. Randomly assign persons.
206
207    // PHASE 6 - generate integrations -
208
209    // PHASE 7 - given the integrations and groupings,
210
211    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
212                                 //the group names from it
213
214    // Return the state.
215    let state = State {
216        profile,
217        // ---------------
218        groups,
219        preflight_flags,
220        persons,
221        thread_count,
222    };
223
224    Ok(state)
225}