From ca7929bae59641d77cd4587f06118de019ea0bb5 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Tue, 14 Apr 2020 17:25:36 +0100 Subject: [PATCH] Add db version check --- .../backup-endpoints.service.ts | 16 ++- .../restore-endpoints.service.ts | 107 ++++++++++++++---- .../restore-endpoints.component.html | 26 +++-- .../restore-endpoints.component.scss | 9 +- .../restore-endpoints.component.ts | 54 +-------- .../store/src/reducers/auth.reducer.ts | 2 +- src/jetstream/cnsi.go | 98 +++++++++++----- 7 files changed, 197 insertions(+), 115 deletions(-) diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints.service.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints.service.ts index 9b5479d444..51b4d4ecf5 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints.service.ts +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints.service.ts @@ -19,12 +19,10 @@ import { BaseEndpointConfig, } from './backup-restore-endpoints.service'; -interface BackupEndpointConfigRequest extends BaseEndpointConfig { - // connectedUser: string; -} -interface BackupEndpointRequestData { - state: BackupEndpointsConfig; + +interface BackupRequest { + state: BackupEndpointsConfig; userId: string; password: string; } @@ -144,10 +142,10 @@ export class BackupEndpointsService extends BackupRestoreEndpointService { ); } - private createBodyToSend(sd: SessionData): BackupEndpointRequestData { - const state: BackupEndpointsConfig = Object.entries(this.state).reduce((res, [endpointId, endpoint]) => { + private createBodyToSend(sd: SessionData): BackupRequest { + const state: BackupEndpointsConfig = Object.entries(this.state).reduce((res, [endpointId, endpoint]) => { const { entity, ...rest } = endpoint; - const requestConfig: BackupEndpointConfigRequest = { + const requestConfig: BaseEndpointConfig = { ...rest, }; res[endpointId] = requestConfig; @@ -156,7 +154,7 @@ export class BackupEndpointsService extends BackupRestoreEndpointService { return { state, userId: this.getUserIdFromSessionData(sd), - password: this.password + password: this.password, }; } diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints.service.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints.service.ts index ca3daa19b9..b0b977d388 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints.service.ts +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints.service.ts @@ -1,31 +1,79 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; import { GeneralEntityAppState } from '../../../../../store/src/app-state'; +import { selectSessionData } from '../../../../../store/src/reducers/auth.reducer'; +import { SessionData } from '../../../../../store/src/types/auth.types'; +import { LoggerService } from '../../../core/logger.service'; import { BrowserStandardEncoder } from '../../../helper'; import { BackupRestoreEndpointService } from './backup-restore-endpoints.service'; +interface BackupContent { + payload: string; + dbVersion: number; +} + interface RestoreEndpointsData { data: string; password: string; + ignoreDbVersion: boolean; } @Injectable() export class RestoreEndpointsService extends BackupRestoreEndpointService { - validFile = new BehaviorSubject(false); - validFile$ = this.validFile.asObservable(); + // Step 1 + validFileContent = new BehaviorSubject(false); + validFileContent$: Observable = this.validFileContent.asObservable(); + + file = new BehaviorSubject<{ + name: string, + content: BackupContent + }>(null); + file$ = this.file.asObservable(); + + validDb = new BehaviorSubject(false); + validDb$: Observable; + currentDbVersion$: Observable; + ignoreDbVersion = new BehaviorSubject(false); + ignoreDbVersion$ = this.ignoreDbVersion.asObservable(); + + // Step 2 password: string; // TODO: RC use set password in both services - fileName: string; - private fileContent: string; constructor( private store: Store, - private http: HttpClient + private http: HttpClient, + private logger: LoggerService ) { super(); + this.setupStep1(); + } + + private setupStep1() { + this.currentDbVersion$ = this.store.select(selectSessionData()).pipe( + map((sd: SessionData) => sd.version.database_version) + ); + + this.validDb$ = combineLatest([ + this.file$, + this.currentDbVersion$ + ]).pipe( + map(([file, currentDbVersion]) => { + return file && file.content.dbVersion === currentDbVersion; + }) + ); + + this.validFileContent$ = combineLatest([ + this.file$, + this.validDb$, + this.ignoreDbVersion$ + ]).pipe( + map(([file, validDb, ignoreDb]) => !!file && (ignoreDb || validDb)) + ); } setFile(file): Promise { @@ -43,17 +91,28 @@ export class RestoreEndpointsService extends BackupRestoreEndpointService { } private setFileResult(content: string, fileName: string) { - if (!!content) { - this.validFile.next(true); - this.fileName = fileName; - this.fileContent = content; + let parsedContent: BackupContent; + try { + parsedContent = JSON.parse(content); + } catch (e) { + this.logger.warn('Failed to parse file contents: ', e); + } + + if (!!parsedContent) { + this.file.next({ + name: fileName, + content: parsedContent + }); } else { - this.validFile.next(false); - this.fileName = ''; - this.fileContent = ''; + this.file.next(null); } + + // this.updateFileContentValidation(); } + setIgnoreDbVersion(ignore: boolean) { + this.ignoreDbVersion.next(ignore); + } restoreBackup(): Observable { const url = '/pp/v1/endpoints/restore'; @@ -62,15 +121,19 @@ export class RestoreEndpointsService extends BackupRestoreEndpointService { fromObject, encoder: new BrowserStandardEncoder() }); - return this.http.post(url, this.createBodyToSend(), { - params - }); - } - createBodyToSend(): RestoreEndpointsData { - return { - data: this.fileContent, - password: this.password - }; + return combineLatest([ + this.file$, + this.ignoreDbVersion$ + ]).pipe( + switchMap(([file, ignoreDb]) => { + const body: RestoreEndpointsData = { + data: JSON.stringify(file.content), + password: this.password, + ignoreDbVersion: ignoreDb + }; + return this.http.post(url, body, { params }); + }) + ); } } diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.html b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.html index 7b9ef8f1f0..2611a539fd 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.html +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.html @@ -3,15 +3,27 @@

Restore Endpoints

- +
-

Provide the backup file to restore from.

-
- Choose - - {{service.fileName}} +
+

Provide the backup file to restore from.

+
+ Choose + + {{file.name}} +
+ +
+

The database version of Stratos ({{service.currentDbVersion$ | async}}) and the backup + ({{file.content.dbVersion}}) are different. Restoring this file may have adverse affects. +

+ + Ignore different database versions + +
+
diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.scss b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.scss index 89184f73a4..60b6ddd949 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.scss +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.scss @@ -2,8 +2,13 @@ flex: 1; } .file-step { - display: flex; - flex-direction: column; + + &, + &__chunk { + display: flex; + flex-direction: column; + } + &__input { &--input { height: 0; diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.ts index e35d5af48a..8762a498f2 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.ts @@ -1,11 +1,9 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { Store } from '@ngrx/store'; +import { MatCheckboxChange } from '@angular/material'; import { Observable, of, Subject } from 'rxjs'; import { first, map, tap } from 'rxjs/operators'; -import { AppState } from '../../../../../../store/src/app-state'; -import { PaginationMonitorFactory } from '../../../../../../store/src/monitors/pagination-monitor.factory'; import { getEventFiles } from '../../../../core/browser-helper'; import { ConfirmationDialogConfig } from '../../../../shared/components/confirmation-dialog.config'; import { ConfirmationDialogService } from '../../../../shared/components/confirmation-dialog.service'; @@ -21,11 +19,7 @@ import { RestoreEndpointsService } from '../restore-endpoints.service'; RestoreEndpointsService ] }) -export class RestoreEndpointsComponent implements OnInit { - - // Step 1 - fileValid$: Observable; - fileName: string; +export class RestoreEndpointsComponent { // Step 2 passwordValid$: Observable; @@ -33,29 +27,9 @@ export class RestoreEndpointsComponent implements OnInit { constructor( public service: RestoreEndpointsService, - store: Store, - paginationMonitorFactory: PaginationMonitorFactory, private confirmDialog: ConfirmationDialogService, ) { - - // const endpoints$ = of([]); - // this.endpointDataSource = { - // isTableLoading$: of(false), - // connect: () => endpoints$.pipe( - // map(endpoints => endpoints.sort((a, b) => a.name.localeCompare(b.name))) - // ), - // disconnect: () => { }, - // trackBy: (index, row) => row.guid - // }; - - this.setupFileStep(); - this.setupPasswordStep(); - - } - - setupFileStep() { - this.fileValid$ = this.service.validFile$; } setupPasswordStep() { @@ -70,33 +44,17 @@ export class RestoreEndpointsComponent implements OnInit { ); } - ngOnInit() { - } - onFileChange(event) { const files = getEventFiles(event); if (!files.length) { return; } const file = files[0]; - console.log(event, file); this.service.setFile(file); + } - // console.log(files); - // TODO: RC file load - get content of file - // TODO: RC file load - validate correct file - // TODO: RC file load - parse file - // TODO: RC file load - enable next step - - - // const utils = new DeployApplicationFsUtils(); - // utils.handleFileInputSelection(files).pipe( - // filter(res => !!res), - // first() - // ).subscribe((res) => { - // this.propagateChange(res); - // this.sourceData$.next(res); - // }); + onIgnoreDbChange(event: MatCheckboxChange) { + this.service.setIgnoreDbVersion(event.checked); } restore: StepOnNextFunction = () => { diff --git a/src/frontend/packages/store/src/reducers/auth.reducer.ts b/src/frontend/packages/store/src/reducers/auth.reducer.ts index 06294413ef..c8ae1c2dd7 100644 --- a/src/frontend/packages/store/src/reducers/auth.reducer.ts +++ b/src/frontend/packages/store/src/reducers/auth.reducer.ts @@ -103,5 +103,5 @@ export function authReducer(state: AuthState = defaultState, action): AuthState } export function selectSessionData() { - return (state: AuthOnlyAppState) => state.auth.sessionData; + return (state: AuthOnlyAppState): SessionData => state.auth.sessionData; } diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 779645b1ba..d32e53fa6d 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -3,6 +3,7 @@ package main import ( "crypto/x509" "encoding/json" + "errors" "fmt" "io/ioutil" "net/http" @@ -646,25 +647,34 @@ const ( ) // TODO: RC position -type BackupDataRequest struct { - State map[string]BackupEndpointsState `json:"state"` - UserID string `json:"userId"` - Password string `json:"password"` -} - type BackupEndpointsState struct { Endpoint bool `json:"endpoint"` Connect BackupConnectionType `json:"connect"` } -type BackupRestoreState struct { +// Sent to backup +type BackupRequest struct { + State map[string]BackupEndpointsState `json:"state"` + UserID string `json:"userId"` + DBVersion string `json:"dbVersion"` + Password string `json:"password"` +} + +type BackupRequestEndpointsResponse struct { Endpoints []*interfaces.CNSIRecord Tokens []interfaces.BackupTokenRecord } -type RestoreDataRequest struct { - Data string `json:"data"` - Password string `json:"password"` +type BackupContent struct { + payload BackupRequestEndpointsResponse `json:"payload"` + DBVersion int64 `json:"dbVersion"` +} + +type RestoreRequest struct { + // Data - Encrypted version of BackupContent + Data string `json:"data"` + Password string `json:"password"` + IgnoreDbVersion bool `json:"ignoreDbVersion"` } // TODO: RC split out to new file? @@ -677,7 +687,7 @@ func (p *portalProxy) backupEndpoints(c echo.Context) error { return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body") } - data := &BackupDataRequest{} + data := &BackupRequest{} if err = json.Unmarshal(body, data); err != nil { return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body - could not parse JSON") } @@ -692,6 +702,8 @@ func (p *portalProxy) backupEndpoints(c echo.Context) error { return err } + log.Infof("response: %+v", response) + // Send back the response to the client // TODO: RC Missing client_secret when serialised, `-` in definition jsonString, err := json.Marshal(response) @@ -699,16 +711,15 @@ func (p *portalProxy) backupEndpoints(c echo.Context) error { return interfaces.NewHTTPError(http.StatusInternalServerError, "Failed to serialize response") } - // Encrypt data (see above) // TODO: RC leave until last - encryptedResponse := jsonString + log.Infof("jsonString: %+v", jsonString) // Return data c.Response().Header().Set("Content-Type", "application/json") - c.Response().Write(encryptedResponse) + c.Response().Write(jsonString) return nil } -func (p *portalProxy) createBackup(data *BackupDataRequest) (*BackupRestoreState, error) { +func (p *portalProxy) createBackup(data *BackupRequest) (*BackupContent, error) { log.Debug("createBackup") allEndpoints, err := p.ListEndpoints() if err != nil { @@ -807,11 +818,28 @@ func (p *portalProxy) createBackup(data *BackupDataRequest) (*BackupRestoreState // log.Infof("userTokenFrom: %+v", userTokenFrom) log.Infof("tokens: %+v", tokens) - response := &BackupRestoreState{ + payload := &BackupRequestEndpointsResponse{ Endpoints: endpoints, Tokens: tokens, } + // Encrypt data (see above) // TODO: RC leave until last + // encryptedPayload := payload + + versions, err := p.getVersionsData() + if err != nil { + return nil, errors.New("Could not find database version") + } + + // log.Infof("payload: %+v", payload) + + response := &BackupContent{ + payload: *payload, + DBVersion: versions.DatabaseVersion, + } + + // log.Infof("response: %+v", response) + return response, nil } @@ -854,18 +882,14 @@ func (p *portalProxy) restoreEndpoints(c echo.Context) error { return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body") } - data := &RestoreDataRequest{} + data := &RestoreRequest{} if err = json.Unmarshal(body, data); err != nil { return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body - could not parse JSON") } - backup := &BackupRestoreState{} - if err = json.Unmarshal([]byte(data.Data), backup); err != nil { - return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid backup - could not parse JSON") - } - - err = p.restoreBackup(backup) + err = p.restoreBackup(data) if err != nil { + // TODO: RC write error? return err } @@ -875,14 +899,36 @@ func (p *portalProxy) restoreEndpoints(c echo.Context) error { } -func (p *portalProxy) restoreBackup(backup *BackupRestoreState) error { +func (p *portalProxy) restoreBackup(backup *RestoreRequest) error { log.Debug("restoreBackup") + + // TODO: RC check return types of these functions... if we return shadow error + + data := &BackupContent{} + if err := json.Unmarshal([]byte(backup.Data), backup); err != nil { + return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid backup - could not parse JSON") + } + + if backup.IgnoreDbVersion == false { + versions, err := p.getVersionsData() + if err != nil { + return errors.New("Could not find database version") + } + + if versions.DatabaseVersion != data.DBVersion { + errorStr := fmt.Sprintf("Incompatible database versions. Expected %+v but got %+v", versions.DatabaseVersion, data.DBVersion) + return interfaces.NewHTTPShadowError(http.StatusBadRequest, errorStr, errorStr) + } + } + + unencryptedBackup := data.payload + cnsiRepo, err := cnsis.NewPostgresCNSIRepository(p.DatabaseConnectionPool) if err != nil { return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to connect to db", "Failed to connect to db: %+v", err) } - for _, endpoint := range backup.Endpoints { + for _, endpoint := range unencryptedBackup.Endpoints { if err := cnsiRepo.Overwrite(*endpoint, p.Config.EncryptionKeyInBytes); err != nil { return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to overwrite endpoints", "Failed to overwrite endpoint: %+v", endpoint.Name) } @@ -893,7 +939,7 @@ func (p *portalProxy) restoreBackup(backup *BackupRestoreState) error { return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to connect to db", "Failed to connect to db: %+v", err) } - for _, tr := range backup.Tokens { + for _, tr := range unencryptedBackup.Tokens { if err := tokenRepo.SaveCNSIToken(tr.EndpointGUID, tr.UserGUID, tr.TokenRecord, p.Config.EncryptionKeyInBytes); err != nil { return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to overwrite token", "Failed to overwrite token: %+v", tr.TokenRecord.TokenGUID) }