mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +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:
@@ -1,15 +1,19 @@
|
||||
import { Observable } from "rxjs";
|
||||
import type { Simplify } from "type-fest";
|
||||
|
||||
import { CombinedState } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { SendData } from "../models/data/send.data";
|
||||
import { SendView } from "../models/view/send.view";
|
||||
|
||||
type EncryptedSendState = Simplify<CombinedState<Record<string, SendData>>>;
|
||||
export abstract class SendStateProvider {
|
||||
encryptedState$: Observable<Record<string, SendData>>;
|
||||
encryptedState$: Observable<EncryptedSendState>;
|
||||
decryptedState$: Observable<SendView[]>;
|
||||
|
||||
getEncryptedSends: () => Promise<{ [id: string]: SendData }>;
|
||||
getEncryptedSends: () => Promise<EncryptedSendState>;
|
||||
|
||||
setEncryptedSends: (value: { [id: string]: SendData }) => Promise<void>;
|
||||
setEncryptedSends: (value: { [id: string]: SendData }, userId: UserId) => Promise<void>;
|
||||
|
||||
getDecryptedSends: () => Promise<SendView[]>;
|
||||
|
||||
|
||||
@@ -27,11 +27,11 @@ describe("Send State Provider", () => {
|
||||
describe("Encrypted Sends", () => {
|
||||
it("should return SendData", async () => {
|
||||
const sendData = { "1": testSendData("1", "Test Send Data") };
|
||||
await sendStateProvider.setEncryptedSends(sendData);
|
||||
await sendStateProvider.setEncryptedSends(sendData, mockUserId);
|
||||
await awaitAsync();
|
||||
|
||||
const actual = await sendStateProvider.getEncryptedSends();
|
||||
expect(actual).toStrictEqual(sendData);
|
||||
expect(actual).toStrictEqual([mockUserId, sendData]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Observable, firstValueFrom } from "rxjs";
|
||||
|
||||
import { ActiveUserState, StateProvider } from "../../../platform/state";
|
||||
import { ActiveUserState, CombinedState, StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { SendData } from "../models/data/send.data";
|
||||
import { SendView } from "../models/view/send.view";
|
||||
|
||||
@@ -10,7 +11,7 @@ import { SendStateProvider as SendStateProviderAbstraction } from "./send-state.
|
||||
/** State provider for sends */
|
||||
export class SendStateProvider implements SendStateProviderAbstraction {
|
||||
/** Observable for the encrypted sends for an active user */
|
||||
encryptedState$: Observable<Record<string, SendData>>;
|
||||
encryptedState$: Observable<CombinedState<Record<string, SendData>>>;
|
||||
/** Observable with the decrypted sends for an active user */
|
||||
decryptedState$: Observable<SendView[]>;
|
||||
|
||||
@@ -19,20 +20,20 @@ export class SendStateProvider implements SendStateProviderAbstraction {
|
||||
|
||||
constructor(protected stateProvider: StateProvider) {
|
||||
this.activeUserEncryptedState = this.stateProvider.getActive(SEND_USER_ENCRYPTED);
|
||||
this.encryptedState$ = this.activeUserEncryptedState.state$;
|
||||
this.encryptedState$ = this.activeUserEncryptedState.combinedState$;
|
||||
|
||||
this.activeUserDecryptedState = this.stateProvider.getActive(SEND_USER_DECRYPTED);
|
||||
this.decryptedState$ = this.activeUserDecryptedState.state$;
|
||||
}
|
||||
|
||||
/** Gets the encrypted sends from state for an active user */
|
||||
async getEncryptedSends(): Promise<{ [id: string]: SendData }> {
|
||||
async getEncryptedSends(): Promise<CombinedState<{ [id: string]: SendData }>> {
|
||||
return await firstValueFrom(this.encryptedState$);
|
||||
}
|
||||
|
||||
/** Sets the encrypted send state for an active user */
|
||||
async setEncryptedSends(value: { [id: string]: SendData }): Promise<void> {
|
||||
await this.activeUserEncryptedState.update(() => value);
|
||||
async setEncryptedSends(value: { [id: string]: SendData }, userId: UserId): Promise<void> {
|
||||
await this.stateProvider.getUser(userId, SEND_USER_ENCRYPTED).update(() => value);
|
||||
}
|
||||
|
||||
/** Gets the decrypted sends from state for the active user */
|
||||
|
||||
@@ -55,6 +55,6 @@ export abstract class SendService implements UserKeyRotationDataProvider<SendWit
|
||||
|
||||
export abstract class InternalSendService extends SendService {
|
||||
upsert: (send: SendData | SendData[]) => Promise<any>;
|
||||
replace: (sends: { [id: string]: SendData }) => Promise<void>;
|
||||
replace: (sends: { [id: string]: SendData }, userId: UserId) => Promise<void>;
|
||||
delete: (id: string | string[]) => Promise<any>;
|
||||
}
|
||||
|
||||
@@ -110,9 +110,12 @@ describe("SendService", () => {
|
||||
const result = await firstValueFrom(singleSendObservable);
|
||||
expect(result).toEqual(testSend("1", "Test Send"));
|
||||
|
||||
await sendService.replace({
|
||||
"1": testSendData("1", "Test Send Updated"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": testSendData("1", "Test Send Updated"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
const result2 = await firstValueFrom(singleSendObservable);
|
||||
expect(result2).toEqual(testSend("1", "Test Send Updated"));
|
||||
@@ -127,10 +130,13 @@ describe("SendService", () => {
|
||||
|
||||
//it is immediately called when subscribed, we need to reset the value
|
||||
changed = false;
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
expect(changed).toEqual(true);
|
||||
});
|
||||
@@ -138,10 +144,13 @@ describe("SendService", () => {
|
||||
it("reports a change when notes changes on a new send", async () => {
|
||||
const sendDataObject = createSendData() as SendData;
|
||||
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
let changed = false;
|
||||
sendService.get$("1").subscribe(() => {
|
||||
@@ -152,10 +161,13 @@ describe("SendService", () => {
|
||||
//it is immediately called when subscribed, we need to reset the value
|
||||
changed = false;
|
||||
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
expect(changed).toEqual(true);
|
||||
});
|
||||
@@ -163,10 +175,13 @@ describe("SendService", () => {
|
||||
it("reports a change when Text changes on a new send", async () => {
|
||||
const sendDataObject = createSendData() as SendData;
|
||||
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
let changed = false;
|
||||
sendService.get$("1").subscribe(() => {
|
||||
@@ -177,10 +192,13 @@ describe("SendService", () => {
|
||||
changed = false;
|
||||
|
||||
sendDataObject.text.text = "new text";
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
expect(changed).toEqual(true);
|
||||
});
|
||||
@@ -188,10 +206,13 @@ describe("SendService", () => {
|
||||
it("reports a change when Text is set as null on a new send", async () => {
|
||||
const sendDataObject = createSendData() as SendData;
|
||||
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
let changed = false;
|
||||
sendService.get$("1").subscribe(() => {
|
||||
@@ -202,10 +223,13 @@ describe("SendService", () => {
|
||||
changed = false;
|
||||
|
||||
sendDataObject.text = null;
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
expect(changed).toEqual(true);
|
||||
});
|
||||
@@ -215,10 +239,13 @@ describe("SendService", () => {
|
||||
type: SendType.File,
|
||||
file: new SendFileData(new SendFileApi({ FileName: "name of file" })),
|
||||
}) as SendData;
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
sendDataObject.file = new SendFileData(new SendFileApi({ FileName: "updated name of file" }));
|
||||
let changed = false;
|
||||
@@ -229,10 +256,13 @@ describe("SendService", () => {
|
||||
//it is immediately called when subscribed, we need to reset the value
|
||||
changed = false;
|
||||
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
expect(changed).toEqual(false);
|
||||
});
|
||||
@@ -240,10 +270,13 @@ describe("SendService", () => {
|
||||
it("reports a change when key changes on a new send", async () => {
|
||||
const sendDataObject = createSendData() as SendData;
|
||||
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
let changed = false;
|
||||
sendService.get$("1").subscribe(() => {
|
||||
@@ -254,10 +287,13 @@ describe("SendService", () => {
|
||||
changed = false;
|
||||
|
||||
sendDataObject.key = "newKey";
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
expect(changed).toEqual(true);
|
||||
});
|
||||
@@ -265,10 +301,13 @@ describe("SendService", () => {
|
||||
it("reports a change when revisionDate changes on a new send", async () => {
|
||||
const sendDataObject = createSendData() as SendData;
|
||||
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
let changed = false;
|
||||
sendService.get$("1").subscribe(() => {
|
||||
@@ -279,10 +318,13 @@ describe("SendService", () => {
|
||||
changed = false;
|
||||
|
||||
sendDataObject.revisionDate = "2025-04-05";
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
expect(changed).toEqual(true);
|
||||
});
|
||||
@@ -290,10 +332,13 @@ describe("SendService", () => {
|
||||
it("reports a change when a property is set as null on a new send", async () => {
|
||||
const sendDataObject = createSendData() as SendData;
|
||||
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
let changed = false;
|
||||
sendService.get$("1").subscribe(() => {
|
||||
@@ -304,10 +349,13 @@ describe("SendService", () => {
|
||||
changed = false;
|
||||
|
||||
sendDataObject.name = null;
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
expect(changed).toEqual(true);
|
||||
});
|
||||
@@ -317,10 +365,13 @@ describe("SendService", () => {
|
||||
text: new SendTextData(new SendTextApi({ Text: null })),
|
||||
}) as SendData;
|
||||
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
let changed = false;
|
||||
sendService.get$("1").subscribe(() => {
|
||||
@@ -330,23 +381,29 @@ describe("SendService", () => {
|
||||
//it is immediately called when subscribed, we need to reset the value
|
||||
changed = false;
|
||||
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
expect(changed).toEqual(false);
|
||||
|
||||
sendDataObject.text.text = "Asdf";
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
expect(changed).toEqual(true);
|
||||
});
|
||||
|
||||
it("do not reports a change when nothing changes on the observed send", async () => {
|
||||
it("do not report a change when nothing changes on the observed send", async () => {
|
||||
let changed = false;
|
||||
sendService.get$("1").subscribe(() => {
|
||||
changed = true;
|
||||
@@ -357,10 +414,13 @@ describe("SendService", () => {
|
||||
//it is immediately called when subscribed, we need to reset the value
|
||||
changed = false;
|
||||
|
||||
await sendService.replace({
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("3", "Test Send 3"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"1": sendDataObject,
|
||||
"2": testSendData("3", "Test Send 3"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
expect(changed).toEqual(false);
|
||||
});
|
||||
@@ -373,9 +433,12 @@ describe("SendService", () => {
|
||||
//it is immediately called when subscribed, we need to reset the value
|
||||
changed = false;
|
||||
|
||||
await sendService.replace({
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
});
|
||||
await sendService.replace(
|
||||
{
|
||||
"2": testSendData("2", "Test Send 2"),
|
||||
},
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
expect(changed).toEqual(true);
|
||||
});
|
||||
@@ -426,7 +489,7 @@ describe("SendService", () => {
|
||||
});
|
||||
|
||||
it("returns empty array if there are no sends", async () => {
|
||||
await sendService.replace(null);
|
||||
await sendService.replace(null, mockUserId);
|
||||
|
||||
await awaitAsync();
|
||||
|
||||
@@ -461,16 +524,11 @@ describe("SendService", () => {
|
||||
});
|
||||
|
||||
it("replace", async () => {
|
||||
await sendService.replace({ "2": testSendData("2", "test 2") });
|
||||
await sendService.replace({ "2": testSendData("2", "test 2") }, mockUserId);
|
||||
|
||||
expect(await firstValueFrom(sendService.sends$)).toEqual([testSend("2", "test 2")]);
|
||||
});
|
||||
|
||||
it("clear", async () => {
|
||||
await sendService.clear();
|
||||
await awaitAsync();
|
||||
expect(await firstValueFrom(sendService.sends$)).toEqual([]);
|
||||
});
|
||||
describe("Delete", () => {
|
||||
it("Sends count should decrease after delete", async () => {
|
||||
const sendsBeforeDelete = await firstValueFrom(sendService.sends$);
|
||||
@@ -488,7 +546,7 @@ describe("SendService", () => {
|
||||
});
|
||||
|
||||
it("Deleting on an empty sends array should not throw", async () => {
|
||||
sendStateProvider.getEncryptedSends = jest.fn().mockResolvedValue(null);
|
||||
stateProvider.activeUser.getFake(SEND_USER_ENCRYPTED).nextState(null);
|
||||
await expect(sendService.delete("2")).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
|
||||
@@ -28,10 +28,10 @@ export class SendService implements InternalSendServiceAbstraction {
|
||||
readonly sendKeyPurpose = "send";
|
||||
|
||||
sends$ = this.stateProvider.encryptedState$.pipe(
|
||||
map((record) => Object.values(record || {}).map((data) => new Send(data))),
|
||||
map(([, record]) => Object.values(record || {}).map((data) => new Send(data))),
|
||||
);
|
||||
sendViews$ = this.stateProvider.encryptedState$.pipe(
|
||||
concatMap((record) =>
|
||||
concatMap(([, record]) =>
|
||||
this.decryptSends(Object.values(record || {}).map((data) => new Send(data))),
|
||||
),
|
||||
);
|
||||
@@ -167,7 +167,7 @@ export class SendService implements InternalSendServiceAbstraction {
|
||||
}
|
||||
|
||||
async getFromState(id: string): Promise<Send> {
|
||||
const sends = await this.stateProvider.getEncryptedSends();
|
||||
const [, sends] = await this.stateProvider.getEncryptedSends();
|
||||
// eslint-disable-next-line
|
||||
if (sends == null || !sends.hasOwnProperty(id)) {
|
||||
return null;
|
||||
@@ -177,7 +177,7 @@ export class SendService implements InternalSendServiceAbstraction {
|
||||
}
|
||||
|
||||
async getAll(): Promise<Send[]> {
|
||||
const sends = await this.stateProvider.getEncryptedSends();
|
||||
const [, sends] = await this.stateProvider.getEncryptedSends();
|
||||
const response: Send[] = [];
|
||||
for (const id in sends) {
|
||||
// eslint-disable-next-line
|
||||
@@ -214,7 +214,8 @@ export class SendService implements InternalSendServiceAbstraction {
|
||||
}
|
||||
|
||||
async upsert(send: SendData | SendData[]): Promise<any> {
|
||||
let sends = await this.stateProvider.getEncryptedSends();
|
||||
const [userId, currentSends] = await this.stateProvider.getEncryptedSends();
|
||||
let sends = currentSends;
|
||||
if (sends == null) {
|
||||
sends = {};
|
||||
}
|
||||
@@ -227,16 +228,11 @@ export class SendService implements InternalSendServiceAbstraction {
|
||||
});
|
||||
}
|
||||
|
||||
await this.replace(sends);
|
||||
}
|
||||
|
||||
async clear(userId?: string): Promise<any> {
|
||||
await this.stateProvider.setDecryptedSends(null);
|
||||
await this.stateProvider.setEncryptedSends(null);
|
||||
await this.replace(sends, userId);
|
||||
}
|
||||
|
||||
async delete(id: string | string[]): Promise<any> {
|
||||
const sends = await this.stateProvider.getEncryptedSends();
|
||||
const [userId, sends] = await this.stateProvider.getEncryptedSends();
|
||||
if (sends == null) {
|
||||
return;
|
||||
}
|
||||
@@ -252,11 +248,11 @@ export class SendService implements InternalSendServiceAbstraction {
|
||||
});
|
||||
}
|
||||
|
||||
await this.replace(sends);
|
||||
await this.replace(sends, userId);
|
||||
}
|
||||
|
||||
async replace(sends: { [id: string]: SendData }): Promise<any> {
|
||||
await this.stateProvider.setEncryptedSends(sends);
|
||||
async replace(sends: { [id: string]: SendData }, userId: UserId): Promise<any> {
|
||||
await this.stateProvider.setEncryptedSends(sends, userId);
|
||||
}
|
||||
|
||||
async getRotatedData(
|
||||
|
||||
Reference in New Issue
Block a user