mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00: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:
@@ -5,6 +5,7 @@ import {
|
|||||||
CachedServices,
|
CachedServices,
|
||||||
factory,
|
factory,
|
||||||
} from "../../../platform/background/service-factories/factory-options";
|
} from "../../../platform/background/service-factories/factory-options";
|
||||||
|
import { stateProviderFactory } from "../../../platform/background/service-factories/state-provider.factory";
|
||||||
import {
|
import {
|
||||||
stateServiceFactory,
|
stateServiceFactory,
|
||||||
StateServiceInitOptions,
|
StateServiceInitOptions,
|
||||||
@@ -24,6 +25,10 @@ export function organizationServiceFactory(
|
|||||||
cache,
|
cache,
|
||||||
"organizationService",
|
"organizationService",
|
||||||
opts,
|
opts,
|
||||||
async () => new BrowserOrganizationService(await stateServiceFactory(cache, opts)),
|
async () =>
|
||||||
|
new BrowserOrganizationService(
|
||||||
|
await stateServiceFactory(cache, opts),
|
||||||
|
await stateProviderFactory(cache, opts),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -442,7 +442,10 @@ export default class MainBackground {
|
|||||||
this.stateService,
|
this.stateService,
|
||||||
);
|
);
|
||||||
this.syncNotifierService = new SyncNotifierService();
|
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.policyService = new BrowserPolicyService(this.stateService, this.organizationService);
|
||||||
this.policyApiService = new PolicyApiService(
|
this.policyApiService = new PolicyApiService(
|
||||||
this.policyService,
|
this.policyService,
|
||||||
|
|||||||
@@ -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 { ConfigService } from "@bitwarden/common/platform/services/config/config.service";
|
||||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||||
import { ContainerService } from "@bitwarden/common/platform/services/container.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 { SearchService } from "@bitwarden/common/services/search.service";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||||
@@ -415,8 +415,8 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: OrganizationService,
|
provide: OrganizationService,
|
||||||
useFactory: (stateService: StateServiceAbstraction) => {
|
useFactory: (stateService: StateServiceAbstraction, stateProvider: StateProvider) => {
|
||||||
return new BrowserOrganizationService(stateService);
|
return new BrowserOrganizationService(stateService, stateProvider);
|
||||||
},
|
},
|
||||||
deps: [StateServiceAbstraction],
|
deps: [StateServiceAbstraction],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -352,7 +352,7 @@ export class Main {
|
|||||||
|
|
||||||
this.providerService = new ProviderService(this.stateService);
|
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);
|
this.organizationUserService = new OrganizationUserServiceImplementation(this.apiService);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { ProductType } from "../../../enums";
|
import { ProductType } from "../../../enums";
|
||||||
import { OrganizationUserStatusType, OrganizationUserType, ProviderType } from "../../enums";
|
import { OrganizationUserStatusType, OrganizationUserType, ProviderType } from "../../enums";
|
||||||
import { PermissionsApi } from "../api/permissions.api";
|
import { PermissionsApi } from "../api/permissions.api";
|
||||||
@@ -54,12 +56,16 @@ export class OrganizationData {
|
|||||||
flexibleCollections: boolean;
|
flexibleCollections: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
response: ProfileOrganizationResponse,
|
response?: ProfileOrganizationResponse,
|
||||||
options: {
|
options?: {
|
||||||
isMember: boolean;
|
isMember: boolean;
|
||||||
isProviderUser: boolean;
|
isProviderUser: boolean;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
if (response == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.id = response.id;
|
this.id = response.id;
|
||||||
this.name = response.name;
|
this.name = response.name;
|
||||||
this.status = response.status;
|
this.status = response.status;
|
||||||
@@ -110,4 +116,17 @@ export class OrganizationData {
|
|||||||
this.isMember = options.isMember;
|
this.isMember = options.isMember;
|
||||||
this.isProviderUser = options.isProviderUser;
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { MockProxy, mock, any, mockClear } from "jest-mock-extended";
|
import { MockProxy, mock, any, mockClear } from "jest-mock-extended";
|
||||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
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 { 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 { OrganizationData } from "../../models/data/organization.data";
|
||||||
|
|
||||||
import { OrganizationService } from "./organization.service";
|
import { OrganizationService, ORGANIZATIONS } from "./organization.service";
|
||||||
|
|
||||||
describe("Organization Service", () => {
|
describe("Organization Service", () => {
|
||||||
let organizationService: OrganizationService;
|
let organizationService: OrganizationService;
|
||||||
@@ -13,6 +17,11 @@ describe("Organization Service", () => {
|
|||||||
let activeAccount: BehaviorSubject<string>;
|
let activeAccount: BehaviorSubject<string>;
|
||||||
let activeAccountUnlocked: BehaviorSubject<boolean>;
|
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 (
|
const resetStateService = async (
|
||||||
customizeStateService: (stateService: MockProxy<StateService>) => void,
|
customizeStateService: (stateService: MockProxy<StateService>) => void,
|
||||||
) => {
|
) => {
|
||||||
@@ -21,10 +30,20 @@ describe("Organization Service", () => {
|
|||||||
stateService.activeAccount$ = activeAccount;
|
stateService.activeAccount$ = activeAccount;
|
||||||
stateService.activeAccountUnlocked$ = activeAccountUnlocked;
|
stateService.activeAccountUnlocked$ = activeAccountUnlocked;
|
||||||
customizeStateService(stateService);
|
customizeStateService(stateService);
|
||||||
organizationService = new OrganizationService(stateService);
|
organizationService = new OrganizationService(stateService, stateProvider);
|
||||||
await new Promise((r) => setTimeout(r, 50));
|
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(() => {
|
beforeEach(() => {
|
||||||
activeAccount = new BehaviorSubject("123");
|
activeAccount = new BehaviorSubject("123");
|
||||||
activeAccountUnlocked = new BehaviorSubject(true);
|
activeAccountUnlocked = new BehaviorSubject(true);
|
||||||
@@ -37,7 +56,11 @@ describe("Organization Service", () => {
|
|||||||
"1": organizationData("1", "Test Org"),
|
"1": organizationData("1", "Test Org"),
|
||||||
});
|
});
|
||||||
|
|
||||||
organizationService = new OrganizationService(stateService);
|
prepareStateProvider();
|
||||||
|
|
||||||
|
organizationService = new OrganizationService(stateService, stateProvider);
|
||||||
|
|
||||||
|
seedTestData();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { BehaviorSubject, concatMap, map, Observable } from "rxjs";
|
import { BehaviorSubject, concatMap, map, Observable } from "rxjs";
|
||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { StateService } from "../../../platform/abstractions/state.service";
|
import { StateService } from "../../../platform/abstractions/state.service";
|
||||||
|
import { KeyDefinition, ORGANIZATIONS_DISK, StateProvider } from "../../../platform/state";
|
||||||
import {
|
import {
|
||||||
InternalOrganizationServiceAbstraction,
|
InternalOrganizationServiceAbstraction,
|
||||||
isMember,
|
isMember,
|
||||||
@@ -8,13 +10,37 @@ import {
|
|||||||
import { OrganizationData } from "../../models/data/organization.data";
|
import { OrganizationData } from "../../models/data/organization.data";
|
||||||
import { Organization } from "../../models/domain/organization";
|
import { Organization } from "../../models/domain/organization";
|
||||||
|
|
||||||
export class OrganizationService implements InternalOrganizationServiceAbstraction {
|
export const ORGANIZATIONS = KeyDefinition.record<OrganizationData>(
|
||||||
protected _organizations = new BehaviorSubject<Organization[]>([]);
|
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();
|
organizations$ = this._organizations.asObservable();
|
||||||
|
// marked for removal during AC-2009
|
||||||
memberOrganizations$ = this.organizations$.pipe(map((orgs) => orgs.filter(isMember)));
|
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$
|
this.stateService.activeAccountUnlocked$
|
||||||
.pipe(
|
.pipe(
|
||||||
concatMap(async (unlocked) => {
|
concatMap(async (unlocked) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user