fxa_client/state_machine/internal_machines/
mod.rs
1mod 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 fn initial_state(&self, event: FxaEvent) -> Result<State>;
27
28 fn next_state(&self, state: State, event: Event) -> Result<State>;
30}
31
32#[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(FxaState),
61 Cancel,
63}
64
65#[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 EnsureCapabilitiesAuthError,
92}
93
94impl State {
95 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
174const 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 matches!(e, Error::StateMachineLogicError(_)) {
195 return CallResult::InternalError(e);
196 }
197 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 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
241enum CallResult {
243 Finished(Event),
246 Retry,
248 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 fn next_state(&mut self, event: Event) {
277 self.state = self.peek_next_state(event);
278 }
279
280 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}