[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]="['people']">{{ "people" | 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>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
@ -2,6 +2,8 @@ import { NgModule } from "@angular/core";
|
|||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
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 { ServiceAccountProjectsComponent } from "./projects/service-account-projects.component";
|
||||
import { ServiceAccountComponent } from "./service-account.component";
|
||||
|
@ -15,6 +17,7 @@ const routes: Routes = [
|
|||
{
|
||||
path: ":serviceAccountId",
|
||||
component: ServiceAccountComponent,
|
||||
canActivate: [serviceAccountAccessGuard],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
|
@ -33,6 +36,10 @@ const routes: Routes = [
|
|||
path: "projects",
|
||||
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 { ServiceAccountDeleteDialogComponent } from "./dialog/service-account-delete-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 { ServiceAccountProjectsComponent } from "./projects/service-account-projects.component";
|
||||
import { ServiceAccountComponent } from "./service-account.component";
|
||||
|
@ -29,6 +30,7 @@ import { ServiceAccountsComponent } from "./service-accounts.component";
|
|||
ServiceAccountComponent,
|
||||
ServiceAccountDeleteDialogComponent,
|
||||
ServiceAccountDialogComponent,
|
||||
ServiceAccountEventsComponent,
|
||||
ServiceAccountPeopleComponent,
|
||||
ServiceAccountProjectsComponent,
|
||||
ServiceAccountsComponent,
|
||||
|
|
Loading…
Reference in New Issue