From 208e3f30b47e0fc435ed9127779176b665f91022 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Wed, 26 Apr 2023 13:09:30 -0500 Subject: [PATCH] [SM-670] Restrict UI actions based on user permission (#5090) * Restrict UI actions based on user permission * Swap to hiding bulk option without permission * Fix read/write assignment in project service * Filter projects based on permission in dialog * Fix encryption error for updating secret result * Fix spinner (#5182) * Swap to bit-no-items * [SM-699] Projects bulk delete - add bulk confirmation dialog (#5200) * Add bulk confirmation dialog * Code review updates * Code review - load projects * code review - swap to observable * Code review - remove oninit --- apps/web/src/locales/en/messages.json | 14 ++++- .../models/view/project-list.view.ts | 2 + .../models/view/project.view.ts | 3 - .../responses/project-list-item.response.ts | 4 ++ .../models/responses/project.response.ts | 11 +--- .../projects/project.service.ts | 31 ++++------- .../project/project-secrets.component.html | 43 ++++++++++----- .../project/project-secrets.component.ts | 10 +++- .../projects/project/project.component.ts | 4 +- .../projects/projects/projects.component.ts | 55 +++++++++++++++++-- .../secrets/dialog/secret-dialog.component.ts | 19 +++++-- .../secrets-manager/secrets/secret.service.ts | 7 +-- .../access-removal-dialog.component.ts | 9 ++- .../bulk-confirmation-dialog.component.html | 38 +++++++++++++ .../bulk-confirmation-dialog.component.ts | 48 ++++++++++++++++ .../shared/projects-list.component.html | 10 +++- .../shared/projects-list.component.ts | 36 ++++++------ .../shared/sm-shared.module.ts | 3 + 18 files changed, 259 insertions(+), 88 deletions(-) create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.html create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.ts diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 0382895caf..feb8815676 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1264,7 +1264,7 @@ "example": "2" } } - }, + }, "dataExportSuccess": { "message": "Data successfully exported" }, @@ -6736,7 +6736,19 @@ "notAvailableForFreeOrganization": { "message": "This feature is not available for free organizations. Contact your organization owner to upgrade." }, + "smProjectSecretsNoItemsNoAccess": { + "message": "Contact your organization's admin to manage secrets for this project.", + "description": "The message shown to the user under a project's secrets tab when the user only has read access to the project." + }, "enforceOnLoginDesc": { "message": "Require existing members to change their passwords" + }, + "smProjectDeleteAccessRestricted": { + "message": "You don't have permissions to delete this project", + "description": "The individual description shown to the user when the user doesn't have access to delete a project." + }, + "smProjectsDeleteBulkConfirmation": { + "message": "The following projects can not be deleted. Would you like to continue?", + "description": "The message shown to the user when bulk deleting projects and the user doesn't have access to some projects." } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/project-list.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/project-list.view.ts index 5fe8a2b1fa..03a0353352 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/project-list.view.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/project-list.view.ts @@ -4,4 +4,6 @@ export class ProjectListView { name: string; creationDate: string; revisionDate: string; + read: boolean; + write: boolean; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/project.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/project.view.ts index 0d8c55afee..09091d2a75 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/project.view.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/project.view.ts @@ -4,9 +4,6 @@ export class ProjectView { name: string; creationDate: string; revisionDate: string; -} - -export class ProjectPermissionDetailsView extends ProjectView { read: boolean; write: boolean; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/models/responses/project-list-item.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/models/responses/project-list-item.response.ts index dce0f7ffbb..89e5fe8db3 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/models/responses/project-list-item.response.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/models/responses/project-list-item.response.ts @@ -6,6 +6,8 @@ export class ProjectListItemResponse extends BaseResponse { name: string; creationDate: string; revisionDate: string; + read: boolean; + write: boolean; constructor(response: any) { super(response); @@ -14,5 +16,7 @@ export class ProjectListItemResponse extends BaseResponse { this.name = this.getResponseProperty("Name"); this.creationDate = this.getResponseProperty("CreationDate"); this.revisionDate = this.getResponseProperty("RevisionDate"); + this.read = this.getResponseProperty("Read"); + this.write = this.getResponseProperty("Write"); } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/models/responses/project.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/models/responses/project.response.ts index 06fe62e6e1..de02a0d23c 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/models/responses/project.response.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/models/responses/project.response.ts @@ -6,6 +6,8 @@ export class ProjectResponse extends BaseResponse { name: string; creationDate: string; revisionDate: string; + read: boolean; + write: boolean; constructor(response: any) { super(response); @@ -14,15 +16,6 @@ export class ProjectResponse extends BaseResponse { this.name = this.getResponseProperty("Name"); this.creationDate = this.getResponseProperty("CreationDate"); this.revisionDate = this.getResponseProperty("RevisionDate"); - } -} - -export class ProjectPermissionDetailsResponse extends ProjectResponse { - read: boolean; - write: boolean; - - constructor(response: any) { - super(response); this.read = this.getResponseProperty("Read"); this.write = this.getResponseProperty("Write"); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project.service.ts index 9484b9604c..513601c5ba 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project.service.ts @@ -9,15 +9,12 @@ import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-cr import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { ProjectListView } from "../models/view/project-list.view"; -import { ProjectPermissionDetailsView, ProjectView } from "../models/view/project.view"; +import { ProjectView } from "../models/view/project.view"; import { BulkOperationStatus } from "../shared/dialogs/bulk-status-dialog.component"; import { ProjectRequest } from "./models/requests/project.request"; import { ProjectListItemResponse } from "./models/responses/project-list-item.response"; -import { - ProjectPermissionDetailsResponse, - ProjectResponse, -} from "./models/responses/project.response"; +import { ProjectResponse } from "./models/responses/project.response"; @Injectable({ providedIn: "root", @@ -32,10 +29,10 @@ export class ProjectService { private encryptService: EncryptService ) {} - async getByProjectId(projectId: string): Promise { + async getByProjectId(projectId: string): Promise { const r = await this.apiService.send("GET", "/projects/" + projectId, null, true, true); - const projectResponse = new ProjectPermissionDetailsResponse(r); - return await this.createProjectPermissionDetailsView(projectResponse); + const projectResponse = new ProjectResponse(r); + return await this.createProjectView(projectResponse); } async getProjects(organizationId: string): Promise { @@ -99,9 +96,7 @@ export class ProjectService { return request; } - private async createProjectView( - projectResponse: ProjectResponse | ProjectPermissionDetailsResponse - ) { + private async createProjectView(projectResponse: ProjectResponse) { const orgKey = await this.getOrganizationKey(projectResponse.organizationId); const projectView = new ProjectView(); @@ -109,6 +104,8 @@ export class ProjectService { projectView.organizationId = projectResponse.organizationId; projectView.creationDate = projectResponse.creationDate; projectView.revisionDate = projectResponse.revisionDate; + projectView.read = projectResponse.read; + projectView.write = projectResponse.write; projectView.name = await this.encryptService.decryptToUtf8( new EncString(projectResponse.name), orgKey @@ -116,16 +113,6 @@ export class ProjectService { return projectView; } - private async createProjectPermissionDetailsView( - projectResponse: ProjectPermissionDetailsResponse - ): Promise { - return { - ...(await this.createProjectView(projectResponse)), - read: projectResponse.read, - write: projectResponse.write, - }; - } - private async createProjectsListView( organizationId: string, projects: ProjectListItemResponse[] @@ -136,6 +123,8 @@ export class ProjectService { const projectListView = new ProjectListView(); projectListView.id = s.id; projectListView.organizationId = s.organizationId; + projectListView.read = s.read; + projectListView.write = s.write; projectListView.name = await this.encryptService.decryptToUtf8( new EncString(s.name), orgKey diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html index 4a1a430c11..f5edf2de0e 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html @@ -1,18 +1,24 @@ - -
- -
- + + +
+ +
+ +
@@ -20,3 +26,10 @@ + + + + {{ "secretsNoItemsTitle" | i18n }} + {{ "smProjectSecretsNoItemsNoAccess" | i18n }} + + diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts index 3ca398babf..2cac1cc451 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts @@ -1,11 +1,12 @@ import { Component } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { combineLatestWith, filter, Observable, startWith, switchMap } from "rxjs"; +import { combineLatest, combineLatestWith, filter, Observable, startWith, switchMap } from "rxjs"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { DialogService } from "@bitwarden/components"; +import { ProjectView } from "../../models/view/project.view"; import { SecretListView } from "../../models/view/secret-list.view"; import { SecretDeleteDialogComponent, @@ -29,6 +30,7 @@ export class ProjectSecretsComponent { private organizationId: string; private projectId: string; + protected project$: Observable; constructor( private route: ActivatedRoute, @@ -46,6 +48,12 @@ export class ProjectSecretsComponent { startWith(null) ); + this.project$ = combineLatest([this.route.params, currentProjectEdited]).pipe( + switchMap(([params, _]) => { + return this.projectService.getByProjectId(params.projectId); + }) + ); + this.secrets$ = this.secretService.secret$.pipe( startWith(null), combineLatestWith(this.route.params, currentProjectEdited), diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts index 083e581ae3..900402440b 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts @@ -4,7 +4,7 @@ import { combineLatest, filter, Observable, startWith, Subject, switchMap, takeU import { DialogService } from "@bitwarden/components"; -import { ProjectPermissionDetailsView } from "../../models/view/project.view"; +import { ProjectView } from "../../models/view/project.view"; import { OperationType, ProjectDialogComponent, @@ -17,7 +17,7 @@ import { ProjectService } from "../project.service"; templateUrl: "./project.component.html", }) export class ProjectComponent implements OnInit, OnDestroy { - protected project$: Observable; + protected project$: Observable; private organizationId: string; private projectId: string; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts index 35a5dc8773..7128e26a3d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts @@ -1,11 +1,17 @@ import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { combineLatest, Observable, startWith, switchMap } from "rxjs"; +import { combineLatest, lastValueFrom, Observable, startWith, switchMap } from "rxjs"; import { DialogService } from "@bitwarden/components"; import { ProjectListView } from "../../models/view/project-list.view"; import { AccessPolicyService } from "../../shared/access-policies/access-policy.service"; +import { + BulkConfirmationDetails, + BulkConfirmationDialogComponent, + BulkConfirmationResult, + BulkConfirmationStatus, +} from "../../shared/dialogs/bulk-confirmation-dialog.component"; import { ProjectDeleteDialogComponent, ProjectDeleteOperation, @@ -70,11 +76,48 @@ export class ProjectsComponent implements OnInit { }); } - openDeleteProjectDialog(event: ProjectListView[]) { - this.dialogService.open(ProjectDeleteDialogComponent, { - data: { - projects: event, - }, + async openDeleteProjectDialog(projects: ProjectListView[]) { + if (projects.some((project) => project.write == false)) { + const readOnlyProjects = projects.filter((project) => project.write == false); + const writeProjects = projects.filter((project) => project.write); + + const dialogRef = this.dialogService.open( + BulkConfirmationDialogComponent, + { + data: { + title: "deleteProjects", + columnTitle: "projectName", + message: "smProjectsDeleteBulkConfirmation", + details: this.getBulkConfirmationDetails(readOnlyProjects), + }, + } + ); + + const result = await lastValueFrom(dialogRef.closed); + + if (result == BulkConfirmationResult.Continue) { + this.dialogService.open(ProjectDeleteDialogComponent, { + data: { + projects: writeProjects, + }, + }); + } + } else { + this.dialogService.open(ProjectDeleteDialogComponent, { + data: { + projects, + }, + }); + } + } + + private getBulkConfirmationDetails(projects: ProjectListView[]): BulkConfirmationStatus[] { + return projects.map((project) => { + return { + id: project.id, + name: project.name, + description: "smProjectDeleteAccessRestricted", + }; }); } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts index bb44b84bea..6d300d057a 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts @@ -62,6 +62,8 @@ export class SecretDialogComponent implements OnInit { } else if (this.data.operation !== OperationType.Add) { this.dialogRef.close(); throw new Error(`The secret dialog was not called with the appropriate operation values.`); + } else if (this.data.operation == OperationType.Add) { + await this.loadProjects(true); } if (this.data.projectId) { @@ -72,15 +74,14 @@ export class SecretDialogComponent implements OnInit { this.formGroup.get("project").removeValidators(Validators.required); this.formGroup.get("project").updateValueAndValidity(); } - - this.projects = await this.projectService - .getProjects(this.data.organizationId) - .then((projects) => projects.sort((a, b) => a.name.localeCompare(b.name))); } async loadData() { this.formGroup.disable(); const secret: SecretView = await this.secretService.getBySecretId(this.data.secretId); + + await this.loadProjects(secret.write); + this.formGroup.setValue({ name: secret.name, value: secret.value, @@ -95,6 +96,16 @@ export class SecretDialogComponent implements OnInit { } } + async loadProjects(filterByPermission: boolean) { + this.projects = await this.projectService + .getProjects(this.data.organizationId) + .then((projects) => projects.sort((a, b) => a.name.localeCompare(b.name))); + + if (filterByPermission) { + this.projects = this.projects.filter((p) => p.write); + } + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secret.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secret.service.ts index 3d63368bae..0acc292b9c 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secret.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secret.service.ts @@ -233,10 +233,9 @@ export class SecretService { projects.map(async (s: SecretProjectResponse) => { const projectsMappedToSecretView = new SecretProjectView(); projectsMappedToSecretView.id = s.id; - projectsMappedToSecretView.name = await this.encryptService.decryptToUtf8( - new EncString(s.name), - orgKey - ); + projectsMappedToSecretView.name = s.name + ? await this.encryptService.decryptToUtf8(new EncString(s.name), orgKey) + : null; return projectsMappedToSecretView; }) ); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/dialogs/access-removal-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/dialogs/access-removal-dialog.component.ts index ecf80041ee..5d1f49c099 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/dialogs/access-removal-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/dialogs/access-removal-dialog.component.ts @@ -50,16 +50,21 @@ export class AccessRemovalDialogComponent implements OnInit { await this.accessPolicyService.updateAccessPolicy( AccessSelectorComponent.getBaseAccessPolicyView(this.data.policy) ); + this.refreshPolicyChanges(); } this.dialogRef.close(); }; cancel = () => { + this.refreshPolicyChanges(); + this.dialogRef.close(); + }; + + private refreshPolicyChanges() { if (this.data.type == "project") { this.accessPolicyService.refreshProjectAccessPolicyChanges(); } else if (this.data.type == "service-account") { this.accessPolicyService.refreshServiceAccountAccessPolicyChanges(); } - this.dialogRef.close(); - }; + } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.html new file mode 100644 index 0000000000..ff0c21e452 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.html @@ -0,0 +1,38 @@ + + + {{ data.title | i18n }} + + +
+ {{ data.message | i18n }} + + + + {{ data.columnTitle | i18n }} + {{ "description" | i18n }} + + + + + {{ detail.name }} + {{ detail.description | i18n }} + + + +
+ +
+ + +
+
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.ts new file mode 100644 index 0000000000..f42351c2b1 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/dialogs/bulk-confirmation-dialog.component.ts @@ -0,0 +1,48 @@ +import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { Component, Inject, OnInit } from "@angular/core"; + +export interface BulkConfirmationDetails { + title: string; + columnTitle: string; + message: string; + details: BulkConfirmationStatus[]; +} + +export interface BulkConfirmationStatus { + id: string; + name: string; + description: string; +} + +export enum BulkConfirmationResult { + Continue, + Cancel, +} + +@Component({ + selector: "sm-bulk-confirmation-dialog", + templateUrl: "./bulk-confirmation-dialog.component.html", +}) +export class BulkConfirmationDialogComponent implements OnInit { + constructor( + public dialogRef: DialogRef, + @Inject(DIALOG_DATA) public data: BulkConfirmationDetails + ) {} + + protected bulkConfirmationResult = BulkConfirmationResult; + + ngOnInit(): void { + // TODO remove null checks once strictNullChecks in TypeScript is turned on. + if ( + !this.data.title || + !this.data.columnTitle || + !this.data.message || + !(this.data.details?.length >= 1) + ) { + this.dialogRef.close(); + throw new Error( + "The bulk confirmation dialog was not called with the appropriate operation values." + ); + } + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.html index 8705c7b2cf..8d9d6e3c35 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.html @@ -35,6 +35,7 @@ {{ "lastEdited" | i18n }} - @@ -82,7 +88,7 @@ {{ "viewProject" | i18n }} - diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts index 1824c7b62d..e327547c6d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts @@ -1,6 +1,6 @@ import { SelectionModel } from "@angular/cdk/collections"; -import { Component, EventEmitter, Input, OnDestroy, Output } from "@angular/core"; -import { Subject, takeUntil } from "rxjs"; +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { map } from "rxjs"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -12,9 +12,7 @@ import { ProjectListView } from "../models/view/project-list.view"; selector: "sm-projects-list", templateUrl: "./projects-list.component.html", }) -export class ProjectsListComponent implements OnDestroy { - protected dataSource = new TableDataSource(); - +export class ProjectsListComponent { @Input() get projects(): ProjectListView[] { return this._projects; @@ -33,26 +31,18 @@ export class ProjectsListComponent implements OnDestroy { @Output() editProjectEvent = new EventEmitter(); @Output() deleteProjectEvent = new EventEmitter(); - @Output() onProjectCheckedEvent = new EventEmitter(); @Output() newProjectEvent = new EventEmitter(); - private destroy$: Subject = new Subject(); - selection = new SelectionModel(true, []); + protected dataSource = new TableDataSource(); + protected hasWriteAccessOnSelected$ = this.selection.changed.pipe( + map((_) => this.selectedHasWriteAccess()) + ); constructor( private i18nService: I18nService, private platformUtilsService: PlatformUtilsService - ) { - this.selection.changed - .pipe(takeUntil(this.destroy$)) - .subscribe((_) => this.onProjectCheckedEvent.emit(this.selection.selected)); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } + ) {} isAllSelected() { const numSelected = this.selection.selected.length; @@ -83,4 +73,14 @@ export class ProjectsListComponent implements OnDestroy { ); } } + + private selectedHasWriteAccess() { + const selectedProjects = this.projects.filter((project) => + this.selection.isSelected(project.id) + ); + if (selectedProjects.some((project) => project.write)) { + return true; + } + return false; + } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts index 49622c9110..e1cba09183 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts @@ -8,6 +8,7 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { AccessSelectorComponent } from "./access-policies/access-selector.component"; import { AccessRemovalDialogComponent } from "./access-policies/dialogs/access-removal-dialog.component"; +import { BulkConfirmationDialogComponent } from "./dialogs/bulk-confirmation-dialog.component"; import { BulkStatusDialogComponent } from "./dialogs/bulk-status-dialog.component"; import { HeaderComponent } from "./header.component"; import { NewMenuComponent } from "./new-menu.component"; @@ -30,6 +31,7 @@ import { SecretsListComponent } from "./secrets-list.component"; AccessRemovalDialogComponent, AccessSelectorComponent, BulkStatusDialogComponent, + BulkConfirmationDialogComponent, HeaderComponent, NewMenuComponent, ProjectsListComponent, @@ -40,6 +42,7 @@ import { SecretsListComponent } from "./secrets-list.component"; declarations: [ AccessRemovalDialogComponent, BulkStatusDialogComponent, + BulkConfirmationDialogComponent, HeaderComponent, NewMenuComponent, ProjectsListComponent,