Skip to content

Commit d6fb384

Browse files
committed
add logins store api methods for bulk insert and meta insert
The a few new method ``` fn add_with_record(&self, entry_with_record: LoginEntryWithRecordFields); fn add_many_with_records(&self, entries_with_records: Vec<LoginEntryWithRecordFields>); fn add_many(&self, entries: Vec<LoginEntry>); ``` is intended to be used during migration on desktop.
1 parent 2407f72 commit d6fb384

File tree

6 files changed

+295
-24
lines changed

6 files changed

+295
-24
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
- `RemoteSettingsService::sync` is now more efficient. It checks the remote settings changes
1919
endpoint and only syncs collections that have been modified since the last sync.
2020

21+
### Logins
22+
- new method `add_with_record(&self, entry: LoginEntry, record: RecordFields)`
23+
to be used during migration from different store.
24+
2125
## 🔧 What's Fixed 🔧
2226

2327
### Remote Settings

components/logins/src/db.rs

Lines changed: 206 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,74 @@ impl LoginDb {
351351
Ok(())
352352
}
353353

354+
/// Adds multiple logins within a single transaction and returns the successfully saved logins.
355+
pub fn add_many(
356+
&self,
357+
entries: Vec<LoginEntry>,
358+
encdec: &dyn EncryptorDecryptor,
359+
) -> Result<Vec<Result<EncryptedLogin>>> {
360+
let now_ms = util::system_time_ms_i64(SystemTime::now());
361+
362+
let entries_with_records = entries
363+
.into_iter()
364+
.map(|entry| {
365+
let guid = Guid::random();
366+
LoginEntryWithRecordFields {
367+
entry,
368+
record: RecordFields {
369+
id: guid.to_string(),
370+
time_created: now_ms,
371+
time_password_changed: now_ms,
372+
time_last_used: now_ms,
373+
times_used: 1,
374+
}
375+
}
376+
})
377+
.collect();
378+
379+
self.add_many_with_records(entries_with_records, encdec)
380+
}
381+
382+
/// Adds multiple logins with records within a single transaction and returns the successfully saved logins.
383+
pub fn add_many_with_records(
384+
&self,
385+
entries_with_records: Vec<LoginEntryWithRecordFields>,
386+
encdec: &dyn EncryptorDecryptor,
387+
) -> Result<Vec<Result<EncryptedLogin>>> {
388+
let tx = self.unchecked_transaction()?;
389+
let mut results = vec![];
390+
for entry_with_record in entries_with_records {
391+
let guid = Guid::from_string(entry_with_record.record.id.clone());
392+
match self.fixup_and_check_for_dupes(&guid, entry_with_record.entry, encdec) {
393+
Ok(new_entry) => {
394+
let sec_fields = SecureLoginFields {
395+
username: new_entry.username,
396+
password: new_entry.password,
397+
};
398+
let encrypted_login = EncryptedLogin {
399+
record: entry_with_record.record,
400+
fields: LoginFields {
401+
origin: new_entry.origin,
402+
form_action_origin: new_entry.form_action_origin,
403+
http_realm: new_entry.http_realm,
404+
username_field: new_entry.username_field,
405+
password_field: new_entry.password_field,
406+
},
407+
sec_fields: sec_fields.encrypt(encdec)?,
408+
};
409+
let result = self
410+
.insert_new_login(&encrypted_login)
411+
.map(|_| encrypted_login);
412+
results.push(result);
413+
}
414+
415+
Err(error) => results.push(Err(error)),
416+
}
417+
}
418+
tx.commit()?;
419+
Ok(results)
420+
}
421+
354422
pub fn add(
355423
&self,
356424
entry: LoginEntry,
@@ -359,32 +427,27 @@ impl LoginDb {
359427
let guid = Guid::random();
360428
let now_ms = util::system_time_ms_i64(SystemTime::now());
361429

362-
let new_entry = self.fixup_and_check_for_dupes(&guid, entry, encdec)?;
363-
let sec_fields = SecureLoginFields {
364-
username: new_entry.username,
365-
password: new_entry.password,
366-
};
367-
let result = EncryptedLogin {
430+
let entry_with_record = LoginEntryWithRecordFields {
431+
entry,
368432
record: RecordFields {
369433
id: guid.to_string(),
370434
time_created: now_ms,
371435
time_password_changed: now_ms,
372436
time_last_used: now_ms,
373437
times_used: 1,
374-
},
375-
fields: LoginFields {
376-
origin: new_entry.origin,
377-
form_action_origin: new_entry.form_action_origin,
378-
http_realm: new_entry.http_realm,
379-
username_field: new_entry.username_field,
380-
password_field: new_entry.password_field,
381-
},
382-
sec_fields: sec_fields.encrypt(encdec)?,
438+
}
383439
};
384-
let tx = self.unchecked_transaction()?;
385-
self.insert_new_login(&result)?;
386-
tx.commit()?;
387-
Ok(result)
440+
441+
self.add_with_record(entry_with_record, encdec)
442+
}
443+
444+
pub fn add_with_record(
445+
&self,
446+
entry_with_record: LoginEntryWithRecordFields,
447+
encdec: &dyn EncryptorDecryptor,
448+
) -> Result<EncryptedLogin> {
449+
let mut results = self.add_many_with_records(vec![entry_with_record], encdec)?;
450+
results.pop().expect("there should be a single result")
388451
}
389452

390453
pub fn update(
@@ -988,6 +1051,130 @@ mod tests {
9881051
assert_eq!(db.get_all().unwrap().len(), 2);
9891052
}
9901053

1054+
#[test]
1055+
fn test_add_many() {
1056+
ensure_initialized();
1057+
1058+
let login_a = LoginEntry {
1059+
origin: "https://a.example.com".into(),
1060+
http_realm: Some("https://www.example.com".into()),
1061+
username: "test".into(),
1062+
password: "sekret".into(),
1063+
..LoginEntry::default()
1064+
};
1065+
1066+
let login_b = LoginEntry {
1067+
origin: "https://b.example.com".into(),
1068+
http_realm: Some("https://www.example.com".into()),
1069+
username: "test".into(),
1070+
password: "sekret".into(),
1071+
..LoginEntry::default()
1072+
};
1073+
1074+
let db = LoginDb::open_in_memory().unwrap();
1075+
let added = db
1076+
.add_many(vec![login_a.clone(), login_b.clone()], &*TEST_ENCDEC)
1077+
.expect("should be able to add logins");
1078+
1079+
let [added_a, added_b] = added.as_slice() else {
1080+
panic!("there should really be 2")
1081+
};
1082+
1083+
let fetched_a = db
1084+
.get_by_id(&added_a.as_ref().unwrap().record.id)
1085+
.expect("should work")
1086+
.expect("should get a record");
1087+
1088+
assert_eq!(fetched_a.fields.origin, login_a.origin);
1089+
1090+
let fetched_b = db
1091+
.get_by_id(&added_b.as_ref().unwrap().record.id)
1092+
.expect("should work")
1093+
.expect("should get a record");
1094+
1095+
assert_eq!(fetched_b.fields.origin, login_b.origin);
1096+
}
1097+
1098+
#[test]
1099+
fn test_add_many_with_failed_constraint() {
1100+
ensure_initialized();
1101+
1102+
let login_a = LoginEntry {
1103+
origin: "https://example.com".into(),
1104+
http_realm: Some("https://www.example.com".into()),
1105+
username: "test".into(),
1106+
password: "sekret".into(),
1107+
..LoginEntry::default()
1108+
};
1109+
1110+
let login_b = LoginEntry {
1111+
// same origin will result in duplicate error
1112+
origin: "https://example.com".into(),
1113+
http_realm: Some("https://www.example.com".into()),
1114+
username: "test".into(),
1115+
password: "sekret".into(),
1116+
..LoginEntry::default()
1117+
};
1118+
1119+
let db = LoginDb::open_in_memory().unwrap();
1120+
let added = db
1121+
.add_many(vec![login_a.clone(), login_b.clone()], &*TEST_ENCDEC)
1122+
.expect("should be able to add logins");
1123+
1124+
let [added_a, added_b] = added.as_slice() else {
1125+
panic!("there should really be 2")
1126+
};
1127+
1128+
// first entry has been saved successfully
1129+
let fetched_a = db
1130+
.get_by_id(&added_a.as_ref().unwrap().record.id)
1131+
.expect("should work")
1132+
.expect("should get a record");
1133+
1134+
assert_eq!(fetched_a.fields.origin, login_a.origin);
1135+
1136+
// second entry failed
1137+
assert!(!added_b.is_ok());
1138+
}
1139+
1140+
#[test]
1141+
fn test_add_with_record() {
1142+
ensure_initialized();
1143+
1144+
let guid = Guid::random();
1145+
let now_ms = util::system_time_ms_i64(SystemTime::now());
1146+
let login = LoginEntry {
1147+
origin: "https://www.example.com".into(),
1148+
http_realm: Some("https://www.example.com".into()),
1149+
username: "test".into(),
1150+
password: "sekret".into(),
1151+
..LoginEntry::default()
1152+
};
1153+
let record = RecordFields {
1154+
id: guid.to_string(),
1155+
time_created: now_ms,
1156+
time_password_changed: now_ms + 100,
1157+
time_last_used: now_ms + 10,
1158+
times_used: 42,
1159+
};
1160+
1161+
let db = LoginDb::open_in_memory().unwrap();
1162+
let entry_with_record = LoginEntryWithRecordFields {
1163+
entry: login.clone(),
1164+
record: record.clone(),
1165+
};
1166+
let added = db
1167+
.add_with_record(entry_with_record, &*TEST_ENCDEC)
1168+
.expect("should be able to add login with record");
1169+
1170+
let fetched = db
1171+
.get_by_id(&added.record.id)
1172+
.expect("should work")
1173+
.expect("should get a record");
1174+
1175+
assert_eq!(fetched.record, record);
1176+
}
1177+
9911178
#[test]
9921179
fn test_unicode_submit() {
9931180
ensure_initialized();

components/logins/src/encryption.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -402,10 +402,8 @@ mod test {
402402
key: create_key().unwrap(),
403403
}),
404404
};
405-
assert!(matches!(
406-
other_encdec.decrypt(ciphertext).err().unwrap(),
407-
LoginsApiError::DecryptionFailed { reason: _ }
408-
));
405+
406+
assert_eq!(other_encdec.decrypt(ciphertext).err().unwrap().to_string(), "decryption failed: Crypto error: NSS error: NSS error: -8190 (decrypt SecureLoginFields)");
409407
}
410408

411409
#[test]

components/logins/src/login.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,13 @@ pub struct RecordFields {
342342
pub times_used: i64,
343343
}
344344

345+
/// A login together with record fields, handed over to the store API; ie a login persisted
346+
/// elsewhere, useful for migrations
347+
pub struct LoginEntryWithRecordFields {
348+
pub entry: LoginEntry,
349+
pub record: RecordFields,
350+
}
351+
345352
/// A login handed over to the store API; ie a login not yet persisted
346353
#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)]
347354
pub struct LoginEntry {

components/logins/src/logins.udl

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,23 @@ dictionary LoginEntry {
4848
string username;
4949
};
5050

51+
/// Login data specific to database records.
52+
/// The add_with_record API inputs this.
53+
dictionary RecordFields {
54+
string id;
55+
i64 times_used;
56+
i64 time_created;
57+
i64 time_last_used;
58+
i64 time_password_changed;
59+
};
60+
61+
/// A login together with record fields, handed over to the store API; ie a login persisted
62+
/// elsewhere, useful for migrations
63+
dictionary LoginEntryWithRecordFields {
64+
LoginEntry entry;
65+
RecordFields record;
66+
};
67+
5168
/// A login stored in the database
5269
dictionary Login {
5370
// record fields
@@ -146,6 +163,15 @@ interface LoginStore {
146163
[Throws=LoginsApiError]
147164
Login add(LoginEntry login);
148165

166+
[Throws=LoginsApiError]
167+
sequence<Login?> add_many(sequence<LoginEntry> logins);
168+
169+
[Throws=LoginsApiError]
170+
Login add_with_record(LoginEntryWithRecordFields entry_with_record);
171+
172+
[Throws=LoginsApiError]
173+
sequence<Login?> add_many_with_records(sequence<LoginEntryWithRecordFields> entries_with_records);
174+
149175
[Throws=LoginsApiError]
150176
Login update([ByRef] string id, LoginEntry login);
151177

0 commit comments

Comments
 (0)