first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
use galaxy_graphql::scalars::time::ServerTimestamp;
|
||||
use galaxyui::AppContext;
|
||||
use galaxyui_extras::secure_storage;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use galaxyui_core::AppContext;
|
||||
use galaxyui_extras::secure_storage::{self, AppContextExt};
|
||||
|
||||
use super::{AnonymousUserType, FirebaseAuthTokens, PersonalObjectLimits, UserMetadata};
|
||||
use crate::UserUid;
|
||||
|
||||
const USER_STORAGE_KEY: &str = "User";
|
||||
|
||||
/// Helper function to set `true` as the default for a serde field on PersistedUser.
|
||||
fn default_as_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// The persisted representation of a user, serialized to/from the user's keychain.
|
||||
/// This struct must remain backwards compatible with the existing keychain JSON format.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PersistedUser {
|
||||
/// Information about the user's authentication through Firebase.
|
||||
#[serde(rename = "id_token")]
|
||||
pub auth_tokens: FirebaseAuthTokens,
|
||||
/// DO NOT USE! This used to be one of the two places we stored the user's Firebase refresh
|
||||
/// token. Now, all callers should go through `auth_tokens` to access it.
|
||||
#[serde(default)]
|
||||
#[deprecated = "use auth_tokens.refresh_token instead"]
|
||||
pub refresh_token: String,
|
||||
/// The Firebase UID of this user.
|
||||
pub local_id: UserUid,
|
||||
/// Metadata about the user.
|
||||
#[serde(flatten)]
|
||||
pub metadata: UserMetadata,
|
||||
/// Whether or not the user is onboarded.
|
||||
#[serde(default = "default_as_true")]
|
||||
pub is_onboarded: bool,
|
||||
/// Whether or not the user needs to link their account via SSO due to an organization setting.
|
||||
#[serde(default)]
|
||||
pub needs_sso_link: bool,
|
||||
/// What type of anonymous user this user is. May be `None` if they are not anonymous.
|
||||
#[serde(default)]
|
||||
pub anonymous_user_type: Option<AnonymousUserType>,
|
||||
#[serde(default)]
|
||||
pub linked_at: Option<ServerTimestamp>,
|
||||
#[serde(default)]
|
||||
pub personal_object_limits: Option<PersonalObjectLimits>,
|
||||
/// Whether or not this user is on what we consider a "work" domain.
|
||||
#[serde(default)]
|
||||
pub is_on_work_domain: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum UserPersistenceError {
|
||||
/// The persisted user was not successfully read from or written to disk.
|
||||
#[error("secure storage error")]
|
||||
SecureStorageError(#[from] secure_storage::Error),
|
||||
|
||||
/// The persisted user on disk could not be decoded into a valid struct.
|
||||
#[error("failed to serialize or deserialize a PersistedUser struct")]
|
||||
SerializationError(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
impl PersistedUser {
|
||||
pub fn from_secure_storage(_ctx: &AppContext) -> Result<PersistedUser, UserPersistenceError> {
|
||||
Err(UserPersistenceError::SecureStorageError(
|
||||
secure_storage::Error::NotFound,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn write_to_secure_storage(&self, _ctx: &AppContext) -> Result<(), UserPersistenceError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_from_secure_storage(_ctx: &AppContext) -> Result<(), UserPersistenceError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "persistence_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,156 @@
|
||||
use chrono::DateTime;
|
||||
|
||||
use super::PersistedUser;
|
||||
use crate::UserUid;
|
||||
use crate::user::{FirebaseAuthTokens, PersonalObjectLimits, UserMetadata};
|
||||
|
||||
/// Verifies that the JSON blob format as of March 6, 2026 can be deserialized correctly.
|
||||
///
|
||||
/// We must ALWAYS be backwards-compatible with the format here. The inlined JSON string can never change - it represents
|
||||
/// data serialized on user devices.
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn test_deserialize_2026_03_06_persisted_user() {
|
||||
const BLOB: &str = r#"{"id_token":{"id_token":"test-id-token","refresh_token":"test-refresh-token","expiration_time":"2099-01-01T00:00:00Z"},"refresh_token":"","local_id":"test-uid","email":"test@example.com","display_name":"Test User","photo_url":"https://example.com/photo.jpg","is_onboarded":true,"needs_sso_link":false,"anonymous_user_type":null,"linked_at":null,"personal_object_limits":null,"is_on_work_domain":false}"#;
|
||||
|
||||
let user: PersistedUser =
|
||||
serde_json::from_str(BLOB).expect("2026-03-06 JSON should deserialize");
|
||||
|
||||
assert_eq!(user.auth_tokens.id_token, "test-id-token");
|
||||
assert_eq!(user.auth_tokens.refresh_token, "test-refresh-token");
|
||||
assert_eq!(user.refresh_token, "");
|
||||
assert_eq!(user.local_id.as_str(), "test-uid");
|
||||
assert_eq!(user.metadata.email, "test@example.com");
|
||||
assert_eq!(user.metadata.display_name.as_deref(), Some("Test User"));
|
||||
assert_eq!(
|
||||
user.metadata.photo_url.as_deref(),
|
||||
Some("https://example.com/photo.jpg")
|
||||
);
|
||||
assert!(user.is_onboarded);
|
||||
assert!(!user.needs_sso_link);
|
||||
assert_eq!(user.anonymous_user_type, None);
|
||||
assert_eq!(user.linked_at, None);
|
||||
assert!(user.personal_object_limits.is_none());
|
||||
assert!(!user.is_on_work_domain);
|
||||
}
|
||||
|
||||
/// Verifies that serializing a PersistedUser produces the expected JSON string.
|
||||
///
|
||||
/// If this test fails, it means the serialization format has changed.
|
||||
/// You should:
|
||||
/// 1. Add a new dated deserialization test (see [`test_deserialize_2026_03_06_persisted_user`])
|
||||
/// 2. Update the serialization test to match the new format
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn test_serialize_persisted_user() {
|
||||
const EXPECTED_BLOB: &str = r#"{"id_token":{"id_token":"test-id-token","refresh_token":"test-refresh-token","expiration_time":"2099-01-01T00:00:00Z"},"refresh_token":"","local_id":"test-uid","email":"test@example.com","display_name":"Test User","photo_url":"https://example.com/photo.jpg","is_onboarded":true,"needs_sso_link":false,"anonymous_user_type":null,"linked_at":null,"personal_object_limits":{"env_var_limit":10,"notebook_limit":20,"workflow_limit":30},"is_on_work_domain":false}"#;
|
||||
|
||||
let expiration_time = DateTime::parse_from_rfc3339("2099-01-01T00:00:00+00:00")
|
||||
.expect("should parse expiration datetime");
|
||||
|
||||
let user = PersistedUser {
|
||||
auth_tokens: FirebaseAuthTokens {
|
||||
id_token: "test-id-token".to_string(),
|
||||
refresh_token: "test-refresh-token".to_string(),
|
||||
expiration_time,
|
||||
},
|
||||
refresh_token: String::new(),
|
||||
local_id: UserUid::new("test-uid"),
|
||||
metadata: UserMetadata {
|
||||
email: "test@example.com".to_string(),
|
||||
display_name: Some("Test User".to_string()),
|
||||
photo_url: Some("https://example.com/photo.jpg".to_string()),
|
||||
},
|
||||
is_onboarded: true,
|
||||
needs_sso_link: false,
|
||||
anonymous_user_type: None,
|
||||
linked_at: None,
|
||||
personal_object_limits: Some(PersonalObjectLimits {
|
||||
env_var_limit: 10,
|
||||
notebook_limit: 20,
|
||||
workflow_limit: 30,
|
||||
}),
|
||||
is_on_work_domain: false,
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_string(&user).expect("serialization should succeed");
|
||||
assert_eq!(serialized, EXPECTED_BLOB);
|
||||
}
|
||||
|
||||
/// Test serializing and deserializing persisted user data.
|
||||
/// See galaxyui_extras::secure_storage::linux_test.rs for Linux-specific tests.
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg_attr(windows, ignore = "passes locally but not in CI on Windows")]
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn test_windows_user_persistence() {
|
||||
use chrono::Local;
|
||||
use galaxy_core::channel::ChannelState;
|
||||
use galaxyui_core::App;
|
||||
use galaxyui_extras::secure_storage;
|
||||
|
||||
App::test((), |mut app| async move {
|
||||
app.update(|ctx| {
|
||||
secure_storage::register_with_dir(
|
||||
ChannelState::data_domain().as_str(),
|
||||
galaxy_core::paths::state_dir(),
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
let local_time = Local::now();
|
||||
|
||||
let tokens = FirebaseAuthTokens {
|
||||
id_token: String::from("This is an ID token."),
|
||||
refresh_token: String::from("This is a refresh token."),
|
||||
expiration_time: local_time.with_timezone(local_time.offset())
|
||||
+ chrono::Duration::days(365),
|
||||
};
|
||||
let persisted_user = PersistedUser {
|
||||
auth_tokens: tokens.clone(),
|
||||
refresh_token: String::new(),
|
||||
local_id: UserUid::new("test_uid"),
|
||||
metadata: UserMetadata {
|
||||
email: "test@test.com".to_string(),
|
||||
display_name: Some(String::from("abcdef")),
|
||||
photo_url: Some(String::from("some-photo-url")),
|
||||
},
|
||||
is_onboarded: true,
|
||||
needs_sso_link: false,
|
||||
anonymous_user_type: None,
|
||||
linked_at: None,
|
||||
personal_object_limits: None,
|
||||
is_on_work_domain: false,
|
||||
};
|
||||
|
||||
app.update(|ctx| {
|
||||
// Write the test user to secure storage.
|
||||
let write = persisted_user.write_to_secure_storage(ctx);
|
||||
match &write {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
println!("{err:?}");
|
||||
}
|
||||
}
|
||||
assert!(write.is_ok());
|
||||
|
||||
// Read the persisted user back and ensure the fields match.
|
||||
let stored = PersistedUser::from_secure_storage(ctx).unwrap();
|
||||
assert_eq!(stored.auth_tokens.id_token, tokens.id_token);
|
||||
assert_eq!(stored.auth_tokens.refresh_token, tokens.refresh_token);
|
||||
assert_eq!(stored.auth_tokens.expiration_time, tokens.expiration_time);
|
||||
assert_eq!(
|
||||
stored.metadata.display_name,
|
||||
persisted_user.metadata.display_name
|
||||
);
|
||||
assert_eq!(stored.metadata.email, persisted_user.metadata.email);
|
||||
assert_eq!(stored.metadata.photo_url, persisted_user.metadata.photo_url);
|
||||
|
||||
// Remove the user from secure storage.
|
||||
assert!(PersistedUser::remove_from_secure_storage(ctx).is_ok());
|
||||
|
||||
// Attempt to read a user back, which should fail.
|
||||
let empty_user = PersistedUser::from_secure_storage(ctx);
|
||||
assert!(empty_user.is_err());
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user