Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM1724] State Management v2 #64

Closed
wants to merge 25 commits into from
Closed

Conversation

dani-garcia
Copy link
Member

@dani-garcia dani-garcia commented Jun 8, 2023

Type of change

- [ ] Bug fix
- [x] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [ ] Other

Objective

Version two of the state service implementation, now supporting more granular changes (instead of just overwriting the whole profile) and using file locks as well.

Also implemented state services with different views over the global state to isolate the changes a bit better, this is put to the test on a separate PR (#65) handling folder creation/update/deletion. Example:

// Create a new folder
client
    .get_state_service(FOLDERS_SERVICE)
    // This applies the change in the callback to the state in a synchronised way to avoid overwriting other changes
    .modify(move |folders| { // `folders` here is a mutable reference to the Folders HashMap
        folders.insert(id, Folder { id, name, revision_date });
        Ok(())
    })
    .await?;

Code changes

The state code is stored inside crates/bitwarden/src/state/, inside there you can find the following:

  • state.rs: Contains all the state handling code, and is mostly meant to be generic over state contents and storage medium, to implement another storage type, like secure storage, biometric storage, etc we'd just need to implement the trait StateStorageMedium. Currently implemented are file based storage for non-WASM and LocalStorage for WASM.
  • state_service.rs: Contains the implementation for the different state "views" that will allow the services to access only the part of the state that they require. Check the separate Folders PR (Folders state service using Typestate pattern #65) for an example on how it's used for the Folders service, there's also src/client/[auth,keys,profile].rs, which handle some parts of the state that are used internally.
  • The sync function has the response type removed, the response is processed internally by the state handling code, and clients should access the data through there, instead of through the raw sync response.
  • Client has been modified to move most of it's internal state into the new state management solution. This has caused some functions to need to become async, to handle the synchronised access to the state.
  • The client now also has methods to access the state, plus a session_login method to restore the client from a session persisted to disk. At the moment, session_login also forces a vault unlock, though in the future we probably want to create separate lock and unlock functionality.

@dani-garcia dani-garcia requested review from Hinton and MGibson1 June 8, 2023 12:51
@dani-garcia dani-garcia mentioned this pull request Jun 8, 2023
@dani-garcia dani-garcia changed the title Pm 1724 state management v2 [PM1724] State Management v2 Jun 8, 2023
@MGibson1 MGibson1 force-pushed the PM-1724-state-management-v2 branch from bf8c233 to b1a7e2e Compare June 9, 2023 01:08
# Conflicts:
#	crates/bitwarden-json/src/client.rs
#	crates/bitwarden-json/src/command.rs
#	crates/bitwarden-napi/src-ts/bitwarden_client/index.ts
#	crates/bitwarden-napi/src-ts/bitwarden_client/schemas.ts
#	crates/bitwarden/src/auth/commands/login.rs
#	crates/bitwarden/src/client/auth_settings.rs
#	crates/bitwarden/src/client/client.rs
#	crates/bitwarden/src/client/mod.rs
#	crates/bitwarden/src/commands/mod.rs
#	crates/bitwarden/src/commands/secrets.rs
#	crates/bitwarden/src/commands/sync.rs
#	crates/bitwarden/src/crypto.rs
#	crates/bitwarden/src/platform/empty_request.rs
#	crates/bitwarden/src/platform/folders_request.rs
#	crates/bitwarden/src/platform/sync.rs
#	crates/bitwarden/src/sdk/mod.rs
#	crates/bitwarden/src/sdk/request/mod.rs
#	crates/bitwarden/src/sdk/response/mod.rs
#	crates/bitwarden/src/sdk/response/secrets_response.rs
#	crates/bitwarden/src/util.rs
#	crates/sdk-schemas/src/main.rs
#	languages/csharp/schemas.cs
#	languages/js_webassembly/bitwarden_client/schemas.ts
#	languages/python/BitwardenClient/schemas.py
#	support/schemas/bitwarden/client/ClientSettings.json
@dani-garcia dani-garcia requested review from Hinton and removed request for Hinton June 28, 2023 18:17
crates/bws/src/main.rs Outdated Show resolved Hide resolved
crates/bitwarden-json/src/command.rs Outdated Show resolved Hide resolved
crates/bitwarden-json/src/response.rs Outdated Show resolved Hide resolved
# Conflicts:
#	crates/bitwarden-json/src/command.rs
#	crates/bitwarden-napi/src-ts/bitwarden_client/schemas.ts
#	languages/csharp/schemas.cs
#	languages/js_webassembly/bitwarden_client/schemas.ts
#	languages/python/BitwardenClient/schemas.py
#	support/schemas/bitwarden_json/Command.json
Copy link
Member

@Hinton Hinton Jul 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I like the filename of this. Ideally different models should live in different files. I also don't necessarily associate domain with state.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't think it's a great place for them, I've renamed the file to models which I think is somewhat better, and I'll see where else we can move those models to, the only remaining ones are the Encryption Keys, Profile and Auth tokens, so I guess the best place to put them would be auth and platform? Or maybe client, next to AuthSettings and EncryptionSettings?

pub profile: Option<Profile>,

pub ciphers: HashMap<Uuid, Cipher>,
pub folders: HashMap<Uuid, Folder>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to have a generic way to assign data to an account, so that we don't necessarily fill this with 20 hashmaps, and everyone has to constantly modify it.

That would also allow the consumers to store data without needing to maintain a separate system for it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This struct wasn't in use in the last revision of the PR, it remained there from when the state was handled as a single block. I've deleted it now.

I think what you want to do can be done with client .get_state_service(FOLDERS_SERVICE), by defining any service, it's just a type and a namespace. It's not exposed outside of the SDK but it can be done, we'd just need to avoid conflicting namespaces.

crates/bitwarden/src/state/domain.rs Outdated Show resolved Hide resolved
crates/bitwarden/src/state/domain.rs Outdated Show resolved Hide resolved
@bitwarden-bot
Copy link

bitwarden-bot commented Jul 10, 2023

Logo
Checkmarx One – Scan Summary & Details1bb9fef7-d521-4aab-9c17-2a9cef17de92

No New Or Fixed Issues Found

# Conflicts:
#	Cargo.lock
#	crates/bitwarden/src/auth/commands/login.rs
#	crates/bitwarden/src/platform/get_user_api_key.rs
# Conflicts:
#	crates/bitwarden-napi/src-ts/bitwarden_client/schemas.ts
#	crates/sdk-schemas/Cargo.toml
#	languages/csharp/schemas.cs
#	languages/js_webassembly/bitwarden_client/schemas.ts
#	languages/python/BitwardenClient/schemas.py
#	support/schemas/bitwarden/client/ClientSettings.json
#	support/schemas/bitwarden_json/Command.json
# Conflicts:
#	Cargo.lock
#	crates/bitwarden-json/Cargo.toml
#	crates/bitwarden/Cargo.toml
@Hinton
Copy link
Member

Hinton commented Jul 17, 2023

We have 36 warnings building this. I think we need to put more things behind flags.

@dani-garcia
Copy link
Member Author

A few of those warnings are on the master branch, I'll make a new PR to fix them

# Conflicts:
#	crates/bitwarden/src/auth/commands/login.rs
#	crates/bitwarden/src/client/client.rs
#	crates/bitwarden/src/client/mod.rs
@dani-garcia dani-garcia requested a review from Hinton July 17, 2023 16:09
Copy link
Member

@Hinton Hinton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did an initial review.

There are two architectural changes in this PR.

  1. Introducing state_services which are loosely stored on the client object.
  2. State persistent mechanism.

I will need to do some deeper digging into this to properly understand everything.

crates/bitwarden-json/src/client.rs Outdated Show resolved Hide resolved
@@ -0,0 +1,293 @@
use std::{collections::HashMap, fmt::Debug, path::PathBuf};

use async_lock::Mutex;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use tokio instead? It's already a dependency and would let us remove async-lock.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't actually use tokio on the bitwarden library crate, for compatibility reasons with WASM

crates/bitwarden/src/crypto.rs Outdated Show resolved Hide resolved
Comment on lines 183 to 184
let Some(expires) = auth.token_expiration else {return Err(Error::VaultLocked)};
let Some(login_method) = auth.login_method else {return Err(Error::VaultLocked)};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error doesn't really make sense.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is removed with the set_access_token change mentioned below.


let Some(expires) = auth.token_expiration else {return Err(Error::VaultLocked)};
let Some(login_method) = auth.login_method else {return Err(Error::VaultLocked)};
let expires_seconds = (expires.timestamp() - chrono::Utc::now().timestamp()) as u64;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should have two set_tokens methods, one that accepts a DateTime, and one that accepts seconds in the future.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realised that calling set_tokens here didn't make sense. set_tokens both sets the access token for the API clients, and also stores the access and refresh tokens and their expiration in the state.

In this case we are only interested in the first part, it doesn't make sense here to save it in the state when we just read it from state, so I've separated that part to a new set_access_token, which means we don't have to use the expiration here at all.

@@ -86,6 +84,8 @@ impl Client {
.build()
.unwrap();

let state = State::load_state(&settings);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know how long time it takes to load the state of a large account?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't actually load and parse the file yet, it's just initializing the state with the correct path and everything. Probably worth renaming to initialize_state.

The file would be loaded in the current session_login, by the line client.state.load_account(input.user_id).await?;.

I'll do some testing, but I expect it to be plenty fast.

Comment on lines +171 to 173
pub(crate) async fn get_auth_settings(&self) -> Option<AuthSettings> {
Auth::get(self).await.kdf
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks odd, kdf shouldn't be auth settings?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I feel like it should be kdf_settings and the struct renamed to KdfSettings. It only holds the email and KDF parameters, it's only tangentially related to authentication. What do you think?

}
}

fn load_medium(path: &Option<String>) -> Box<dyn StateStorageMedium> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is medium some terminology?

Copy link
Member Author

@dani-garcia dani-garcia Jul 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've used storage medium to refer to the different storage backends, in a similar way than CD's or DVD's would be different storage media.

It doesn't really add anything to the name though, we can just rename the StateStorageMedium trait to Storage plainly, and rename load_medium to initialize_storage (Because it's not actually loading anything yet).

Comment on lines 156 to 160
#[cfg(not(target_arch = "wasm32"))]
#[derive(Debug)]
struct FileStateStorageMedium {
path: PathBuf,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably move out wasm code to it's own file to easier conditionally compile it.

Box::new(FileStateStorageMedium::new(path.into()))
}
} else {
Box::new(InMemoryStateStorageMedium::new())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a in memory state?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is meant to handle two cases:

  • The SDK doesn't have configured the storage path, like for secrets manager use, or for a one-off operation. In this case we still use the state to store authentication tokens, etc. But they are not persisted to disk.
  • During login, we don't have the users UUID until after a sync is done, so we don't create the persistent storage until then, we use the in memory storage as a temporary place until we can dump everything in the persistent storage

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants