1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-09-30 04:28:19 +02:00

Wire up key definitions for OrganizationService (#7781)

* Wire up key definitions for OrganizationService

[`AC-2009`: Transition OrganizationService to use StateProvider](
https://bitwarden.atlassian.net/browse/AC-2009)

In order to support the new `StateProvider` APIs for managing
application state this commit modifies `OrganizationService` in the following
ways:

1. Adding a `KeyDefinition` object to `OrganizationService` to store the
   `organization` record in `StateProvider`.
1. Injecting `StateProvider` and wiring up `OrganizationService` to read
   from the `organizations` key definition for the active user account.
1. Expanding the capabilities of `OrganizationData` to be able to read
   itself from a JSON string. Previously this was handled directly by
   `StateService`.
1. Updating tests to include requirements for testing against
   `StateProvider`.
1. Marking the existing `StateService`-backed `organizations`
   `Observable` and `BehaviorSubject` as deprecated.

This is largely unimplemented code with no intended visible effects to
the system. Implementing getting & updating the `organizations` value
from `StateProvider` will the next step in this work.

* Rework null check on OrganizationData

* Remove deprecation signals for the time being

* Move key definition inline with its service

* Create date objects when deserialzing json from state
This commit is contained in:
Addison Beck 2024-02-06 18:48:34 -06:00 committed by GitHub
parent fb17cb7b99
commit b3135403e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 152 additions and 14 deletions

View File

@ -5,6 +5,7 @@ import {
CachedServices,
factory,
} from "../../../platform/background/service-factories/factory-options";
import { stateProviderFactory } from "../../../platform/background/service-factories/state-provider.factory";
import {
stateServiceFactory,
StateServiceInitOptions,
@ -24,6 +25,10 @@ export function organizationServiceFactory(
cache,
"organizationService",
opts,
async () => new BrowserOrganizationService(await stateServiceFactory(cache, opts)),
async () =>
new BrowserOrganizationService(
await stateServiceFactory(cache, opts),
await stateProviderFactory(cache, opts),
),
);
}

View File

@ -442,7 +442,10 @@ export default class MainBackground {
this.stateService,
);
this.syncNotifierService = new SyncNotifierService();
this.organizationService = new BrowserOrganizationService(this.stateService);
this.organizationService = new BrowserOrganizationService(
this.stateService,
this.stateProvider,
);
this.policyService = new BrowserPolicyService(this.stateService, this.organizationService);
this.policyApiService = new PolicyApiService(
this.policyService,

View File

@ -68,7 +68,7 @@ import { GlobalState } from "@bitwarden/common/platform/models/domain/global-sta
import { ConfigService } from "@bitwarden/common/platform/services/config/config.service";
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
import { DerivedStateProvider } from "@bitwarden/common/platform/state";
import { DerivedStateProvider, StateProvider } from "@bitwarden/common/platform/state";
import { SearchService } from "@bitwarden/common/services/search.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
@ -415,8 +415,8 @@ function getBgService<T>(service: keyof MainBackground) {
},
{
provide: OrganizationService,
useFactory: (stateService: StateServiceAbstraction) => {
return new BrowserOrganizationService(stateService);
useFactory: (stateService: StateServiceAbstraction, stateProvider: StateProvider) => {
return new BrowserOrganizationService(stateService, stateProvider);
},
deps: [StateServiceAbstraction],
},

View File

@ -352,7 +352,7 @@ export class Main {
this.providerService = new ProviderService(this.stateService);
this.organizationService = new OrganizationService(this.stateService);
this.organizationService = new OrganizationService(this.stateService, this.stateProvider);
this.organizationUserService = new OrganizationUserServiceImplementation(this.apiService);

View File

@ -0,0 +1,62 @@
import { ProductType } from "../../../enums/product-type.enum";
import { OrganizationUserStatusType, OrganizationUserType } from "../../enums";
import { ORGANIZATIONS } from "../../services/organization/organization.service";
import { OrganizationData } from "./organization.data";
describe("ORGANIZATIONS state", () => {
const sut = ORGANIZATIONS;
it("should deserialize JSON string to proper object", async () => {
const expectedResult: Record<string, OrganizationData> = {
"1": {
id: "id",
name: "name",
status: OrganizationUserStatusType.Invited,
type: OrganizationUserType.Owner,
enabled: false,
usePolicies: false,
useGroups: false,
useDirectory: false,
useEvents: false,
useTotp: false,
use2fa: false,
useApi: false,
useSso: false,
useKeyConnector: false,
useScim: false,
useCustomPermissions: false,
useResetPassword: false,
useSecretsManager: false,
usePasswordManager: false,
useActivateAutofillPolicy: false,
selfHost: false,
usersGetPremium: false,
seats: 0,
maxCollections: 0,
ssoBound: false,
identifier: "identifier",
permissions: undefined,
resetPasswordEnrolled: false,
userId: "userId",
hasPublicAndPrivateKeys: false,
providerId: "providerId",
providerName: "providerName",
isProviderUser: false,
isMember: false,
familySponsorshipFriendlyName: "fsfn",
familySponsorshipAvailable: false,
planProductType: ProductType.Free,
keyConnectorEnabled: false,
keyConnectorUrl: "kcu",
accessSecretsManager: false,
limitCollectionCreationDeletion: false,
allowAdminAccessToAllCollectionItems: false,
flexibleCollections: false,
familySponsorshipLastSyncDate: new Date(),
},
};
const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult)));
expect(result).toEqual(expectedResult);
});
});

View File

@ -1,3 +1,5 @@
import { Jsonify } from "type-fest";
import { ProductType } from "../../../enums";
import { OrganizationUserStatusType, OrganizationUserType, ProviderType } from "../../enums";
import { PermissionsApi } from "../api/permissions.api";
@ -54,12 +56,16 @@ export class OrganizationData {
flexibleCollections: boolean;
constructor(
response: ProfileOrganizationResponse,
options: {
response?: ProfileOrganizationResponse,
options?: {
isMember: boolean;
isProviderUser: boolean;
},
) {
if (response == null) {
return;
}
this.id = response.id;
this.name = response.name;
this.status = response.status;
@ -110,4 +116,17 @@ export class OrganizationData {
this.isMember = options.isMember;
this.isProviderUser = options.isProviderUser;
}
static fromJSON(obj: Jsonify<OrganizationData>) {
return Object.assign(new OrganizationData(), obj, {
familySponsorshipLastSyncDate:
obj.familySponsorshipLastSyncDate != null
? new Date(obj.familySponsorshipLastSyncDate)
: obj.familySponsorshipLastSyncDate,
familySponsorshipValidUntil:
obj.familySponsorshipValidUntil != null
? new Date(obj.familySponsorshipValidUntil)
: obj.familySponsorshipValidUntil,
});
}
}

View File

@ -1,10 +1,14 @@
import { MockProxy, mock, any, mockClear } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
import { FakeActiveUserState } from "../../../../spec/fake-state";
import { StateService } from "../../../platform/abstractions/state.service";
import { Utils } from "../../../platform/misc/utils";
import { UserId } from "../../../types/guid";
import { OrganizationData } from "../../models/data/organization.data";
import { OrganizationService } from "./organization.service";
import { OrganizationService, ORGANIZATIONS } from "./organization.service";
describe("Organization Service", () => {
let organizationService: OrganizationService;
@ -13,6 +17,11 @@ describe("Organization Service", () => {
let activeAccount: BehaviorSubject<string>;
let activeAccountUnlocked: BehaviorSubject<boolean>;
const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
let stateProvider: FakeStateProvider;
let activeUserOrganizationsState: FakeActiveUserState<Record<string, OrganizationData>>;
const resetStateService = async (
customizeStateService: (stateService: MockProxy<StateService>) => void,
) => {
@ -21,10 +30,20 @@ describe("Organization Service", () => {
stateService.activeAccount$ = activeAccount;
stateService.activeAccountUnlocked$ = activeAccountUnlocked;
customizeStateService(stateService);
organizationService = new OrganizationService(stateService);
organizationService = new OrganizationService(stateService, stateProvider);
await new Promise((r) => setTimeout(r, 50));
};
function prepareStateProvider(): void {
accountService = mockAccountServiceWith(mockUserId);
stateProvider = new FakeStateProvider(accountService);
}
function seedTestData(): void {
activeUserOrganizationsState = stateProvider.activeUser.getFake(ORGANIZATIONS);
activeUserOrganizationsState.nextState({ "1": organizationData("1", "Test Org") });
}
beforeEach(() => {
activeAccount = new BehaviorSubject("123");
activeAccountUnlocked = new BehaviorSubject(true);
@ -37,7 +56,11 @@ describe("Organization Service", () => {
"1": organizationData("1", "Test Org"),
});
organizationService = new OrganizationService(stateService);
prepareStateProvider();
organizationService = new OrganizationService(stateService, stateProvider);
seedTestData();
});
afterEach(() => {

View File

@ -1,6 +1,8 @@
import { BehaviorSubject, concatMap, map, Observable } from "rxjs";
import { Jsonify } from "type-fest";
import { StateService } from "../../../platform/abstractions/state.service";
import { KeyDefinition, ORGANIZATIONS_DISK, StateProvider } from "../../../platform/state";
import {
InternalOrganizationServiceAbstraction,
isMember,
@ -8,13 +10,37 @@ import {
import { OrganizationData } from "../../models/data/organization.data";
import { Organization } from "../../models/domain/organization";
export class OrganizationService implements InternalOrganizationServiceAbstraction {
protected _organizations = new BehaviorSubject<Organization[]>([]);
export const ORGANIZATIONS = KeyDefinition.record<OrganizationData>(
ORGANIZATIONS_DISK,
"organizations",
{
deserializer: (obj: Jsonify<OrganizationData>) => OrganizationData.fromJSON(obj),
},
);
export class OrganizationService implements InternalOrganizationServiceAbstraction {
// marked for removal during AC-2009
protected _organizations = new BehaviorSubject<Organization[]>([]);
// marked for removal during AC-2009
organizations$ = this._organizations.asObservable();
// marked for removal during AC-2009
memberOrganizations$ = this.organizations$.pipe(map((orgs) => orgs.filter(isMember)));
constructor(private stateService: StateService) {
activeUserOrganizations$: Observable<Organization[]>;
activeUserMemberOrganizations$: Observable<Organization[]>;
constructor(
private stateService: StateService,
private stateProvider: StateProvider,
) {
this.activeUserOrganizations$ = this.stateProvider
.getActive(ORGANIZATIONS)
.state$.pipe(map((data) => Object.values(data).map((o) => new Organization(o))));
this.activeUserMemberOrganizations$ = this.activeUserOrganizations$.pipe(
map((orgs) => orgs.filter(isMember)),
);
this.stateService.activeAccountUnlocked$
.pipe(
concatMap(async (unlocked) => {