1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 23:33:31 +00:00

Pm-10953/add-user-context-to-sync-replaces (#10627)

* Require userId for setting masterKeyEncryptedUserKey

* Replace folders for specified user

* Require userId for collection replace

* Cipher Replace requires userId

* Require UserId to update equivalent domains

* Require userId for policy replace

* sync state updates between fake state for better testing

* Revert to public observable tests

Since they now sync, we can test single-user updates impacting active user observables

* Do not init fake states through sync

Do not sync initial null values, that might wipe out already existing data.

* Require userId for Send replace

* Include userId for organization replace

* Require userId for billing sync data

* Require user Id for key connector sync data

* Allow decode of token by userId

* Require userId for synced key connector updates

* Add userId to policy setting during organization invite accept

* Fix cli

* Handle null userId

---------

Co-authored-by: bnagawiecki <107435978+bnagawiecki@users.noreply.github.com>
This commit is contained in:
Matt Gibson
2024-08-26 17:44:08 -07:00
committed by GitHub
parent 866a624e44
commit 9459cda304
46 changed files with 666 additions and 484 deletions

View File

@@ -143,7 +143,7 @@ export abstract class CryptoService {
* @param userKeyMasterKey The master key encrypted user key to set
* @param userId The desired user
*/
abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId?: string): Promise<void>;
abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId: string): Promise<void>;
/**
* @param password The user's master password that will be used to derive a master key if one isn't found
* @param userId The desired user

View File

@@ -119,7 +119,7 @@ describe("BiometricStateService", () => {
describe("getRequirePasswordOnStart", () => {
it("returns the requirePasswordOnStart state value", async () => {
stateProvider.singleUser.mockFor(userId, REQUIRE_PASSWORD_ON_START.key, true);
stateProvider.singleUser.mockFor(userId, REQUIRE_PASSWORD_ON_START, true);
expect(await sut.getRequirePasswordOnStart(userId)).toBe(true);
});

View File

@@ -365,9 +365,9 @@ describe("cryptoService", () => {
const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY);
const fakeMasterKey = makeMasterKey ? makeSymmetricCryptoKey<MasterKey>(64) : null;
masterPasswordService.masterKeySubject.next(fakeMasterKey);
userKeyState.stateSubject.next([mockUserId, null]);
userKeyState.nextState(null);
const fakeUserKey = makeUserKey ? makeSymmetricCryptoKey<UserKey>(64) : null;
userKeyState.stateSubject.next([mockUserId, fakeUserKey]);
userKeyState.nextState(fakeUserKey);
return [fakeUserKey, fakeMasterKey];
}
@@ -384,10 +384,7 @@ describe("cryptoService", () => {
const fakeEncryptedUserPrivateKey = makeEncString("1");
userEncryptedPrivateKeyState.stateSubject.next([
mockUserId,
fakeEncryptedUserPrivateKey.encryptedString,
]);
userEncryptedPrivateKeyState.nextState(fakeEncryptedUserPrivateKey.encryptedString);
// Decryption of the user private key
const fakeDecryptedUserPrivateKey = makeStaticByteArray(10, 1);
@@ -423,7 +420,7 @@ describe("cryptoService", () => {
mockUserId,
USER_ENCRYPTED_PRIVATE_KEY,
);
encryptedUserPrivateKeyState.stateSubject.next([mockUserId, null]);
encryptedUserPrivateKeyState.nextState(null);
const userPrivateKey = await firstValueFrom(cryptoService.userPrivateKey$(mockUserId));
expect(userPrivateKey).toBeFalsy();
@@ -463,7 +460,7 @@ describe("cryptoService", () => {
function updateKeys(keys: Partial<UpdateKeysParams> = {}) {
if ("userKey" in keys) {
const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY);
userKeyState.stateSubject.next([mockUserId, keys.userKey]);
userKeyState.nextState(keys.userKey);
}
if ("encryptedPrivateKey" in keys) {
@@ -471,10 +468,7 @@ describe("cryptoService", () => {
mockUserId,
USER_ENCRYPTED_PRIVATE_KEY,
);
userEncryptedPrivateKey.stateSubject.next([
mockUserId,
keys.encryptedPrivateKey.encryptedString,
]);
userEncryptedPrivateKey.nextState(keys.encryptedPrivateKey.encryptedString);
}
if ("orgKeys" in keys) {
@@ -482,7 +476,7 @@ describe("cryptoService", () => {
mockUserId,
USER_ENCRYPTED_ORGANIZATION_KEYS,
);
orgKeysState.stateSubject.next([mockUserId, keys.orgKeys]);
orgKeysState.nextState(keys.orgKeys);
}
if ("providerKeys" in keys) {
@@ -490,7 +484,7 @@ describe("cryptoService", () => {
mockUserId,
USER_ENCRYPTED_PROVIDER_KEYS,
);
providerKeysState.stateSubject.next([mockUserId, keys.providerKeys]);
providerKeysState.nextState(keys.providerKeys);
}
encryptService.decryptToBytes.mockImplementation((encryptedPrivateKey, userKey) => {

View File

@@ -225,7 +225,7 @@ export class CryptoService implements CryptoServiceAbstraction {
}
}
async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId?: UserId): Promise<void> {
async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId: UserId): Promise<void> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
await this.masterPasswordService.setMasterKeyEncryptedUserKey(
new EncString(userKeyMasterKey),

View File

@@ -143,7 +143,7 @@ describe("DefaultStateProvider", () => {
it("should not emit any values until a truthy user id is supplied", async () => {
accountService.activeAccountSubject.next(null);
const state = singleUserStateProvider.getFake(userId, keyDefinition);
state.stateSubject.next([userId, "value"]);
state.nextState("value");
const emissions = trackEmissions(sut.getUserState$(keyDefinition));

View File

@@ -124,12 +124,12 @@ export class DefaultSyncService extends CoreSyncService {
const response = await this.apiService.getSync();
await this.syncProfile(response.profile);
await this.syncFolders(response.folders);
await this.syncCollections(response.collections);
await this.syncCiphers(response.ciphers);
await this.syncSends(response.sends);
await this.syncSettings(response.domains);
await this.syncPolicies(response.policies);
await this.syncFolders(response.folders, response.profile.id);
await this.syncCollections(response.collections, response.profile.id);
await this.syncCiphers(response.ciphers, response.profile.id);
await this.syncSends(response.sends, response.profile.id);
await this.syncSettings(response.domains, response.profile.id);
await this.syncPolicies(response.policies, response.profile.id);
await this.setLastSync(now, userId);
return this.syncCompleted(true);
@@ -190,8 +190,9 @@ export class DefaultSyncService extends CoreSyncService {
await this.billingAccountProfileStateService.setHasPremium(
response.premiumPersonally,
response.premiumFromOrganization,
response.id,
);
await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector);
await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector, response.id);
await this.setForceSetPasswordReasonIfNeeded(response);
@@ -200,17 +201,17 @@ export class DefaultSyncService extends CoreSyncService {
providers[p.id] = new ProviderData(p);
});
await this.providerService.save(providers);
await this.providerService.save(providers, response.id);
await this.syncProfileOrganizations(response);
await this.syncProfileOrganizations(response, response.id);
if (await this.keyConnectorService.userNeedsMigration()) {
await this.keyConnectorService.setConvertAccountRequired(true);
if (await this.keyConnectorService.userNeedsMigration(response.id)) {
await this.keyConnectorService.setConvertAccountRequired(true, response.id);
this.messageSender.send("convertAccountToKeyConnector");
} else {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.keyConnectorService.removeConvertAccountRequired();
this.keyConnectorService.removeConvertAccountRequired(response.id);
}
}
@@ -261,7 +262,7 @@ export class DefaultSyncService extends CoreSyncService {
}
}
private async syncProfileOrganizations(response: ProfileResponse) {
private async syncProfileOrganizations(response: ProfileResponse, userId: UserId) {
const organizations: { [id: string]: OrganizationData } = {};
response.organizations.forEach((o) => {
organizations[o.id] = new OrganizationData(o, {
@@ -281,42 +282,42 @@ export class DefaultSyncService extends CoreSyncService {
}
});
await this.organizationService.replace(organizations);
await this.organizationService.replace(organizations, userId);
}
private async syncFolders(response: FolderResponse[]) {
private async syncFolders(response: FolderResponse[], userId: UserId) {
const folders: { [id: string]: FolderData } = {};
response.forEach((f) => {
folders[f.id] = new FolderData(f);
});
return await this.folderService.replace(folders);
return await this.folderService.replace(folders, userId);
}
private async syncCollections(response: CollectionDetailsResponse[]) {
private async syncCollections(response: CollectionDetailsResponse[], userId: UserId) {
const collections: { [id: string]: CollectionData } = {};
response.forEach((c) => {
collections[c.id] = new CollectionData(c);
});
return await this.collectionService.replace(collections);
return await this.collectionService.replace(collections, userId);
}
private async syncCiphers(response: CipherResponse[]) {
private async syncCiphers(response: CipherResponse[], userId: UserId) {
const ciphers: { [id: string]: CipherData } = {};
response.forEach((c) => {
ciphers[c.id] = new CipherData(c);
});
return await this.cipherService.replace(ciphers);
return await this.cipherService.replace(ciphers, userId);
}
private async syncSends(response: SendResponse[]) {
private async syncSends(response: SendResponse[], userId: UserId) {
const sends: { [id: string]: SendData } = {};
response.forEach((s) => {
sends[s.id] = new SendData(s);
});
return await this.sendService.replace(sends);
return await this.sendService.replace(sends, userId);
}
private async syncSettings(response: DomainsResponse) {
private async syncSettings(response: DomainsResponse, userId: UserId) {
let eqDomains: string[][] = [];
if (response != null && response.equivalentDomains != null) {
eqDomains = eqDomains.concat(response.equivalentDomains);
@@ -330,16 +331,16 @@ export class DefaultSyncService extends CoreSyncService {
});
}
return this.domainSettingsService.setEquivalentDomains(eqDomains);
return this.domainSettingsService.setEquivalentDomains(eqDomains, userId);
}
private async syncPolicies(response: PolicyResponse[]) {
private async syncPolicies(response: PolicyResponse[], userId: UserId) {
const policies: { [id: string]: PolicyData } = {};
if (response != null) {
response.forEach((p) => {
policies[p.id] = new PolicyData(p);
});
}
return await this.policyService.replace(policies);
return await this.policyService.replace(policies, userId);
}
}