Browse Source
* SM-365: Add scaffolding for settings, import, and export components * SM-365: Build out SM export component and retrieve org name * Add password verification * Add SMExportService * SM-365: Add full export functionality for client side * SM-365: Add SM Import UI, combine import & export services, general cleanup * SM-365: Small updates, fix settings navigation for SM * SM-365: Refactorings based on PR comments, part 1 * SM-365: Refactorings based on PR comments, part 2 * SM-365: remove unneeded import file parsing code * Attempt New SM Export Auth Flow (#4596) * Attempt new sm-export auth flow * Fix component * SM-365: Add error messaging for failed import * SM-365: Fix import error dialog * SM-365: Fix layout of pages, title, and success messaging * SM-365: Address majority of PR comments, clear import form on success * SM-365: Refactor error handling, refactor date formatting * SM-365: Refactored names, logic, added SM porting api service, added needed error checking, etc. * SM-365: Refactor fileContents to pastedContents to be more clear * SM-365: Refactoring based on PR comments * SM-365: Update based on PR comments, refactoring ngOnInit for sm-import * SM-365: Fix wrong type on choose import file buttonpull/4746/head
23 changed files with 817 additions and 2 deletions
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
<bit-dialog> |
||||
<span bitDialogTitle> |
||||
{{ "importError" | i18n }} |
||||
</span> |
||||
<span bitDialogContent> |
||||
<div>{{ "resolveTheErrorsBelowAndTryAgain" | i18n }}</div> |
||||
<bit-table> |
||||
<ng-container header> |
||||
<tr> |
||||
<th bitCell>{{ "name" | i18n }}</th> |
||||
<th bitCell>{{ "description" | i18n }}</th> |
||||
</tr> |
||||
</ng-container> |
||||
<ng-template body> |
||||
<tr bitRow *ngFor="let line of errorLines"> |
||||
<td bitCell class="tw-break-all">[{{ line.id }}] [{{ line.type }}] {{ line.key }}</td> |
||||
<td bitCell>{{ line.errorMessage }}</td> |
||||
</tr> |
||||
</ng-template> |
||||
</bit-table> |
||||
</span> |
||||
<div bitDialogFooter> |
||||
<button bitButton bitDialogClose buttonType="primary" type="button"> |
||||
{{ "ok" | i18n }} |
||||
</button> |
||||
</div> |
||||
</bit-dialog> |
||||
@ -0,0 +1,27 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
export class SecretsManagerImportErrorLine { |
||||
id: number; |
||||
type: "Project" | "Secret"; |
||||
key: "string"; |
||||
errorMessage: string; |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
import { SecretsManagerImportErrorLine } from "./sm-import-error-line"; |
||||
|
||||
export class SecretsManagerImportError extends Error { |
||||
constructor(message?: string) { |
||||
super(message); |
||||
} |
||||
|
||||
lines: SecretsManagerImportErrorLine[]; |
||||
} |
||||
@ -0,0 +1,7 @@
@@ -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[]; |
||||
} |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
import { EncString } from "@bitwarden/common/models/domain/enc-string"; |
||||
|
||||
export class SecretsManagerImportedProjectRequest { |
||||
id: string; |
||||
name: EncString; |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -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[]; |
||||
} |
||||
@ -0,0 +1,19 @@
@@ -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)); |
||||
} |
||||
} |
||||
@ -0,0 +1,13 @@
@@ -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"); |
||||
} |
||||
} |
||||
@ -0,0 +1,21 @@
@@ -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()); |
||||
} |
||||
} |
||||
@ -0,0 +1,17 @@
@@ -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[]; |
||||
} |
||||
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
<sm-header></sm-header> |
||||
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit"> |
||||
<div class="tw-my-4 tw-max-w-xl"> |
||||
<app-callout type="info" title="{{ 'exportingOrganizationSecretDataTitle' | i18n }}"> |
||||
{{ "exportingOrganizationSecretDataDescription" | i18n: orgName }} |
||||
</app-callout> |
||||
</div> |
||||
|
||||
<bit-form-field class="tw-max-w-sm"> |
||||
<bit-label>{{ "fileFormat" | i18n }}</bit-label> |
||||
<select bitInput formControlName="format"> |
||||
<option *ngFor="let format of exportFormats" [ngValue]="format">{{ format }}</option> |
||||
</select> |
||||
</bit-form-field> |
||||
|
||||
<button bitButton bitFormButton type="submit" buttonType="primary"> |
||||
{{ "exportData" | i18n }} |
||||
</button> |
||||
</form> |
||||
@ -0,0 +1,117 @@
@@ -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<void>(); |
||||
|
||||
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(); |
||||
} |
||||
} |
||||
@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
|
||||
<sm-header></sm-header> |
||||
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit" class="tw-max-w-xl"> |
||||
<bit-form-field> |
||||
<bit-label>{{ "fileUpload" | i18n }}</bit-label> |
||||
<div class="file-selector"> |
||||
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()"> |
||||
{{ "chooseFile" | i18n }} |
||||
</button> |
||||
{{ selectedFile?.name ?? ("noFileChosen" | i18n) }} |
||||
</div> |
||||
<input |
||||
#fileSelector |
||||
hidden |
||||
bitInput |
||||
type="file" |
||||
id="file" |
||||
class="form-control-file" |
||||
name="file" |
||||
(change)="setSelectedFile($event)" |
||||
accept="application/JSON" |
||||
/> |
||||
<bit-hint>{{ "acceptedFormats" | i18n }} JSON</bit-hint> |
||||
</bit-form-field> |
||||
<div class="my-4"> |
||||
{{ "or" | i18n }} |
||||
</div> |
||||
<bit-form-field> |
||||
<bit-label for="pastedContents">{{ "copyPasteImportContents" | i18n }}</bit-label> |
||||
<textarea |
||||
bitInput |
||||
id="pastedContents" |
||||
class="form-control" |
||||
name="FileContents" |
||||
formControlName="pastedContents" |
||||
></textarea> |
||||
<bit-hint>{{ "acceptedFormats" | i18n }} JSON</bit-hint> |
||||
</bit-form-field> |
||||
<button bitButton bitformButton type="submit" buttonType="primary"> |
||||
{{ "importData" | i18n }} |
||||
</button> |
||||
</form> |
||||
@ -0,0 +1,166 @@
@@ -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<void>(); |
||||
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<string> { |
||||
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 = <HTMLInputElement>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<string> { |
||||
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<unknown, SecretsManagerImportErrorDialogOperation>( |
||||
SecretsManagerImportErrorDialogComponent, |
||||
{ |
||||
data: { |
||||
error: error, |
||||
}, |
||||
} |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,194 @@
@@ -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<string> { |
||||
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<SecretsManagerImportError> { |
||||
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<SecretsManagerImportRequest> { |
||||
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<SecretsManagerExport> { |
||||
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; |
||||
} |
||||
} |
||||
@ -0,0 +1,18 @@
@@ -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<string> { |
||||
const locale = await firstValueFrom(this.i18nService.locale$); |
||||
const dateString = formatDate(new Date(), "yyyyMMddHHmmss", locale); |
||||
return "bitwarden" + (prefix ? "_" + prefix : "") + "_export_" + dateString + "." + extension; |
||||
} |
||||
} |
||||
@ -0,0 +1,28 @@
@@ -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 {} |
||||
@ -0,0 +1,21 @@
@@ -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 {} |
||||
Loading…
Reference in new issue