diff --git a/apps/web/src/app/admin-console/common/base.events.component.ts b/apps/web/src/app/admin-console/common/base.events.component.ts index 9d06be92eb8..ba315dee7fb 100644 --- a/apps/web/src/app/admin-console/common/base.events.component.ts +++ b/apps/web/src/app/admin-console/common/base.events.component.ts @@ -1,8 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Directive } from "@angular/core"; +import { Directive, OnDestroy } from "@angular/core"; import { FormControl, FormGroup } from "@angular/forms"; +import { ActivatedRoute } from "@angular/router"; +import { combineLatest, filter, map, Observable, Subject, switchMap, takeUntil } from "rxjs"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EventResponse } from "@bitwarden/common/models/response/event.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { EventView } from "@bitwarden/common/models/view/event.view"; @@ -12,16 +17,17 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; -import { EventService } from "../../core"; +import { EventOptions, EventService } from "../../core"; import { EventExportService } from "../../tools/event-export"; @Directive() -export abstract class BaseEventsComponent { +export abstract class BaseEventsComponent implements OnDestroy { loading = true; loaded = false; events: EventView[]; dirtyDates = true; continuationToken: string; + canUseSM = false; abstract readonly exportFileName: string; @@ -30,6 +36,15 @@ export abstract class BaseEventsComponent { end: new FormControl(null), }); + protected canUseSM$: Observable; + protected activeOrganization$: Observable; + protected organizations$: Observable; + private destroySubject$ = new Subject(); + + protected get destroy$(): Observable { + return this.destroySubject$.asObservable(); + } + constructor( protected eventService: EventService, protected i18nService: I18nService, @@ -38,12 +53,39 @@ export abstract class BaseEventsComponent { protected logService: LogService, protected fileDownloadService: FileDownloadService, private toastService: ToastService, + protected activeRoute: ActivatedRoute, + protected accountService: AccountService, + protected organizationService: OrganizationService, ) { const defaultDates = this.eventService.getDefaultDateFilters(); this.start = defaultDates[0]; this.end = defaultDates[1]; } + protected initBase(): void { + this.organizations$ = this.accountService.activeAccount$.pipe( + filter((account): account is Account => !!account?.id), + switchMap((account) => this.organizationService.organizations$(account.id)), + ); + + this.activeOrganization$ = combineLatest([this.activeRoute.paramMap, this.organizations$]).pipe( + map(([params, orgs]) => orgs.find((org) => org.id === params.get("organizationId"))), + ); + + this.canUseSM$ = this.activeOrganization$.pipe( + map((org) => org?.canAccessSecretsManager ?? false), + ); + + this.canUseSM$.pipe(takeUntil(this.destroy$)).subscribe((value) => { + this.canUseSM = value; + }); + } + + ngOnDestroy(): void { + this.destroySubject$.next(); + this.destroySubject$.complete(); + } + get start(): string { return this.eventsForm.value.start; } @@ -139,7 +181,10 @@ export abstract class BaseEventsComponent { const events = await Promise.all( response.data.map(async (r) => { const userId = r.actingUserId == null ? r.userId : r.actingUserId; - const eventInfo = await this.eventService.getEventInfo(r); + const options = new EventOptions(); + options.disableLink = !this.canUseSM; + + const eventInfo = await this.eventService.getEventInfo(r, options); const user = this.getUserName(r, userId); const userName = user != null ? user.name : this.i18nService.t("unknown"); diff --git a/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts b/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts index 10f68695e88..8484b05283d 100644 --- a/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts @@ -8,6 +8,8 @@ import { firstValueFrom, switchMap } from "rxjs"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EventResponse } from "@bitwarden/common/models/response/event.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { EventView } from "@bitwarden/common/models/view/event.view"; @@ -26,7 +28,7 @@ import { EventService } from "../../../core"; import { SharedModule } from "../../../shared"; export interface EntityEventsDialogParams { - entity: "user" | "cipher"; + entity: "user" | "cipher" | "secret" | "project"; entityId: string; organizationId?: string; @@ -72,6 +74,8 @@ export class EntityEventsComponent implements OnInit, OnDestroy { private toastService: ToastService, private router: Router, private activeRoute: ActivatedRoute, + private accountService: AccountService, + protected organizationService: OrganizationService, ) {} async ngOnInit() { @@ -162,6 +166,22 @@ export class EntityEventsComponent implements OnInit, OnDestroy { dates[1], clearExisting ? null : this.continuationToken, ); + } else if (this.params.entity === "secret") { + response = await this.apiService.getEventsSecret( + this.params.organizationId, + this.params.entityId, + dates[0], + dates[1], + clearExisting ? null : this.continuationToken, + ); + } else if (this.params.entity === "project") { + response = await this.apiService.getEventsProject( + this.params.organizationId, + this.params.entityId, + dates[0], + dates[1], + clearExisting ? null : this.continuationToken, + ); } else { response = await this.apiService.getEventsCipher( this.params.entityId, diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.ts b/apps/web/src/app/admin-console/organizations/manage/events.component.ts index 07f6be7d7f6..f442d429767 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.ts @@ -1,8 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { concatMap, firstValueFrom, lastValueFrom, Subject, takeUntil } from "rxjs"; +import { ActivatedRoute } from "@angular/router"; +import { concatMap, firstValueFrom, lastValueFrom, takeUntil } from "rxjs"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; @@ -60,8 +60,6 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe placeholderEvents = placeholderEvents as EventView[]; private orgUsersUserIdMap = new Map(); - private destroy$ = new Subject(); - readonly ProductTierType = ProductTierType; protected isBreadcrumbEventLogsEnabled$ = this.configService.getFeatureFlag$( @@ -75,18 +73,18 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe i18nService: I18nService, exportService: EventExportService, platformUtilsService: PlatformUtilsService, - private router: Router, logService: LogService, private userNamePipe: UserNamePipe, - private organizationService: OrganizationService, + protected organizationService: OrganizationService, private organizationUserApiService: OrganizationUserApiService, private organizationApiService: OrganizationApiServiceAbstraction, private providerService: ProviderService, fileDownloadService: FileDownloadService, toastService: ToastService, - private accountService: AccountService, + protected accountService: AccountService, private dialogService: DialogService, private configService: ConfigService, + protected activeRoute: ActivatedRoute, ) { super( eventService, @@ -96,10 +94,15 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe logService, fileDownloadService, toastService, + activeRoute, + accountService, + organizationService, ); } async ngOnInit() { + this.initBase(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.route.params .pipe( @@ -233,9 +236,4 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe } await this.load(); } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } } diff --git a/apps/web/src/app/core/event.service.ts b/apps/web/src/app/core/event.service.ts index 36d591cc390..ab4d21dab54 100644 --- a/apps/web/src/app/core/event.service.ts +++ b/apps/web/src/app/core/event.service.ts @@ -467,21 +467,60 @@ export class EventService { break; // Secrets Manager case EventType.Secret_Retrieved: - msg = this.i18nService.t("accessedSecretWithId", this.formatSecretId(ev)); + msg = this.i18nService.t("accessedSecretWithId", this.formatSecretId(ev, options)); humanReadableMsg = this.i18nService.t("accessedSecretWithId", this.getShortId(ev.secretId)); break; case EventType.Secret_Created: - msg = this.i18nService.t("createdSecretWithId", this.formatSecretId(ev)); + msg = this.i18nService.t("createdSecretWithId", this.formatSecretId(ev, options)); humanReadableMsg = this.i18nService.t("createdSecretWithId", this.getShortId(ev.secretId)); break; case EventType.Secret_Deleted: - msg = this.i18nService.t("deletedSecretWithId", this.formatSecretId(ev)); + msg = this.i18nService.t("deletedSecretWithId", this.formatSecretId(ev, options)); humanReadableMsg = this.i18nService.t("deletedSecretWithId", this.getShortId(ev.secretId)); break; + case EventType.Secret_Permanently_Deleted: + msg = this.i18nService.t( + "permanentlyDeletedSecretWithId", + this.formatSecretId(ev, options), + ); + humanReadableMsg = this.i18nService.t( + "permanentlyDeletedSecretWithId", + this.getShortId(ev.secretId), + ); + break; + case EventType.Secret_Restored: + msg = this.i18nService.t("restoredSecretWithId", this.formatSecretId(ev, options)); + humanReadableMsg = this.i18nService.t("restoredSecretWithId", this.getShortId(ev.secretId)); + break; case EventType.Secret_Edited: - msg = this.i18nService.t("editedSecretWithId", this.formatSecretId(ev)); + msg = this.i18nService.t("editedSecretWithId", this.formatSecretId(ev, options)); humanReadableMsg = this.i18nService.t("editedSecretWithId", this.getShortId(ev.secretId)); break; + case EventType.Project_Retrieved: + msg = this.i18nService.t("accessedProjectWithId", this.formatProjectId(ev, options)); + humanReadableMsg = this.i18nService.t( + "accessedProjectWithId", + this.getShortId(ev.projectId), + ); + break; + case EventType.Project_Created: + msg = this.i18nService.t("createdProjectWithId", this.formatProjectId(ev, options)); + humanReadableMsg = this.i18nService.t( + "createdProjectWithId", + this.getShortId(ev.projectId), + ); + break; + case EventType.Project_Deleted: + msg = this.i18nService.t("deletedProjectWithId", this.formatProjectId(ev, options)); + humanReadableMsg = this.i18nService.t( + "deletedProjectWithId", + this.getShortId(ev.projectId), + ); + break; + case EventType.Project_Edited: + msg = this.i18nService.t("editedProjectWithId", this.formatProjectId(ev, options)); + humanReadableMsg = this.i18nService.t("editedProjectWithId", this.getShortId(ev.projectId)); + break; default: break; } @@ -637,10 +676,41 @@ export class EventService { return a.outerHTML; } - formatSecretId(ev: EventResponse): string { + formatSecretId(ev: EventResponse, options: EventOptions): string { const shortId = this.getShortId(ev.secretId); + if (options.disableLink) { + return shortId; + } const a = this.makeAnchor(shortId); - a.setAttribute("href", "#/sm/" + ev.organizationId + "/secrets?search=" + shortId); + a.setAttribute( + "href", + "#/sm/" + + ev.organizationId + + "/secrets?search=" + + shortId + + "&viewEvents=" + + ev.secretId + + "&type=all", + ); + return a.outerHTML; + } + + formatProjectId(ev: EventResponse, options: EventOptions): string { + const shortId = this.getShortId(ev.projectId); + if (options.disableLink) { + return shortId; + } + const a = this.makeAnchor(shortId); + a.setAttribute( + "href", + "#/sm/" + + ev.organizationId + + "/projects?search=" + + shortId + + "&viewEvents=" + + ev.projectId + + "&type=all", + ); return a.outerHTML; } @@ -684,4 +754,5 @@ export class EventInfo { export class EventOptions { cipherInfo = true; + disableLink = false; } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 98e3a4d5d11..cdb6bdc1e7e 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7039,6 +7039,12 @@ "unknownCipher": { "message": "Unknown item, you may need to request permission to access this item." }, + "unknownSecret": { + "message": "Unknown secret, you may need to request permission to access this secret." + }, + "unknownProject": { + "message": "Unknown project, you may need to request permission to access this project." + }, "cannotSponsorSelf": { "message": "You cannot redeem for the active account. Enter a different email." }, @@ -8361,6 +8367,24 @@ "example": "4d34e8a8" } } + }, + "permanentlyDeletedSecretWithId": { + "message": "Permanently deleted a secret with identifier: $SECRET_ID$", + "placeholders": { + "secret_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "restoredSecretWithId": { + "message": "Restored a secret with identifier: $SECRET_ID$", + "placeholders": { + "secret_id": { + "content": "$1", + "example": "4d34e8a8" + } + } }, "createdSecretWithId": { "message": "Created a new secret with identifier: $SECRET_ID$", @@ -8371,6 +8395,60 @@ } } }, + "accessedProjectWithId": { + "message": "Accessed a project with Id: $PROJECT_ID$.", + "placeholders": { + "project_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "nameUnavailableProjectDeleted": { + "message": "Deleted project Id: $PROJECT_ID$", + "placeholders": { + "project_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "nameUnavailableSecretDeleted": { + "message": "Deleted secret Id: $SECRET_ID$", + "placeholders": { + "secret_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "editedProjectWithId": { + "message": "Edited a project with identifier: $PROJECT_ID$", + "placeholders": { + "project_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "deletedProjectWithId": { + "message": "Deleted a project with identifier: $PROJECT_ID$", + "placeholders": { + "project_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, + "createdProjectWithId": { + "message": "Created a new project with identifier: $PROJECT_ID$", + "placeholders": { + "project_id": { + "content": "$1", + "example": "4d34e8a8" + } + } + }, "sdk": { "message": "SDK", "description": "Software Development Kit" @@ -10755,6 +10833,9 @@ "upgradeEventLogMessage":{ "message" : "These events are examples only and do not reflect real events within your Bitwarden organization." }, + "viewEvents":{ + "message" : "View Events" + }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." }, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts index 2ad2ecdccbd..d8ad2e01343 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts @@ -5,7 +5,9 @@ import { ActivatedRoute, Router } from "@angular/router"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EventResponse } from "@bitwarden/common/models/response/event.response"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -41,6 +43,8 @@ export class EventsComponent extends BaseEventsComponent implements OnInit { private userNamePipe: UserNamePipe, fileDownloadService: FileDownloadService, toastService: ToastService, + accountService: AccountService, + organizationService: OrganizationService, ) { super( eventService, @@ -50,6 +54,9 @@ export class EventsComponent extends BaseEventsComponent implements OnInit { logService, fileDownloadService, toastService, + route, + accountService, + organizationService, ); } @@ -69,6 +76,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit { } async load() { + this.initBase(); const response = await this.apiService.getProviderUsers(this.providerId); response.data.forEach((u) => { const name = this.userNamePipe.transform(u); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts index ea5294624af..81a568f0c65 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { combineLatest, firstValueFrom, @@ -17,9 +17,12 @@ import { } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { DialogService } from "@bitwarden/components"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; +import { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component"; import { ProjectListView } from "../../models/view/project-list.view"; +import { ProjectView } from "../../models/view/project.view"; import { BulkConfirmationDetails, BulkConfirmationDialogComponent, @@ -55,6 +58,9 @@ export class ProjectsComponent implements OnInit { private dialogService: DialogService, private organizationService: OrganizationService, private accountService: AccountService, + private toastService: ToastService, + private i18nService: I18nService, + private router: Router, ) {} ngOnInit() { @@ -73,9 +79,53 @@ export class ProjectsComponent implements OnInit { ) )?.enabled; - return await this.getProjects(); + const projects = await this.getProjects(); + const viewEvents = this.route.snapshot.queryParams.viewEvents; + + if (viewEvents) { + const targetProject = projects.find((project) => project.id === viewEvents); + + const userIsAdmin = ( + await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ) + )?.isAdmin; + + // They would fall into here if they don't have access to a project, or if it has been permanently deleted. + if (!targetProject) { + //If they are an admin it was permanently deleted and we can show the events with project name redacted + if (userIsAdmin) { + this.openEventsDialogFromEntityId( + this.i18nService.t("nameUnavailableProjectDeleted", viewEvents), + params.organizationId, + viewEvents, + ); + } else { + //They aren't an admin so we don't know if they have access to it, lets show the unknown cipher toast. + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("unknownProject"), + }); + } + } else { + this.openEventsDialog(targetProject); + } + + await this.router.navigate([], { + queryParams: { search: this.search }, + }); + } + + return projects; }), ); + + if (this.route.snapshot.queryParams.search) { + this.search = this.route.snapshot.queryParams.search; + } } private async getProjects(): Promise { @@ -103,6 +153,30 @@ export class ProjectsComponent implements OnInit { }); } + openEventsDialog = (project: ProjectView): DialogRef => + openEntityEventsDialog(this.dialogService, { + data: { + name: project.name, + organizationId: project.organizationId, + entityId: project.id, + entity: "project", + }, + }); + + openEventsDialogFromEntityId = ( + headerName: string, + organizationId: string, + entityId: string, + ): DialogRef => + openEntityEventsDialog(this.dialogService, { + data: { + name: headerName, + organizationId: organizationId, + entityId: entityId, + entity: "project", + }, + }); + async openDeleteProjectDialog(projects: ProjectListView[]) { let projectsToDelete = projects; const readOnlyProjects = projects.filter((project) => project.write == false); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts index 18ecd2e3b51..ca093f449c9 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { combineLatestWith, firstValueFrom, Observable, startWith, switchMap } from "rxjs"; import { @@ -13,7 +13,8 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; +import { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component"; import { SecretListView } from "../models/view/secret-list.view"; import { SecretsListComponent } from "../shared/secrets-list.component"; @@ -54,6 +55,8 @@ export class SecretsComponent implements OnInit { private organizationService: OrganizationService, private accountService: AccountService, private logService: LogService, + private toastService: ToastService, + private router: Router, ) {} ngOnInit() { @@ -71,7 +74,53 @@ export class SecretsComponent implements OnInit { ) )?.enabled; - return await this.getSecrets(); + const secrets = await this.getSecrets(); + const viewEvents = this.route.snapshot.queryParams.viewEvents; + + if (viewEvents) { + let targetSecret = secrets.find((secret) => secret.id === viewEvents); + + const userIsAdmin = ( + await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ) + )?.isAdmin; + + // Secret might be deleted, make sure they are an admin before checking the trashed secrets + if (!targetSecret && userIsAdmin) { + targetSecret = (await this.secretService.getTrashedSecrets(this.organizationId)).find( + (e) => e.id == viewEvents, + ); + } + + // They would fall into here if they don't have access to a secret, or if it has been permanently deleted. + if (!targetSecret) { + //If they are an admin it was permanently deleted and we can show the events even though we don't have the secret name + if (userIsAdmin) { + this.openEventsDialogByEntityId( + this.i18nService.t("nameUnavailableSecretDeleted", viewEvents), + viewEvents, + ); + } else { + //They aren't an admin so we don't know if they have access to it, lets show the unknown secret toast. + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("unknownSecret"), + }); + } + } else { + this.openEventsDialog(targetSecret); + } + + await this.router.navigate([], { + queryParams: { search: this.search }, + }); + } + + return secrets; }), ); @@ -80,6 +129,26 @@ export class SecretsComponent implements OnInit { } } + openEventsDialogByEntityId = (secretName: string, secretId: string): DialogRef => + openEntityEventsDialog(this.dialogService, { + data: { + name: secretName, + organizationId: this.organizationId, + entityId: secretId, + entity: "secret", + }, + }); + + openEventsDialog = (secret: SecretListView): DialogRef => + openEntityEventsDialog(this.dialogService, { + data: { + name: secret.name, + organizationId: this.organizationId, + entityId: secret.id, + entity: "secret", + }, + }); + private async getSecrets(): Promise { return await this.secretService.getSecrets(this.organizationId); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts index ddaa0937e6f..2e364df1423 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts @@ -2,8 +2,10 @@ // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; +import { takeUntil } from "rxjs"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -25,7 +27,6 @@ export class ServiceAccountEventsComponent implements OnInit, OnDestroy { exportFileName = "machine-account-events"; - private destroy$ = new Subject(); private serviceAccountId: string; constructor( @@ -38,6 +39,8 @@ export class ServiceAccountEventsComponent logService: LogService, fileDownloadService: FileDownloadService, toastService: ToastService, + protected organizationService: OrganizationService, + protected accountService: AccountService, ) { super( eventService, @@ -47,10 +50,14 @@ export class ServiceAccountEventsComponent logService, fileDownloadService, toastService, + route, + accountService, + organizationService, ); } async ngOnInit() { + this.initBase(); // eslint-disable-next-line rxjs/no-async-subscribe this.route.params.pipe(takeUntil(this.destroy$)).subscribe(async (params) => { this.serviceAccountId = params.serviceAccountId; @@ -78,9 +85,4 @@ export class ServiceAccountEventsComponent email: "", }; } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.ts index 47d5dd63806..ac3defaf5dd 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.ts @@ -5,7 +5,6 @@ import { Component, EventEmitter, Input, OnDestroy, Output } from "@angular/core import { Subject, takeUntil } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { TableDataSource, ToastService } from "@bitwarden/components"; import { @@ -49,7 +48,6 @@ export class ServiceAccountsListComponent implements OnDestroy { constructor( private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, private toastService: ToastService, ) { this.selection.changed diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.html index 236af0d414c..9e31ff628fb 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.html @@ -114,6 +114,15 @@ {{ "deleteProject" | i18n }} + diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts index 9774172cd4b..31114bcd1c4 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts @@ -1,21 +1,31 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { SelectionModel } from "@angular/cdk/collections"; -import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { map } from "rxjs"; +import { Component, EventEmitter, Input, Output, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { catchError, concatMap, map, Observable, of, Subject, switchMap, takeUntil } from "rxjs"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { TableDataSource, ToastService } from "@bitwarden/components"; +import { DialogRef, DialogService, TableDataSource, ToastService } from "@bitwarden/components"; +import { LogService } from "@bitwarden/logging"; +import { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component"; import { ProjectListView } from "../models/view/project-list.view"; +import { ProjectView } from "../models/view/project.view"; @Component({ selector: "sm-projects-list", templateUrl: "./projects-list.component.html", standalone: false, }) -export class ProjectsListComponent { +export class ProjectsListComponent implements OnInit { @Input() get projects(): ProjectListView[] { return this._projects; @@ -26,6 +36,9 @@ export class ProjectsListComponent { this.dataSource.data = projects; } private _projects: ProjectListView[]; + protected viewEventsAllowed$: Observable; + protected isAdmin$: Observable; + private destroy$: Subject = new Subject(); @Input() showMenus?: boolean = true; @@ -50,8 +63,41 @@ export class ProjectsListComponent { private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private toastService: ToastService, + private dialogService: DialogService, + private organizationService: OrganizationService, + private activatedRoute: ActivatedRoute, + private accountService: AccountService, + private logService: LogService, ) {} + ngOnInit(): void { + this.viewEventsAllowed$ = this.activatedRoute.params.pipe( + concatMap((params) => + getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ), + ), + ), + map((org) => org.canAccessEventLogs), + catchError((error: unknown) => { + if (typeof error === "string") { + this.toastService.showToast({ + message: error, + variant: "error", + title: "", + }); + } else { + this.logService.error(error); + } + return of(false); + }), + takeUntil(this.destroy$), + ); + } + isAllSelected() { if (this.selection.selected?.length > 0) { const numSelected = this.selection.selected.length; @@ -87,6 +133,16 @@ export class ProjectsListComponent { } } + openEventsDialog = (project: ProjectView): DialogRef => + openEntityEventsDialog(this.dialogService, { + data: { + name: project.name, + organizationId: project.organizationId, + entityId: project.id, + entity: "project", + }, + }); + private selectedHasWriteAccess() { const selectedProjects = this.projects.filter((project) => this.selection.isSelected(project.id), diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html index e5d22a01502..25f2447246a 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html @@ -148,6 +148,15 @@ {{ "restoreSecret" | i18n }} +