diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index e3c25c55e9..9ccd70e324 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1249,6 +1249,9 @@ "importSuccess": { "message": "Data successfully imported" }, + "dataExportSuccess": { + "message": "Data successfully exported" + }, "importWarning": { "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", "placeholders": { @@ -6111,6 +6114,33 @@ } } }, + "exportData": { + "message": "Export data" + }, + "exportingOrganizationSecretDataTitle": { + "message": "Exporting Organization Secret Data" + }, + "exportingOrganizationSecretDataDescription": { + "message": "Only the Secrets Manager data associated with $ORGANIZATION$ will be exported. Items in other products or from other organizations will not be included.", + "placeholders": { + "ORGANIZATION": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "fileUpload": { + "message": "File upload" + }, + "acceptedFormats": { + "message": "Accepted Formats:" + }, + "copyPasteImportContents": { + "message": "Copy & paste import contents:" + }, + "or": { + "message": "or" + }, "licenseAndBillingManagement": { "message": "License and billing management" }, @@ -6188,5 +6218,14 @@ }, "userAccessSecretsManager": { "message": "This user can access the Secrets Manager Beta" + }, + "resolveTheErrorsBelowAndTryAgain": { + "message": "Resolve the errors below and try again." + }, + "description": { + "message": "Description" + }, + "errorReadingImportFile": { + "message": "An error occurred when trying to read the import file" } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html index ce43bef331..0ce398b131 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html @@ -11,4 +11,7 @@ route="service-accounts" > - + + + + diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/dialog/sm-import-error-dialog.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/settings/dialog/sm-import-error-dialog.component.html new file mode 100644 index 0000000000..ea1bc9288a --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/dialog/sm-import-error-dialog.component.html @@ -0,0 +1,27 @@ + + + {{ "importError" | i18n }} + + +
{{ "resolveTheErrorsBelowAndTryAgain" | i18n }}
+ + + + {{ "name" | i18n }} + {{ "description" | i18n }} + + + + + [{{ line.id }}] [{{ line.type }}] {{ line.key }} + {{ line.errorMessage }} + + + +
+
+ +
+
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/dialog/sm-import-error-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/dialog/sm-import-error-dialog.component.ts new file mode 100644 index 0000000000..ea62c47cda --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/dialog/sm-import-error-dialog.component.ts @@ -0,0 +1,27 @@ +import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; + +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; + +import { SecretsManagerImportError } from "../models/error/sm-import-error"; +import { SecretsManagerImportErrorLine } from "../models/error/sm-import-error-line"; + +export interface SecretsManagerImportErrorDialogOperation { + error: SecretsManagerImportError; +} + +@Component({ + selector: "sm-import-error-dialog", + templateUrl: "./sm-import-error-dialog.component.html", +}) +export class SecretsManagerImportErrorDialogComponent { + errorLines: SecretsManagerImportErrorLine[]; + + constructor( + public dialogRef: DialogRef, + private i18nService: I18nService, + @Inject(DIALOG_DATA) public data: SecretsManagerImportErrorDialogOperation + ) { + this.errorLines = data.error.lines; + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/error/sm-import-error-line.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/error/sm-import-error-line.ts new file mode 100644 index 0000000000..45c923f423 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/error/sm-import-error-line.ts @@ -0,0 +1,6 @@ +export class SecretsManagerImportErrorLine { + id: number; + type: "Project" | "Secret"; + key: "string"; + errorMessage: string; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/error/sm-import-error.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/error/sm-import-error.ts new file mode 100644 index 0000000000..e362a8f259 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/error/sm-import-error.ts @@ -0,0 +1,9 @@ +import { SecretsManagerImportErrorLine } from "./sm-import-error-line"; + +export class SecretsManagerImportError extends Error { + constructor(message?: string) { + super(message); + } + + lines: SecretsManagerImportErrorLine[]; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/requests/sm-import.request.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/requests/sm-import.request.ts new file mode 100644 index 0000000000..4ef2a7181e --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/requests/sm-import.request.ts @@ -0,0 +1,7 @@ +import { SecretsManagerImportedProjectRequest } from "./sm-imported-project.request"; +import { SecretsManagerImportedSecretRequest } from "./sm-imported-secret.request"; + +export class SecretsManagerImportRequest { + projects: SecretsManagerImportedProjectRequest[]; + secrets: SecretsManagerImportedSecretRequest[]; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/requests/sm-imported-project.request.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/requests/sm-imported-project.request.ts new file mode 100644 index 0000000000..ff509e084f --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/requests/sm-imported-project.request.ts @@ -0,0 +1,6 @@ +import { EncString } from "@bitwarden/common/models/domain/enc-string"; + +export class SecretsManagerImportedProjectRequest { + id: string; + name: EncString; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/requests/sm-imported-secret.request.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/requests/sm-imported-secret.request.ts new file mode 100644 index 0000000000..e6e73b7fe2 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/requests/sm-imported-secret.request.ts @@ -0,0 +1,9 @@ +import { EncString } from "@bitwarden/common/models/domain/enc-string"; + +export class SecretsManagerImportedSecretRequest { + id: string; + key: EncString; + value: EncString; + note: EncString; + projectIds: string[]; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/responses/sm-export.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/responses/sm-export.response.ts new file mode 100644 index 0000000000..b208490717 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/responses/sm-export.response.ts @@ -0,0 +1,19 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +import { SecretsManagerExportedProjectResponse } from "./sm-exported-project.response"; +import { SecretsManagerExportedSecretResponse } from "./sm-exported-secret.response"; + +export class SecretsManagerExportResponse extends BaseResponse { + projects: SecretsManagerExportedProjectResponse[]; + secrets: SecretsManagerExportedSecretResponse[]; + + constructor(response: any) { + super(response); + + const projects = this.getResponseProperty("Projects"); + const secrets = this.getResponseProperty("Secrets"); + + this.projects = projects?.map((k: any) => new SecretsManagerExportedProjectResponse(k)); + this.secrets = secrets?.map((k: any) => new SecretsManagerExportedSecretResponse(k)); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/responses/sm-exported-project.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/responses/sm-exported-project.response.ts new file mode 100644 index 0000000000..8e26333111 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/responses/sm-exported-project.response.ts @@ -0,0 +1,13 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export class SecretsManagerExportedProjectResponse extends BaseResponse { + id: string; + name: string; + + constructor(response: any) { + super(response); + + this.id = this.getResponseProperty("Id"); + this.name = this.getResponseProperty("Name"); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/responses/sm-exported-secret.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/responses/sm-exported-secret.response.ts new file mode 100644 index 0000000000..89578b3b4c --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/responses/sm-exported-secret.response.ts @@ -0,0 +1,21 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export class SecretsManagerExportedSecretResponse extends BaseResponse { + id: string; + key: string; + value: string; + note: string; + projectIds: string[]; + + constructor(response: any) { + super(response); + + this.id = this.getResponseProperty("Id"); + this.key = this.getResponseProperty("Key"); + this.value = this.getResponseProperty("Value"); + this.note = this.getResponseProperty("Note"); + + const projectIds = this.getResponseProperty("ProjectIds"); + this.projectIds = projectIds?.map((id: any) => id.toString()); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/sm-export.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/sm-export.ts new file mode 100644 index 0000000000..9620321078 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/models/sm-export.ts @@ -0,0 +1,17 @@ +export class SecretsManagerExport { + projects: SecretsManagerExportProject[]; + secrets: SecretsManagerExportSecret[]; +} + +export class SecretsManagerExportProject { + id: string; + name: string; +} + +export class SecretsManagerExportSecret { + id: string; + key: string; + value: string; + note: string; + projectIds: string[]; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.html new file mode 100644 index 0000000000..65740bba97 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.html @@ -0,0 +1,20 @@ + + +
+
+ + {{ "exportingOrganizationSecretDataDescription" | i18n: orgName }} + +
+ + + {{ "fileFormat" | i18n }} + + + + +
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts new file mode 100644 index 0000000000..6f98c17b9c --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts @@ -0,0 +1,117 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; +import { ActivatedRoute } from "@angular/router"; +import { Subject, switchMap, takeUntil } from "rxjs"; + +import { ModalService } from "@bitwarden/angular/services/modal.service"; +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; +import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; +import { UserVerificationPromptComponent } from "@bitwarden/web-vault/app/components/user-verification-prompt.component"; + +import { SecretsManagerPortingApiService } from "../services/sm-porting-api.service"; +import { SecretsManagerPortingService } from "../services/sm-porting.service"; + +@Component({ + selector: "sm-export", + templateUrl: "./sm-export.component.html", +}) +export class SecretsManagerExportComponent implements OnInit, OnDestroy { + private destroy$ = new Subject(); + + protected orgName: string; + protected orgId: string; + protected exportFormats: string[] = ["json"]; + + protected formGroup = new FormGroup({ + format: new FormControl("json", [Validators.required]), + }); + + constructor( + private route: ActivatedRoute, + private i18nService: I18nService, + private organizationService: OrganizationService, + private platformUtilsService: PlatformUtilsService, + private smPortingService: SecretsManagerPortingService, + private fileDownloadService: FileDownloadService, + private logService: LogService, + private modalService: ModalService, + private secretsManagerApiService: SecretsManagerPortingApiService + ) {} + + async ngOnInit() { + this.route.params + .pipe( + switchMap(async (params) => await this.organizationService.get(params.organizationId)), + takeUntil(this.destroy$) + ) + .subscribe((organization) => { + this.orgName = organization.name; + this.orgId = organization.id; + }); + + this.formGroup.get("format").disable(); + } + + async ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + submit = async () => { + this.formGroup.markAllAsTouched(); + + if (this.formGroup.invalid) { + return; + } + + const userVerified = await this.verifyUser(); + if (!userVerified) { + return; + } + + await this.doExport(); + }; + + private async doExport() { + try { + const exportData = await this.secretsManagerApiService.export( + this.orgId, + this.formGroup.get("format").value + ); + + await this.downloadFile(exportData, this.formGroup.get("format").value); + this.platformUtilsService.showToast("success", null, this.i18nService.t("dataExportSuccess")); + } catch (e) { + this.logService.error(e); + } + } + + private async downloadFile(data: string, format: string) { + const fileName = await this.smPortingService.getFileName(null, format); + this.fileDownloadService.download({ + fileName: fileName, + blobData: data, + blobOptions: { type: "text/plain" }, + }); + } + + private verifyUser() { + const ref = this.modalService.open(UserVerificationPromptComponent, { + allowMultipleModals: true, + data: { + confirmDescription: "exportWarningDesc", + confirmButtonText: "exportVault", + modalTitle: "confirmVaultExport", + }, + }); + + if (ref == null) { + return; + } + + return ref.onClosedPromise(); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.html new file mode 100644 index 0000000000..79743d7275 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.html @@ -0,0 +1,42 @@ + + +
+ + {{ "fileUpload" | i18n }} +
+ + {{ selectedFile?.name ?? ("noFileChosen" | i18n) }} +
+ + {{ "acceptedFormats" | i18n }} JSON +
+
+ {{ "or" | i18n }} +
+ + {{ "copyPasteImportContents" | i18n }} + + {{ "acceptedFormats" | i18n }} JSON + + +
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.ts new file mode 100644 index 0000000000..19d764658e --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.ts @@ -0,0 +1,166 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { FormControl, FormGroup } from "@angular/forms"; +import { ActivatedRoute } from "@angular/router"; +import { Subject, takeUntil } from "rxjs"; + +import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; +import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; +import { DialogService } from "@bitwarden/components"; + +import { + SecretsManagerImportErrorDialogComponent, + SecretsManagerImportErrorDialogOperation, +} from "../dialog/sm-import-error-dialog.component"; +import { SecretsManagerImportError } from "../models/error/sm-import-error"; +import { SecretsManagerPortingApiService } from "../services/sm-porting-api.service"; + +@Component({ + selector: "sm-import", + templateUrl: "./sm-import.component.html", +}) +export class SecretsManagerImportComponent implements OnInit, OnDestroy { + private destroy$ = new Subject(); + protected orgId: string = null; + protected selectedFile: File; + protected formGroup = new FormGroup({ + pastedContents: new FormControl(""), + }); + + constructor( + private route: ActivatedRoute, + private i18nService: I18nService, + private organizationService: OrganizationService, + private platformUtilsService: PlatformUtilsService, + protected fileDownloadService: FileDownloadService, + private logService: LogService, + private secretsManagerPortingApiService: SecretsManagerPortingApiService, + private dialogService: DialogService + ) {} + + async ngOnInit() { + this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => { + this.orgId = params.organizationId; + }); + } + + async ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + submit = async () => { + const fileElement = document.getElementById("file") as HTMLInputElement; + const importContents = await this.getImportContents( + fileElement, + this.formGroup.get("pastedContents").value.trim() + ); + + if (importContents == null) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("selectFile") + ); + return; + } + + try { + const error = await this.secretsManagerPortingApiService.import(this.orgId, importContents); + + if (error?.lines?.length > 0) { + this.openImportErrorDialog(error); + return; + } else if (error != null) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("errorReadingImportFile") + ); + return; + } + + this.platformUtilsService.showToast("success", null, this.i18nService.t("importSuccess")); + this.clearForm(); + } catch (error) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("errorReadingImportFile") + ); + this.logService.error(error); + } + }; + + protected async getImportContents( + fileElement: HTMLInputElement, + pastedContents: string + ): Promise { + const files = fileElement.files; + + if ( + (files == null || files.length === 0) && + (pastedContents == null || pastedContents === "") + ) { + return null; + } + + let fileContents = pastedContents; + if (files != null && files.length > 0) { + try { + const content = await this.getFileContents(files[0]); + if (content != null) { + fileContents = content; + } + } catch (e) { + this.logService.error(e); + } + } + + if (fileContents == null || fileContents === "") { + return null; + } + + return fileContents; + } + + protected setSelectedFile(event: Event) { + const fileInputEl = event.target; + const file = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null; + this.selectedFile = file; + } + + private clearForm() { + (document.getElementById("file") as HTMLInputElement).value = ""; + this.selectedFile = null; + this.formGroup.reset({ + pastedContents: "", + }); + } + + private getFileContents(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsText(file, "utf-8"); + reader.onload = (evt) => { + resolve((evt.target as any).result); + }; + reader.onerror = () => { + reject(); + }; + }); + } + + private openImportErrorDialog(error: SecretsManagerImportError) { + this.dialogService.open( + SecretsManagerImportErrorDialogComponent, + { + data: { + error: error, + }, + } + ); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/services/sm-porting-api.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/services/sm-porting-api.service.ts new file mode 100644 index 0000000000..a2861a5ad1 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/services/sm-porting-api.service.ts @@ -0,0 +1,194 @@ +import { Injectable } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/abstractions/encrypt.service"; +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { EncString } from "@bitwarden/common/models/domain/enc-string"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; + +import { SecretsManagerImportError } from "../models/error/sm-import-error"; +import { SecretsManagerImportRequest } from "../models/requests/sm-import.request"; +import { SecretsManagerImportedProjectRequest } from "../models/requests/sm-imported-project.request"; +import { SecretsManagerImportedSecretRequest } from "../models/requests/sm-imported-secret.request"; +import { SecretsManagerExportResponse } from "../models/responses/sm-export.response"; +import { + SecretsManagerExport, + SecretsManagerExportProject, + SecretsManagerExportSecret, +} from "../models/sm-export"; + +@Injectable({ + providedIn: "root", +}) +export class SecretsManagerPortingApiService { + constructor( + private apiService: ApiService, + private encryptService: EncryptService, + private cryptoService: CryptoService, + private i18nService: I18nService + ) {} + + async export(organizationId: string, exportFormat = "json"): Promise { + let response = {}; + + try { + response = await this.apiService.send( + "GET", + "/sm/" + organizationId + "/export?format=" + exportFormat, + null, + true, + true + ); + } catch (error) { + return null; + } + + return JSON.stringify( + await this.decryptExport(organizationId, new SecretsManagerExportResponse(response)), + null, + " " + ); + } + + async import(organizationId: string, fileContents: string): Promise { + let requestObject = {}; + + try { + requestObject = JSON.parse(fileContents); + const requestBody = await this.encryptImport(organizationId, requestObject); + + await this.apiService.send( + "POST", + "/sm/" + organizationId + "/import", + requestBody, + true, + true + ); + } catch (error) { + const errorResponse = new ErrorResponse(error, 400); + return this.handleServerError(errorResponse, requestObject); + } + } + + private async encryptImport( + organizationId: string, + importData: any + ): Promise { + const encryptedImport = new SecretsManagerImportRequest(); + + try { + const orgKey = await this.cryptoService.getOrgKey(organizationId); + encryptedImport.projects = []; + encryptedImport.secrets = []; + + encryptedImport.projects = await Promise.all( + importData.projects.map(async (p: any) => { + const project = new SecretsManagerImportedProjectRequest(); + project.id = p.id; + project.name = await this.encryptService.encrypt(p.name, orgKey); + return project; + }) + ); + + encryptedImport.secrets = await Promise.all( + importData.secrets.map(async (s: any) => { + const secret = new SecretsManagerImportedSecretRequest(); + + [secret.key, secret.value, secret.note] = await Promise.all([ + this.encryptService.encrypt(s.key, orgKey), + this.encryptService.encrypt(s.value, orgKey), + this.encryptService.encrypt(s.note, orgKey), + ]); + + secret.id = s.id; + secret.projectIds = s.projectIds; + + return secret; + }) + ); + } catch (error) { + return null; + } + + return encryptedImport; + } + + private async decryptExport( + organizationId: string, + exportData: SecretsManagerExportResponse + ): Promise { + const orgKey = await this.cryptoService.getOrgKey(organizationId); + const decryptedExport = new SecretsManagerExport(); + decryptedExport.projects = []; + decryptedExport.secrets = []; + + decryptedExport.projects = await Promise.all( + exportData.projects.map(async (p) => { + const project = new SecretsManagerExportProject(); + project.id = p.id; + project.name = await this.encryptService.decryptToUtf8(new EncString(p.name), orgKey); + return project; + }) + ); + + decryptedExport.secrets = await Promise.all( + exportData.secrets.map(async (s) => { + const secret = new SecretsManagerExportSecret(); + + [secret.key, secret.value, secret.note] = await Promise.all([ + this.encryptService.decryptToUtf8(new EncString(s.key), orgKey), + this.encryptService.decryptToUtf8(new EncString(s.value), orgKey), + this.encryptService.decryptToUtf8(new EncString(s.note), orgKey), + ]); + + secret.id = s.id; + secret.projectIds = s.projectIds; + + return secret; + }) + ); + + return decryptedExport; + } + + private handleServerError( + errorResponse: ErrorResponse, + importResult: any + ): SecretsManagerImportError { + if (errorResponse.validationErrors == null) { + return new SecretsManagerImportError(errorResponse.message); + } + + const result = new SecretsManagerImportError(); + result.lines = []; + + Object.entries(errorResponse.validationErrors).forEach(([key, value], index) => { + let item; + let itemType; + const id = Number(key.match(/[0-9]+/)[0]); + + switch (key.match(/^\w+/)[0]) { + case "Projects": + item = importResult.projects[id]; + itemType = "Project"; + break; + case "Secrets": + item = importResult.secrets[id]; + itemType = "Secret"; + break; + default: + return; + } + + result.lines.push({ + id: id + 1, + type: itemType == "Project" ? "Project" : "Secret", + key: item.key, + errorMessage: value.length > 0 ? value[0] : "", + }); + }); + + return result; + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/services/sm-porting.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/services/sm-porting.service.ts new file mode 100644 index 0000000000..eb92a4a58b --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/services/sm-porting.service.ts @@ -0,0 +1,18 @@ +import { formatDate } from "@angular/common"; +import { Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; + +@Injectable({ + providedIn: "root", +}) +export class SecretsManagerPortingService { + constructor(private i18nService: I18nService) {} + + async getFileName(prefix: string = null, extension = "json"): Promise { + const locale = await firstValueFrom(this.i18nService.locale$); + const dateString = formatDate(new Date(), "yyyyMMddHHmmss", locale); + return "bitwarden" + (prefix ? "_" + prefix : "") + "_export_" + dateString + "." + extension; + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/settings-routing.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/settings-routing.module.ts new file mode 100644 index 0000000000..6cd6249d33 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/settings-routing.module.ts @@ -0,0 +1,28 @@ +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; + +import { SecretsManagerExportComponent } from "./porting/sm-export.component"; +import { SecretsManagerImportComponent } from "./porting/sm-import.component"; + +const routes: Routes = [ + { + path: "import", + component: SecretsManagerImportComponent, + data: { + titleId: "importData", + }, + }, + { + path: "export", + component: SecretsManagerExportComponent, + data: { + titleId: "exportData", + }, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class SettingsRoutingModule {} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/settings.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/settings.module.ts new file mode 100644 index 0000000000..300a17caa0 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/settings.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from "@angular/core"; + +import { SecretsManagerSharedModule } from "../shared/sm-shared.module"; + +import { SecretsManagerImportErrorDialogComponent } from "./dialog/sm-import-error-dialog.component"; +import { SecretsManagerExportComponent } from "./porting/sm-export.component"; +import { SecretsManagerImportComponent } from "./porting/sm-import.component"; +import { SecretsManagerPortingApiService } from "./services/sm-porting-api.service"; +import { SecretsManagerPortingService } from "./services/sm-porting.service"; +import { SettingsRoutingModule } from "./settings-routing.module"; + +@NgModule({ + imports: [SecretsManagerSharedModule, SettingsRoutingModule], + declarations: [ + SecretsManagerImportComponent, + SecretsManagerExportComponent, + SecretsManagerImportErrorDialogComponent, + ], + providers: [SecretsManagerPortingService, SecretsManagerPortingApiService], +}) +export class SettingsModule {} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/sm-routing.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/sm-routing.module.ts index 19cfd97f33..34605e1114 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/sm-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/sm-routing.module.ts @@ -11,6 +11,7 @@ import { OverviewModule } from "./overview/overview.module"; import { ProjectsModule } from "./projects/projects.module"; import { SecretsModule } from "./secrets/secrets.module"; import { ServiceAccountsModule } from "./service-accounts/service-accounts.module"; +import { SettingsModule } from "./settings/settings.module"; import { SMGuard } from "./sm.guard"; const routes: Routes = [ @@ -48,6 +49,10 @@ const routes: Routes = [ titleId: "serviceAccounts", }, }, + { + path: "settings", + loadChildren: () => SettingsModule, + }, { path: "", loadChildren: () => OverviewModule, diff --git a/libs/components/src/form-field/form-field-control.ts b/libs/components/src/form-field/form-field-control.ts index 52f3018f79..e510b4570f 100644 --- a/libs/components/src/form-field/form-field-control.ts +++ b/libs/components/src/form-field/form-field-control.ts @@ -5,7 +5,8 @@ export type InputTypes = | "datetime-local" | "email" | "checkbox" - | "search"; + | "search" + | "file"; export abstract class BitFormFieldControl { ariaDescribedBy: string;