Skip to content

[PM-19479] Client-Managed SDK state definition #14839

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/browser/src/background/main.background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,7 @@ export default class MainBackground {
this.accountService,
this.kdfConfigService,
this.keyService,
this.stateProvider,
);

this.passwordStrengthService = new PasswordStrengthService();
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/service-container/service-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,7 @@ export class ServiceContainer {
this.accountService,
this.kdfConfigService,
this.keyService,
this.stateProvider,
customUserAgent,
);

Expand Down
1 change: 1 addition & 0 deletions libs/angular/src/services/jslib-services.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1475,6 +1475,7 @@ const safeProviders: SafeProvider[] = [
AccountServiceAbstraction,
KdfConfigService,
KeyService,
StateProvider,
],
}),
safeProvider({
Expand Down
64 changes: 64 additions & 0 deletions libs/common/src/platform/services/sdk/client-managed-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { firstValueFrom, map } from "rxjs";

import { UserId } from "@bitwarden/common/types/guid";
import { CipherRecordMapper } from "@bitwarden/common/vault/models/domain/cipher";
import { StateClient, Repository } from "@bitwarden/sdk-internal";

import { StateProvider, UserKeyDefinition } from "../../state";

export async function initializeState(
userId: UserId,
stateClient: StateClient,
stateProvider: StateProvider,
): Promise<void> {
await stateClient.register_cipher_repository(

Check warning on line 14 in libs/common/src/platform/services/sdk/client-managed-state.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/platform/services/sdk/client-managed-state.ts#L14

Added line #L14 was not covered by tests
new RepositoryRecord(userId, stateProvider, new CipherRecordMapper()),
);
}

export interface SdkRecordMapper<ClientType, SdkType> {
userKeyDefinition(): UserKeyDefinition<Record<string, ClientType>>;
toSdk(value: ClientType): SdkType;
fromSdk(value: SdkType): ClientType;
}

class RepositoryRecord<ClientType, SdkType> implements Repository<SdkType> {
constructor(
private userId: UserId,
private stateProvider: StateProvider,
private mapper: SdkRecordMapper<ClientType, SdkType>,

Check warning on line 29 in libs/common/src/platform/services/sdk/client-managed-state.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/platform/services/sdk/client-managed-state.ts#L27-L29

Added lines #L27 - L29 were not covered by tests
) {}

async get(id: string): Promise<SdkType | null> {
const prov = this.stateProvider.getUser(this.userId, this.mapper.userKeyDefinition());

Check warning on line 33 in libs/common/src/platform/services/sdk/client-managed-state.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/platform/services/sdk/client-managed-state.ts#L33

Added line #L33 was not covered by tests
const data = await firstValueFrom(prov.state$.pipe(map((data) => data ?? {})));
const element = data[id];

Check warning on line 35 in libs/common/src/platform/services/sdk/client-managed-state.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/platform/services/sdk/client-managed-state.ts#L35

Added line #L35 was not covered by tests
if (!element) {
return null;

Check warning on line 37 in libs/common/src/platform/services/sdk/client-managed-state.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/platform/services/sdk/client-managed-state.ts#L37

Added line #L37 was not covered by tests
}
return this.mapper.toSdk(element);

Check warning on line 39 in libs/common/src/platform/services/sdk/client-managed-state.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/platform/services/sdk/client-managed-state.ts#L39

Added line #L39 was not covered by tests
}

async list(): Promise<SdkType[]> {
const prov = this.stateProvider.getUser(this.userId, this.mapper.userKeyDefinition());

Check warning on line 43 in libs/common/src/platform/services/sdk/client-managed-state.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/platform/services/sdk/client-managed-state.ts#L43

Added line #L43 was not covered by tests
const elements = await firstValueFrom(prov.state$.pipe(map((data) => data ?? {})));
return Object.values(elements).map((element) => this.mapper.toSdk(element));

Check warning on line 45 in libs/common/src/platform/services/sdk/client-managed-state.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/platform/services/sdk/client-managed-state.ts#L45

Added line #L45 was not covered by tests
}

async set(id: string, value: SdkType): Promise<void> {
const prov = this.stateProvider.getUser(this.userId, this.mapper.userKeyDefinition());

Check warning on line 49 in libs/common/src/platform/services/sdk/client-managed-state.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/platform/services/sdk/client-managed-state.ts#L49

Added line #L49 was not covered by tests
const elements = await firstValueFrom(prov.state$.pipe(map((data) => data ?? {})));
elements[id] = this.mapper.fromSdk(value);
await prov.update(() => elements);

Check warning on line 52 in libs/common/src/platform/services/sdk/client-managed-state.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/platform/services/sdk/client-managed-state.ts#L51-L52

Added lines #L51 - L52 were not covered by tests
}

async remove(id: string): Promise<void> {
const prov = this.stateProvider.getUser(this.userId, this.mapper.userKeyDefinition());

Check warning on line 56 in libs/common/src/platform/services/sdk/client-managed-state.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/platform/services/sdk/client-managed-state.ts#L56

Added line #L56 was not covered by tests
const elements = await firstValueFrom(prov.state$.pipe(map((data) => data ?? {})));
if (!elements[id]) {
return;

Check warning on line 59 in libs/common/src/platform/services/sdk/client-managed-state.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/platform/services/sdk/client-managed-state.ts#L59

Added line #L59 was not covered by tests
}
delete elements[id];
await prov.update(() => elements);

Check warning on line 62 in libs/common/src/platform/services/sdk/client-managed-state.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/platform/services/sdk/client-managed-state.ts#L61-L62

Added lines #L61 - L62 were not covered by tests
}
}
10 changes: 10 additions & 0 deletions libs/common/src/platform/services/sdk/default-sdk.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
import { compareValues } from "../../misc/compare-values";
import { Rc } from "../../misc/reference-counting/rc";
import { EncryptedString } from "../../models/domain/enc-string";
import { StateProvider } from "../../state";

import { initializeState } from "./client-managed-state";

// A symbol that represents an overriden client that is explicitly set to undefined,
// blocking the creation of an internal client for that user.
Expand Down Expand Up @@ -68,6 +71,7 @@
private accountService: AccountService,
private kdfConfigService: KdfConfigService,
private keyService: KeyService,
private stateProvider?: StateProvider,
private userAgent: string | null = null,
) {}

Expand Down Expand Up @@ -224,6 +228,12 @@
.map(([k, v]) => [k, v.key]),
),
});

// This is optional to avoid having to mock it on the tests
if (this.stateProvider) {
// Initialize the SDK managed database and the client managed repositories.
await initializeState(userId, client.platform().state(), this.stateProvider);

Check warning on line 235 in libs/common/src/platform/services/sdk/default-sdk.service.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/platform/services/sdk/default-sdk.service.ts#L235

Added line #L235 was not covered by tests
}
}

private toSettings(env: Environment): ClientSettings {
Expand Down
17 changes: 17 additions & 0 deletions libs/common/src/vault/models/domain/cipher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";

import { SdkRecordMapper } from "@bitwarden/common/platform/services/sdk/client-managed-state";
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
import { Cipher as SdkCipher } from "@bitwarden/sdk-internal";

import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
Expand All @@ -12,6 +14,7 @@
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherType } from "../../enums/cipher-type";
import { ENCRYPTED_CIPHERS } from "../../services/key-state/ciphers.state";
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
import { CipherData } from "../data/cipher.data";
import { LocalData } from "../data/local.data";
Expand Down Expand Up @@ -409,3 +412,17 @@
return sdkCipher;
}
}

export class CipherRecordMapper implements SdkRecordMapper<CipherData, SdkCipher> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐ŸŽจ non blocking and could always be done in the future: I probably would put this into a separate file only to keep this file dedicated to Cipher

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

userKeyDefinition(): UserKeyDefinition<Record<string, CipherData>> {
return ENCRYPTED_CIPHERS;

Check warning on line 418 in libs/common/src/vault/models/domain/cipher.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/vault/models/domain/cipher.ts#L418

Added line #L418 was not covered by tests
}

toSdk(value: CipherData): SdkCipher {
return new Cipher(value).toSdkCipher();

Check warning on line 422 in libs/common/src/vault/models/domain/cipher.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/vault/models/domain/cipher.ts#L422

Added line #L422 was not covered by tests
}

fromSdk(value: SdkCipher): CipherData {
throw new Error("Cipher.fromSdk is not implemented yet");

Check warning on line 426 in libs/common/src/vault/models/domain/cipher.ts

View check run for this annotation

Codecov / codecov/patch

libs/common/src/vault/models/domain/cipher.ts#L426

Added line #L426 was not covered by tests
}
}
Loading