mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 01:03:35 +00:00
[PM-5572] Event upload and collection state provider migration (#7863)
* event upload and collection state provider migration * cipher can be null when exporting org * Addressing pr comments. Casting UserId from calling methods * fixing userAuth observable in event collection service * Adding more documentation for the changes. * cli needed state provider and account services added * Addressing pr comments on modifying should update * No need to auth on event upload * Simplifying the takeEvents for pulling user events * Reverting shouldUpdate to previous state * Removing redundant comment * Removing account service for event upload * Modifying the shouldUpdate to evaluate the logic outside of the observable * Adding back in the auth for event upload service and adding event upload to the cli logout method * Adding the browser service factories * Updating the browser services away from get background * Removing event collect and upload services from browser services * Removing the audit service import * Adding the event collection migration and migration test * Event collection state needs to be stored on disk * removing event collection from state service and abstraction * removing event collection from the account data * Saving the migrations themselves
This commit is contained in:
@@ -1,61 +1,105 @@
|
||||
import { firstValueFrom, map, from, zip } from "rxjs";
|
||||
|
||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service";
|
||||
import { EventUploadService } from "../../abstractions/event/event-upload.service";
|
||||
import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||
import { EventType } from "../../enums";
|
||||
import { EventData } from "../../models/data/event.data";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { StateProvider } from "../../platform/state";
|
||||
import { CipherService } from "../../vault/abstractions/cipher.service";
|
||||
|
||||
import { EVENT_COLLECTION } from "./key-definitions";
|
||||
|
||||
export class EventCollectionService implements EventCollectionServiceAbstraction {
|
||||
constructor(
|
||||
private cipherService: CipherService,
|
||||
private stateService: StateService,
|
||||
private stateProvider: StateProvider,
|
||||
private organizationService: OrganizationService,
|
||||
private eventUploadService: EventUploadService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
/** Adds an event to the active user's event collection
|
||||
* @param eventType the event type to be added
|
||||
* @param cipherId if provided the id of the cipher involved in the event
|
||||
* @param uploadImmediately in some cases the recorded events should be uploaded right after being added
|
||||
* @param organizationId the organizationId involved in the event. If the cipherId is not provided an organizationId is required
|
||||
*/
|
||||
async collect(
|
||||
eventType: EventType,
|
||||
cipherId: string = null,
|
||||
uploadImmediately = false,
|
||||
organizationId: string = null,
|
||||
): Promise<any> {
|
||||
const authed = await this.stateService.getIsAuthenticated();
|
||||
if (!authed) {
|
||||
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION);
|
||||
|
||||
if (!(await this.shouldUpdate(cipherId, organizationId))) {
|
||||
return;
|
||||
}
|
||||
const organizations = await this.organizationService.getAll();
|
||||
if (organizations == null) {
|
||||
return;
|
||||
}
|
||||
const orgIds = new Set<string>(organizations.filter((o) => o.useEvents).map((o) => o.id));
|
||||
if (orgIds.size === 0) {
|
||||
return;
|
||||
}
|
||||
if (cipherId != null) {
|
||||
const cipher = await this.cipherService.get(cipherId);
|
||||
if (cipher == null || cipher.organizationId == null || !orgIds.has(cipher.organizationId)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (organizationId != null) {
|
||||
if (!orgIds.has(organizationId)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let eventCollection = await this.stateService.getEventCollection();
|
||||
if (eventCollection == null) {
|
||||
eventCollection = [];
|
||||
}
|
||||
|
||||
const event = new EventData();
|
||||
event.type = eventType;
|
||||
event.cipherId = cipherId;
|
||||
event.date = new Date().toISOString();
|
||||
event.organizationId = organizationId;
|
||||
eventCollection.push(event);
|
||||
await this.stateService.setEventCollection(eventCollection);
|
||||
|
||||
await eventStore.update((events) => {
|
||||
events = events ?? [];
|
||||
events.push(event);
|
||||
return events;
|
||||
});
|
||||
|
||||
if (uploadImmediately) {
|
||||
await this.eventUploadService.uploadEvents();
|
||||
}
|
||||
}
|
||||
|
||||
/** Verifies if the event collection should be updated for the provided information
|
||||
* @param cipherId the cipher for the event
|
||||
* @param organizationId the organization for the event
|
||||
*/
|
||||
private async shouldUpdate(
|
||||
cipherId: string = null,
|
||||
organizationId: string = null,
|
||||
): Promise<boolean> {
|
||||
const orgIds$ = this.organizationService.organizations$.pipe(
|
||||
map((orgs) => orgs?.filter((o) => o.useEvents)?.map((x) => x.id) ?? []),
|
||||
);
|
||||
|
||||
const cipher$ = from(this.cipherService.get(cipherId));
|
||||
|
||||
const [accountInfo, orgIds, cipher] = await firstValueFrom(
|
||||
zip(this.accountService.activeAccount$, orgIds$, cipher$),
|
||||
);
|
||||
|
||||
// The user must be authorized
|
||||
if (accountInfo.status != AuthenticationStatus.Unlocked) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// User must have organizations assigned to them
|
||||
if (orgIds == null || orgIds.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the cipher is null there must be an organization id provided
|
||||
if (cipher == null && organizationId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the cipher is present it must be in the user's org list
|
||||
if (cipher != null && !orgIds.includes(cipher?.organizationId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the organization id is provided it must be in the user's org list
|
||||
if (organizationId != null && !orgIds.includes(organizationId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { EventUploadService as EventUploadServiceAbstraction } from "../../abstractions/event/event-upload.service";
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||
import { EventData } from "../../models/data/event.data";
|
||||
import { EventRequest } from "../../models/request/event.request";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { StateProvider } from "../../platform/state";
|
||||
import { UserId } from "../../types/guid";
|
||||
|
||||
import { EVENT_COLLECTION } from "./key-definitions";
|
||||
|
||||
export class EventUploadService implements EventUploadServiceAbstraction {
|
||||
private inited = false;
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private stateService: StateService,
|
||||
private stateProvider: StateProvider,
|
||||
private logService: LogService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
init(checkOnInterval: boolean) {
|
||||
@@ -26,12 +35,26 @@ export class EventUploadService implements EventUploadServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
async uploadEvents(userId?: string): Promise<void> {
|
||||
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
|
||||
if (!authed) {
|
||||
/** Upload the event collection from state.
|
||||
* @param userId upload events for provided user. If not active user will be used.
|
||||
*/
|
||||
async uploadEvents(userId?: UserId): Promise<void> {
|
||||
if (!userId) {
|
||||
userId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
}
|
||||
|
||||
// Get the auth status from the provided user or the active user
|
||||
const userAuth$ = this.accountService.accounts$.pipe(
|
||||
map((accounts) => accounts[userId]?.status === AuthenticationStatus.Unlocked),
|
||||
);
|
||||
|
||||
const isAuthenticated = await firstValueFrom(userAuth$);
|
||||
if (!isAuthenticated) {
|
||||
return;
|
||||
}
|
||||
const eventCollection = await this.stateService.getEventCollection({ userId: userId });
|
||||
|
||||
const eventCollection = await this.takeEvents(userId);
|
||||
|
||||
if (eventCollection == null || eventCollection.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -45,15 +68,23 @@ export class EventUploadService implements EventUploadServiceAbstraction {
|
||||
});
|
||||
try {
|
||||
await this.apiService.postEventsCollect(request);
|
||||
// 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.clearEvents(userId);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
// Add the events back to state if there was an error and they were not uploaded.
|
||||
await this.stateProvider.setUserState(EVENT_COLLECTION, eventCollection, userId);
|
||||
}
|
||||
}
|
||||
|
||||
private async clearEvents(userId?: string): Promise<any> {
|
||||
await this.stateService.setEventCollection(null, { userId: userId });
|
||||
/** Return user's events and then clear them from state
|
||||
* @param userId the user to grab and clear events for
|
||||
*/
|
||||
private async takeEvents(userId: UserId): Promise<EventData[]> {
|
||||
let taken = null;
|
||||
await this.stateProvider.getUser(userId, EVENT_COLLECTION).update((current) => {
|
||||
taken = current ?? [];
|
||||
return [];
|
||||
});
|
||||
|
||||
return taken;
|
||||
}
|
||||
}
|
||||
|
||||
10
libs/common/src/services/event/key-definitions.ts
Normal file
10
libs/common/src/services/event/key-definitions.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { EventData } from "../../models/data/event.data";
|
||||
import { KeyDefinition, EVENT_COLLECTION_DISK } from "../../platform/state";
|
||||
|
||||
export const EVENT_COLLECTION: KeyDefinition<EventData[]> = KeyDefinition.array<EventData>(
|
||||
EVENT_COLLECTION_DISK,
|
||||
"events",
|
||||
{
|
||||
deserializer: (s) => EventData.fromJSON(s),
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user