1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-08 12:40:26 +00:00

Merge branch 'main' into feature/passkey-provider

This commit is contained in:
Jeffrey Holland
2025-08-20 17:10:08 +02:00
committed by GitHub
18 changed files with 611 additions and 47 deletions

View File

@@ -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<boolean>;
protected activeOrganization$: Observable<Organization | undefined>;
protected organizations$: Observable<Organization[]>;
private destroySubject$ = new Subject<void>();
protected get destroy$(): Observable<void> {
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");

View File

@@ -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,

View File

@@ -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<string, any>();
private destroy$ = new Subject<void>();
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();
}
}

View File

@@ -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;
}

View File

@@ -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."
},

View File

@@ -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);

View File

@@ -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<ProjectListView[]> {
@@ -103,6 +153,30 @@ export class ProjectsComponent implements OnInit {
});
}
openEventsDialog = (project: ProjectView): DialogRef<void> =>
openEntityEventsDialog(this.dialogService, {
data: {
name: project.name,
organizationId: project.organizationId,
entityId: project.id,
entity: "project",
},
});
openEventsDialogFromEntityId = (
headerName: string,
organizationId: string,
entityId: string,
): DialogRef<void> =>
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);

View File

@@ -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<void> =>
openEntityEventsDialog(this.dialogService, {
data: {
name: secretName,
organizationId: this.organizationId,
entityId: secretId,
entity: "secret",
},
});
openEventsDialog = (secret: SecretListView): DialogRef<void> =>
openEntityEventsDialog(this.dialogService, {
data: {
name: secret.name,
organizationId: this.organizationId,
entityId: secret.id,
entity: "secret",
},
});
private async getSecrets(): Promise<SecretListView[]> {
return await this.secretService.getSecrets(this.organizationId);
}

View File

@@ -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<void>();
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();
}
}

View File

@@ -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

View File

@@ -114,6 +114,15 @@
<i class="bwi bwi-fw bwi-trash tw-text-danger" aria-hidden="true"></i>
<span class="tw-text-danger">{{ "deleteProject" | i18n }}</span>
</button>
<button
type="button"
bitMenuItem
*ngIf="viewEventsAllowed$ | async as allowed"
(click)="openEventsDialog(project)"
>
<i class="bwi bwi-fw bwi-billing" aria-hidden="true"></i>
<span> {{ "viewEvents" | i18n }} </span>
</button>
</bit-menu>
</tr>
</ng-template>

View File

@@ -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<boolean>;
protected isAdmin$: Observable<boolean>;
private destroy$: Subject<void> = new Subject<void>();
@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<void> =>
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),

View File

@@ -148,6 +148,15 @@
<i class="bwi bwi-fw bwi-refresh" aria-hidden="true"></i>
{{ "restoreSecret" | i18n }}
</button>
<button
type="button"
bitMenuItem
*ngIf="viewEventsAllowed$ | async as allowed"
(click)="openEventsDialog(secret)"
>
<i class="bwi bwi-fw bwi-billing" aria-hidden="true"></i>
<span> {{ "viewEvents" | i18n }} </span>
</button>
<button
type="button"
bitMenuItem

View File

@@ -1,15 +1,24 @@
// 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, OnDestroy, Output } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { Component, EventEmitter, Input, OnDestroy, 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 { LogService } from "@bitwarden/common/platform/abstractions/log.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 { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component";
import { SecretListView } from "../models/view/secret-list.view";
import { SecretView } from "../models/view/secret.view";
import { SecretService } from "../secrets/secret.service";
@Component({
@@ -17,7 +26,7 @@ import { SecretService } from "../secrets/secret.service";
templateUrl: "./secrets-list.component.html",
standalone: false,
})
export class SecretsListComponent implements OnDestroy {
export class SecretsListComponent implements OnDestroy, OnInit {
protected dataSource = new TableDataSource<SecretListView>();
@Input()
@@ -52,17 +61,51 @@ export class SecretsListComponent implements OnDestroy {
private destroy$: Subject<void> = new Subject<void>();
selection = new SelectionModel<string>(true, []);
protected viewEventsAllowed$: Observable<boolean>;
protected isAdmin$: Observable<boolean>;
constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private toastService: ToastService,
private dialogService: DialogService,
private organizationService: OrganizationService,
private activatedRoute: ActivatedRoute,
private accountService: AccountService,
private logService: LogService,
) {
this.selection.changed
.pipe(takeUntil(this.destroy$))
.subscribe((_) => this.onSecretCheckedEvent.emit(this.selection.selected));
}
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$),
);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
@@ -76,6 +119,15 @@ export class SecretsListComponent implements OnDestroy {
}
return false;
}
openEventsDialog = (secret: SecretView): DialogRef<void> =>
openEntityEventsDialog(this.dialogService, {
data: {
name: secret.name,
organizationId: secret.organizationId,
entityId: secret.id,
entity: "secret",
},
});
toggleAll() {
if (this.isAllSelected()) {

View File

@@ -388,19 +388,23 @@ export abstract class ApiService {
id: string,
request: ProviderUserAcceptRequest,
): Promise<any>;
abstract postProviderUserConfirm(
providerId: string,
id: string,
request: ProviderUserConfirmRequest,
): Promise<any>;
abstract postProviderUsersPublicKey(
providerId: string,
request: ProviderUserBulkRequest,
): Promise<ListResponse<ProviderUserBulkPublicKeyResponse>>;
abstract postProviderUserBulkConfirm(
providerId: string,
request: ProviderUserBulkConfirmRequest,
): Promise<ListResponse<ProviderUserBulkResponse>>;
abstract putProviderUser(
providerId: string,
id: string,
@@ -435,6 +439,21 @@ export abstract class ApiService {
end: string,
token: string,
): Promise<ListResponse<EventResponse>>;
abstract getEventsSecret(
orgId: string,
id: string,
start: string,
end: string,
token: string,
): Promise<ListResponse<EventResponse>>;
abstract getEventsProject(
orgId: string,
id: string,
start: string,
end: string,
token: string,
): Promise<ListResponse<EventResponse>>;
abstract getEventsOrganization(
id: string,
start: string,

View File

@@ -93,4 +93,11 @@ export enum EventType {
Secret_Created = 2101,
Secret_Edited = 2102,
Secret_Deleted = 2103,
Secret_Permanently_Deleted = 2104,
Secret_Restored = 2105,
Project_Retrieved = 2200,
Project_Created = 2201,
Project_Edited = 2202,
Project_Deleted = 2203,
}

View File

@@ -22,6 +22,7 @@ export class EventResponse extends BaseResponse {
systemUser: EventSystemUser;
domainName: string;
secretId: string;
projectId: string;
serviceAccountId: string;
constructor(response: any) {
@@ -45,6 +46,7 @@ export class EventResponse extends BaseResponse {
this.systemUser = this.getResponseProperty("SystemUser");
this.domainName = this.getResponseProperty("DomainName");
this.secretId = this.getResponseProperty("SecretId");
this.projectId = this.getResponseProperty("ProjectId");
this.serviceAccountId = this.getResponseProperty("ServiceAccountId");
}
}

View File

@@ -1267,6 +1267,50 @@ export class ApiService implements ApiServiceAbstraction {
return new ListResponse(r, EventResponse);
}
async getEventsSecret(
orgId: string,
id: string,
start: string,
end: string,
token: string,
): Promise<ListResponse<EventResponse>> {
const r = await this.send(
"GET",
this.addEventParameters(
"/organization/" + orgId + "/secrets/" + id + "/events",
start,
end,
token,
),
null,
true,
true,
);
return new ListResponse(r, EventResponse);
}
async getEventsProject(
orgId: string,
id: string,
start: string,
end: string,
token: string,
): Promise<ListResponse<EventResponse>> {
const r = await this.send(
"GET",
this.addEventParameters(
"/organization/" + orgId + "/projects/" + id + "/events",
start,
end,
token,
),
null,
true,
true,
);
return new ListResponse(r, EventResponse);
}
async getEventsOrganization(
id: string,
start: string,