[SM-568] Delete service accounts (#4881)

This commit is contained in:
Oscar Hinton 2023-03-06 20:25:06 +01:00 committed by GitHub
parent c711312fee
commit fbd0d41b51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 227 additions and 3 deletions

View File

@ -5845,6 +5845,33 @@
"message": "View service account",
"description": "Action to view the details of a service account."
},
"deleteServiceAccountDialogMessage": {
"message": "Deleting service account $SERVICE_ACCOUNT$ is permanent and irreversible.",
"placeholders": {
"service_account": {
"content": "$1",
"example": "Service account name"
}
}
},
"deleteServiceAccountsDialogMessage":{
"message": "Deleting service accounts is permanent and irreversible."
},
"deleteServiceAccountsConfirmMessage":{
"message": "Delete $COUNT$ service accounts",
"placeholders": {
"count": {
"content": "$1",
"example": "2"
}
}
},
"deleteServiceAccountToast":{
"message": "The service account have been deleted"
},
"deleteServiceAccountsToast":{
"message": "Service accounts deleted"
},
"searchServiceAccounts": {
"message": "Search service accounts",
"description": "Placeholder text for searching service accounts."

View File

@ -0,0 +1,35 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog dialogSize="small">
<ng-container bitDialogTitle>
<span>{{ title }}</span>
<span class="tw-text-sm tw-normal-case tw-text-muted">
<ng-container *ngIf="data.serviceAccounts.length == 1">
{{ data.serviceAccounts[0].name }}
</ng-container>
<ng-container *ngIf="data.serviceAccounts.length > 1">
{{ data.serviceAccounts.length }}
{{ "serviceAccounts" | i18n }}
</ng-container>
</span>
</ng-container>
<div bitDialogContent>
<bit-callout type="warning" [title]="'warning' | i18n">
{{ dialogContent }}
</bit-callout>
<bit-form-field>
<bit-label>{{ dialogConfirmationLabel }}</bit-label>
<input bitInput formControlName="confirmDelete" />
</bit-form-field>
</div>
<div bitDialogFooter class="tw-flex tw-gap-2">
<button type="submit" bitButton buttonType="danger" bitFormButton>
{{ title }}
</button>
<button type="button" bitButton buttonType="secondary" bitFormButton bitDialogClose>
{{ "cancel" | i18n }}
</button>
</div>
</bit-dialog>
</form>

View File

@ -0,0 +1,122 @@
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import {
FormControl,
FormGroup,
ValidationErrors,
ValidatorFn,
AbstractControl,
} from "@angular/forms";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { DialogService } from "@bitwarden/components";
import { ServiceAccountView } from "../../models/view/service-account.view";
import {
BulkOperationStatus,
BulkStatusDetails,
BulkStatusDialogComponent,
} from "../../shared/dialogs/bulk-status-dialog.component";
import { ServiceAccountService } from "../service-account.service";
export interface ServiceAccountDeleteOperation {
serviceAccounts: ServiceAccountView[];
}
@Component({
selector: "sm-service-account-delete-dialog",
templateUrl: "./service-account-delete-dialog.component.html",
})
export class ServiceAccountDeleteDialogComponent {
formGroup = new FormGroup({
confirmDelete: new FormControl("", [this.matchConfirmationMessageValidator()]),
});
constructor(
public dialogRef: DialogRef,
@Inject(DIALOG_DATA) public data: ServiceAccountDeleteOperation,
private serviceAccountService: ServiceAccountService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private dialogService: DialogService
) {}
get title() {
return this.data.serviceAccounts.length === 1
? this.i18nService.t("deleteServiceAccount")
: this.i18nService.t("deleteServiceAccounts");
}
get dialogContent() {
return this.data.serviceAccounts.length === 1
? this.i18nService.t("deleteServiceAccountDialogMessage", this.data.serviceAccounts[0].name)
: this.i18nService.t("deleteServiceAccountsDialogMessage");
}
get dialogConfirmationLabel() {
return this.i18nService.t("deleteProjectInputLabel", this.dialogConfirmationMessage);
}
submit = async () => {
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid) {
return;
}
await this.delete();
this.dialogRef.close();
};
async delete() {
const bulkResponses = await this.serviceAccountService.delete(this.data.serviceAccounts);
const errors = bulkResponses.filter((response) => response.errorMessage);
if (errors.length > 0) {
this.openBulkStatusDialog(errors);
return;
}
const message =
this.data.serviceAccounts.length === 1
? "deleteServiceAccountToast"
: "deleteServiceAccountsToast";
this.platformUtilsService.showToast("success", null, this.i18nService.t(message));
}
openBulkStatusDialog(bulkStatusResults: BulkOperationStatus[]) {
this.dialogService.open<unknown, BulkStatusDetails>(BulkStatusDialogComponent, {
data: {
title: "deleteServiceAccounts",
subTitle: "serviceAccounts",
columnTitle: "serviceAccountName",
message: "bulkDeleteProjectsErrorMessage",
details: bulkStatusResults,
},
});
}
private get dialogConfirmationMessage() {
return this.data.serviceAccounts?.length === 1
? this.i18nService.t("deleteProjectConfirmMessage", this.data.serviceAccounts[0].name)
: this.i18nService.t(
"deleteServiceAccountsConfirmMessage",
this.data.serviceAccounts?.length.toString()
);
}
private matchConfirmationMessageValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (this.dialogConfirmationMessage.toLowerCase() == control.value.toLowerCase()) {
return null;
} else {
return {
confirmationDoesntMatchError: {
message: this.i18nService.t("smConfirmationRequired"),
},
};
}
};
}
}

View File

@ -9,6 +9,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-cr
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { ServiceAccountView } from "../models/view/service-account.view";
import { BulkOperationStatus } from "../shared/dialogs/bulk-status-dialog.component";
import { ServiceAccountRequest } from "./models/requests/service-account.request";
import { ServiceAccountResponse } from "./models/responses/service-account.response";
@ -54,6 +55,21 @@ export class ServiceAccountService {
);
}
async delete(serviceAccounts: ServiceAccountView[]): Promise<BulkOperationStatus[]> {
const ids = serviceAccounts.map((serviceAccount) => serviceAccount.id);
const r = await this.apiService.send("POST", "/service-accounts/delete", ids, true, true);
this._serviceAccount.next(null);
return r.data.map((element: { id: string; error: string }) => {
const bulkOperationStatus = new BulkOperationStatus();
bulkOperationStatus.id = element.id;
bulkOperationStatus.name = serviceAccounts.find((sa) => sa.id == element.id).name;
bulkOperationStatus.errorMessage = element.error;
return bulkOperationStatus;
});
}
private async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
return await this.cryptoService.getOrgKey(organizationId);
}

View File

@ -83,7 +83,7 @@
<i class="bwi bwi-fw bwi-eye" aria-hidden="true"></i>
{{ "viewServiceAccount" | i18n }}
</a>
<button type="button" bitMenuItem>
<button type="button" bitMenuItem (click)="delete(serviceAccount)">
<i class="bwi bwi-fw bwi-trash tw-text-danger" aria-hidden="true"></i>
<span class="tw-text-danger">
{{ "deleteServiceAccount" | i18n }}

View File

@ -20,7 +20,7 @@ export class ServiceAccountsListComponent implements OnDestroy {
private _serviceAccounts: ServiceAccountView[];
@Output() newServiceAccountEvent = new EventEmitter();
@Output() deleteServiceAccountsEvent = new EventEmitter<string[]>();
@Output() deleteServiceAccountsEvent = new EventEmitter<ServiceAccountView[]>();
@Output() onServiceAccountCheckedEvent = new EventEmitter<string[]>();
private destroy$: Subject<void> = new Subject<void>();
@ -50,9 +50,15 @@ export class ServiceAccountsListComponent implements OnDestroy {
: this.selection.select(...this.serviceAccounts.map((s) => s.id));
}
delete(serviceAccount: ServiceAccountView) {
this.deleteServiceAccountsEvent.emit([serviceAccount]);
}
bulkDeleteServiceAccounts() {
if (this.selection.selected.length >= 1) {
this.deleteServiceAccountsEvent.emit(this.selection.selected);
this.deleteServiceAccountsEvent.emit(
this.serviceAccounts.filter((sa) => this.selection.isSelected(sa.id))
);
}
}
}

View File

@ -5,4 +5,5 @@
<sm-service-accounts-list
[serviceAccounts]="serviceAccounts$ | async"
(newServiceAccountEvent)="openNewServiceAccountDialog()"
(deleteServiceAccountsEvent)="openDeleteDialog($event)"
></sm-service-accounts-list>

View File

@ -7,6 +7,10 @@ import { DialogService } from "@bitwarden/components";
import { ServiceAccountView } from "../models/view/service-account.view";
import { AccessPolicyService } from "../shared/access-policies/access-policy.service";
import {
ServiceAccountDeleteDialogComponent,
ServiceAccountDeleteOperation,
} from "./dialog/service-account-delete-dialog.component";
import {
ServiceAccountDialogComponent,
ServiceAccountOperation,
@ -50,6 +54,17 @@ export class ServiceAccountsComponent implements OnInit {
});
}
openDeleteDialog(event: ServiceAccountView[]) {
this.dialogService.open<unknown, ServiceAccountDeleteOperation>(
ServiceAccountDeleteDialogComponent,
{
data: {
serviceAccounts: event,
},
}
);
}
private async getServiceAccounts(): Promise<ServiceAccountView[]> {
return await this.serviceAccountService.getServiceAccounts(this.organizationId);
}

View File

@ -9,6 +9,7 @@ import { AccessTokenComponent } from "./access/access-tokens.component";
import { AccessTokenCreateDialogComponent } from "./access/dialogs/access-token-create-dialog.component";
import { AccessTokenDialogComponent } from "./access/dialogs/access-token-dialog.component";
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 { ServiceAccountPeopleComponent } from "./people/service-account-people.component";
import { ServiceAccountProjectsComponent } from "./projects/service-account-projects.component";
@ -26,6 +27,7 @@ import { ServiceAccountsComponent } from "./service-accounts.component";
AccessTokenDialogComponent,
ExpirationOptionsComponent,
ServiceAccountComponent,
ServiceAccountDeleteDialogComponent,
ServiceAccountDialogComponent,
ServiceAccountPeopleComponent,
ServiceAccountProjectsComponent,