diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 798b7f7d9f..4c48322cf3 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5561,6 +5561,15 @@ "deleteSecrets":{ "message": "Delete Secrets" }, + "secretProjectAssociationDescription" :{ + "message": "Select projects that the secret will be associated with. Only organization users with access to these projects will be able to see the secret." + }, + "typeOrSelectProjects" :{ + "message": "Type or select Projects" + }, + "typeOrSelectProject" :{ + "message": "Type or select Project" + }, "project":{ "message": "Project" }, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/secret.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/secret.view.ts index 7c949c9dde..c7f132ebdf 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/secret.view.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/secret.view.ts @@ -1,5 +1,7 @@ import { View } from "@bitwarden/common/models/view/view"; +import { SecretProjectView } from "./secret-project.view"; + export class SecretView implements View { id: string; organizationId: string; @@ -8,4 +10,5 @@ export class SecretView implements View { note: string; creationDate: string; revisionDate: string; + projects: SecretProjectView[]; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.html index 6f88cb69c1..8c230472b7 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.html @@ -23,7 +23,43 @@ - + + {{ + "secretProjectAssociationDescription" | i18n + }} + + {{ "project" | i18n }} + + + {{ "typeOrSelectProject" | i18n }} + + + + + {{ "project" | i18n }} + + + + + + {{ e.name }} + + + + + + +
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 2f7a64b555..ebd50fbbd4 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 @@ -1,11 +1,15 @@ import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; import { Component, Inject, OnInit } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; +import { Subject, takeUntil } from "rxjs"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; +import { ProjectListView } from "../../models/view/project-list.view"; +import { SecretProjectView } from "../../models/view/secret-project.view"; import { SecretView } from "../../models/view/secret.view"; +import { ProjectService } from "../../projects/project.service"; import { SecretService } from "../secret.service"; export enum OperationType { @@ -29,37 +33,87 @@ export class SecretDialogComponent implements OnInit { name: new FormControl("", [Validators.required]), value: new FormControl("", [Validators.required]), notes: new FormControl(""), + project: new FormControl(""), }); - protected loading = false; + protected loading = false; + projects: ProjectListView[]; + selectedProjects: SecretProjectView[] = []; + + private destroy$ = new Subject(); constructor( public dialogRef: DialogRef, @Inject(DIALOG_DATA) private data: SecretOperation, private secretService: SecretService, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService + private platformUtilsService: PlatformUtilsService, + private projectService: ProjectService ) {} async ngOnInit() { + this.projects = await this.projectService.getProjects(this.data.organizationId); + if (this.data.operation === OperationType.Edit && this.data.secretId) { await this.loadData(); } else if (this.data.operation !== OperationType.Add) { this.dialogRef.close(); throw new Error(`The secret dialog was not called with the appropriate operation values.`); } + + this.formGroup + .get("project") + .valueChanges.pipe(takeUntil(this.destroy$)) + .subscribe(() => this.updateProjectList()); } async loadData() { this.loading = true; const secret: SecretView = await this.secretService.getBySecretId(this.data.secretId); this.loading = false; - this.formGroup.setValue({ name: secret.name, value: secret.value, notes: secret.note }); + this.selectedProjects = secret.projects; + this.loading = false; + this.formGroup.setValue({ + name: secret.name, + value: secret.value, + notes: secret.note, + project: "", + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); } get title() { return this.data.operation === OperationType.Add ? "newSecret" : "editSecret"; } + async removeProjectAssociation(id: string) { + this.selectedProjects = this.selectedProjects.filter((e) => e.id != id); + this.formGroup.get("project").setValue(""); + } + + updateProjectList() { + const newList: SecretProjectView[] = []; + const projectId = this.formGroup.get("project").value; + + if (projectId) { + const selectedProject = this.projects?.filter((p) => p.id == projectId)[0]; + + if (selectedProject != undefined) { + const projectSecretView = new SecretProjectView(); + + projectSecretView.id = selectedProject.id; + projectSecretView.name = selectedProject.name; + + newList.push(projectSecretView); + } + } + + this.selectedProjects = newList; + } + submit = async () => { this.formGroup.markAllAsTouched(); @@ -69,7 +123,7 @@ export class SecretDialogComponent implements OnInit { const secretView = this.getSecretView(); if (this.data.operation === OperationType.Add) { - await this.createSecret(secretView, this.data.projectId); + await this.createSecret(secretView); } else { secretView.id = this.data.secretId; await this.updateSecret(secretView); @@ -77,8 +131,8 @@ export class SecretDialogComponent implements OnInit { this.dialogRef.close(); }; - private async createSecret(secretView: SecretView, projectId?: string) { - await this.secretService.create(this.data.organizationId, secretView, projectId); + private async createSecret(secretView: SecretView) { + await this.secretService.create(this.data.organizationId, secretView); this.platformUtilsService.showToast("success", null, this.i18nService.t("secretCreated")); } @@ -88,11 +142,14 @@ export class SecretDialogComponent implements OnInit { } private getSecretView() { + const emptyProjects: SecretProjectView[] = []; + const secretView = new SecretView(); secretView.organizationId = this.data.organizationId; secretView.name = this.formGroup.value.name; secretView.value = this.formGroup.value.value; secretView.note = this.formGroup.value.notes; + secretView.projects = this.selectedProjects ? this.selectedProjects : emptyProjects; return secretView; } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/requests/secret.request.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/requests/secret.request.ts index d84641341d..9fdd7567f1 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/requests/secret.request.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/requests/secret.request.ts @@ -2,5 +2,5 @@ export class SecretRequest { key: string; value: string; note: string; - projectId?: string; + projectIds?: string[]; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/responses/secret-list-item.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/responses/secret-list-item.response.ts index 3819f95870..bfac1dfe75 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/responses/secret-list-item.response.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/responses/secret-list-item.response.ts @@ -1,12 +1,14 @@ import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { SecretProjectResponse } from "./secret-project.response"; + export class SecretListItemResponse extends BaseResponse { id: string; organizationId: string; name: string; creationDate: string; revisionDate: string; - projects: string[]; + projects: SecretProjectResponse[]; constructor(response: any) { super(response); @@ -15,6 +17,8 @@ export class SecretListItemResponse extends BaseResponse { this.name = this.getResponseProperty("Key"); this.creationDate = this.getResponseProperty("CreationDate"); this.revisionDate = this.getResponseProperty("RevisionDate"); - this.projects = this.getResponseProperty("projects"); + + const project = this.getResponseProperty("projects"); + this.projects = project == null ? null : project.map((k: any) => new SecretProjectResponse(k)); } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/responses/secret.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/responses/secret.response.ts index c873ac6206..9296c3d869 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/responses/secret.response.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/responses/secret.response.ts @@ -1,5 +1,7 @@ import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { SecretProjectResponse } from "./secret-project.response"; + export class SecretResponse extends BaseResponse { id: string; organizationId: string; @@ -8,6 +10,7 @@ export class SecretResponse extends BaseResponse { note: string; creationDate: string; revisionDate: string; + projects: SecretProjectResponse[]; constructor(response: any) { super(response); @@ -18,5 +21,9 @@ export class SecretResponse extends BaseResponse { this.note = this.getResponseProperty("Note"); this.creationDate = this.getResponseProperty("CreationDate"); this.revisionDate = this.getResponseProperty("RevisionDate"); + + const projects = this.getResponseProperty("Projects"); + this.projects = + projects == null ? null : projects.map((k: any) => new SecretProjectResponse(k)); } } 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 1b87652ee6..9e7e1d5fbe 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 @@ -34,6 +34,7 @@ export class SecretService { async getBySecretId(secretId: string): Promise { const r = await this.apiService.send("GET", "/secrets/" + secretId, null, true, true); const secretResponse = new SecretResponse(r); + return await this.createSecretView(secretResponse); } @@ -63,8 +64,8 @@ export class SecretService { return await this.createSecretsListView(organizationId, results); } - async create(organizationId: string, secretView: SecretView, projectId?: string) { - const request = await this.getSecretRequest(organizationId, secretView, projectId); + async create(organizationId: string, secretView: SecretView) { + const request = await this.getSecretRequest(organizationId, secretView); const r = await this.apiService.send( "POST", "/organizations/" + organizationId + "/secrets", @@ -106,8 +107,7 @@ export class SecretService { private async getSecretRequest( organizationId: string, - secretView: SecretView, - projectId?: string + secretView: SecretView ): Promise { const orgKey = await this.getOrganizationKey(organizationId); const request = new SecretRequest(); @@ -119,7 +119,10 @@ export class SecretService { request.key = key.encryptedString; request.value = value.encryptedString; request.note = note.encryptedString; - request.projectId = projectId; + request.projectIds = []; + + secretView.projects?.forEach((e) => request.projectIds.push(e.id)); + return request; } @@ -141,6 +144,13 @@ export class SecretService { secretView.value = value; secretView.note = note; + if (secretResponse.projects != null) { + secretView.projects = await this.decryptProjectsMappedToSecrets( + orgKey, + secretResponse.projects + ); + } + return secretView; } @@ -150,7 +160,7 @@ export class SecretService { ): Promise { const orgKey = await this.getOrganizationKey(organizationId); - const projectsMappedToSecretsView = this.decryptProjectsMappedToSecrets( + const projectsMappedToSecretsView = await this.decryptProjectsMappedToSecrets( orgKey, secrets.projects ); @@ -166,9 +176,12 @@ export class SecretService { ); secretListView.creationDate = s.creationDate; secretListView.revisionDate = s.revisionDate; - secretListView.projects = (await projectsMappedToSecretsView).filter((p) => - s.projects.includes(p.id) + + const projectIds = s.projects?.map((p) => p.id); + secretListView.projects = projectsMappedToSecretsView.filter((p) => + projectIds.includes(p.id) ); + return secretListView; }) ); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html index 33f6298ec7..46a0502145 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html @@ -26,7 +26,7 @@ {{ "name" | i18n }} - {{ "projects" | i18n }} + {{ "project" | i18n }} {{ "lastEdited" | i18n }}