Multi-user: email sign-in, invite/open registration, admin calls
- kv-proto: email on setup, AccountInfo.email/is_admin, ServerInfo.registration, admin DTOs. - kv-sync: prelogin by email or account id (setup QR / Emergency Kit), register with email, set_email, /v1/admin/* client calls. - kv-ffi: register_account(code, email), login(.., email), account_details, set_account_email, admin_create_invite. kv-wasm: the same plus the full admin API for the web vault. - Live test: two users on one server stay isolated; a new device signs in by email. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -333,13 +333,13 @@ mod tests {
|
||||
let a = VaultCore::open(dir.path().join("a.sqlite").to_string_lossy().into_owned()).unwrap();
|
||||
let sk = a.create_account(PW.into()).unwrap();
|
||||
connect(&a, &server);
|
||||
a.register_account(SETUP_TOKEN.into()).await.unwrap();
|
||||
a.register_account(SETUP_TOKEN.into(), None).await.unwrap();
|
||||
let r = a.import_commit(onepux(), ImportFormat::OnePux, skip()).unwrap();
|
||||
assert_eq!(a.sync_now().await.unwrap().pushed, r.imported);
|
||||
|
||||
let b = VaultCore::open(dir.path().join("b.sqlite").to_string_lossy().into_owned()).unwrap();
|
||||
connect(&b, &server);
|
||||
assert_eq!(b.login(PW.into(), sk, None).await.unwrap(), LoginResult::Ok);
|
||||
assert_eq!(b.login(PW.into(), sk, None, None).await.unwrap(), LoginResult::Ok);
|
||||
b.sync_now().await.unwrap();
|
||||
assert_eq!(b.list_items().unwrap().len(), 8);
|
||||
let id = r.item_ids[0].clone();
|
||||
|
||||
@@ -340,6 +340,22 @@ struct Inner {
|
||||
api: Option<Arc<ApiClient>>,
|
||||
}
|
||||
|
||||
/// A new invite, shown once.
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct InviteCode {
|
||||
pub code: String,
|
||||
/// Unix seconds.
|
||||
pub expires_at: i64,
|
||||
}
|
||||
|
||||
/// What `account_details` returns: never anything secret.
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct AccountDetails {
|
||||
pub email: Option<String>,
|
||||
/// This account administers the server (invites, users, sign-up): it still can't read anyone's vault.
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
/// One account's vault on this device.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct VaultCore {
|
||||
@@ -750,16 +766,18 @@ impl VaultCore {
|
||||
})
|
||||
}
|
||||
|
||||
/// First device only: registers the account created by `create_account` with a fresh server,
|
||||
/// using the one-time setup token the server printed on first boot. Must be unlocked with the
|
||||
/// master password (not biometrics) in this session. Logs in afterwards.
|
||||
pub async fn register_account(&self, setup_token: String) -> Result<()> {
|
||||
/// Registers the account created by `create_account` with the server. `setup_token` is the server's setup code
|
||||
/// (its first account, which becomes the admin), an invite code from the admin, or empty with open sign-up;
|
||||
/// `email` is what the account signs in with (required once the server has an account; docs/03 "Accounts").
|
||||
/// Must be unlocked with the master password (not biometrics) in this session. Logs in afterwards.
|
||||
pub async fn register_account(&self, setup_token: String, email: Option<String>) -> Result<()> {
|
||||
let api = self.api()?;
|
||||
let (acct, u) = self.unlocked_snapshot()?;
|
||||
let auth = u.auth_input.clone().ok_or(CoreError::NotAuthenticated)?;
|
||||
let device = self.enroll()?;
|
||||
api.register(
|
||||
&setup_token,
|
||||
email.as_deref(),
|
||||
acct.account_id,
|
||||
&acct.kdf,
|
||||
auth.as_ref(),
|
||||
@@ -772,7 +790,8 @@ impl VaultCore {
|
||||
self.do_login(&api, acct.account_id, auth.as_ref(), None).await.map(|_| ())
|
||||
}
|
||||
|
||||
/// Logs in with the master password and Secret Key (the server is single-tenant, so no account id is needed).
|
||||
/// Logs in with the master password, Secret Key and the account's `email` (may be omitted on a server with
|
||||
/// a single account, or when this device already has the account).
|
||||
/// * New device (no local account): fetches the account from the server, stores it and unlocks.
|
||||
/// Then call `sync_now` to download the vault.
|
||||
/// * Existing account: renews the session and unlocks.
|
||||
@@ -781,9 +800,28 @@ impl VaultCore {
|
||||
master_password: String,
|
||||
secret_key: String,
|
||||
totp: Option<String>,
|
||||
email: Option<String>,
|
||||
) -> Result<LoginResult> {
|
||||
let sk = SecretKey::parse(&secret_key)?;
|
||||
self.login_inner(master_password, sk, totp, None, None).await
|
||||
self.login_inner(master_password, sk, totp, None, None, email).await
|
||||
}
|
||||
|
||||
/// The account's email and whether it administers the server (`GET /v1/account`).
|
||||
pub async fn account_details(&self) -> Result<AccountDetails> {
|
||||
let info = self.revoked(self.api()?.account().await)?;
|
||||
Ok(AccountDetails { email: info.email, is_admin: info.is_admin })
|
||||
}
|
||||
|
||||
/// Sets the address this account signs in with (accounts from before multi-user have none).
|
||||
pub async fn set_account_email(&self, email: String) -> Result<()> {
|
||||
self.revoked(self.api()?.set_email(&email).await)
|
||||
}
|
||||
|
||||
/// Admins only: a one-time invite code for a new account (optionally only for `email`, valid `days`, default 7).
|
||||
/// The full admin page is in the web vault.
|
||||
pub async fn admin_create_invite(&self, email: Option<String>, days: Option<u32>) -> Result<InviteCode> {
|
||||
let c = self.revoked(self.api()?.admin_create_invite(&kv_proto::InviteCreate { email, days }).await)?;
|
||||
Ok(InviteCode { code: c.code, expires_at: c.expires_at })
|
||||
}
|
||||
|
||||
/// Pull, merge, push and verify every vault. Renews an expired session automatically when the
|
||||
@@ -802,6 +840,7 @@ impl VaultCore {
|
||||
totp: Option<String>,
|
||||
ticket: Option<String>,
|
||||
expect_account: Option<Uuid>,
|
||||
email: Option<String>,
|
||||
) -> Result<LoginResult> {
|
||||
let api = self.api()?;
|
||||
let local = self.lock_inner().account.clone();
|
||||
@@ -810,7 +849,12 @@ impl VaultCore {
|
||||
let (account_id, kdf_params) = match &local {
|
||||
Some(a) => (a.account_id, a.kdf.clone()),
|
||||
None => {
|
||||
let pre = api.prelogin().await?;
|
||||
let by = match (expect_account, email.as_deref().map(str::trim).filter(|e| !e.is_empty())) {
|
||||
(Some(id), _) => kv_sync::PreloginBy::Account(id),
|
||||
(None, Some(e)) => kv_sync::PreloginBy::Email(e),
|
||||
(None, None) => kv_sync::PreloginBy::OnlyAccount,
|
||||
};
|
||||
let pre = api.prelogin(by).await?;
|
||||
(pre.account_id, pre.kdf)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -155,7 +155,7 @@ impl VaultCore {
|
||||
}
|
||||
self.configure_server(p.server_url.clone(), p.spki_pins.clone())?;
|
||||
let sk = SecretKey::parse(&p.secret_key)?;
|
||||
self.login_inner(master_password, sk, None, p.ticket.clone(), Some(p.account_id)).await
|
||||
self.login_inner(master_password, sk, None, p.ticket.clone(), Some(p.account_id), None).await
|
||||
}
|
||||
|
||||
/// Joining device (no camera): opens a rendezvous on `server_url` and returns the QR to show.
|
||||
|
||||
@@ -194,14 +194,14 @@ async fn passkey_created_on_one_device_signs_on_another() {
|
||||
let a = VaultCore::new_in_memory();
|
||||
let sk = a.create_account(PW.into()).unwrap();
|
||||
connect(&a, &server);
|
||||
a.register_account(SETUP_TOKEN.into()).await.unwrap();
|
||||
a.register_account(SETUP_TOKEN.into(), None).await.unwrap();
|
||||
let reg = a.create_passkey(RP.into(), b"u".to_vec(), "me".into(), cdh(b"r"), None).unwrap();
|
||||
let vk = rp_verify_registration(®, RP);
|
||||
assert_eq!(a.sync_now().await.unwrap().pushed, 1);
|
||||
|
||||
let b = VaultCore::new_in_memory();
|
||||
connect(&b, &server);
|
||||
b.login(PW.into(), sk, None).await.unwrap();
|
||||
b.login(PW.into(), sk, None, None).await.unwrap();
|
||||
b.sync_now().await.unwrap();
|
||||
let on_b = b.passkeys_for_rp(RP.into(), vec![]).unwrap();
|
||||
assert_eq!(on_b.len(), 1);
|
||||
|
||||
+19
-19
@@ -172,7 +172,7 @@ async fn two_devices_sync_through_server() {
|
||||
let sk = a.create_account(PW.into()).unwrap();
|
||||
connect(&a, &server);
|
||||
assert!(a.server_info().await.unwrap().setup_open);
|
||||
a.register_account(SETUP_TOKEN.into()).await.unwrap();
|
||||
a.register_account(SETUP_TOKEN.into(), None).await.unwrap();
|
||||
assert!(a.is_logged_in());
|
||||
let id = a.save_item(login_item("Bank", "pw-a")).unwrap();
|
||||
let r = a.sync_now().await.unwrap();
|
||||
@@ -182,8 +182,8 @@ async fn two_devices_sync_through_server() {
|
||||
let b_path = dir.path().join("b.sqlite").to_string_lossy().into_owned();
|
||||
let b = VaultCore::open(b_path.clone()).unwrap();
|
||||
connect(&b, &server);
|
||||
assert!(matches!(b.login("wrong password!!".into(), sk.clone(), None).await, Err(CoreError::BadCredentials)));
|
||||
assert_eq!(b.login(PW.into(), sk.clone(), None).await.unwrap(), LoginResult::Ok);
|
||||
assert!(matches!(b.login("wrong password!!".into(), sk.clone(), None, None).await, Err(CoreError::BadCredentials)));
|
||||
assert_eq!(b.login(PW.into(), sk.clone(), None, None).await.unwrap(), LoginResult::Ok);
|
||||
assert!(b.has_account() && !b.is_locked());
|
||||
b.sync_now().await.unwrap();
|
||||
assert_eq!(b.get_item(id.clone()).unwrap().password.as_deref(), Some("pw-a"));
|
||||
@@ -233,7 +233,7 @@ async fn integrity_alarm_reaches_the_app() {
|
||||
let a = VaultCore::new_in_memory();
|
||||
a.create_account(PW.into()).unwrap();
|
||||
connect(&a, &server);
|
||||
a.register_account(SETUP_TOKEN.into()).await.unwrap();
|
||||
a.register_account(SETUP_TOKEN.into(), None).await.unwrap();
|
||||
for i in 0..2 {
|
||||
a.save_item(login_item(&format!("item {i}"), "p")).unwrap();
|
||||
a.sync_now().await.unwrap();
|
||||
@@ -258,13 +258,13 @@ async fn pending_approval_and_biometric_session_expiry() {
|
||||
let a = VaultCore::new_in_memory();
|
||||
let sk = a.create_account(PW.into()).unwrap();
|
||||
connect(&a, &server);
|
||||
a.register_account(SETUP_TOKEN.into()).await.unwrap();
|
||||
a.register_account(SETUP_TOKEN.into(), None).await.unwrap();
|
||||
a.sync_now().await.unwrap();
|
||||
|
||||
server.state().require_approval = true;
|
||||
let b = VaultCore::new_in_memory();
|
||||
connect(&b, &server);
|
||||
assert_eq!(b.login(PW.into(), sk.clone(), None).await.unwrap(), LoginResult::PendingApproval);
|
||||
assert_eq!(b.login(PW.into(), sk.clone(), None, None).await.unwrap(), LoginResult::PendingApproval);
|
||||
assert!(!b.has_account());
|
||||
// A approves B after comparing fingerprints, then B's next login succeeds.
|
||||
let devices = a.list_devices().await.unwrap();
|
||||
@@ -272,7 +272,7 @@ async fn pending_approval_and_biometric_session_expiry() {
|
||||
assert_eq!(pending.key_fingerprint, b.device_key_fingerprint().unwrap());
|
||||
assert!(devices.iter().any(|d| d.current && d.approved));
|
||||
a.approve_device(pending.device_id.clone()).await.unwrap();
|
||||
assert_eq!(b.login(PW.into(), sk, None).await.unwrap(), LoginResult::Ok);
|
||||
assert_eq!(b.login(PW.into(), sk, None, None).await.unwrap(), LoginResult::Ok);
|
||||
b.sync_now().await.unwrap();
|
||||
// Revoking B kills its session; B learns it was removed and resets its identity.
|
||||
a.revoke_device(pending.device_id.clone()).await.unwrap();
|
||||
@@ -294,7 +294,7 @@ async fn pending_approval_and_biometric_session_expiry() {
|
||||
c.lock();
|
||||
c.unlock_with_local_key(luk).unwrap();
|
||||
connect(&c, &server);
|
||||
assert!(matches!(c.register_account("x".into()).await, Err(CoreError::NotAuthenticated)));
|
||||
assert!(matches!(c.register_account("x".into(), None).await, Err(CoreError::NotAuthenticated)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -303,7 +303,7 @@ async fn save_item_bumps_seq_and_replays_are_refused() {
|
||||
let a = VaultCore::new_in_memory();
|
||||
let sk = a.create_account(PW.into()).unwrap();
|
||||
connect(&a, &server);
|
||||
a.register_account(SETUP_TOKEN.into()).await.unwrap();
|
||||
a.register_account(SETUP_TOKEN.into(), None).await.unwrap();
|
||||
let id = a.save_item(login_item("Bank", "pw1")).unwrap();
|
||||
a.sync_now().await.unwrap();
|
||||
let vid = *server.state().vaults.keys().next().unwrap();
|
||||
@@ -326,7 +326,7 @@ async fn save_item_bumps_seq_and_replays_are_refused() {
|
||||
|
||||
let b = VaultCore::new_in_memory();
|
||||
connect(&b, &server);
|
||||
b.login(PW.into(), sk, None).await.unwrap();
|
||||
b.login(PW.into(), sk, None, None).await.unwrap();
|
||||
b.sync_now().await.unwrap();
|
||||
assert_eq!(seq_of(&b), 3);
|
||||
|
||||
@@ -344,7 +344,7 @@ async fn delete_item_seals_the_tombstone_and_forged_deletes_are_refused() {
|
||||
let a = VaultCore::new_in_memory();
|
||||
let sk = a.create_account(PW.into()).unwrap();
|
||||
connect(&a, &server);
|
||||
a.register_account(SETUP_TOKEN.into()).await.unwrap();
|
||||
a.register_account(SETUP_TOKEN.into(), None).await.unwrap();
|
||||
let gone = a.save_item(login_item("Old", "pw0")).unwrap();
|
||||
let kept = a.save_item(login_item("Bank", "pw1")).unwrap();
|
||||
a.sync_now().await.unwrap();
|
||||
@@ -357,7 +357,7 @@ async fn delete_item_seals_the_tombstone_and_forged_deletes_are_refused() {
|
||||
|
||||
let b = VaultCore::new_in_memory();
|
||||
connect(&b, &server);
|
||||
b.login(PW.into(), sk, None).await.unwrap();
|
||||
b.login(PW.into(), sk, None, None).await.unwrap();
|
||||
b.sync_now().await.unwrap();
|
||||
assert_eq!(b.list_items().unwrap().len(), 1);
|
||||
|
||||
@@ -378,7 +378,7 @@ async fn phone_with_item(server: &TestServer) -> (Arc<VaultCore>, String, String
|
||||
let a = VaultCore::new_in_memory();
|
||||
let sk = a.create_account(PW.into()).unwrap();
|
||||
connect(&a, server);
|
||||
a.register_account(SETUP_TOKEN.into()).await.unwrap();
|
||||
a.register_account(SETUP_TOKEN.into(), None).await.unwrap();
|
||||
let id = a.save_item(login_item("Bank", "pw-a")).unwrap();
|
||||
a.sync_now().await.unwrap();
|
||||
server.state().require_approval = true;
|
||||
@@ -574,7 +574,7 @@ async fn revoked_device_resets_and_reenrols_with_the_same_ticket() {
|
||||
phone.configure_server(server.url(), vec![]).unwrap();
|
||||
phone.set_device_signer(signer.clone(), "iPhone".into(), PlatformDto::Ios).unwrap();
|
||||
let old_id = signer.device_id();
|
||||
assert_eq!(phone.login(PW.into(), sk.clone(), None).await.unwrap(), LoginResult::PendingApproval);
|
||||
assert_eq!(phone.login(PW.into(), sk.clone(), None, None).await.unwrap(), LoginResult::PendingApproval);
|
||||
a.revoke_device(old_id.clone()).await.unwrap();
|
||||
|
||||
// Reinstall: a new core, the same platform identity. A wrong password reveals nothing and resets nothing.
|
||||
@@ -615,7 +615,7 @@ async fn revoked_device_without_a_ticket_resets_and_asks_to_set_up_again() {
|
||||
let b = VaultCore::new_in_memory();
|
||||
b.configure_server(server.url(), vec![]).unwrap();
|
||||
b.set_device_signer(signer.clone(), "laptop".into(), PlatformDto::Linux).unwrap();
|
||||
assert_eq!(b.login(PW.into(), sk.clone(), None).await.unwrap(), LoginResult::Ok);
|
||||
assert_eq!(b.login(PW.into(), sk.clone(), None, None).await.unwrap(), LoginResult::Ok);
|
||||
b.sync_now().await.unwrap();
|
||||
let old_id = signer.device_id();
|
||||
|
||||
@@ -629,11 +629,11 @@ async fn revoked_device_without_a_ticket_resets_and_asks_to_set_up_again() {
|
||||
assert_eq!(signer.resets.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
|
||||
server.state().require_approval = true;
|
||||
assert_eq!(b.login(PW.into(), sk.clone(), None).await.unwrap(), LoginResult::PendingApproval);
|
||||
assert_eq!(b.login(PW.into(), sk.clone(), None, None).await.unwrap(), LoginResult::PendingApproval);
|
||||
let new_id = signer.device_id();
|
||||
assert_ne!(new_id, old_id);
|
||||
a.approve_device(new_id).await.unwrap();
|
||||
assert_eq!(b.login(PW.into(), sk, None).await.unwrap(), LoginResult::Ok);
|
||||
assert_eq!(b.login(PW.into(), sk, None, None).await.unwrap(), LoginResult::Ok);
|
||||
b.sync_now().await.unwrap();
|
||||
assert_eq!(b.get_item(id).unwrap().password.as_deref(), Some("pw-a"));
|
||||
// The old id can never come back.
|
||||
@@ -682,7 +682,7 @@ async fn sign_out_device_keeps_the_account_on_the_server() {
|
||||
assert_eq!(a.server_url(), None);
|
||||
connect(&a, &server);
|
||||
assert_eq!(a.server_url(), Some(server.url()));
|
||||
a.register_account(SETUP_TOKEN.into()).await.unwrap();
|
||||
a.register_account(SETUP_TOKEN.into(), None).await.unwrap();
|
||||
let id = a.save_item(login_item("Bank", "pw-a")).unwrap(); // not synced yet: sign-out syncs it first
|
||||
a.sign_out_device(false).await.unwrap();
|
||||
assert!(!a.has_account() && !a.is_logged_in());
|
||||
@@ -690,7 +690,7 @@ async fn sign_out_device_keeps_the_account_on_the_server() {
|
||||
|
||||
// Same device, signed in again: the item came back from the server.
|
||||
connect(&a, &server);
|
||||
assert_eq!(a.login(PW.into(), sk, None).await.unwrap(), LoginResult::Ok);
|
||||
assert_eq!(a.login(PW.into(), sk, None, None).await.unwrap(), LoginResult::Ok);
|
||||
a.sync_now().await.unwrap();
|
||||
assert_eq!(a.get_item(id).unwrap().password.as_deref(), Some("pw-a"));
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ async fn two_devices_against_local_server() {
|
||||
let sk = a.create_account(pw.into()).unwrap();
|
||||
connect(&a, &url, "device A");
|
||||
assert!(a.server_info().await.unwrap().setup_open, "server must be fresh (setup open)");
|
||||
a.register_account(token).await.unwrap();
|
||||
a.register_account(token, None).await.unwrap();
|
||||
let id = a.save_item(item("Bank", "pw-a")).unwrap();
|
||||
let r = a.sync_now().await.unwrap();
|
||||
assert_eq!(r.pushed, 1);
|
||||
@@ -79,13 +79,13 @@ async fn two_devices_against_local_server() {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(61)).await;
|
||||
let b = VaultCore::new_in_memory();
|
||||
connect(&b, &url, "device B");
|
||||
if b.login(pw.into(), sk.clone(), None).await.unwrap() == LoginResult::PendingApproval {
|
||||
if b.login(pw.into(), sk.clone(), None, None).await.unwrap() == LoginResult::PendingApproval {
|
||||
let devices = a.list_devices().await.unwrap();
|
||||
let pending = devices.iter().find(|d| !d.approved).expect("B listed as pending");
|
||||
assert_eq!(pending.key_fingerprint, b.device_key_fingerprint().unwrap());
|
||||
a.approve_device(pending.device_id.clone()).await.unwrap();
|
||||
tokio::time::sleep(std::time::Duration::from_secs(61)).await; // rate limit again
|
||||
assert_eq!(b.login(pw.into(), sk.clone(), None).await.unwrap(), LoginResult::Ok);
|
||||
assert_eq!(b.login(pw.into(), sk.clone(), None, None).await.unwrap(), LoginResult::Ok);
|
||||
}
|
||||
b.sync_now().await.unwrap();
|
||||
assert_eq!(b.get_item(id.clone()).unwrap().password.as_deref(), Some("pw-a"));
|
||||
@@ -167,7 +167,7 @@ async fn removed_device_reenrols_against_local_server() {
|
||||
let web = VaultCore::new_in_memory();
|
||||
web.create_account(pw.into()).unwrap();
|
||||
connect(&web, &url, "web vault");
|
||||
web.register_account(token).await.unwrap();
|
||||
web.register_account(token, None).await.unwrap();
|
||||
let id = web.save_item(item("Bank", "pw-a")).unwrap();
|
||||
web.sync_now().await.unwrap();
|
||||
|
||||
@@ -206,7 +206,7 @@ async fn delete_account_against_local_server() {
|
||||
let app = VaultCore::new_in_memory();
|
||||
app.create_account(pw.into()).unwrap();
|
||||
connect(&app, &url, "phone");
|
||||
app.register_account(token.clone()).await.unwrap();
|
||||
app.register_account(token.clone(), None).await.unwrap();
|
||||
app.save_item(item("Bank", "pw-a")).unwrap();
|
||||
app.sync_now().await.unwrap();
|
||||
|
||||
@@ -217,6 +217,55 @@ async fn delete_account_against_local_server() {
|
||||
let fresh = VaultCore::new_in_memory();
|
||||
fresh.create_account(pw.into()).unwrap();
|
||||
connect(&fresh, &url, "new phone");
|
||||
fresh.register_account(token).await.unwrap();
|
||||
fresh.register_account(token, None).await.unwrap();
|
||||
println!("account deleted and server reusable OK against {url}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "needs a fresh local kestrove-server; see the module docs"]
|
||||
async fn two_users_on_one_server() {
|
||||
let url = std::env::var("KESTROVE_SERVER_URL").unwrap_or_else(|_| "http://127.0.0.1:8080".into());
|
||||
let token =
|
||||
std::env::var("KESTROVE_SETUP_TOKEN").expect("set KESTROVE_SETUP_TOKEN to the token the server printed");
|
||||
let (pw_a, pw_b) = ("the admin's long master password", "the second user's long master password");
|
||||
|
||||
// The admin: first account, with the setup code.
|
||||
let admin = VaultCore::new_in_memory();
|
||||
admin.create_account(pw_a.into()).unwrap();
|
||||
connect(&admin, &url, "admin laptop");
|
||||
admin.register_account(token, Some("[email protected]".into())).await.unwrap();
|
||||
let details = admin.account_details().await.unwrap();
|
||||
assert_eq!((details.email.as_deref(), details.is_admin), (Some("[email protected]"), true));
|
||||
admin.save_item(item("Admin bank", "admin-secret")).unwrap();
|
||||
admin.sync_now().await.unwrap();
|
||||
|
||||
// A second user, invited by the admin.
|
||||
let invite = admin.admin_create_invite(Some("[email protected]".into()), None).await.unwrap();
|
||||
let b = VaultCore::new_in_memory();
|
||||
let sk_b = b.create_account(pw_b.into()).unwrap();
|
||||
connect(&b, &url, "b phone");
|
||||
b.register_account(invite.code.clone(), Some("[email protected]".into())).await.unwrap();
|
||||
assert!(!b.account_details().await.unwrap().is_admin);
|
||||
let b_item = b.save_item(item("B mail", "b-secret")).unwrap();
|
||||
b.sync_now().await.unwrap();
|
||||
// The invite is used up.
|
||||
let c = VaultCore::new_in_memory();
|
||||
c.create_account("someone else's password!!".into()).unwrap();
|
||||
connect(&c, &url, "c");
|
||||
assert!(c.register_account(invite.code, Some("[email protected]".into())).await.is_err());
|
||||
|
||||
// Neither sees the other's items after syncing.
|
||||
admin.sync_now().await.unwrap();
|
||||
let titles = |core: &VaultCore| core.list_items().unwrap().into_iter().map(|i| i.title).collect::<Vec<_>>();
|
||||
assert_eq!(titles(&admin), vec!["Admin bank".to_string()]);
|
||||
assert_eq!(titles(&b), vec!["B mail".to_string()]);
|
||||
|
||||
// B's new device signs in with email + password + Secret Key and gets only B's vault (after approval).
|
||||
let laptop = VaultCore::new_in_memory();
|
||||
connect(&laptop, &url, "b laptop");
|
||||
let r = laptop.login(pw_b.into(), sk_b, None, Some("[email protected]".into())).await.unwrap();
|
||||
assert_eq!(r, LoginResult::PendingApproval);
|
||||
let pending = b.list_devices().await.unwrap().into_iter().find(|d| !d.approved).unwrap();
|
||||
b.approve_device(pending.device_id).await.unwrap();
|
||||
println!("two users on {url}: isolated OK ({b_item})");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user