fix(sync): bind browser data to one account owner

This commit is contained in:
2026-07-10 10:46:52 -04:00
parent 46eac43326
commit 3be13ca295
16 changed files with 678 additions and 24 deletions
+36 -9
View File
@@ -11,8 +11,8 @@ use std::path::Path;
use ely_domain::ProfileId;
use ely_sync_client::{
ApiClientConfig, BearerToken, BearerTokenStore, SyncApiClient, SyncClientError, send_email_otp,
verify_email_otp,
ApiClientConfig, BearerToken, BearerTokenStore, SyncApiClient, SyncClientError, SyncOwnerStore,
send_email_otp, verify_email_otp,
};
use gpui::Context;
@@ -386,7 +386,21 @@ fn spawn_verify_otp(profile_id: ProfileId, email: String, otp: String, tx: SyncS
return;
}
};
let update = SyncStateUpdate::AuthVerified { profile_id, email, token };
let user_id = match SyncApiClient::new(config, token.clone())
.and_then(|client| client.authenticated_user_id())
{
Ok(user_id) => user_id,
Err(error) => {
retire_bearer(token);
let _ = tx.send(SyncStateUpdate::AuthError {
profile_id,
email,
message: error.to_string(),
});
return;
}
};
let update = SyncStateUpdate::AuthVerified { profile_id, email, user_id, token };
if let Err(error) = tx.send(update) {
retire_stale_auth_update(error.0.update);
}
@@ -407,18 +421,20 @@ pub(super) fn retire_stale_auth_update(update: SyncStateUpdate) {
};
std::thread::Builder::new()
.name("ely-sync-auth-retire".to_string())
.spawn(move || {
let client = SyncApiClient::new(ApiClientConfig::production(), token);
if let Ok(client) = client {
let _ = client.sign_out();
}
})
.spawn(move || retire_bearer(token))
.map(|_| ())
.unwrap_or_else(|error| {
tracing::warn!(target: "ely::sync", error = %error, "spawn auth retire failed");
});
}
fn retire_bearer(token: BearerToken) {
let client = SyncApiClient::new(ApiClientConfig::production(), token);
if let Ok(client) = client {
let _ = client.sign_out();
}
}
fn verified_bearer_from(update: SyncStateUpdate) -> Option<BearerToken> {
match update {
SyncStateUpdate::AuthVerified { token, .. } => Some(token),
@@ -428,17 +444,28 @@ fn verified_bearer_from(update: SyncStateUpdate) -> Option<BearerToken> {
pub(super) fn save_verified_bearer(
profile_id: &ProfileId,
user_id: &str,
token: &BearerToken,
default_profile_id: Option<&ProfileId>,
) -> Result<(), SyncClientError> {
let profile_root = default_profile_data_root().ok_or_else(|| {
SyncClientError::BearerCredentialStorage("profile data root is unavailable".to_string())
})?;
SyncOwnerStore::new(&profile_root).claim(user_id)?;
let profile_dir = sync_profile_data_dir(&profile_root, profile_id);
bearer_store_for_profile(profile_id, &profile_dir, &profile_root, default_profile_id)
.save(token)
}
pub(super) fn verified_session_persistence_message(error: &SyncClientError) -> String {
match error {
SyncClientError::SyncOwnerMismatch
| SyncClientError::SyncOwnerUnclaimed
| SyncClientError::SyncOwnerStorage(_) => error.to_string(),
_ => "System credential access failed.".to_string(),
}
}
#[cfg(test)]
#[path = "auth_tests.rs"]
mod tests;
+10 -1
View File
@@ -4,7 +4,7 @@ use ely_domain::ProfileId;
use super::super::{ShellState, sync_state::SyncStateUpdate};
use super::{
AuthFlowPhase, active_profile_sync_context_for, bearer_store_for_profile, normalize_email,
verified_bearer_from,
verified_bearer_from, verified_session_persistence_message,
};
#[test]
@@ -47,6 +47,7 @@ fn stale_verified_updates_retain_the_token_for_server_retirement()
let update = SyncStateUpdate::AuthVerified {
profile_id,
email: "user@example.com".to_string(),
user_id: "user-01".to_string(),
token: token.clone(),
};
@@ -54,6 +55,14 @@ fn stale_verified_updates_retain_the_token_for_server_retirement()
Ok(())
}
#[test]
fn owner_mismatch_is_actionable_in_the_account_form() {
assert_eq!(
verified_session_persistence_message(&ely_sync_client::SyncClientError::SyncOwnerMismatch),
"This browser data belongs to a different Ely account"
);
}
#[test]
fn private_profile_has_no_sync_auth_context() -> Result<(), Box<dyn std::error::Error>> {
let state =
+6 -3
View File
@@ -45,7 +45,7 @@ pub(crate) enum SyncStateUpdate {
SignOutSucceeded { profile_id: ProfileId },
SignOutFailed { profile_id: ProfileId, message: String },
AuthOtpSent { profile_id: ProfileId, email: String },
AuthVerified { profile_id: ProfileId, email: String, token: BearerToken },
AuthVerified { profile_id: ProfileId, email: String, user_id: String, token: BearerToken },
AuthError { profile_id: ProfileId, email: String, message: String },
}
@@ -321,7 +321,7 @@ impl ElyShell {
self.release_auth_flow_barrier();
}
}
SyncStateUpdate::AuthVerified { profile_id, email, token } => {
SyncStateUpdate::AuthVerified { profile_id, email, user_id, token } => {
let attempt_matches = active_profile_id(&self.state).as_ref()
== Some(&profile_id)
&& matches!(
@@ -336,12 +336,14 @@ impl ElyShell {
auth::retire_stale_auth_update(SyncStateUpdate::AuthVerified {
profile_id,
email,
user_id,
token,
});
continue;
}
match auth::save_verified_bearer(
&profile_id,
&user_id,
&token,
self.default_profile_id.as_ref(),
) {
@@ -357,9 +359,10 @@ impl ElyShell {
auth::retire_stale_auth_update(SyncStateUpdate::AuthVerified {
profile_id: profile_id.clone(),
email: email.clone(),
user_id,
token,
});
let message = "System credential access failed.".to_string();
let message = auth::verified_session_persistence_message(&error);
self.auth_flow_phase = auth::AuthFlowPhase::Error {
profile_id,
email,