1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-25 12:15:18 +01: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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 666 additions and 484 deletions

View File

@ -342,7 +342,7 @@ export class LoginCommand {
}
}
return await this.handleSuccessResponse();
return await this.handleSuccessResponse(response);
} catch (e) {
return Response.error(e);
}
@ -353,8 +353,8 @@ export class LoginCommand {
process.env.BW_SESSION = Utils.fromBufferToB64(key);
}
private async handleSuccessResponse(): Promise<Response> {
const usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector();
private async handleSuccessResponse(response: AuthResult): Promise<Response> {
const usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector(response.userId);
if (
(this.options.sso != null || this.options.apikey != null) &&

View File

@ -73,6 +73,7 @@ export class UnlockCommand {
if (await this.keyConnectorService.getConvertAccountRequired()) {
const convertToKeyConnectorCommand = new ConvertToKeyConnectorCommand(
userId,
this.keyConnectorService,
this.environmentService,
this.syncService,

View File

@ -116,20 +116,30 @@ export abstract class BaseProgram {
}
}
/**
* Exist if no user is authenticated
* @returns the userId of the active account
*/
protected async exitIfNotAuthed() {
const authed = await this.serviceContainer.stateService.getIsAuthenticated();
if (!authed) {
this.processResponse(Response.error("You are not logged in."), true);
const fail = () => this.processResponse(Response.error("You are not logged in."), true);
const userId = (await firstValueFrom(this.serviceContainer.accountService.activeAccount$))?.id;
if (!userId) {
fail();
}
const authed = await this.serviceContainer.stateService.getIsAuthenticated({ userId });
if (!authed) {
fail();
}
return userId;
}
protected async exitIfLocked() {
await this.exitIfNotAuthed();
const userId = await this.exitIfNotAuthed();
if (await this.serviceContainer.cryptoService.hasUserKey()) {
return;
} else if (process.env.BW_NOINTERACTION !== "true") {
// must unlock
if (await this.serviceContainer.keyConnectorService.getUsesKeyConnector()) {
if (await this.serviceContainer.keyConnectorService.getUsesKeyConnector(userId)) {
const response = Response.error(
"Your vault is locked. You must unlock your vault using your session key.\n" +
"If you do not have your session key, you can get a new one by logging out and logging in again.",

View File

@ -7,6 +7,7 @@ import {
EnvironmentService,
Region,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { UserId } from "@bitwarden/common/types/guid";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { Response } from "../models/response";
@ -14,6 +15,7 @@ import { MessageResponse } from "../models/response/message.response";
export class ConvertToKeyConnectorCommand {
constructor(
private readonly userId: UserId,
private keyConnectorService: KeyConnectorService,
private environmentService: EnvironmentService,
private syncService: SyncService,
@ -68,7 +70,7 @@ export class ConvertToKeyConnectorCommand {
}
await this.keyConnectorService.removeConvertAccountRequired();
await this.keyConnectorService.setUsesKeyConnector(true);
await this.keyConnectorService.setUsesKeyConnector(true, this.userId);
// Update environment URL - required for api key login
const env = await firstValueFrom(this.environmentService.environment$);

View File

@ -206,9 +206,9 @@ export class Program extends BaseProgram {
writeLn("", true);
})
.action(async (cmd) => {
await this.exitIfNotAuthed();
const userId = await this.exitIfNotAuthed();
if (await this.serviceContainer.keyConnectorService.getUsesKeyConnector()) {
if (await this.serviceContainer.keyConnectorService.getUsesKeyConnector(userId)) {
const logoutCommand = new LogoutCommand(
this.serviceContainer.authService,
this.serviceContainer.i18nService,

View File

@ -28,6 +28,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { UserId } from "@bitwarden/common/types/guid";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { flagEnabled } from "../../../utils/flags";
@ -129,7 +130,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
}
}
async goAfterLogIn() {
async goAfterLogIn(userId: UserId) {
const masterPassword = this.formGroup.value.masterPassword;
// Check master password against policy
@ -150,7 +151,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
) {
const policiesData: { [id: string]: PolicyData } = {};
this.policies.map((p) => (policiesData[p.id] = PolicyData.fromPolicy(p)));
await this.policyService.replace(policiesData);
await this.policyService.replace(policiesData, userId);
await this.router.navigate(["update-password"]);
return;
}

View File

@ -23,6 +23,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import {
@ -39,7 +40,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
showPassword = false;
formPromise: Promise<AuthResult>;
onSuccessfulLogin: () => Promise<any>;
onSuccessfulLoginNavigate: () => Promise<any>;
onSuccessfulLoginNavigate: (userId: UserId) => Promise<any>;
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
showLoginWithDevice: boolean;
@ -185,7 +186,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
if (this.onSuccessfulLoginNavigate != null) {
// 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.onSuccessfulLoginNavigate();
this.onSuccessfulLoginNavigate(response.userId);
} else {
this.loginEmailService.clearValues();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.

View File

@ -158,7 +158,10 @@ describe("AuthRequestLoginStrategy", () => {
decMasterKeyHash,
mockUserId,
);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
tokenResponse.key,
mockUserId,
);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, mockUserId);
expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled();
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId);
@ -183,7 +186,10 @@ describe("AuthRequestLoginStrategy", () => {
expect(masterPasswordService.mock.setMasterKeyHash).not.toHaveBeenCalled();
// setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
tokenResponse.key,
mockUserId,
);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(decUserKey, mockUserId);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId);

View File

@ -99,7 +99,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
const authRequestCredentials = this.cache.value.authRequestCredentials;
// User now may or may not have a master password
// but set the master key encrypted user key if it exists regardless
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key, userId);
if (authRequestCredentials.decryptedUserKey) {
await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey, userId);

View File

@ -222,7 +222,11 @@ export abstract class LoginStrategy {
),
);
await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false);
await this.billingAccountProfileStateService.setHasPremium(
accountInformation.premium,
false,
userId,
);
return userId;
}

View File

@ -172,7 +172,10 @@ describe("UserApiLoginStrategy", () => {
await apiLogInStrategy.logIn(credentials);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
tokenResponse.key,
userId,
);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId);
});

View File

@ -64,7 +64,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
response: IdentityTokenResponse,
userId: UserId,
): Promise<void> {
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key, userId);
if (response.apiUseKeyConnector) {
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));

View File

@ -32,7 +32,7 @@ export class FakeGlobalStateProvider implements GlobalStateProvider {
states: Map<string, GlobalState<unknown>> = new Map();
get<T>(keyDefinition: KeyDefinition<T>): GlobalState<T> {
this.mock.get(keyDefinition);
const cacheKey = `${keyDefinition.fullName}_${keyDefinition.stateDefinition.defaultStorageLocation}`;
const cacheKey = this.cacheKey(keyDefinition);
let result = this.states.get(cacheKey);
if (result == null) {
@ -53,94 +53,143 @@ export class FakeGlobalStateProvider implements GlobalStateProvider {
return result as GlobalState<T>;
}
private cacheKey(keyDefinition: KeyDefinition<unknown>) {
return `${keyDefinition.fullName}_${keyDefinition.stateDefinition.defaultStorageLocation}`;
}
getFake<T>(keyDefinition: KeyDefinition<T>): FakeGlobalState<T> {
return this.get(keyDefinition) as FakeGlobalState<T>;
}
mockFor<T>(keyDefinitionKey: string, initialValue?: T): FakeGlobalState<T> {
if (!this.establishedMocks.has(keyDefinitionKey)) {
this.establishedMocks.set(keyDefinitionKey, new FakeGlobalState<T>(initialValue));
mockFor<T>(keyDefinition: KeyDefinition<T>, initialValue?: T): FakeGlobalState<T> {
const cacheKey = this.cacheKey(keyDefinition);
if (!this.states.has(cacheKey)) {
this.states.set(cacheKey, new FakeGlobalState<T>(initialValue));
}
return this.establishedMocks.get(keyDefinitionKey) as FakeGlobalState<T>;
return this.states.get(cacheKey) as FakeGlobalState<T>;
}
}
export class FakeSingleUserStateProvider implements SingleUserStateProvider {
mock = mock<SingleUserStateProvider>();
establishedMocks: Map<string, FakeSingleUserState<unknown>> = new Map();
states: Map<string, SingleUserState<unknown>> = new Map();
constructor(
readonly updateSyncCallback?: (
key: UserKeyDefinition<unknown>,
userId: UserId,
newValue: unknown,
) => Promise<void>,
) {}
get<T>(userId: UserId, userKeyDefinition: UserKeyDefinition<T>): SingleUserState<T> {
this.mock.get(userId, userKeyDefinition);
const cacheKey = `${userKeyDefinition.fullName}_${userKeyDefinition.stateDefinition.defaultStorageLocation}_${userId}`;
const cacheKey = this.cacheKey(userId, userKeyDefinition);
let result = this.states.get(cacheKey);
if (result == null) {
let fake: FakeSingleUserState<T>;
// Look for established mock
if (this.establishedMocks.has(userKeyDefinition.key)) {
fake = this.establishedMocks.get(userKeyDefinition.key) as FakeSingleUserState<T>;
} else {
fake = new FakeSingleUserState<T>(userId);
}
fake.keyDefinition = userKeyDefinition;
result = fake;
result = this.buildFakeState(userId, userKeyDefinition);
this.states.set(cacheKey, result);
}
return result as SingleUserState<T>;
}
getFake<T>(userId: UserId, userKeyDefinition: UserKeyDefinition<T>): FakeSingleUserState<T> {
getFake<T>(
userId: UserId,
userKeyDefinition: UserKeyDefinition<T>,
{ allowInit }: { allowInit: boolean } = { allowInit: true },
): FakeSingleUserState<T> {
if (!allowInit && this.states.get(this.cacheKey(userId, userKeyDefinition)) == null) {
return null;
}
return this.get(userId, userKeyDefinition) as FakeSingleUserState<T>;
}
mockFor<T>(userId: UserId, keyDefinitionKey: string, initialValue?: T): FakeSingleUserState<T> {
if (!this.establishedMocks.has(keyDefinitionKey)) {
this.establishedMocks.set(keyDefinitionKey, new FakeSingleUserState<T>(userId, initialValue));
mockFor<T>(
userId: UserId,
userKeyDefinition: UserKeyDefinition<T>,
initialValue?: T,
): FakeSingleUserState<T> {
const cacheKey = this.cacheKey(userId, userKeyDefinition);
if (!this.states.has(cacheKey)) {
this.states.set(cacheKey, this.buildFakeState(userId, userKeyDefinition, initialValue));
}
return this.establishedMocks.get(keyDefinitionKey) as FakeSingleUserState<T>;
return this.states.get(cacheKey) as FakeSingleUserState<T>;
}
private buildFakeState<T>(
userId: UserId,
userKeyDefinition: UserKeyDefinition<T>,
initialValue?: T,
) {
const state = new FakeSingleUserState(userId, initialValue, async (...args) => {
await this.updateSyncCallback?.(userKeyDefinition, ...args);
});
state.keyDefinition = userKeyDefinition;
return state;
}
private cacheKey(userId: UserId, userKeyDefinition: UserKeyDefinition<unknown>) {
return `${userKeyDefinitionCacheKey(userKeyDefinition)}_${userId}`;
}
}
export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
activeUserId$: Observable<UserId>;
establishedMocks: Map<string, FakeActiveUserState<unknown>> = new Map();
states: Map<string, FakeActiveUserState<unknown>> = new Map();
constructor(public accountService: FakeAccountService) {
constructor(
public accountService: FakeAccountService,
readonly updateSyncCallback?: (
key: UserKeyDefinition<unknown>,
userId: UserId,
newValue: unknown,
) => Promise<void>,
) {
this.activeUserId$ = accountService.activeAccountSubject.asObservable().pipe(map((a) => a?.id));
}
get<T>(userKeyDefinition: UserKeyDefinition<T>): ActiveUserState<T> {
const cacheKey = `${userKeyDefinition.fullName}_${userKeyDefinition.stateDefinition.defaultStorageLocation}`;
const cacheKey = userKeyDefinitionCacheKey(userKeyDefinition);
let result = this.states.get(cacheKey);
if (result == null) {
// Look for established mock
if (this.establishedMocks.has(userKeyDefinition.key)) {
result = this.establishedMocks.get(userKeyDefinition.key);
} else {
result = new FakeActiveUserState<T>(this.accountService);
}
result.keyDefinition = userKeyDefinition;
result = this.buildFakeState(userKeyDefinition);
this.states.set(cacheKey, result);
}
return result as ActiveUserState<T>;
}
getFake<T>(userKeyDefinition: UserKeyDefinition<T>): FakeActiveUserState<T> {
getFake<T>(
userKeyDefinition: UserKeyDefinition<T>,
{ allowInit }: { allowInit: boolean } = { allowInit: true },
): FakeActiveUserState<T> {
if (!allowInit && this.states.get(userKeyDefinitionCacheKey(userKeyDefinition)) == null) {
return null;
}
return this.get(userKeyDefinition) as FakeActiveUserState<T>;
}
mockFor<T>(keyDefinitionKey: string, initialValue?: T): FakeActiveUserState<T> {
if (!this.establishedMocks.has(keyDefinitionKey)) {
this.establishedMocks.set(
keyDefinitionKey,
new FakeActiveUserState<T>(this.accountService, initialValue),
);
mockFor<T>(userKeyDefinition: UserKeyDefinition<T>, initialValue?: T): FakeActiveUserState<T> {
const cacheKey = userKeyDefinitionCacheKey(userKeyDefinition);
if (!this.states.has(cacheKey)) {
this.states.set(cacheKey, this.buildFakeState(userKeyDefinition, initialValue));
}
return this.establishedMocks.get(keyDefinitionKey) as FakeActiveUserState<T>;
return this.states.get(cacheKey) as FakeActiveUserState<T>;
}
private buildFakeState<T>(userKeyDefinition: UserKeyDefinition<T>, initialValue?: T) {
const state = new FakeActiveUserState<T>(this.accountService, initialValue, async (...args) => {
await this.updateSyncCallback?.(userKeyDefinition, ...args);
});
state.keyDefinition = userKeyDefinition;
return state;
}
}
function userKeyDefinitionCacheKey(userKeyDefinition: UserKeyDefinition<unknown>) {
return `${userKeyDefinition.fullName}_${userKeyDefinition.stateDefinition.defaultStorageLocation}`;
}
export class FakeStateProvider implements StateProvider {
@ -207,9 +256,35 @@ export class FakeStateProvider implements StateProvider {
constructor(public accountService: FakeAccountService) {}
private distributeSingleUserUpdate(
key: UserKeyDefinition<unknown>,
userId: UserId,
newState: unknown,
) {
if (this.activeUser.accountService.activeUserId === userId) {
const state = this.activeUser.getFake(key, { allowInit: false });
state?.nextState(newState, { syncValue: false });
}
}
private distributeActiveUserUpdate(
key: UserKeyDefinition<unknown>,
userId: UserId,
newState: unknown,
) {
this.singleUser
.getFake(userId, key, { allowInit: false })
?.nextState(newState, { syncValue: false });
}
global: FakeGlobalStateProvider = new FakeGlobalStateProvider();
singleUser: FakeSingleUserStateProvider = new FakeSingleUserStateProvider();
activeUser: FakeActiveUserStateProvider = new FakeActiveUserStateProvider(this.accountService);
singleUser: FakeSingleUserStateProvider = new FakeSingleUserStateProvider(
this.distributeSingleUserUpdate.bind(this),
);
activeUser: FakeActiveUserStateProvider = new FakeActiveUserStateProvider(
this.accountService,
this.distributeActiveUserUpdate.bind(this),
);
derived: FakeDerivedStateProvider = new FakeDerivedStateProvider();
activeUserId$: Observable<UserId> = this.activeUser.activeUserId$;
}

View File

@ -1,4 +1,4 @@
import { Observable, ReplaySubject, concatMap, firstValueFrom, map, timeout } from "rxjs";
import { Observable, ReplaySubject, concatMap, filter, firstValueFrom, map, timeout } from "rxjs";
import {
DerivedState,
@ -41,6 +41,10 @@ export class FakeGlobalState<T> implements GlobalState<T> {
this.stateSubject.next(initialValue ?? null);
}
nextState(state: T) {
this.stateSubject.next(state);
}
async update<TCombine>(
configureState: (state: T, dependency: TCombine) => T,
options?: StateUpdateOptions<T, TCombine>,
@ -89,7 +93,10 @@ export class FakeGlobalState<T> implements GlobalState<T> {
export class FakeSingleUserState<T> implements SingleUserState<T> {
// eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup
stateSubject = new ReplaySubject<CombinedState<T>>(1);
stateSubject = new ReplaySubject<{
syncValue: boolean;
combinedState: CombinedState<T>;
}>(1);
state$: Observable<T>;
combinedState$: Observable<CombinedState<T>>;
@ -97,15 +104,28 @@ export class FakeSingleUserState<T> implements SingleUserState<T> {
constructor(
readonly userId: UserId,
initialValue?: T,
updateSyncCallback?: (userId: UserId, newValue: T) => Promise<void>,
) {
this.stateSubject.next([userId, initialValue ?? null]);
// Inform the state provider of updates to keep active user states in sync
this.stateSubject
.pipe(
filter((next) => next.syncValue),
concatMap(async ({ combinedState }) => {
await updateSyncCallback?.(...combinedState);
}),
)
.subscribe();
this.nextState(initialValue ?? null, { syncValue: initialValue != null });
this.combinedState$ = this.stateSubject.asObservable();
this.combinedState$ = this.stateSubject.pipe(map((v) => v.combinedState));
this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state));
}
nextState(state: T) {
this.stateSubject.next([this.userId, state]);
nextState(state: T, { syncValue }: { syncValue: boolean } = { syncValue: true }) {
this.stateSubject.next({
syncValue,
combinedState: [this.userId, state],
});
}
async update<TCombine>(
@ -122,7 +142,7 @@ export class FakeSingleUserState<T> implements SingleUserState<T> {
return current;
}
const newState = configureState(current, combinedDependencies);
this.stateSubject.next([this.userId, newState]);
this.nextState(newState);
this.nextMock(newState);
return newState;
}
@ -146,7 +166,10 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
[activeMarker]: true;
// eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup
stateSubject = new ReplaySubject<CombinedState<T>>(1);
stateSubject = new ReplaySubject<{
syncValue: boolean;
combinedState: CombinedState<T>;
}>(1);
state$: Observable<T>;
combinedState$: Observable<CombinedState<T>>;
@ -154,10 +177,18 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
constructor(
private accountService: FakeAccountService,
initialValue?: T,
updateSyncCallback?: (userId: UserId, newValue: T) => Promise<void>,
) {
this.stateSubject.next([accountService.activeUserId, initialValue ?? null]);
// Inform the state provider of updates to keep single user states in sync
this.stateSubject.pipe(
filter((next) => next.syncValue),
concatMap(async ({ combinedState }) => {
await updateSyncCallback?.(...combinedState);
}),
);
this.nextState(initialValue ?? null, { syncValue: initialValue != null });
this.combinedState$ = this.stateSubject.asObservable();
this.combinedState$ = this.stateSubject.pipe(map((v) => v.combinedState));
this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state));
}
@ -165,8 +196,11 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
return this.accountService.activeUserId;
}
nextState(state: T) {
this.stateSubject.next([this.userId, state]);
nextState(state: T, { syncValue }: { syncValue: boolean } = { syncValue: true }) {
this.stateSubject.next({
syncValue,
combinedState: [this.userId, state],
});
}
async update<TCombine>(
@ -183,7 +217,7 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
return [this.userId, current];
}
const newState = configureState(current, combinedDependencies);
this.stateSubject.next([this.userId, newState]);
this.nextState(newState);
this.nextMock([this.userId, newState]);
return [this.userId, newState];
}

View File

@ -77,5 +77,5 @@ export abstract class PolicyService {
export abstract class InternalPolicyService extends PolicyService {
upsert: (policy: PolicyData) => Promise<void>;
replace: (policies: { [id: string]: PolicyData }) => Promise<void>;
replace: (policies: { [id: string]: PolicyData }, userId: UserId) => Promise<void>;
}

View File

@ -20,6 +20,7 @@ import { POLICIES, PolicyService } from "../../../admin-console/services/policy/
import { PolicyId, UserId } from "../../../types/guid";
describe("PolicyService", () => {
const userId = "userId" as UserId;
let stateProvider: FakeStateProvider;
let organizationService: MockProxy<OrganizationService>;
let activeUserState: FakeActiveUserState<Record<PolicyId, PolicyData>>;
@ -27,7 +28,7 @@ describe("PolicyService", () => {
let policyService: PolicyService;
beforeEach(() => {
const accountService = mockAccountServiceWith("userId" as UserId);
const accountService = mockAccountServiceWith(userId);
stateProvider = new FakeStateProvider(accountService);
organizationService = mock<OrganizationService>();
@ -95,9 +96,12 @@ describe("PolicyService", () => {
]),
);
await policyService.replace({
"2": policyData("2", "test-organization", PolicyType.DisableSend, true),
});
await policyService.replace(
{
"2": policyData("2", "test-organization", PolicyType.DisableSend, true),
},
userId,
);
expect(await firstValueFrom(policyService.policies$)).toEqual([
{

View File

@ -219,8 +219,8 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
});
}
async replace(policies: { [id: string]: PolicyData }): Promise<void> {
await this.activeUserPolicyState.update(() => policies);
async replace(policies: { [id: string]: PolicyData }, userId: UserId): Promise<void> {
await this.stateProvider.setUserState(POLICIES, policies, userId);
}
/**

View File

@ -4,17 +4,17 @@ import { IdentityTokenResponse } from "../models/response/identity-token.respons
export abstract class KeyConnectorService {
setMasterKeyFromUrl: (url: string, userId: UserId) => Promise<void>;
getManagingOrganization: () => Promise<Organization>;
getUsesKeyConnector: () => Promise<boolean>;
migrateUser: () => Promise<void>;
userNeedsMigration: () => Promise<boolean>;
getManagingOrganization: (userId?: UserId) => Promise<Organization>;
getUsesKeyConnector: (userId: UserId) => Promise<boolean>;
migrateUser: (userId?: UserId) => Promise<void>;
userNeedsMigration: (userId: UserId) => Promise<boolean>;
convertNewSsoUserToKeyConnector: (
tokenResponse: IdentityTokenResponse,
orgId: string,
userId: UserId,
) => Promise<void>;
setUsesKeyConnector: (enabled: boolean) => Promise<void>;
setConvertAccountRequired: (status: boolean) => Promise<void>;
setUsesKeyConnector: (enabled: boolean, userId: UserId) => Promise<void>;
setConvertAccountRequired: (status: boolean, userId?: UserId) => Promise<void>;
getConvertAccountRequired: () => Promise<boolean>;
removeConvertAccountRequired: () => Promise<void>;
removeConvertAccountRequired: (userId?: UserId) => Promise<void>;
}

View File

@ -148,10 +148,11 @@ export abstract class TokenService {
/**
* Decodes the access token.
* @param token The access token to decode.
* @param tokenOrUserId The access token to decode or the user id to retrieve the access token for, and then decode.
* If null, the currently active user's token is used.
* @returns A promise that resolves with the decoded access token.
*/
decodeAccessToken: (token?: string) => Promise<DecodedAccessToken>;
decodeAccessToken: (tokenOrUserId?: string | UserId) => Promise<DecodedAccessToken>;
/**
* Gets the expiration date for the access token. Returns if token can't be decoded or has no expiration
@ -212,9 +213,10 @@ export abstract class TokenService {
/**
* Gets whether or not the user authenticated via an external mechanism.
* @param userId The optional user id to check for external authN status; if not provided, the active user is used.
* @returns A promise that resolves with a boolean representing the user's external authN status.
*/
getIsExternal: () => Promise<boolean>;
getIsExternal: (userId: UserId) => Promise<boolean>;
/** Gets the active or passed in user's security stamp */
getSecurityStamp: (userId?: UserId) => Promise<string | null>;

View File

@ -78,9 +78,9 @@ describe("KeyConnectorService", () => {
const newValue = true;
await keyConnectorService.setUsesKeyConnector(newValue);
await keyConnectorService.setUsesKeyConnector(newValue, mockUserId);
expect(await keyConnectorService.getUsesKeyConnector()).toBe(newValue);
expect(await keyConnectorService.getUsesKeyConnector(mockUserId)).toBe(newValue);
});
});
@ -185,7 +185,7 @@ describe("KeyConnectorService", () => {
const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR);
state.nextState(false);
const result = await keyConnectorService.userNeedsMigration();
const result = await keyConnectorService.userNeedsMigration(mockUserId);
expect(result).toBe(true);
});
@ -197,7 +197,7 @@ describe("KeyConnectorService", () => {
const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR);
state.nextState(true);
const result = await keyConnectorService.userNeedsMigration();
const result = await keyConnectorService.userNeedsMigration(mockUserId);
expect(result).toBe(false);
});

View File

@ -69,25 +69,25 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
);
}
async setUsesKeyConnector(usesKeyConnector: boolean) {
await this.usesKeyConnectorState.update(() => usesKeyConnector);
async setUsesKeyConnector(usesKeyConnector: boolean, userId: UserId) {
await this.stateProvider.getUser(userId, USES_KEY_CONNECTOR).update(() => usesKeyConnector);
}
getUsesKeyConnector(): Promise<boolean> {
return firstValueFrom(this.usesKeyConnectorState.state$);
getUsesKeyConnector(userId: UserId): Promise<boolean> {
return firstValueFrom(this.stateProvider.getUserState$(USES_KEY_CONNECTOR, userId));
}
async userNeedsMigration() {
const loggedInUsingSso = await this.tokenService.getIsExternal();
const requiredByOrganization = (await this.getManagingOrganization()) != null;
const userIsNotUsingKeyConnector = !(await this.getUsesKeyConnector());
async userNeedsMigration(userId: UserId) {
const loggedInUsingSso = await this.tokenService.getIsExternal(userId);
const requiredByOrganization = (await this.getManagingOrganization(userId)) != null;
const userIsNotUsingKeyConnector = !(await this.getUsesKeyConnector(userId));
return loggedInUsingSso && requiredByOrganization && userIsNotUsingKeyConnector;
}
async migrateUser() {
const organization = await this.getManagingOrganization();
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
async migrateUser(userId?: UserId) {
userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
const organization = await this.getManagingOrganization(userId);
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
@ -115,8 +115,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
}
}
async getManagingOrganization(): Promise<Organization> {
const orgs = await this.organizationService.getAll();
async getManagingOrganization(userId?: UserId): Promise<Organization> {
const orgs = await this.organizationService.getAll(userId);
return orgs.find(
(o) =>
o.keyConnectorEnabled &&
@ -178,16 +178,16 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
await this.apiService.postSetKeyConnectorKey(setPasswordRequest);
}
async setConvertAccountRequired(status: boolean) {
await this.convertAccountToKeyConnectorState.update(() => status);
async setConvertAccountRequired(status: boolean, userId?: UserId) {
await this.stateProvider.setUserState(CONVERT_ACCOUNT_TO_KEY_CONNECTOR, status, userId);
}
getConvertAccountRequired(): Promise<boolean> {
return firstValueFrom(this.convertAccountToKeyConnectorState.state$);
}
async removeConvertAccountRequired() {
await this.setConvertAccountRequired(null);
async removeConvertAccountRequired(userId?: UserId) {
await this.setConvertAccountRequired(null, userId);
}
private handleKeyConnectorError(e: any) {

View File

@ -126,7 +126,7 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
.nextState(accessTokenJwt);
// Act
const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken));
@ -139,11 +139,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
.nextState(accessTokenJwt);
// Act
const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken));
@ -156,7 +156,7 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, "encryptedAccessToken"]);
.nextState("encryptedAccessToken");
secureStorageService.get.mockResolvedValue(accessTokenKeyB64);
@ -282,7 +282,7 @@ describe("TokenService", () => {
// For testing purposes, let's assume that the access token is already in memory
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
.nextState(accessTokenJwt);
keyGenerationService.createKey.mockResolvedValue(accessTokenKey);
@ -411,9 +411,7 @@ describe("TokenService", () => {
it("returns null when no access token is found in memory, disk, or secure storage", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getAccessToken();
@ -429,18 +427,16 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
.nextState(accessTokenJwt);
// set disk to undefined
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
// Need to have global active id set to the user id
if (!userId) {
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
}
// Act
@ -459,17 +455,15 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
.nextState(accessTokenJwt);
// Need to have global active id set to the user id
if (!userId) {
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
}
// Act
@ -498,20 +492,18 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, "encryptedAccessToken"]);
.nextState("encryptedAccessToken");
secureStorageService.get.mockResolvedValue(accessTokenKeyB64);
encryptService.decryptToUtf8.mockResolvedValue("decryptedAccessToken");
// Need to have global active id set to the user id
if (!userId) {
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
}
// Act
@ -534,17 +526,15 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
.nextState(accessTokenJwt);
// Need to have global active id set to the user id
if (!userId) {
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
}
// No access token key set
@ -564,11 +554,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, encryptedAccessToken]);
.nextState(encryptedAccessToken);
// No access token key set
@ -596,11 +586,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, encryptedAccessToken]);
.nextState(encryptedAccessToken);
// Mock linux secure storage error
const secureStorageError = "Secure storage error";
@ -655,17 +645,15 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
.nextState(accessTokenJwt);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
.nextState(accessTokenJwt);
// Need to have global active id set to the user id
if (!userId) {
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
}
// Act
@ -688,8 +676,32 @@ describe("TokenService", () => {
});
describe("decodeAccessToken", () => {
it("retrieves the requested user's token when the passed in parameter is a Guid", async () => {
// Arrange
tokenService.getAccessToken = jest.fn().mockResolvedValue(accessTokenJwt);
// Act
const result = await tokenService.decodeAccessToken(userIdFromAccessToken);
// Assert
expect(result).toEqual(accessTokenDecoded);
expect(tokenService.getAccessToken).toHaveBeenCalledWith(userIdFromAccessToken);
});
it("decodes the given token when a string is passed in that is not a Guid", async () => {
// Arrange
tokenService.getAccessToken = jest.fn();
// Act
const result = await tokenService.decodeAccessToken(accessTokenJwt);
// Assert
expect(result).toEqual(accessTokenDecoded);
expect(tokenService.getAccessToken).not.toHaveBeenCalled();
});
it("throws an error when no access token is provided or retrievable from state", async () => {
// Access
// Arrange
tokenService.getAccessToken = jest.fn().mockResolvedValue(null);
// Act
@ -1194,7 +1206,7 @@ describe("TokenService", () => {
// Act
// note: don't await here because we want to test the error
const result = tokenService.getIsExternal();
const result = tokenService.getIsExternal(null);
// Assert
await expect(result).rejects.toThrow("Failed to decode access token: Mock error");
});
@ -1210,7 +1222,7 @@ describe("TokenService", () => {
.mockResolvedValue(accessTokenDecodedWithoutExternalAmr);
// Act
const result = await tokenService.getIsExternal();
const result = await tokenService.getIsExternal(null);
// Assert
expect(result).toEqual(false);
@ -1227,11 +1239,22 @@ describe("TokenService", () => {
.mockResolvedValue(accessTokenDecodedWithExternalAmr);
// Act
const result = await tokenService.getIsExternal();
const result = await tokenService.getIsExternal(null);
// Assert
expect(result).toEqual(true);
});
it("passes the requested userId to decode", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockResolvedValue(accessTokenDecoded);
// Act
await tokenService.getIsExternal(userIdFromAccessToken);
// Assert
expect(tokenService.decodeAccessToken).toHaveBeenCalledWith(userIdFromAccessToken);
});
});
});
});
@ -1326,11 +1349,11 @@ describe("TokenService", () => {
// For testing purposes, let's assume that the token is already in disk and memory
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
.nextState(refreshToken);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
.nextState(refreshToken);
// We immediately call to get the refresh token from secure storage after setting it to ensure it was set.
secureStorageService.get.mockResolvedValue(refreshToken);
@ -1423,11 +1446,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
.nextState(accessTokenJwt);
// Mock linux secure storage error
const secureStorageError = "Secure storage error";
@ -1480,11 +1503,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, encryptedAccessToken]);
.nextState(encryptedAccessToken);
secureStorageService.get.mockResolvedValue(accessTokenKeyB64);
encryptService.decryptToUtf8.mockRejectedValue(new Error("Decryption error"));
@ -1520,9 +1543,7 @@ describe("TokenService", () => {
it("returns null when no refresh token is found in memory, disk, or secure storage", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await (tokenService as any).getRefreshToken();
@ -1535,16 +1556,14 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
.nextState(refreshToken);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getRefreshToken();
@ -1557,11 +1576,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
.nextState(refreshToken);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
// Act
const result = await tokenService.getRefreshToken(userIdFromAccessToken);
@ -1575,16 +1594,14 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
.nextState(refreshToken);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getRefreshToken();
@ -1596,11 +1613,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
.nextState(refreshToken);
// Act
const result = await tokenService.getRefreshToken(userIdFromAccessToken);
@ -1619,18 +1636,16 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
secureStorageService.get.mockResolvedValue(refreshToken);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getRefreshToken();
@ -1643,11 +1658,11 @@ describe("TokenService", () => {
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
secureStorageService.get.mockResolvedValue(refreshToken);
@ -1661,11 +1676,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
.nextState(refreshToken);
// Act
const result = await tokenService.getRefreshToken(userIdFromAccessToken);
@ -1681,16 +1696,14 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
.nextState(refreshToken);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getRefreshToken();
@ -1719,11 +1732,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
secureStorageService.get.mockResolvedValue(null);
@ -1743,11 +1756,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
const secureStorageSvcMockErrorMsg = "Secure storage retrieval error";
@ -1792,11 +1805,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
.nextState(refreshToken);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
.nextState(refreshToken);
// Act
await (tokenService as any).clearRefreshToken(userIdFromAccessToken);
@ -1833,9 +1846,7 @@ describe("TokenService", () => {
it("should throw an error if the vault timeout is missing", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = tokenService.setClientId(clientId, VaultTimeoutAction.Lock, null);
@ -1847,9 +1858,7 @@ describe("TokenService", () => {
it("should throw an error if the vault timeout action is missing", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = tokenService.setClientId(clientId, null, VaultTimeoutStringType.Never);
@ -1861,9 +1870,7 @@ describe("TokenService", () => {
describe("Memory storage tests", () => {
it("sets the client id in memory when there is an active user in global state", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
await tokenService.setClientId(clientId, memoryVaultTimeoutAction, memoryVaultTimeout);
@ -1895,9 +1902,7 @@ describe("TokenService", () => {
describe("Disk storage tests", () => {
it("sets the client id in disk when there is an active user in global state", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
await tokenService.setClientId(clientId, diskVaultTimeoutAction, diskVaultTimeout);
@ -1935,9 +1940,7 @@ describe("TokenService", () => {
it("returns null when no client id is found in memory or disk", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getClientId();
@ -1950,17 +1953,15 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
.stateSubject.next([userIdFromAccessToken, clientId]);
.nextState(clientId);
// set disk to undefined
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getClientId();
@ -1973,12 +1974,12 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
.stateSubject.next([userIdFromAccessToken, clientId]);
.nextState(clientId);
// set disk to undefined
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
// Act
const result = await tokenService.getClientId(userIdFromAccessToken);
@ -1992,16 +1993,14 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK)
.stateSubject.next([userIdFromAccessToken, clientId]);
.nextState(clientId);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getClientId();
@ -2013,11 +2012,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK)
.stateSubject.next([userIdFromAccessToken, clientId]);
.nextState(clientId);
// Act
const result = await tokenService.getClientId(userIdFromAccessToken);
@ -2040,11 +2039,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
.stateSubject.next([userIdFromAccessToken, clientId]);
.nextState(clientId);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK)
.stateSubject.next([userIdFromAccessToken, clientId]);
.nextState(clientId);
// Act
await (tokenService as any).clearClientId(userIdFromAccessToken);
@ -2062,16 +2061,14 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
.stateSubject.next([userIdFromAccessToken, clientId]);
.nextState(clientId);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK)
.stateSubject.next([userIdFromAccessToken, clientId]);
.nextState(clientId);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
await (tokenService as any).clearClientId();
@ -2106,9 +2103,7 @@ describe("TokenService", () => {
it("should throw an error if the vault timeout is missing", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = tokenService.setClientSecret(clientSecret, VaultTimeoutAction.Lock, null);
@ -2120,9 +2115,7 @@ describe("TokenService", () => {
it("should throw an error if the vault timeout action is missing", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = tokenService.setClientSecret(
@ -2138,9 +2131,7 @@ describe("TokenService", () => {
describe("Memory storage tests", () => {
it("sets the client secret in memory when there is an active user in global state", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
await tokenService.setClientSecret(
@ -2176,9 +2167,7 @@ describe("TokenService", () => {
describe("Disk storage tests", () => {
it("sets the client secret on disk when there is an active user in global state", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
await tokenService.setClientSecret(
@ -2222,9 +2211,7 @@ describe("TokenService", () => {
it("returns null when no client secret is found in memory or disk", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getClientSecret();
@ -2237,17 +2224,15 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
.stateSubject.next([userIdFromAccessToken, clientSecret]);
.nextState(clientSecret);
// set disk to undefined
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getClientSecret();
@ -2260,12 +2245,12 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
.stateSubject.next([userIdFromAccessToken, clientSecret]);
.nextState(clientSecret);
// set disk to undefined
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
// Act
const result = await tokenService.getClientSecret(userIdFromAccessToken);
@ -2279,16 +2264,14 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
.stateSubject.next([userIdFromAccessToken, clientSecret]);
.nextState(clientSecret);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
const result = await tokenService.getClientSecret();
@ -2300,11 +2283,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
.nextState(undefined);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
.stateSubject.next([userIdFromAccessToken, clientSecret]);
.nextState(clientSecret);
// Act
const result = await tokenService.getClientSecret(userIdFromAccessToken);
@ -2327,11 +2310,11 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
.stateSubject.next([userIdFromAccessToken, clientSecret]);
.nextState(clientSecret);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
.stateSubject.next([userIdFromAccessToken, clientSecret]);
.nextState(clientSecret);
// Act
await (tokenService as any).clearClientSecret(userIdFromAccessToken);
@ -2351,16 +2334,14 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
.stateSubject.next([userIdFromAccessToken, clientSecret]);
.nextState(clientSecret);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
.stateSubject.next([userIdFromAccessToken, clientSecret]);
.nextState(clientSecret);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
await (tokenService as any).clearClientSecret();
@ -2634,7 +2615,7 @@ describe("TokenService", () => {
// Arrange
const userId = "userId" as UserId;
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).stateSubject.next(userId);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userId);
tokenService.clearAccessToken = jest.fn();
(tokenService as any).clearRefreshToken = jest.fn();
@ -2693,7 +2674,7 @@ describe("TokenService", () => {
globalStateProvider
.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL)
.stateSubject.next(initialTwoFactorTokenRecord);
.nextState(initialTwoFactorTokenRecord);
// Act
await tokenService.setTwoFactorToken(email, twoFactorToken);
@ -2716,7 +2697,7 @@ describe("TokenService", () => {
globalStateProvider
.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL)
.stateSubject.next(initialTwoFactorTokenRecord);
.nextState(initialTwoFactorTokenRecord);
// Act
const result = await tokenService.getTwoFactorToken(email);
@ -2734,7 +2715,7 @@ describe("TokenService", () => {
globalStateProvider
.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL)
.stateSubject.next(initialTwoFactorTokenRecord);
.nextState(initialTwoFactorTokenRecord);
// Act
const result = await tokenService.getTwoFactorToken(email);
@ -2745,9 +2726,7 @@ describe("TokenService", () => {
it("returns null when there is no two factor token record", async () => {
// Arrange
globalStateProvider
.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL)
.stateSubject.next(null);
globalStateProvider.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL).nextState(null);
// Act
const result = await tokenService.getTwoFactorToken("testUser");
@ -2768,7 +2747,7 @@ describe("TokenService", () => {
globalStateProvider
.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL)
.stateSubject.next(initialTwoFactorTokenRecord);
.nextState(initialTwoFactorTokenRecord);
// Act
await tokenService.clearTwoFactorToken(email);
@ -2808,9 +2787,7 @@ describe("TokenService", () => {
it("sets the security stamp in memory when there is an active user in global state", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
// Act
await tokenService.setSecurityStamp(mockSecurityStamp);
@ -2843,13 +2820,11 @@ describe("TokenService", () => {
it("returns the security stamp from memory when no user id is specified (uses global active user)", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken);
singleUserStateProvider
.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY)
.stateSubject.next([userIdFromAccessToken, mockSecurityStamp]);
.nextState(mockSecurityStamp);
// Act
const result = await tokenService.getSecurityStamp();
@ -2862,7 +2837,7 @@ describe("TokenService", () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY)
.stateSubject.next([userIdFromAccessToken, mockSecurityStamp]);
.nextState(mockSecurityStamp);
// Act
const result = await tokenService.getSecurityStamp(userIdFromAccessToken);

View File

@ -9,6 +9,7 @@ import { KeyGenerationService } from "../../platform/abstractions/key-generation
import { LogService } from "../../platform/abstractions/log.service";
import { AbstractStorageService } from "../../platform/abstractions/storage.service";
import { StorageLocation } from "../../platform/enums";
import { Utils } from "../../platform/misc/utils";
import { EncString, EncryptedString } from "../../platform/models/domain/enc-string";
import { StorageOptions } from "../../platform/models/domain/storage-options";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
@ -875,8 +876,13 @@ export class TokenService implements TokenServiceAbstraction {
// jwthelper methods
// ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js
async decodeAccessToken(token?: string): Promise<DecodedAccessToken> {
token = token ?? (await this.getAccessToken());
async decodeAccessToken(tokenOrUserId?: string | UserId): Promise<DecodedAccessToken> {
let token = tokenOrUserId as string;
if (Utils.isGuid(tokenOrUserId)) {
token = await this.getAccessToken(tokenOrUserId as UserId);
} else {
token ??= await this.getAccessToken();
}
if (token == null) {
throw new Error("Access token not found.");
@ -1012,10 +1018,10 @@ export class TokenService implements TokenServiceAbstraction {
return decoded.iss;
}
async getIsExternal(): Promise<boolean> {
async getIsExternal(userId: UserId): Promise<boolean> {
let decoded: DecodedAccessToken;
try {
decoded = await this.decodeAccessToken();
decoded = await this.decodeAccessToken(userId);
} catch (error) {
throw new Error("Failed to decode access token: " + error.message);
}

View File

@ -15,6 +15,7 @@ import {
StateProvider,
UserKeyDefinition,
} from "../../platform/state";
import { UserId } from "../../types/guid";
const SHOW_FAVICONS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "showFavicons", {
deserializer: (value: boolean) => value ?? true,
@ -44,7 +45,7 @@ export abstract class DomainSettingsService {
neverDomains$: Observable<NeverDomains>;
setNeverDomains: (newValue: NeverDomains) => Promise<void>;
equivalentDomains$: Observable<EquivalentDomains>;
setEquivalentDomains: (newValue: EquivalentDomains) => Promise<void>;
setEquivalentDomains: (newValue: EquivalentDomains, userId: UserId) => Promise<void>;
defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
setDefaultUriMatchStrategy: (newValue: UriMatchStrategySetting) => Promise<void>;
getUrlEquivalentDomains: (url: string) => Observable<Set<string>>;
@ -87,8 +88,8 @@ export class DefaultDomainSettingsService implements DomainSettingsService {
await this.neverDomainsState.update(() => newValue);
}
async setEquivalentDomains(newValue: EquivalentDomains): Promise<void> {
await this.equivalentDomainsState.update(() => newValue);
async setEquivalentDomains(newValue: EquivalentDomains, userId: UserId): Promise<void> {
await this.stateProvider.getUser(userId, EQUIVALENT_DOMAINS).update(() => newValue);
}
async setDefaultUriMatchStrategy(newValue: UriMatchStrategySetting): Promise<void> {

View File

@ -1,5 +1,7 @@
import { Observable } from "rxjs";
import { UserId } from "../../../types/guid";
export type BillingAccountProfile = {
hasPremiumPersonally: boolean;
hasPremiumFromAnyOrganization: boolean;
@ -32,5 +34,6 @@ export abstract class BillingAccountProfileStateService {
abstract setHasPremium(
hasPremiumPersonally: boolean,
hasPremiumFromAnyOrganization: boolean,
userId: UserId,
): Promise<void>;
}

View File

@ -3,7 +3,6 @@ import { firstValueFrom } from "rxjs";
import {
FakeAccountService,
mockAccountServiceWith,
FakeActiveUserState,
FakeStateProvider,
FakeSingleUserState,
} from "../../../../spec";
@ -18,7 +17,6 @@ import {
describe("BillingAccountProfileStateService", () => {
let stateProvider: FakeStateProvider;
let sut: DefaultBillingAccountProfileStateService;
let billingAccountProfileState: FakeActiveUserState<BillingAccountProfile>;
let userBillingAccountProfileState: FakeSingleUserState<BillingAccountProfile>;
let accountService: FakeAccountService;
@ -30,10 +28,6 @@ describe("BillingAccountProfileStateService", () => {
sut = new DefaultBillingAccountProfileStateService(stateProvider);
billingAccountProfileState = stateProvider.activeUser.getFake(
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
);
userBillingAccountProfileState = stateProvider.singleUser.getFake(
userId,
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
@ -133,12 +127,11 @@ describe("BillingAccountProfileStateService", () => {
describe("setHasPremium", () => {
it("should update the active users state when called", async () => {
await sut.setHasPremium(true, false);
await sut.setHasPremium(true, false, userId);
expect(billingAccountProfileState.nextMock).toHaveBeenCalledWith([
userId,
{ hasPremiumPersonally: true, hasPremiumFromAnyOrganization: false },
]);
expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false);
expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true);
expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true);
});
});
});

View File

@ -6,6 +6,7 @@ import {
StateProvider,
UserKeyDefinition,
} from "../../../platform/state";
import { UserId } from "../../../types/guid";
import {
BillingAccountProfile,
BillingAccountProfileStateService,
@ -27,7 +28,7 @@ export class DefaultBillingAccountProfileStateService implements BillingAccountP
hasPremiumPersonally$: Observable<boolean>;
hasPremiumFromAnySource$: Observable<boolean>;
constructor(stateProvider: StateProvider) {
constructor(private readonly stateProvider: StateProvider) {
this.billingAccountProfileState = stateProvider.getActive(
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
);
@ -62,8 +63,9 @@ export class DefaultBillingAccountProfileStateService implements BillingAccountP
async setHasPremium(
hasPremiumPersonally: boolean,
hasPremiumFromAnyOrganization: boolean,
userId: UserId,
): Promise<void> {
await this.billingAccountProfileState.update((billingAccountProfile) => {
await this.stateProvider.getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION).update((_) => {
return {
hasPremiumPersonally: hasPremiumPersonally,
hasPremiumFromAnyOrganization: hasPremiumFromAnyOrganization,

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);
}
}

View File

@ -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[]>;

View File

@ -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]);
});
});

View File

@ -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 */

View File

@ -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>;
}

View File

@ -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();
});

View File

@ -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(

View File

@ -133,7 +133,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
* @returns A promise that resolves to a record of updated cipher store, keyed by their cipher ID. Returns all ciphers, not just those updated
*/
upsert: (cipher: CipherData | CipherData[]) => Promise<Record<CipherId, CipherData>>;
replace: (ciphers: { [id: string]: CipherData }) => Promise<any>;
replace: (ciphers: { [id: string]: CipherData }, userId: UserId) => Promise<any>;
clear: (userId?: string) => Promise<void>;
moveManyWithServer: (ids: string[], folderId: string) => Promise<any>;
delete: (id: string | string[]) => Promise<any>;

View File

@ -1,6 +1,6 @@
import { Observable } from "rxjs";
import { CollectionId } from "../../types/guid";
import { CollectionId, UserId } from "../../types/guid";
import { CollectionData } from "../models/data/collection.data";
import { Collection } from "../models/domain/collection";
import { TreeNode } from "../models/domain/tree-node";
@ -22,7 +22,7 @@ export abstract class CollectionService {
getAllNested: (collections?: CollectionView[]) => Promise<TreeNode<CollectionView>[]>;
getNested: (id: string) => Promise<TreeNode<CollectionView>>;
upsert: (collection: CollectionData | CollectionData[]) => Promise<any>;
replace: (collections: { [id: string]: CollectionData }) => Promise<any>;
replace: (collections: { [id: string]: CollectionData }, userId: UserId) => Promise<any>;
clear: (userId?: string) => Promise<void>;
delete: (id: string | string[]) => Promise<any>;
}

View File

@ -45,7 +45,7 @@ export abstract class FolderService implements UserKeyRotationDataProvider<Folde
export abstract class InternalFolderService extends FolderService {
upsert: (folder: FolderData | FolderData[]) => Promise<void>;
replace: (folders: { [id: string]: FolderData }) => Promise<void>;
replace: (folders: { [id: string]: FolderData }, userId: UserId) => Promise<void>;
clear: (userId?: string) => Promise<void>;
delete: (id: string | string[]) => Promise<any>;
}

View File

@ -913,8 +913,8 @@ export class CipherService implements CipherServiceAbstraction {
});
}
async replace(ciphers: { [id: string]: CipherData }): Promise<any> {
await this.updateEncryptedCipherState(() => ciphers);
async replace(ciphers: { [id: string]: CipherData }, userId: UserId): Promise<any> {
await this.updateEncryptedCipherState(() => ciphers, userId);
}
/**
@ -924,15 +924,18 @@ export class CipherService implements CipherServiceAbstraction {
*/
private async updateEncryptedCipherState(
update: (current: Record<CipherId, CipherData>) => Record<CipherId, CipherData>,
userId: UserId = null,
): Promise<Record<CipherId, CipherData>> {
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
userId ||= await firstValueFrom(this.stateProvider.activeUserId$);
// Store that we should wait for an update to return any ciphers
await this.ciphersExpectingUpdate.forceValue(true);
await this.clearDecryptedCiphersState(userId);
const [, updatedCiphers] = await this.encryptedCiphersState.update((current) => {
const result = update(current ?? {});
return result;
});
const updatedCiphers = await this.stateProvider
.getUser(userId, ENCRYPTED_CIPHERS)
.update((current) => {
const result = update(current ?? {});
return result;
});
return updatedCiphers;
}

View File

@ -184,8 +184,10 @@ export class CollectionService implements CollectionServiceAbstraction {
});
}
async replace(collections: Record<CollectionId, CollectionData>): Promise<void> {
await this.encryptedCollectionDataState.update(() => collections);
async replace(collections: Record<CollectionId, CollectionData>, userId: UserId): Promise<void> {
await this.stateProvider
.getUser(userId, ENCRYPTED_COLLECTION_DATA_KEY)
.update(() => collections);
}
async clear(userId?: UserId): Promise<void> {

View File

@ -120,7 +120,7 @@ describe("Folder Service", () => {
});
it("replace", async () => {
await folderService.replace({ "2": folderData("2", "test 2") });
await folderService.replace({ "2": folderData("2", "test 2") }, mockUserId);
expect(await firstValueFrom(folderService.folders$)).toEqual([
{

View File

@ -111,12 +111,12 @@ export class FolderService implements InternalFolderServiceAbstraction {
});
}
async replace(folders: { [id: string]: FolderData }): Promise<void> {
async replace(folders: { [id: string]: FolderData }, userId: UserId): Promise<void> {
if (!folders) {
return;
}
await this.encryptedFoldersState.update(() => {
await this.stateProvider.getUser(userId, FOLDER_ENCRYPTED_FOLDERS).update(() => {
const newFolders: Record<string, FolderData> = { ...folders };
return newFolders;
});