fxa_client/state_machine/internal_machines/
mod.rs1mod 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 account.cancel_existing_oauth_flows();
129 let scopes: Vec<&str> = scopes.iter().map(String::as_str).collect();
130 let oauth_url = account.begin_oauth_flow(&scopes, entrypoint)?;
131 Event::BeginOAuthFlowSuccess { oauth_url }
132 }
133 State::BeginPairingFlow {
134 pairing_url,
135 scopes,
136 entrypoint,
137 } => {
138 account.cancel_existing_oauth_flows();
139 let scopes: Vec<&str> = scopes.iter().map(String::as_str).collect();
140 let oauth_url = account.begin_pairing_flow(pairing_url, &scopes, entrypoint)?;
141 Event::BeginPairingFlowSuccess { oauth_url }
142 }
143 State::CompleteOAuthFlow { code, state } => {
144 account.complete_oauth_flow(code, state)?;
145 Event::CompleteOAuthFlowSuccess
146 }
147 State::InitializeDevice => {
148 account.initialize_device(
149 &device_config.name,
150 device_config.device_type,
151 &device_config.capabilities,
152 )?;
153 Event::InitializeDeviceSuccess
154 }
155 State::CheckAuthorizationStatus => {
156 let active = account.check_authorization_status()?.active;
157 Event::CheckAuthorizationStatusSuccess { active }
158 }
159 State::Disconnect => {
160 account.disconnect();
161 Event::DisconnectSuccess
162 }
163 State::GetProfile => {
164 account.get_profile(true)?;
165 Event::GetProfileSuccess
166 }
167 state => {
168 return Err(Error::StateMachineLogicError(format!(
169 "process_call: Don't know how to handle {state}"
170 )))
171 }
172 })
173 }
174}
175
176const NETWORK_RETRY_LIMIT: usize = 3;
178
179struct CallErrorHandler<'a> {
180 network_retries: usize,
181 auth_retries: usize,
182 state: &'a State,
183}
184
185impl<'a> CallErrorHandler<'a> {
186 fn new(state: &'a State) -> Self {
187 Self {
188 network_retries: 0,
189 auth_retries: 0,
190 state,
191 }
192 }
193
194 fn handle_error(&mut self, e: Error, account: &mut FirefoxAccount) -> CallResult {
195 if matches!(e, Error::StateMachineLogicError(_)) {
197 return CallResult::InternalError(e);
198 }
199 crate::warn!("handling error: {e}");
202 match convert_log_report_error(e) {
203 FxaError::Network => {
204 if self.network_retries < NETWORK_RETRY_LIMIT {
205 self.network_retries += 1;
206 CallResult::Retry
207 } else {
208 CallResult::Finished(Event::CallError)
209 }
210 }
211 FxaError::Authentication => {
212 if self.auth_retries < 1 && !matches!(self.state, State::CheckAuthorizationStatus) {
213 account.clear_access_token_cache();
219 match account.check_authorization_status() {
220 Ok(status) if status.active => {
221 self.auth_retries += 1;
222 CallResult::Retry
223 }
224 _ => CallResult::Finished(self.event_for_auth_error()),
225 }
226 } else {
227 CallResult::Finished(self.event_for_auth_error())
228 }
229 }
230 _ => CallResult::Finished(Event::CallError),
231 }
232 }
233
234 fn event_for_auth_error(&self) -> Event {
235 if matches!(self.state, State::EnsureDeviceCapabilities) {
236 Event::EnsureCapabilitiesAuthError
237 } else {
238 Event::CallError
239 }
240 }
241}
242
243enum CallResult {
245 Finished(Event),
248 Retry,
250 InternalError(Error),
253}
254
255fn invalid_transition(state: State, event: Event) -> Result<State> {
256 Err(Error::InvalidStateTransition(format!("{state} -> {event}")))
257}
258
259#[cfg(test)]
260struct StateMachineTester<T> {
261 state_machine: T,
262 state: State,
263}
264
265#[cfg(test)]
266impl<T: InternalStateMachine> StateMachineTester<T> {
267 fn new(state_machine: T, event: FxaEvent) -> Self {
268 let initial_state = state_machine
269 .initial_state(event)
270 .expect("Error getting initial state");
271 Self {
272 state_machine,
273 state: initial_state,
274 }
275 }
276
277 fn next_state(&mut self, event: Event) {
279 self.state = self.peek_next_state(event);
280 }
281
282 fn peek_next_state(&self, event: Event) -> State {
284 self.state_machine
285 .next_state(self.state.clone(), event.clone())
286 .unwrap_or_else(|e| {
287 panic!(
288 "Error getting next state: {e} state: {:?} event: {event:?}",
289 self.state
290 )
291 })
292 }
293}