diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 875c9efb30..1a6489e61b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5952,6 +5952,16 @@ "revokeAccessToken": { "message": "Revoke access token" }, + "revokeAccessTokens": { + "message": "Revoke access tokens" + }, + "revokeAccessTokenDesc": { + "message": "Revoking access tokens is permanent and irreversible." + }, + "accessTokenRevoked": { + "message": "Access tokens revoked", + "description": "Toast message after deleting one or multiple access tokens." + }, "submenu": { "message": "Submenu" }, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.html index a725997e4a..4e1f21d6ca 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.html @@ -40,6 +40,7 @@ type="button" bitIconButton="bwi-ellipsis-v" buttonType="main" + [bitMenuTriggerFor]="tableMenu" [title]="'options' | i18n" [attr.aria-label]="'options' | i18n" > @@ -73,7 +74,7 @@ - + diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.ts index 483835b2bb..ce7ad6e86d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-list.component.ts @@ -19,6 +19,7 @@ export class AccessListComponent { private _tokens: AccessTokenView[]; @Output() newAccessTokenEvent = new EventEmitter(); + @Output() revokeAccessTokensEvent = new EventEmitter(); protected selection = new SelectionModel(true, []); @@ -34,6 +35,11 @@ export class AccessListComponent { : this.selection.select(...this.tokens.map((s) => s.id)); } + protected revokeSelected() { + const selected = this.tokens.filter((s) => this.selection.selected.includes(s.id)); + this.revokeAccessTokensEvent.emit(selected); + } + protected permission(token: AccessTokenView) { return "canRead"; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-tokens.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-tokens.component.html index 3122d13ae2..744968d22f 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-tokens.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-tokens.component.html @@ -1,4 +1,5 @@ diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-tokens.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-tokens.component.ts index d453afd429..ee6cae35ce 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-tokens.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access-tokens.component.ts @@ -2,7 +2,10 @@ import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { combineLatestWith, Observable, startWith, switchMap } from "rxjs"; +import { ModalService } from "@bitwarden/angular/services/modal.service"; +import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { DialogService } from "@bitwarden/components"; +import { UserVerificationPromptComponent } from "@bitwarden/web-vault/app/components/user-verification-prompt.component"; import { AccessTokenView } from "../models/view/access-token.view"; @@ -22,7 +25,9 @@ export class AccessTokenComponent implements OnInit { constructor( private route: ActivatedRoute, private accessService: AccessService, - private dialogService: DialogService + private dialogService: DialogService, + private modalService: ModalService, + private platformUtilsService: PlatformUtilsService ) {} ngOnInit() { @@ -37,8 +42,17 @@ export class AccessTokenComponent implements OnInit { ); } - private async getAccessTokens(): Promise { - return await this.accessService.getAccessTokens(this.organizationId, this.serviceAccountId); + protected async revoke(tokens: AccessTokenView[]) { + if (!(await this.verifyUser())) { + return; + } + + await this.accessService.revokeAccessTokens( + this.serviceAccountId, + tokens.map((t) => t.id) + ); + + this.platformUtilsService.showToast("success", null, "Access tokens revoked."); } protected openNewAccessTokenDialog() { @@ -48,4 +62,25 @@ export class AccessTokenComponent implements OnInit { this.organizationId ); } + + private verifyUser() { + const ref = this.modalService.open(UserVerificationPromptComponent, { + allowMultipleModals: true, + data: { + confirmDescription: "revokeAccessTokenDesc", + confirmButtonText: "revokeAccessToken", + modalTitle: "revokeAccessToken", + }, + }); + + if (ref == null) { + return; + } + + return ref.onClosedPromise(); + } + + private async getAccessTokens(): Promise { + return await this.accessService.getAccessTokens(this.organizationId, this.serviceAccountId); + } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access.service.ts index a5ad5a0690..92b248999f 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/access.service.ts @@ -11,6 +11,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-cr import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { AccessTokenRequest } from "../models/requests/access-token.request"; +import { RevokeAccessTokensRequest } from "../models/requests/revoke-access-tokens.request"; import { AccessTokenCreationResponse } from "../models/responses/access-token-creation.response"; import { AccessTokenResponse } from "../models/responses/access-tokens.response"; import { AccessTokenView } from "../models/view/access-token.view"; @@ -80,6 +81,21 @@ export class AccessService { return `${this._accessTokenVersion}.${result.id}.${result.clientSecret}:${b64Key}`; } + async revokeAccessTokens(serviceAccountId: string, accessTokenIds: string[]): Promise { + const request = new RevokeAccessTokensRequest(); + request.ids = accessTokenIds; + + await this.apiService.send( + "POST", + "/service-accounts/" + serviceAccountId + "/access-tokens/revoke", + request, + true, + false + ); + + this._accessToken.next(null); + } + private async createAccessTokenRequest( organizationId: string, encryptionKey: SymmetricCryptoKey, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/models/requests/revoke-access-tokens.request.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/models/requests/revoke-access-tokens.request.ts new file mode 100644 index 0000000000..51266e802a --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/models/requests/revoke-access-tokens.request.ts @@ -0,0 +1,3 @@ +export class RevokeAccessTokensRequest { + ids: string[]; +}