-
Notifications
You must be signed in to change notification settings - Fork 50
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
Conversation
bf8c233
to
b1a7e2e
Compare
# 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
# 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
crates/bitwarden/src/state/domain.rs
Outdated
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
?
crates/bitwarden/src/state/domain.rs
Outdated
pub profile: Option<Profile>, | ||
|
||
pub ciphers: HashMap<Uuid, Cipher>, | ||
pub folders: HashMap<Uuid, Folder>, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
…te services in them
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
We have 36 warnings building this. I think we need to put more things behind flags. |
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
There was a problem hiding this 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.
- Introducing
state_services
which are loosely stored on the client object. - State persistent mechanism.
I will need to do some deeper digging into this to properly understand everything.
@@ -0,0 +1,293 @@ | |||
use std::{collections::HashMap, fmt::Debug, path::PathBuf}; | |||
|
|||
use async_lock::Mutex; |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
let Some(expires) = auth.token_expiration else {return Err(Error::VaultLocked)}; | ||
let Some(login_method) = auth.login_method else {return Err(Error::VaultLocked)}; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
pub(crate) async fn get_auth_settings(&self) -> Option<AuthSettings> { | ||
Auth::get(self).await.kdf | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
crates/bitwarden/src/state/state.rs
Outdated
} | ||
} | ||
|
||
fn load_medium(path: &Option<String>) -> Box<dyn StateStorageMedium> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is medium some terminology?
There was a problem hiding this comment.
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).
crates/bitwarden/src/state/state.rs
Outdated
#[cfg(not(target_arch = "wasm32"))] | ||
#[derive(Debug)] | ||
struct FileStateStorageMedium { | ||
path: PathBuf, | ||
} |
There was a problem hiding this comment.
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.
crates/bitwarden/src/state/state.rs
Outdated
Box::new(FileStateStorageMedium::new(path.into())) | ||
} | ||
} else { | ||
Box::new(InMemoryStateStorageMedium::new()) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
…te files, some renames
…y store them in state
Type of change
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:
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 traitStateStorageMedium
. 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 alsosrc/client/[auth,keys,profile].rs
, which handle some parts of the state that are used internally.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.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 separatelock
andunlock
functionality.