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 @@
+
+
+
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 @@
+
+
+
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;