mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-28 12:45:45 +01:00
[SM-949] Add Event Logs to Service Account (#6546)
* Add Event Logs to Service Account * Update bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add takeUntil import * add service account access guard --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Co-authored-by: Thomas Avery <tavery@bitwarden.com>
This commit is contained in:
parent
87dbe8997d
commit
e9f0c07b02
@ -0,0 +1,43 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { EventResponse } from "@bitwarden/common/models/response/event.response";
|
||||||
|
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: "root",
|
||||||
|
})
|
||||||
|
export class ServiceAccountEventLogApiService {
|
||||||
|
constructor(private apiService: ApiService) {}
|
||||||
|
|
||||||
|
async getEvents(
|
||||||
|
serviceAccountId: string,
|
||||||
|
start: string,
|
||||||
|
end: string,
|
||||||
|
token: string
|
||||||
|
): Promise<ListResponse<EventResponse>> {
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"GET",
|
||||||
|
this.addEventParameters("/sm/events/service-accounts/" + serviceAccountId, start, end, token),
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
return new ListResponse(r, EventResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addEventParameters(base: string, start: string, end: string, token: string) {
|
||||||
|
if (start != null) {
|
||||||
|
base += "?start=" + start;
|
||||||
|
}
|
||||||
|
if (end != null) {
|
||||||
|
base += base.indexOf("?") > -1 ? "&" : "?";
|
||||||
|
base += "end=" + end;
|
||||||
|
}
|
||||||
|
if (token != null) {
|
||||||
|
base += base.indexOf("?") > -1 ? "&" : "?";
|
||||||
|
base += "continuationToken=" + token;
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
<div class="tw-mb-4">
|
||||||
|
<h1>{{ "eventLogs" | i18n }}</h1>
|
||||||
|
<div class="tw-mt-4 tw-flex tw-items-center">
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "from" | i18n }}</bit-label>
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
type="datetime-local"
|
||||||
|
placeholder="{{ 'startDate' | i18n }}"
|
||||||
|
[(ngModel)]="start"
|
||||||
|
(change)="dirtyDates = true"
|
||||||
|
/>
|
||||||
|
</bit-form-field>
|
||||||
|
<span class="tw-mx-2">-</span>
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "to" | i18n }}</bit-label>
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
type="datetime-local"
|
||||||
|
placeholder="{{ 'endDate' | i18n }}"
|
||||||
|
[(ngModel)]="end"
|
||||||
|
(change)="dirtyDates = true"
|
||||||
|
/>
|
||||||
|
</bit-form-field>
|
||||||
|
<form #refreshForm [appApiAction]="refreshPromise">
|
||||||
|
<button
|
||||||
|
class="tw-mx-3 tw-mt-1"
|
||||||
|
type="button"
|
||||||
|
bitButton
|
||||||
|
buttonType="primary"
|
||||||
|
(click)="loadEvents(true)"
|
||||||
|
[disabled]="loaded && refreshForm.loading"
|
||||||
|
>
|
||||||
|
{{ "update" | i18n }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form #exportForm [appApiAction]="exportPromise">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tw-mt-1"
|
||||||
|
bitButton
|
||||||
|
[ngClass]="{ loading: exportForm.loading }"
|
||||||
|
(click)="exportEvents()"
|
||||||
|
[disabled]="(loaded && exportForm.loading) || dirtyDates"
|
||||||
|
>
|
||||||
|
<span>{{ "export" | i18n }}</span>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-fw"
|
||||||
|
aria-hidden="true"
|
||||||
|
[ngClass]="{
|
||||||
|
'bwi-sign-in': !exportForm.loading,
|
||||||
|
'bwi-spinner bwi-spin': exportForm.loading
|
||||||
|
}"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ng-container *ngIf="!loaded">
|
||||||
|
<i
|
||||||
|
class="bwi bwi-spinner bwi-spin text-muted"
|
||||||
|
title="{{ 'loading' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="loaded">
|
||||||
|
<p *ngIf="!events || !events.length">{{ "noEventsInList" | i18n }}</p>
|
||||||
|
<bit-table *ngIf="events && events.length">
|
||||||
|
<ng-container header>
|
||||||
|
<tr>
|
||||||
|
<th bitCell>{{ "timestamp" | i18n }}</th>
|
||||||
|
<th bitCell>{{ "client" | i18n }}</th>
|
||||||
|
<th bitCell>{{ "event" | i18n }}</th>
|
||||||
|
</tr>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template body>
|
||||||
|
<tr bitRow *ngFor="let e of events" alignContent="top">
|
||||||
|
<td bitCell class="tw-whitespace-nowrap">{{ e.date | date : "medium" }}</td>
|
||||||
|
<td bitCell>
|
||||||
|
<span title="{{ e.appName }}, {{ e.ip }}">{{ e.appName }}</span>
|
||||||
|
</td>
|
||||||
|
<td bitCell [innerHTML]="e.message"></td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</bit-table>
|
||||||
|
<button
|
||||||
|
#moreBtn
|
||||||
|
[appApiAction]="morePromise"
|
||||||
|
type="button"
|
||||||
|
bitButton
|
||||||
|
buttonType="primary"
|
||||||
|
(click)="loadEvents(false)"
|
||||||
|
[disabled]="loaded && $any(moreBtn).loading"
|
||||||
|
*ngIf="continuationToken"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-spinner bwi-spin"
|
||||||
|
title="{{ 'loading' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
*ngIf="moreBtn.loading"
|
||||||
|
></i>
|
||||||
|
<span>{{ "loadMore" | i18n }}</span>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
@ -0,0 +1,77 @@
|
|||||||
|
import { Component, OnDestroy } from "@angular/core";
|
||||||
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
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";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { BaseEventsComponent } from "@bitwarden/web-vault/app/common/base.events.component";
|
||||||
|
import { EventService } from "@bitwarden/web-vault/app/core";
|
||||||
|
import { EventExportService } from "@bitwarden/web-vault/app/tools/event-export";
|
||||||
|
|
||||||
|
import { ServiceAccountEventLogApiService } from "./service-account-event-log-api.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "sm-service-accounts-events",
|
||||||
|
templateUrl: "./service-accounts-events.component.html",
|
||||||
|
})
|
||||||
|
export class ServiceAccountEventsComponent extends BaseEventsComponent implements OnDestroy {
|
||||||
|
exportFileName = "service-account-events";
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
private serviceAccountId: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
eventService: EventService,
|
||||||
|
private serviceAccountEventsApiService: ServiceAccountEventLogApiService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
i18nService: I18nService,
|
||||||
|
exportService: EventExportService,
|
||||||
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
logService: LogService,
|
||||||
|
fileDownloadService: FileDownloadService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
eventService,
|
||||||
|
i18nService,
|
||||||
|
exportService,
|
||||||
|
platformUtilsService,
|
||||||
|
logService,
|
||||||
|
fileDownloadService
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
// eslint-disable-next-line rxjs/no-async-subscribe
|
||||||
|
this.route.params.pipe(takeUntil(this.destroy$)).subscribe(async (params) => {
|
||||||
|
this.serviceAccountId = params.serviceAccountId;
|
||||||
|
await this.load();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
await this.loadEvents(true);
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected requestEvents(startDate: string, endDate: string, continuationToken: string) {
|
||||||
|
return this.serviceAccountEventsApiService.getEvents(
|
||||||
|
this.serviceAccountId,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
continuationToken
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getUserName() {
|
||||||
|
return {
|
||||||
|
name: this.i18nService.t("serviceAccount") + " " + this.serviceAccountId,
|
||||||
|
email: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import { inject } from "@angular/core";
|
||||||
|
import { ActivatedRouteSnapshot, CanActivateFn, createUrlTreeFromSnapshot } from "@angular/router";
|
||||||
|
|
||||||
|
import { ServiceAccountService } from "../service-account.service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirects to service accounts page if the user doesn't have access to service account.
|
||||||
|
*/
|
||||||
|
export const serviceAccountAccessGuard: CanActivateFn = async (route: ActivatedRouteSnapshot) => {
|
||||||
|
const serviceAccountService = inject(ServiceAccountService);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const serviceAccount = await serviceAccountService.getByServiceAccountId(
|
||||||
|
route.params.serviceAccountId,
|
||||||
|
route.params.organizationId
|
||||||
|
);
|
||||||
|
if (serviceAccount) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return createUrlTreeFromSnapshot(route, [
|
||||||
|
"/sm",
|
||||||
|
route.params.organizationId,
|
||||||
|
"service-accounts",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return createUrlTreeFromSnapshot(route, ["/sm", route.params.organizationId, "service-accounts"]);
|
||||||
|
};
|
@ -13,6 +13,7 @@
|
|||||||
<bit-tab-link [route]="['projects']">{{ "projects" | i18n }}</bit-tab-link>
|
<bit-tab-link [route]="['projects']">{{ "projects" | i18n }}</bit-tab-link>
|
||||||
<bit-tab-link [route]="['people']">{{ "people" | i18n }}</bit-tab-link>
|
<bit-tab-link [route]="['people']">{{ "people" | i18n }}</bit-tab-link>
|
||||||
<bit-tab-link [route]="['access']">{{ "accessTokens" | i18n }}</bit-tab-link>
|
<bit-tab-link [route]="['access']">{{ "accessTokens" | i18n }}</bit-tab-link>
|
||||||
|
<bit-tab-link [route]="['events']">{{ "eventLogs" | i18n }}</bit-tab-link>
|
||||||
</bit-tab-nav-bar>
|
</bit-tab-nav-bar>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -2,6 +2,8 @@ import { NgModule } from "@angular/core";
|
|||||||
import { RouterModule, Routes } from "@angular/router";
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
|
||||||
import { AccessTokenComponent } from "./access/access-tokens.component";
|
import { AccessTokenComponent } from "./access/access-tokens.component";
|
||||||
|
import { ServiceAccountEventsComponent } from "./event-logs/service-accounts-events.component";
|
||||||
|
import { serviceAccountAccessGuard } from "./guards/service-account-access.guard";
|
||||||
import { ServiceAccountPeopleComponent } from "./people/service-account-people.component";
|
import { ServiceAccountPeopleComponent } from "./people/service-account-people.component";
|
||||||
import { ServiceAccountProjectsComponent } from "./projects/service-account-projects.component";
|
import { ServiceAccountProjectsComponent } from "./projects/service-account-projects.component";
|
||||||
import { ServiceAccountComponent } from "./service-account.component";
|
import { ServiceAccountComponent } from "./service-account.component";
|
||||||
@ -15,6 +17,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: ":serviceAccountId",
|
path: ":serviceAccountId",
|
||||||
component: ServiceAccountComponent,
|
component: ServiceAccountComponent,
|
||||||
|
canActivate: [serviceAccountAccessGuard],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
@ -33,6 +36,10 @@ const routes: Routes = [
|
|||||||
path: "projects",
|
path: "projects",
|
||||||
component: ServiceAccountProjectsComponent,
|
component: ServiceAccountProjectsComponent,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "events",
|
||||||
|
component: ServiceAccountEventsComponent,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -11,6 +11,7 @@ import { AccessTokenDialogComponent } from "./access/dialogs/access-token-dialog
|
|||||||
import { ExpirationOptionsComponent } from "./access/dialogs/expiration-options.component";
|
import { ExpirationOptionsComponent } from "./access/dialogs/expiration-options.component";
|
||||||
import { ServiceAccountDeleteDialogComponent } from "./dialog/service-account-delete-dialog.component";
|
import { ServiceAccountDeleteDialogComponent } from "./dialog/service-account-delete-dialog.component";
|
||||||
import { ServiceAccountDialogComponent } from "./dialog/service-account-dialog.component";
|
import { ServiceAccountDialogComponent } from "./dialog/service-account-dialog.component";
|
||||||
|
import { ServiceAccountEventsComponent } from "./event-logs/service-accounts-events.component";
|
||||||
import { ServiceAccountPeopleComponent } from "./people/service-account-people.component";
|
import { ServiceAccountPeopleComponent } from "./people/service-account-people.component";
|
||||||
import { ServiceAccountProjectsComponent } from "./projects/service-account-projects.component";
|
import { ServiceAccountProjectsComponent } from "./projects/service-account-projects.component";
|
||||||
import { ServiceAccountComponent } from "./service-account.component";
|
import { ServiceAccountComponent } from "./service-account.component";
|
||||||
@ -29,6 +30,7 @@ import { ServiceAccountsComponent } from "./service-accounts.component";
|
|||||||
ServiceAccountComponent,
|
ServiceAccountComponent,
|
||||||
ServiceAccountDeleteDialogComponent,
|
ServiceAccountDeleteDialogComponent,
|
||||||
ServiceAccountDialogComponent,
|
ServiceAccountDialogComponent,
|
||||||
|
ServiceAccountEventsComponent,
|
||||||
ServiceAccountPeopleComponent,
|
ServiceAccountPeopleComponent,
|
||||||
ServiceAccountProjectsComponent,
|
ServiceAccountProjectsComponent,
|
||||||
ServiceAccountsComponent,
|
ServiceAccountsComponent,
|
||||||
|
Loading…
Reference in New Issue
Block a user