mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-04 18:37:45 +01:00
[SM-568] Delete service accounts (#4881)
This commit is contained in:
parent
c711312fee
commit
fbd0d41b51
@ -5845,6 +5845,33 @@
|
|||||||
"message": "View service account",
|
"message": "View service account",
|
||||||
"description": "Action to view the details of a 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": {
|
"searchServiceAccounts": {
|
||||||
"message": "Search service accounts",
|
"message": "Search service accounts",
|
||||||
"description": "Placeholder text for searching service accounts."
|
"description": "Placeholder text for searching service accounts."
|
||||||
|
@ -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>
|
@ -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"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-cr
|
|||||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||||
|
|
||||||
import { ServiceAccountView } from "../models/view/service-account.view";
|
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 { ServiceAccountRequest } from "./models/requests/service-account.request";
|
||||||
import { ServiceAccountResponse } from "./models/responses/service-account.response";
|
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> {
|
private async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
|
||||||
return await this.cryptoService.getOrgKey(organizationId);
|
return await this.cryptoService.getOrgKey(organizationId);
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,7 @@
|
|||||||
<i class="bwi bwi-fw bwi-eye" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-eye" aria-hidden="true"></i>
|
||||||
{{ "viewServiceAccount" | i18n }}
|
{{ "viewServiceAccount" | i18n }}
|
||||||
</a>
|
</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>
|
<i class="bwi bwi-fw bwi-trash tw-text-danger" aria-hidden="true"></i>
|
||||||
<span class="tw-text-danger">
|
<span class="tw-text-danger">
|
||||||
{{ "deleteServiceAccount" | i18n }}
|
{{ "deleteServiceAccount" | i18n }}
|
||||||
|
@ -20,7 +20,7 @@ export class ServiceAccountsListComponent implements OnDestroy {
|
|||||||
private _serviceAccounts: ServiceAccountView[];
|
private _serviceAccounts: ServiceAccountView[];
|
||||||
|
|
||||||
@Output() newServiceAccountEvent = new EventEmitter();
|
@Output() newServiceAccountEvent = new EventEmitter();
|
||||||
@Output() deleteServiceAccountsEvent = new EventEmitter<string[]>();
|
@Output() deleteServiceAccountsEvent = new EventEmitter<ServiceAccountView[]>();
|
||||||
@Output() onServiceAccountCheckedEvent = new EventEmitter<string[]>();
|
@Output() onServiceAccountCheckedEvent = new EventEmitter<string[]>();
|
||||||
|
|
||||||
private destroy$: Subject<void> = new Subject<void>();
|
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));
|
: this.selection.select(...this.serviceAccounts.map((s) => s.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete(serviceAccount: ServiceAccountView) {
|
||||||
|
this.deleteServiceAccountsEvent.emit([serviceAccount]);
|
||||||
|
}
|
||||||
|
|
||||||
bulkDeleteServiceAccounts() {
|
bulkDeleteServiceAccounts() {
|
||||||
if (this.selection.selected.length >= 1) {
|
if (this.selection.selected.length >= 1) {
|
||||||
this.deleteServiceAccountsEvent.emit(this.selection.selected);
|
this.deleteServiceAccountsEvent.emit(
|
||||||
|
this.serviceAccounts.filter((sa) => this.selection.isSelected(sa.id))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,4 +5,5 @@
|
|||||||
<sm-service-accounts-list
|
<sm-service-accounts-list
|
||||||
[serviceAccounts]="serviceAccounts$ | async"
|
[serviceAccounts]="serviceAccounts$ | async"
|
||||||
(newServiceAccountEvent)="openNewServiceAccountDialog()"
|
(newServiceAccountEvent)="openNewServiceAccountDialog()"
|
||||||
|
(deleteServiceAccountsEvent)="openDeleteDialog($event)"
|
||||||
></sm-service-accounts-list>
|
></sm-service-accounts-list>
|
||||||
|
@ -7,6 +7,10 @@ import { DialogService } from "@bitwarden/components";
|
|||||||
import { ServiceAccountView } from "../models/view/service-account.view";
|
import { ServiceAccountView } from "../models/view/service-account.view";
|
||||||
import { AccessPolicyService } from "../shared/access-policies/access-policy.service";
|
import { AccessPolicyService } from "../shared/access-policies/access-policy.service";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ServiceAccountDeleteDialogComponent,
|
||||||
|
ServiceAccountDeleteOperation,
|
||||||
|
} from "./dialog/service-account-delete-dialog.component";
|
||||||
import {
|
import {
|
||||||
ServiceAccountDialogComponent,
|
ServiceAccountDialogComponent,
|
||||||
ServiceAccountOperation,
|
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[]> {
|
private async getServiceAccounts(): Promise<ServiceAccountView[]> {
|
||||||
return await this.serviceAccountService.getServiceAccounts(this.organizationId);
|
return await this.serviceAccountService.getServiceAccounts(this.organizationId);
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import { AccessTokenComponent } from "./access/access-tokens.component";
|
|||||||
import { AccessTokenCreateDialogComponent } from "./access/dialogs/access-token-create-dialog.component";
|
import { AccessTokenCreateDialogComponent } from "./access/dialogs/access-token-create-dialog.component";
|
||||||
import { AccessTokenDialogComponent } from "./access/dialogs/access-token-dialog.component";
|
import { AccessTokenDialogComponent } from "./access/dialogs/access-token-dialog.component";
|
||||||
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 { ServiceAccountDialogComponent } from "./dialog/service-account-dialog.component";
|
import { ServiceAccountDialogComponent } from "./dialog/service-account-dialog.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";
|
||||||
@ -26,6 +27,7 @@ import { ServiceAccountsComponent } from "./service-accounts.component";
|
|||||||
AccessTokenDialogComponent,
|
AccessTokenDialogComponent,
|
||||||
ExpirationOptionsComponent,
|
ExpirationOptionsComponent,
|
||||||
ServiceAccountComponent,
|
ServiceAccountComponent,
|
||||||
|
ServiceAccountDeleteDialogComponent,
|
||||||
ServiceAccountDialogComponent,
|
ServiceAccountDialogComponent,
|
||||||
ServiceAccountPeopleComponent,
|
ServiceAccountPeopleComponent,
|
||||||
ServiceAccountProjectsComponent,
|
ServiceAccountProjectsComponent,
|
||||||
|
Loading…
Reference in New Issue
Block a user