fxa_client/state_machine/internal_machines/
mod.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5//! Internal state machine code
6
7mod auth_issues;
8mod authenticating;
9mod connected;
10mod disconnected;
11mod uninitialized;
12
13use crate::{
14    internal::FirefoxAccount, DeviceConfig, Error, FxaError, FxaEvent, FxaRustAuthState, FxaState,
15    Result,
16};
17pub use auth_issues::AuthIssuesStateMachine;
18pub use authenticating::AuthenticatingStateMachine;
19pub use connected::ConnectedStateMachine;
20pub use disconnected::DisconnectedStateMachine;
21use error_support::convert_log_report_error;
22pub use uninitialized::UninitializedStateMachine;
23
24pub trait InternalStateMachine {
25    /// Initial state to start handling an public event
26    fn initial_state(&self, event: FxaEvent) -> Result<State>;
27
28    /// State transition from an internal event
29    fn next_state(&self, state: State, event: Event) -> Result<State>;
30}
31
32/// Internal state machine states
33///
34/// Most variants either represent a [FirefoxAccount] method call.
35/// `Complete` and `Cancel` are a terminal states which indicate the public state transition is complete.
36/// Each internal state machine uses the same `State` enum, but they only actually transition to a subset of the variants.
37#[derive(Clone, Debug, PartialEq, Eq)]
38#[allow(clippy::enum_variant_names)]
39pub enum State {
40    GetAuthState,
41    BeginOAuthFlow {
42        scopes: Vec<String>,
43        entrypoint: String,
44    },
45    BeginPairingFlow {
46        pairing_url: String,
47        scopes: Vec<String>,
48        entrypoint: String,
49    },
50    CompleteOAuthFlow {
51        code: String,
52        state: String,
53    },
54    InitializeDevice,
55    EnsureDeviceCapabilities,
56    CheckAuthorizationStatus,
57    Disconnect,
58    GetProfile,
59    /// Complete the current [FxaState] transition by transitioning to a new state
60    Complete(FxaState),
61    /// Complete the current [FxaState] transition by remaining at the current state
62    Cancel,
63}
64
65/// Internal state machine events
66///
67/// These represent the results of the method calls for each internal state.
68/// Each internal state machine uses the same `Event` enum, but they only actually respond to a subset of the variants.
69#[derive(Clone, Debug)]
70pub enum Event {
71    GetAuthStateSuccess {
72        auth_state: FxaRustAuthState,
73    },
74    BeginOAuthFlowSuccess {
75        oauth_url: String,
76    },
77    BeginPairingFlowSuccess {
78        oauth_url: String,
79    },
80    CompleteOAuthFlowSuccess,
81    InitializeDeviceSuccess,
82    EnsureDeviceCapabilitiesSuccess,
83    CheckAuthorizationStatusSuccess {
84        active: bool,
85    },
86    DisconnectSuccess,
87    GetProfileSuccess,
88    CallError,
89    /// Auth error for the `ensure_capabilities` call that we do on startup.
90    /// This should likely go away when we do https://bugzilla.mozilla.org/show_bug.cgi?id=1868418
91    EnsureCapabilitiesAuthError,
92}
93
94impl State {
95    /// Perform the [FirefoxAccount] method call that corresponds to this state
96    pub fn make_call(
97        &self,
98        account: &mut FirefoxAccount,
99        device_config: &DeviceConfig,
100    ) -> Result<Event> {
101        let mut error_handling = CallErrorHandler::new(self);
102        loop {
103            return match self.make_call_inner(account, device_config) {
104                Ok(event) => Ok(event),
105                Err(e) => match error_handling.handle_error(e, account) {
106                    CallResult::Retry => continue,
107                    CallResult::Finished(event) => Ok(event),
108                    CallResult::InternalError(err) => Err(err),
109                },
110            };
111        }
112    }
113
114    fn make_call_inner(
115        &self,
116        account: &mut FirefoxAccount,
117        device_config: &DeviceConfig,
118    ) -> Result<Event> {
119        Ok(match self {
120            State::GetAuthState => Event::GetAuthStateSuccess {
121                auth_state: account.get_auth_state(),
122            },
123            State::EnsureDeviceCapabilities => {
124                account.ensure_capabilities(&device_config.capabilities)?;
125                Event::EnsureDeviceCapabilitiesSuccess
126            }
127            State::BeginOAuthFlow { scopes, entrypoint } => {
128                let scopes: Vec<&str> = scopes.iter().map(String::as_str).collect();
129                let oauth_url = account.begin_oauth_flow(&scopes, entrypoint)?;
130                Event::BeginOAuthFlowSuccess { oauth_url }
131            }
132            State::BeginPairingFlow {
133                pairing_url,
134                scopes,
135                entrypoint,
136            } => {
137                let scopes: Vec<&str> = scopes.iter().map(String::as_str).collect();
138                let oauth_url = account.begin_pairing_flow(pairing_url, &scopes, entrypoint)?;
139                Event::BeginPairingFlowSuccess { oauth_url }
140            }
141            State::CompleteOAuthFlow { code, state } => {
142                account.complete_oauth_flow(code, state)?;
143                Event::CompleteOAuthFlowSuccess
144            }
145            State::InitializeDevice => {
146                account.initialize_device(
147                    &device_config.name,
148                    device_config.device_type,
149                    &device_config.capabilities,
150                )?;
151                Event::InitializeDeviceSuccess
152            }
153            State::CheckAuthorizationStatus => {
154                let active = account.check_authorization_status()?.active;
155                Event::CheckAuthorizationStatusSuccess { active }
156            }
157            State::Disconnect => {
158                account.disconnect();
159                Event::DisconnectSuccess
160            }
161            State::GetProfile => {
162                account.get_profile(true)?;
163                Event::GetProfileSuccess
164            }
165            state => {
166                return Err(Error::StateMachineLogicError(format!(
167                    "process_call: Don't know how to handle {state}"
168                )))
169            }
170        })
171    }
172}
173
174/// Number of times to retry fxa calls in the face of network errors
175const NETWORK_RETRY_LIMIT: usize = 3;
176
177struct CallErrorHandler<'a> {
178    network_retries: usize,
179    auth_retries: usize,
180    state: &'a State,
181}
182
183impl<'a> CallErrorHandler<'a> {
184    fn new(state: &'a State) -> Self {
185        Self {
186            network_retries: 0,
187            auth_retries: 0,
188            state,
189        }
190    }
191
192    fn handle_error(&mut self, e: Error, account: &mut FirefoxAccount) -> CallResult {
193        // If we see a StateMachineLogicError, return it immediately
194        if matches!(e, Error::StateMachineLogicError(_)) {
195            return CallResult::InternalError(e);
196        }
197        // Report the error and convert it to `FxaError` which makes it easier to handle.
198        // For example, multiple `Error` variants map to `FxaError::Authentication`.
199        crate::warn!("handling error: {e}");
200        match convert_log_report_error(e) {
201            FxaError::Network => {
202                if self.network_retries < NETWORK_RETRY_LIMIT {
203                    self.network_retries += 1;
204                    CallResult::Retry
205                } else {
206                    CallResult::Finished(Event::CallError)
207                }
208            }
209            FxaError::Authentication => {
210                if self.auth_retries < 1 && !matches!(self.state, State::CheckAuthorizationStatus) {
211                    // Operations can fail with authentication errors when we have stale access
212                    // token in our cache.  To try to recover from this we should:
213                    //
214                    //   - Clear the access token
215                    //   - Call `check_authorization_status`.  If successful we can retry the operation.
216                    account.clear_access_token_cache();
217                    match account.check_authorization_status() {
218                        Ok(status) if status.active => {
219                            self.auth_retries += 1;
220                            CallResult::Retry
221                        }
222                        _ => CallResult::Finished(self.event_for_auth_error()),
223                    }
224                } else {
225                    CallResult::Finished(self.event_for_auth_error())
226                }
227            }
228            _ => CallResult::Finished(Event::CallError),
229        }
230    }
231
232    fn event_for_auth_error(&self) -> Event {
233        if matches!(self.state, State::EnsureDeviceCapabilities) {
234            Event::EnsureCapabilitiesAuthError
235        } else {
236            Event::CallError
237        }
238    }
239}
240
241/// The result of a single call to the FxA client
242enum CallResult {
243    /// The call finished, either successfully or unsuccessfully, and we have a new [Event] to
244    /// process.
245    Finished(Event),
246    /// We should to retry the call after an auth/network error.
247    Retry,
248    /// There was an internal error when trying to make the call and we should bail on the internal
249    /// state transition.
250    InternalError(Error),
251}
252
253fn invalid_transition(state: State, event: Event) -> Result<State> {
254    Err(Error::InvalidStateTransition(format!("{state} -> {event}")))
255}
256
257#[cfg(test)]
258struct StateMachineTester<T> {
259    state_machine: T,
260    state: State,
261}
262
263#[cfg(test)]
264impl<T: InternalStateMachine> StateMachineTester<T> {
265    fn new(state_machine: T, event: FxaEvent) -> Self {
266        let initial_state = state_machine
267            .initial_state(event)
268            .expect("Error getting initial state");
269        Self {
270            state_machine,
271            state: initial_state,
272        }
273    }
274
275    /// Transition to a new state based on an event
276    fn next_state(&mut self, event: Event) {
277        self.state = self.peek_next_state(event);
278    }
279
280    /// peek_next_state what the next state would be without transitioning to it
281    fn peek_next_state(&self, event: Event) -> State {
282        self.state_machine
283            .next_state(self.state.clone(), event.clone())
284            .unwrap_or_else(|e| {
285                panic!(
286                    "Error getting next state: {e} state: {:?} event: {event:?}",
287                    self.state
288                )
289            })
290    }
291}