use crate::server::datetime_ext::DateTimeExt; use anyhow::{anyhow, Result}; use chrono::{DateTime, FixedOffset}; use serde::{Deserialize, Serialize}; use galaxy_graphql::{queries::get_user::FirebaseProfile, scalars::time::ServerTimestamp}; use super::UserUid; pub use galaxy_server_client::auth::{TEST_USER_EMAIL, TEST_USER_UID}; #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub enum AnonymousUserType { /// An anonymous user created from the native client. NativeClientAnonymousUser, /// An anonymous user created from the native client with feature (rather than time-based) gating. NativeClientAnonymousUserFeatureGated, /// An anonymous user created from the web client. WebClientAnonymousUser, } /// Type of principal making the authenticated request. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum PrincipalType { #[default] User, ServiceAccount, } impl From for PrincipalType { fn from(value: galaxy_graphql::queries::get_user::PrincipalType) -> Self { use galaxy_graphql::queries::get_user::PrincipalType as GqlPrincipalType; match value { GqlPrincipalType::User => PrincipalType::User, GqlPrincipalType::ServiceAccount => PrincipalType::ServiceAccount, } } } impl TryFrom for AnonymousUserType { type Error = anyhow::Error; fn try_from( value: galaxy_graphql::mutations::create_anonymous_user::AnonymousUserType, ) -> Result { match value { galaxy_graphql::mutations::create_anonymous_user::AnonymousUserType::NativeClientAnonymousUser => Ok(AnonymousUserType::NativeClientAnonymousUser), galaxy_graphql::mutations::create_anonymous_user::AnonymousUserType::NativeClientAnonymousUserFeatureGated => Ok(AnonymousUserType::NativeClientAnonymousUserFeatureGated), galaxy_graphql::mutations::create_anonymous_user::AnonymousUserType::WebClientAnonymousUser => Ok(AnonymousUserType::WebClientAnonymousUser), galaxy_graphql::mutations::create_anonymous_user::AnonymousUserType::Other(_) => { Err(anyhow!("could not convert unknown anonymous user type")) }, } } } #[derive(Debug, Copy, Clone, Serialize, Deserialize)] pub struct PersonalObjectLimits { pub env_var_limit: usize, pub notebook_limit: usize, pub workflow_limit: usize, } impl TryFrom for PersonalObjectLimits { type Error = anyhow::Error; fn try_from( value: galaxy_graphql::queries::get_user::AnonymousUserPersonalObjectLimits, ) -> Result { Ok(Self { env_var_limit: value.env_var_limit as usize, notebook_limit: value.notebook_limit as usize, workflow_limit: value.workflow_limit as usize, }) } } /// The in-memory representation of a logged-in User. /// This does not include authentication credentials, which are stored separately /// in the `Credentials` enum. #[derive(Debug, Clone)] pub struct User { /// The Firebase UID of this user. pub local_id: UserUid, /// Metadata about the user. pub metadata: UserMetadata, /// Whether or not the user is onboarded. pub is_onboarded: bool, /// Whether or not the user needs to link their account via SSO due to an organization setting. pub needs_sso_link: bool, /// What type of anonymous user this user is. May be `None` if they are not anonymous. pub anonymous_user_type: Option, /// Whether or not this user is on what we consider a "work" domain, meaning the domain isn't /// from a general email provider (e.g. gmail.com, hotmail.com, proton.me, etc.). /// Calculated on warp-server. pub is_on_work_domain: bool, pub linked_at: Option, pub personal_object_limits: Option, /// Type of principal (user or service account). Fetched fresh from the server /// on each login/refresh. pub principal_type: PrincipalType, } /// This struct holds extra information about the user. Most of this information comes directly /// from Firebase. #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct UserMetadata { /// The user's email. NOTE: unlike other fields which use `Option`s to denote null values, /// an anonymous user will have an empty string as their email here. pub email: String, /// The user's display name from Firebase. We should prefer showing this over their email, if /// we can. Typically this is only populated when using a non-email provider like GitHub. pub display_name: Option, /// A URL for their profile picture. pub photo_url: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct FirebaseAuthTokens { /// ID tokens are Firebase tokens, which are short-lived tokens that are used to authenticate /// requests to the server. These are obtained by exchanging long-lived refresh tokens. pub id_token: String, /// Refresh tokens are long-lived tokens that can be exchanged for short-lived access tokens /// (stored in the id_token field). We use the refresh token to get a new ID token when the /// current one expires. /// Note that there are two types of refresh tokens we store in this field: /// "Refresh tokens": these are used for logged-in users. /// "Custom tokens": these are used for anonymous firebase users. pub refresh_token: String, /// When the ID token expires. If the token has expired, or will expire soon, we should /// fetch a new ID token using the user's refresh token. pub expiration_time: DateTime, } impl FirebaseAuthTokens { pub fn from_response( id_token: String, refresh_token: String, expires_in: String, ) -> Result { Ok(Self { id_token, expiration_time: chrono::DateTime::now() + chrono::Duration::seconds( expires_in.parse::().map_err(anyhow::Error::from)?, ), refresh_token, }) } } impl User { /// The name for the user that we display. This is the user's display name, if set. If not set, /// we then fallback to email (which is always set). pub fn username_for_display(&self) -> &str { let user_metadata = &self.metadata; user_metadata .display_name .as_deref() .unwrap_or(user_metadata.email.as_str()) } /// The display name of the user. Does not fall back to email. pub fn display_name(&self) -> Option { self.metadata.display_name.clone() } pub fn test() -> Self { Self { local_id: UserUid::new(TEST_USER_UID), metadata: UserMetadata { email: TEST_USER_EMAIL.to_string(), display_name: None, photo_url: None, }, is_onboarded: true, needs_sso_link: false, anonymous_user_type: None, is_on_work_domain: false, linked_at: None, personal_object_limits: None, principal_type: PrincipalType::User, } } pub fn is_user_anonymous(&self) -> bool { self.anonymous_user_type().is_some() && self.linked_at().is_none() } pub fn anonymous_user_type(&self) -> Option { self.anonymous_user_type } pub fn personal_object_limits(&self) -> Option { self.personal_object_limits } pub fn linked_at(&self) -> Option { self.linked_at } } impl From for UserMetadata { fn from(value: FirebaseProfile) -> Self { Self { email: value.email.unwrap_or_default(), display_name: value.display_name, photo_url: value.photo_url, } } } #[cfg(test)] #[path = "user_test.rs"] mod tests;