Skip to content

Commit

Permalink
[sdk] Improve API & internal state
Browse files Browse the repository at this point in the history
We changed the point at which the credentials are fixed in the sdk. Now
they are passed in to login() method and not when creating SDK. This
makes for a cleaner separation of a stateless, shared SDK and stateful,
account-bound LoggedInSDK. Rest resources are now always bound to a
session which allows for parallel logins and prevents accidental bugs
  • Loading branch information
charlag committed Aug 26, 2024
1 parent 9ed7803 commit 70e3b0a
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ class TutanotaNotificationsHandler(
credentialType = CredentialType.INTERNAL
)

val sdk = Sdk(sseInfo.sseOrigin, SdkRestClient(), credentials, BuildConfig.VERSION_NAME).login()
val sdk = Sdk(sseInfo.sseOrigin, SdkRestClient(), BuildConfig.VERSION_NAME).login(credentials)

val mailId = notificationInfo.mailId?.toSdkIdTuple()
?: throw IllegalArgumentException("Missing mailId for notification ${sseInfo.pushIdentifier}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class NotificationService: UNNotificationServiceExtension {
encryptedPassphraseKey: encryptedPassphraseKey.data,
credentialType: tutasdk.CredentialType.internal
)
let sdk = try await Sdk(baseUrl: origin, restClient: SdkRestClient(), credentials: credentials, clientVersion: clientVersion).login()
let sdk = try await Sdk(baseUrl: origin, restClient: SdkRestClient(), clientVersion: clientVersion).login(credentials: credentials)
return try await sdk.mailFacade().loadEmailByIdEncrypted(idTuple: tutasdk.IdTuple(listId: mailId[0], elementId: mailId[1]))
}
private func getSenderOfMail(_ mail: tutasdk.Mail) -> String {
Expand Down
20 changes: 10 additions & 10 deletions tuta-sdk/rust/sdk/src/entity_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::sync::Arc;

use crate::element_value::{ElementValue, ParsedEntity};
use crate::generated_id::GeneratedId;
use crate::{ApiCallError, AuthHeadersProvider, IdTuple, ListLoadDirection, SdkState, TypeRef};
use crate::{ApiCallError, HeadersProvider, IdTuple, ListLoadDirection, TypeRef};
use crate::json_serializer::JsonSerializer;
use crate::json_element::RawEntity;
use crate::metamodel::TypeModel;
Expand All @@ -19,7 +19,7 @@ pub trait IdType: Display + 'static {}
pub struct EntityClient {
rest_client: Arc<dyn RestClient>,
base_url: String,
sdk_state: Arc<SdkState>,
auth_headers_provider: Arc<HeadersProvider>,
json_serializer: Arc<JsonSerializer>,
type_model_provider: Arc<TypeModelProvider>,
}
Expand All @@ -29,15 +29,15 @@ impl EntityClient {
pub(crate) fn new(
rest_client: Arc<dyn RestClient>,
json_serializer: Arc<JsonSerializer>,
base_url: &str,
sdk_state: Arc<SdkState>,
base_url: String,
auth_headers_provider: Arc<HeadersProvider>,
type_model_provider: Arc<TypeModelProvider>,
) -> Self {
EntityClient {
rest_client,
json_serializer,
base_url: base_url.to_owned(),
sdk_state,
base_url,
auth_headers_provider,
type_model_provider,
}
}
Expand All @@ -56,7 +56,7 @@ impl EntityClient {
})?;
let options = RestClientOptions {
body: None,
headers: self.sdk_state.create_auth_headers(model_version),
headers: self.auth_headers_provider.provide_headers(model_version),
};
let response = self
.rest_client
Expand Down Expand Up @@ -137,7 +137,7 @@ impl EntityClient {
let body = serde_json::to_vec(&raw_entity).unwrap();
let options = RestClientOptions {
body: Some(body),
headers: self.sdk_state.create_auth_headers(model_version),
headers: self.auth_headers_provider.provide_headers(model_version),
};
// FIXME we should look at type model whether it is ET or LET
let url = format!(
Expand Down Expand Up @@ -169,8 +169,8 @@ mockall::mock! {
pub fn new(
rest_client: Arc<dyn RestClient>,
json_serializer: Arc<JsonSerializer>,
base_url: &str,
sdk_state: Arc<SdkState>,
base_url: String,
auth_headers_provider: Arc<HeadersProvider>,
type_model_provider: Arc<TypeModelProvider>,
) -> Self;
pub fn get_type_model(&self, type_ref: &TypeRef) -> Result<&'static TypeModel, ApiCallError>;
Expand Down
169 changes: 90 additions & 79 deletions tuta-sdk/rust/sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,30 +37,30 @@ use crate::typed_entity_client::TypedEntityClient;
#[mockall_double::double]
use crate::user_facade::UserFacade;

mod entity_client;
mod json_serializer;
mod json_element;
pub mod rest_client;
mod element_value;
mod metamodel;
mod type_model_provider;
mod mail_facade;
mod user_facade;
mod rest_error;
mod crypto;
mod util;
mod crypto_entity_client;
mod custom_id;
pub mod date;
mod element_value;
mod entities;
mod entity_client;
pub mod generated_id;
mod instance_mapper;
mod typed_entity_client;
mod key_loader_facade;
mod json_element;
mod json_serializer;
mod key_cache;
pub mod date;
pub mod generated_id;
mod custom_id;
pub mod login;
mod crypto_entity_client;
mod key_loader_facade;
mod logging;
pub mod login;
mod mail_facade;
mod metamodel;
pub mod rest_client;
mod rest_error;
mod simple_crypto;
mod type_model_provider;
mod typed_entity_client;
mod user_facade;
mod util;

uniffi::setup_scaffolding!();

Expand Down Expand Up @@ -90,88 +90,90 @@ impl Display for TypeRef {
}
}

pub trait AuthHeadersProvider: Send + Sync {
/// Gets the HTTP request headers used for authorizing REST requests
fn create_auth_headers(&self, model_version: u32) -> HashMap<String, String>;
pub struct HeadersProvider {
client_version: String,
// In the future we might need to make this one optional to support "not authenticated" state
access_token: String,
}

impl AuthHeadersProvider for SdkState {
/// This version has client_version in header, unlike the LoginState version
fn create_auth_headers(&self, model_version: u32) -> HashMap<String, String> {
let mut headers = HashMap::from([
("accessToken".to_string(), self.credentials.access_token.clone()),
]);
headers.insert("cv".to_owned(), self.client_version.clone());
headers.insert("v".to_owned(), model_version.to_string());
headers
impl HeadersProvider {
fn new(client_version: String, access_token: String) -> Self {
Self {
client_version,
access_token,
}
}
}

/// Contains all the high level mutable state of the SDK
struct SdkState {
credentials: Credentials,
client_version: String,
fn provide_headers(&self, model_version: u32) -> HashMap<String, String> {
HashMap::from([
("accessToken".to_string(), self.access_token.clone()),
("cv".to_owned(), self.client_version.clone()),
("v".to_owned(), model_version.to_string()),
])
}
}

/// The external facing interface used by the consuming code via FFI
#[derive(uniffi::Object)]
pub struct Sdk {
state: Arc<SdkState>,
type_model_provider: Arc<TypeModelProvider>,
entity_client: Arc<EntityClient>,
typed_entity_client: Arc<TypedEntityClient>,
json_serializer: Arc<JsonSerializer>,
instance_mapper: Arc<InstanceMapper>,
rest_client: Arc<dyn RestClient>,
base_url: String,
client_version: String,
}

#[uniffi::export]
impl Sdk {
#[uniffi::constructor]
pub fn new(base_url: String, rest_client: Arc<dyn RestClient>, credentials: Credentials, client_version: &str) -> Sdk {
pub fn new(base_url: String, rest_client: Arc<dyn RestClient>, client_version: String) -> Sdk {
logging::init_logger();
log::debug!("Initializing SDK...");

let type_model_provider = Arc::new(init_type_model_provider());
// TODO validate parameters
let json_serializer = Arc::new(JsonSerializer::new(type_model_provider.clone()));
let instance_mapper = Arc::new(InstanceMapper::new());
let state = Arc::new(SdkState {
credentials,
client_version: client_version.to_owned(),
});
let entity_client = Arc::new(EntityClient::new(
rest_client,
json_serializer,
&base_url,
state.clone(),
type_model_provider.clone(),
));
let typed_entity_client: Arc<TypedEntityClient> = Arc::new(TypedEntityClient::new(
entity_client.clone(),
instance_mapper.clone()
));
let json_serializer = Arc::new(JsonSerializer::new(type_model_provider.clone()));

Sdk {
state,
type_model_provider,
entity_client,
typed_entity_client,
instance_mapper
json_serializer,
instance_mapper,
rest_client,
base_url,
client_version,
}
}

/// Authorizes the SDK's REST requests via inserting `access_token` into the HTTP headers
pub async fn login(&self) -> Result<Arc<LoggedInSdk>, LoginError> {
pub async fn login(&self, credentials: Credentials) -> Result<Arc<LoggedInSdk>, LoginError> {
let auth_headers_provider = Arc::new(HeadersProvider::new(
self.client_version.clone(),
credentials.access_token.clone(),
));
let entity_client = Arc::new(EntityClient::new(
self.rest_client.clone(),
self.json_serializer.clone(),
self.base_url.clone(),
auth_headers_provider,
self.type_model_provider.clone(),
));
let typed_entity_client: Arc<TypedEntityClient> = Arc::new(TypedEntityClient::new(
entity_client.clone(),
self.instance_mapper.clone(),
));

// Try to resume session
let login_facade = LoginFacade::new(
self.entity_client.clone(),
self.typed_entity_client.clone(),
|user| UserFacade::new(Arc::new(KeyCache::new()), user)
);
let user_facade = Arc::new(login_facade.resume_session(&self.state.credentials).await?);
let login_facade =
LoginFacade::new(entity_client.clone(), typed_entity_client.clone(), |user| {
UserFacade::new(Arc::new(KeyCache::new()), user)
});
let user_facade = Arc::new(login_facade.resume_session(&credentials).await?);

let key_loader = Arc::new(KeyLoaderFacade::new(
user_facade.clone(),
self.typed_entity_client.clone()
typed_entity_client.clone(),
));
let randomizer = RandomizerFacade::from_core(rand_core::OsRng);
let crypto_facade = Arc::new(CryptoFacade::new(
Expand All @@ -181,15 +183,16 @@ impl Sdk {
));
let entity_facade = Arc::new(EntityFacade::new(self.type_model_provider.clone()));
let crypto_entity_client: Arc<CryptoEntityClient> = Arc::new(CryptoEntityClient::new(
self.entity_client.clone(),
entity_client.clone(),
entity_facade,
crypto_facade,
self.instance_mapper.clone()
self.instance_mapper.clone(),
));

Ok(Arc::new(LoggedInSdk {
user_facade,
typed_entity_client: self.typed_entity_client.clone(),
entity_client,
typed_entity_client,
crypto_entity_client,
}))
}
Expand All @@ -199,8 +202,9 @@ impl Sdk {
#[derive(uniffi::Object)]
pub struct LoggedInSdk {
user_facade: Arc<UserFacade>,
entity_client: Arc<EntityClient>,
typed_entity_client: Arc<TypedEntityClient>,
crypto_entity_client: Arc<CryptoEntityClient>
crypto_entity_client: Arc<CryptoEntityClient>,
}

#[uniffi::export]
Expand Down Expand Up @@ -229,7 +233,10 @@ pub struct IdTuple {
impl IdTuple {
#[must_use]
pub fn new(list_id: GeneratedId, element_id: GeneratedId) -> Self {
Self { list_id, element_id }
Self {
list_id,
element_id,
}
}
}

Expand All @@ -252,32 +259,36 @@ pub enum ApiCallError {
#[error("ServerResponseError: {source}")]
ServerResponseError {
#[from]
source: HttpError
source: HttpError,
},
#[error("InternalSdkError: {error_message}")]
InternalSdkError {
error_message: String,
},
InternalSdkError { error_message: String },
}

impl ApiCallError {
fn internal(message: String) -> ApiCallError {
ApiCallError::InternalSdkError { error_message: message }
}
fn internal_with_err<E: Error>(error: E, message: &str) -> ApiCallError {
ApiCallError::InternalSdkError { error_message: format!("{}: {}", error, message) }
ApiCallError::InternalSdkError {
error_message: format!("{}: {}", error, message),
}
}
}

impl From<InstanceMapperError> for ApiCallError {
fn from(value: InstanceMapperError) -> Self {
ApiCallError::InternalSdkError { error_message: value.to_string() }
ApiCallError::InternalSdkError {
error_message: value.to_string(),
}
}
}

impl From<ParseFailureError> for ApiCallError {
fn from(_value: ParseFailureError) -> Self {
ApiCallError::InternalSdkError { error_message: "Parse error".to_owned() }
ApiCallError::InternalSdkError {
error_message: "Parse error".to_owned(),
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions tuta-sdk/rust/sdk/tests/download_mail_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ async fn test_download_mail() {
encrypted_passphrase_key,
credential_type: CredentialType::Internal,
};
let sdk = Sdk::new("http://localhost:9000".to_string(), rest_client, credentials, "");
let logged_in_sdk = sdk.login().await.unwrap();
let sdk = Sdk::new("http://localhost:9000".to_string(), rest_client, "".to_owned());
let logged_in_sdk = sdk.login(credentials).await.unwrap();
let mail_facade = logged_in_sdk.mail_facade();
let mail = mail_facade.load_email_by_id_encrypted(
&IdTuple { list_id: GeneratedId("O1qC705-17-0".to_string()), element_id: GeneratedId("O1qC7an--3-0".to_string()) }
Expand Down

0 comments on commit 70e3b0a

Please sign in to comment.