orca/models/
basic.rs

1use crate::model::{self, ActorModel, ActorRole, Transition, TransitionAction, TransitionResult};
2
3use crate::error::Error;
4use crate::run::EventRecord;
5use crate::state::*;
6use kanidm_client::KanidmClient;
7
8use async_trait::async_trait;
9use rand::Rng;
10use rand_chacha::ChaCha8Rng;
11
12use std::collections::BTreeSet;
13use std::time::Duration;
14
15enum State {
16    Unauthenticated,
17    Authenticated,
18    AuthenticatedWithReauth,
19}
20
21pub struct ActorBasic {
22    state: State,
23    randomised_backoff_time: Duration,
24}
25
26impl ActorBasic {
27    pub fn new(mut cha_rng: ChaCha8Rng, warmup_time_ms: u64) -> Self {
28        let max_backoff_time_in_ms = 2 * warmup_time_ms / 3;
29        let randomised_backoff_time =
30            Duration::from_millis(cha_rng.random_range(0..max_backoff_time_in_ms));
31        ActorBasic {
32            state: State::Unauthenticated,
33            randomised_backoff_time,
34        }
35    }
36}
37
38#[async_trait]
39impl ActorModel for ActorBasic {
40    async fn transition(
41        &mut self,
42        client: &KanidmClient,
43        person: &Person,
44    ) -> Result<Vec<EventRecord>, Error> {
45        let transition = self.next_transition(&person.roles);
46
47        if let Some(delay) = transition.delay {
48            tokio::time::sleep(delay).await;
49        }
50
51        // Once we get to here, we want the transition to go ahead.
52        let (result, event) = match transition.action {
53            TransitionAction::Login => model::login(client, person).await,
54            TransitionAction::Logout => model::logout(client, person).await,
55            TransitionAction::PrivilegeReauth => model::privilege_reauth(client, person).await,
56            TransitionAction::WriteAttributePersonMail => {
57                let mail = format!("{}@example.com", person.username);
58                let values = &[mail.as_str()];
59                model::person_set_self_mail(client, person, values).await
60            }
61            TransitionAction::ReadSelfAccount => {
62                model::person_get_self_account(client, person).await
63            }
64            TransitionAction::ReadSelfMemberOf => {
65                model::person_get_self_memberof(client, person).await
66            }
67            TransitionAction::WriteSelfPassword => {
68                // I know it's dumb but here we just re-set the same password because it's the simplest thing to do
69                let Credential::Password { plain } = &person.credential;
70                model::person_set_self_password(client, person, plain).await
71            }
72        }?;
73
74        self.next_state(transition.action, result);
75
76        Ok(event)
77    }
78}
79
80impl ActorBasic {
81    fn next_transition(&mut self, roles: &BTreeSet<ActorRole>) -> Transition {
82        let logout_transition = Transition {
83            delay: Some(Duration::from_secs(5)),
84            action: TransitionAction::Logout,
85        };
86        match self.state {
87            State::Unauthenticated => Transition {
88                delay: Some(self.randomised_backoff_time),
89                action: TransitionAction::Login,
90            },
91            State::Authenticated => Transition {
92                delay: Some(Duration::from_secs(2)),
93                action: TransitionAction::PrivilegeReauth,
94            },
95            // Since this is the basic model we don't want to get too fancy and do too many things, but since the struct Person
96            // already comes with a BTreeSet of roles we don't want to change that, so we arbitrarily choose to use just the first role
97            // (which is always deterministic thanks to the rng seed used to choose the roles)
98            State::AuthenticatedWithReauth => match roles.first() {
99                Some(role) => match role {
100                    ActorRole::PeopleSelfMailWrite => Transition {
101                        delay: Some(Duration::from_secs(5)),
102                        action: TransitionAction::WriteAttributePersonMail,
103                    },
104                    ActorRole::PeopleSelfReadProfile => Transition {
105                        delay: Some(Duration::from_secs(2)),
106                        action: TransitionAction::ReadSelfAccount,
107                    },
108                    ActorRole::PeopleSelfReadMemberOf => Transition {
109                        delay: Some(Duration::from_secs(1)),
110                        action: TransitionAction::ReadSelfMemberOf,
111                    },
112                    ActorRole::PeopleSelfSetPassword => Transition {
113                        delay: Some(Duration::from_secs(3)),
114                        action: TransitionAction::WriteSelfPassword,
115                    },
116                    ActorRole::PeoplePiiReader | ActorRole::PeopleGroupAdmin | ActorRole::None => {
117                        logout_transition
118                    }
119                },
120                None => logout_transition,
121            },
122        }
123    }
124
125    fn next_state(&mut self, action: TransitionAction, result: TransitionResult) {
126        // Is this a design flaw? We probably need to know what the state was that we
127        // requested to move to?
128        match (&self.state, action, result) {
129            (State::Unauthenticated, TransitionAction::Login, TransitionResult::Ok) => {
130                self.state = State::Authenticated;
131            }
132            (State::Authenticated, TransitionAction::PrivilegeReauth, TransitionResult::Ok) => {
133                self.state = State::AuthenticatedWithReauth;
134            }
135            (
136                State::AuthenticatedWithReauth,
137                TransitionAction::WriteAttributePersonMail
138                | TransitionAction::ReadSelfAccount
139                | TransitionAction::ReadSelfMemberOf
140                | TransitionAction::WriteSelfPassword,
141                TransitionResult::Ok,
142            ) => {
143                self.state = State::AuthenticatedWithReauth;
144            }
145            (_, TransitionAction::Logout, TransitionResult::Ok) => {
146                self.state = State::Unauthenticated;
147            }
148            #[allow(clippy::unreachable)]
149            (_, _, TransitionResult::Ok) => {
150                unreachable!();
151            }
152            (_, _, TransitionResult::Error) => {
153                self.state = State::Unauthenticated;
154            }
155        }
156    }
157}