-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[PM-11764] Implement account switching and sdk initialization (#11472)
* feat: update sdk service abstraction with documentation and new `userClient$` function * feat: add uninitialized user client with cache * feat: initialize user crypto * feat: initialize org keys * fix: org crypto not initializing properly * feat: avoid creating clients unnecessarily * chore: remove dev print/subscription * fix: clean up cache * chore: update sdk version * feat: implement clean-up logic (#11504) * chore: bump sdk version to fix build issues * chore: bump sdk version to fix build issues * fix: missing constructor parameters * refactor: simplify free() and delete() calls * refactor: use a named function for client creation * fix: client never freeing after refactor * fix: broken impl and race condition in tests
- Loading branch information
Showing
12 changed files
with
355 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,10 @@ | ||
import { Observable } from "rxjs"; | ||
|
||
import { UserId } from "../../types/guid"; | ||
import { KdfConfig } from "../models/domain/kdf-config"; | ||
|
||
export abstract class KdfConfigService { | ||
setKdfConfig: (userId: UserId, KdfConfig: KdfConfig) => Promise<void>; | ||
getKdfConfig: () => Promise<KdfConfig>; | ||
abstract setKdfConfig(userId: UserId, KdfConfig: KdfConfig): Promise<void>; | ||
abstract getKdfConfig(): Promise<KdfConfig>; | ||
abstract getKdfConfig$(userId: UserId): Observable<KdfConfig>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
132 changes: 132 additions & 0 deletions
132
libs/common/src/platform/services/sdk/default-sdk.service.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
import { mock, MockProxy } from "jest-mock-extended"; | ||
import { BehaviorSubject, firstValueFrom, of } from "rxjs"; | ||
|
||
import { BitwardenClient } from "@bitwarden/sdk-internal"; | ||
|
||
import { ApiService } from "../../../abstractions/api.service"; | ||
import { AccountInfo, AccountService } from "../../../auth/abstractions/account.service"; | ||
import { KdfConfigService } from "../../../auth/abstractions/kdf-config.service"; | ||
import { PBKDF2KdfConfig } from "../../../auth/models/domain/kdf-config"; | ||
import { UserId } from "../../../types/guid"; | ||
import { UserKey } from "../../../types/key"; | ||
import { CryptoService } from "../../abstractions/crypto.service"; | ||
import { Environment, EnvironmentService } from "../../abstractions/environment.service"; | ||
import { PlatformUtilsService } from "../../abstractions/platform-utils.service"; | ||
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; | ||
import { EncryptedString } from "../../models/domain/enc-string"; | ||
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; | ||
|
||
import { DefaultSdkService } from "./default-sdk.service"; | ||
|
||
describe("DefaultSdkService", () => { | ||
describe("userClient$", () => { | ||
let sdkClientFactory!: MockProxy<SdkClientFactory>; | ||
let environmentService!: MockProxy<EnvironmentService>; | ||
let platformUtilsService!: MockProxy<PlatformUtilsService>; | ||
let accountService!: MockProxy<AccountService>; | ||
let kdfConfigService!: MockProxy<KdfConfigService>; | ||
let cryptoService!: MockProxy<CryptoService>; | ||
let apiService!: MockProxy<ApiService>; | ||
let service!: DefaultSdkService; | ||
|
||
let mockClient!: MockProxy<BitwardenClient>; | ||
|
||
beforeEach(() => { | ||
sdkClientFactory = mock<SdkClientFactory>(); | ||
environmentService = mock<EnvironmentService>(); | ||
platformUtilsService = mock<PlatformUtilsService>(); | ||
accountService = mock<AccountService>(); | ||
kdfConfigService = mock<KdfConfigService>(); | ||
cryptoService = mock<CryptoService>(); | ||
apiService = mock<ApiService>(); | ||
|
||
// Can't use `of(mock<Environment>())` for some reason | ||
environmentService.environment$ = new BehaviorSubject(mock<Environment>()); | ||
|
||
service = new DefaultSdkService( | ||
sdkClientFactory, | ||
environmentService, | ||
platformUtilsService, | ||
accountService, | ||
kdfConfigService, | ||
cryptoService, | ||
apiService, | ||
); | ||
|
||
mockClient = mock<BitwardenClient>(); | ||
mockClient.crypto.mockReturnValue(mock()); | ||
sdkClientFactory.createSdkClient.mockResolvedValue(mockClient); | ||
}); | ||
|
||
describe("given the user is logged in", () => { | ||
const userId = "user-id" as UserId; | ||
|
||
beforeEach(() => { | ||
accountService.accounts$ = of({ | ||
[userId]: { email: "email", emailVerified: true, name: "name" } as AccountInfo, | ||
}); | ||
kdfConfigService.getKdfConfig$ | ||
.calledWith(userId) | ||
.mockReturnValue(of(new PBKDF2KdfConfig())); | ||
cryptoService.userKey$ | ||
.calledWith(userId) | ||
.mockReturnValue(of(new SymmetricCryptoKey(new Uint8Array(64)) as UserKey)); | ||
cryptoService.userEncryptedPrivateKey$ | ||
.calledWith(userId) | ||
.mockReturnValue(of("private-key" as EncryptedString)); | ||
cryptoService.encryptedOrgKeys$.calledWith(userId).mockReturnValue(of({})); | ||
}); | ||
|
||
it("creates an SDK client when called the first time", async () => { | ||
const result = await firstValueFrom(service.userClient$(userId)); | ||
|
||
expect(result).toBe(mockClient); | ||
expect(sdkClientFactory.createSdkClient).toHaveBeenCalled(); | ||
}); | ||
|
||
it("does not create an SDK client when called the second time with same userId", async () => { | ||
const subject_1 = new BehaviorSubject(undefined); | ||
const subject_2 = new BehaviorSubject(undefined); | ||
|
||
// Use subjects to ensure the subscription is kept alive | ||
service.userClient$(userId).subscribe(subject_1); | ||
service.userClient$(userId).subscribe(subject_2); | ||
|
||
// Wait for the next tick to ensure all async operations are done | ||
await new Promise(process.nextTick); | ||
|
||
expect(subject_1.value).toBe(mockClient); | ||
expect(subject_2.value).toBe(mockClient); | ||
expect(sdkClientFactory.createSdkClient).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it("destroys the SDK client when all subscriptions are closed", async () => { | ||
const subject_1 = new BehaviorSubject(undefined); | ||
const subject_2 = new BehaviorSubject(undefined); | ||
const subscription_1 = service.userClient$(userId).subscribe(subject_1); | ||
const subscription_2 = service.userClient$(userId).subscribe(subject_2); | ||
await new Promise(process.nextTick); | ||
|
||
subscription_1.unsubscribe(); | ||
subscription_2.unsubscribe(); | ||
|
||
expect(mockClient.free).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it("destroys the SDK client when the userKey is unset (i.e. lock or logout)", async () => { | ||
const userKey$ = new BehaviorSubject(new SymmetricCryptoKey(new Uint8Array(64)) as UserKey); | ||
cryptoService.userKey$.calledWith(userId).mockReturnValue(userKey$); | ||
|
||
const subject = new BehaviorSubject(undefined); | ||
service.userClient$(userId).subscribe(subject); | ||
await new Promise(process.nextTick); | ||
|
||
userKey$.next(undefined); | ||
await new Promise(process.nextTick); | ||
|
||
expect(mockClient.free).toHaveBeenCalledTimes(1); | ||
expect(subject.value).toBe(undefined); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.