diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.html b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.html index 752618ca1a..818fadc297 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.html +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.html @@ -2,7 +2,7 @@ None - Current User + Current User All Users \ No newline at end of file diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.ts index 60a5190361..528b256c22 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.ts @@ -3,7 +3,7 @@ import { Component } from '@angular/core'; import { EndpointModel } from '../../../../../../store/src/types/endpoint.types'; import { TableCellCustom } from '../../../../shared/components/list/list.types'; import { BackupEndpointsService } from '../backup-endpoints.service'; -import { BackupEndpointConnectionTypes, BackupEndpointTypes } from '../backup-restore-endpoints.service'; +import { BackupEndpointConnectionTypes, BackupEndpointTypes } from '../backup-restore.types'; @Component({ selector: 'app-backup-connection-cell', 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 51b4d4ecf5..ee1f06fc80 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 @@ -15,10 +15,8 @@ import { BackupEndpointConnectionTypes, BackupEndpointsConfig, BackupEndpointTypes, - BackupRestoreEndpointService, BaseEndpointConfig, -} from './backup-restore-endpoints.service'; - +} from './backup-restore.types'; interface BackupRequest { @@ -28,7 +26,7 @@ interface BackupRequest { } @Injectable() -export class BackupEndpointsService extends BackupRestoreEndpointService { +export class BackupEndpointsService { hasChanges = new BehaviorSubject(false); hasChanges$ = this.hasChanges.asObservable(); @@ -42,7 +40,6 @@ export class BackupEndpointsService extends BackupRestoreEndpointService { private store: Store, private http: HttpClient ) { - super(); } // State Related @@ -106,7 +103,7 @@ export class BackupEndpointsService extends BackupRestoreEndpointService { endpoint[BackupEndpointTypes.ENDPOINT] = true; } if (this.canBackup(endpoint.entity, BackupEndpointTypes.CONNECT)) { - endpoint[BackupEndpointTypes.CONNECT] = BackupEndpointConnectionTypes.CURRENT; + endpoint[BackupEndpointTypes.CONNECT] = BackupEndpointConnectionTypes.ALL; } }); this.validate(); @@ -120,6 +117,10 @@ export class BackupEndpointsService extends BackupRestoreEndpointService { this.validate(); } + hasConnectionDetails(): boolean { + return !!Object.values(this.state).find(e => e[BackupEndpointTypes.CONNECT] !== BackupEndpointConnectionTypes.NONE); + } + // Request Related createBackup(): Observable { @@ -144,11 +145,13 @@ export class BackupEndpointsService extends BackupRestoreEndpointService { private createBodyToSend(sd: SessionData): BackupRequest { const state: BackupEndpointsConfig = Object.entries(this.state).reduce((res, [endpointId, endpoint]) => { - const { entity, ...rest } = endpoint; - const requestConfig: BaseEndpointConfig = { - ...rest, - }; - res[endpointId] = requestConfig; + if (endpoint[BackupEndpointTypes.ENDPOINT]) { + const { entity, ...rest } = endpoint; + const requestConfig: BaseEndpointConfig = { + ...rest, + }; + res[endpointId] = requestConfig; + } return res; }, {}); return { diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.ts index a32fa6f617..882d6113f8 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.ts @@ -11,6 +11,7 @@ import { entityCatalog } from '../../../../../../store/src/entity-catalog/entity import { PaginationMonitorFactory } from '../../../../../../store/src/monitors/pagination-monitor.factory'; import { getPaginationObservables } from '../../../../../../store/src/reducers/pagination-reducer/pagination-reducer.helper'; import { EndpointModel } from '../../../../../../store/src/types/endpoint.types'; +import { httpErrorResponseToSafeString } from '../../../../jetstream.helpers'; import { ConfirmationDialogConfig } from '../../../../shared/components/confirmation-dialog.config'; import { ConfirmationDialogService } from '../../../../shared/components/confirmation-dialog.service'; import { ITableListDataSource } from '../../../../shared/components/list/data-sources-controllers/list-data-source-types'; @@ -19,7 +20,7 @@ import { StepOnNextFunction, StepOnNextResult } from '../../../../shared/compone import { BackupConnectionCellComponent } from '../backup-connection-cell/backup-connection-cell.component'; import { BackupEndpointsService } from '../backup-endpoints.service'; import { BackupRestoreCellComponent } from '../backup-restore-cell/backup-restore-cell.component'; -import { BackupEndpointTypes } from '../backup-restore-endpoints.service'; +import { BackupEndpointTypes } from '../backup-restore.types'; @Component({ selector: 'app-backup-endpoints', @@ -133,10 +134,9 @@ export class BackupEndpointsComponent implements OnInit { } onNext: StepOnNextFunction = () => { - // TODO: RC Complete/Finish token warning const confirmation = new ConfirmationDialogConfig( 'Backup', - 'Backing up connection details ?????????', + 'This backup contains endpoint connection details. The contents will be encrypted, but please still ensure the safety of the file', 'Continue', true ); @@ -157,13 +157,14 @@ export class BackupEndpointsComponent implements OnInit { const downloadURL = window.URL.createObjectURL(data); const link = document.createElement('a'); link.href = downloadURL; - const dateTime = moment().format('YYYYMMDD-HHmmss'); // TODO: RC timezone? + // Time of client, not server + const dateTime = moment().format('YYYYMMDD-HHmmss'); link.download = `stratos_backup_${dateTime}.bk`; link.click(); }; const backupFailure = err => { - const errorMessage = this.service.createError(err); + const errorMessage = httpErrorResponseToSafeString(err); result.next({ success: false, message: `Failed to create backup` + (errorMessage ? `: ${errorMessage}` : '') @@ -173,8 +174,11 @@ export class BackupEndpointsComponent implements OnInit { const createBackup = () => this.service.createBackup().pipe(first()).subscribe(backupSuccess, backupFailure); - // TODO: RC tie in progress indicator (not sure if possible) - this.confirmDialog.openWithCancel(confirmation, createBackup, userCancelledDialog); + if (this.service.hasConnectionDetails()) { + this.confirmDialog.openWithCancel(confirmation, createBackup, userCancelledDialog); + } else { + createBackup(); + } // TODO: RC Remove console.log return result.asObservable().pipe(tap(console.log)); diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints.service.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints.service.ts deleted file mode 100644 index abb46f3628..0000000000 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints.service.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { HttpErrorResponse } from '@angular/common/http'; - -import { EndpointModel } from '../../../../../store/src/types/endpoint.types'; -import { isHttpErrorResponse } from '../../../jetstream.helpers'; - -// TODO: RC move to types - -export enum BackupEndpointTypes { - ENDPOINT = 'endpoint', - CONNECT = 'connect', -} - -export enum BackupEndpointConnectionTypes { - NONE = 'NONE', - CURRENT = 'CURRENT', - ALL = 'ALL' -} - -export interface BackupEndpointsConfig { - [endpointId: string]: T; -} - -export interface BaseEndpointConfig { - [BackupEndpointTypes.ENDPOINT]: boolean; - [BackupEndpointTypes.CONNECT]: BackupEndpointConnectionTypes; -} - -export interface BackupEndpointConfigUI extends BaseEndpointConfig { - entity: EndpointModel; -} - -export class BackupRestoreEndpointService { - - createError(err: any): string { - // TODO: RC tidy. move generic - const httpResponse: HttpErrorResponse = isHttpErrorResponse(err); - if (httpResponse) { - if (httpResponse.error) { - if (typeof (httpResponse.error) === 'string') { - return httpResponse.error + ` (${httpResponse.status})`; - } - return httpResponse.error.error + ` (${httpResponse.status})`; - } - return JSON.stringify(httpResponse.error) + ` (${httpResponse.status})`; - } - return err.message; - } -} diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore.types.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore.types.ts new file mode 100644 index 0000000000..edda11d612 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore.types.ts @@ -0,0 +1,25 @@ +import { EndpointModel } from '../../../../../store/src/types/endpoint.types'; + +export enum BackupEndpointTypes { + ENDPOINT = 'endpoint', + CONNECT = 'connect', +} + +export enum BackupEndpointConnectionTypes { + NONE = 'NONE', + CURRENT = 'CURRENT', + ALL = 'ALL' +} + +export interface BackupEndpointsConfig { + [endpointId: string]: T; +} + +export interface BaseEndpointConfig { + [BackupEndpointTypes.ENDPOINT]: boolean; + [BackupEndpointTypes.CONNECT]: BackupEndpointConnectionTypes; +} + +export interface BackupEndpointConfigUI extends BaseEndpointConfig { + entity: EndpointModel; +} 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 b0b977d388..50d678f33d 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 @@ -9,7 +9,6 @@ import { selectSessionData } from '../../../../../store/src/reducers/auth.reduce 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; @@ -23,7 +22,7 @@ interface RestoreEndpointsData { } @Injectable() -export class RestoreEndpointsService extends BackupRestoreEndpointService { +export class RestoreEndpointsService { // Step 1 validFileContent = new BehaviorSubject(false); @@ -42,14 +41,13 @@ export class RestoreEndpointsService extends BackupRestoreEndpointService { ignoreDbVersion$ = this.ignoreDbVersion.asObservable(); // Step 2 - password: string; // TODO: RC use set password in both services + private password: string; constructor( private store: Store, private http: HttpClient, private logger: LoggerService ) { - super(); this.setupStep1(); } @@ -114,6 +112,10 @@ export class RestoreEndpointsService extends BackupRestoreEndpointService { this.ignoreDbVersion.next(ignore); } + setPassword(password: string) { + this.password = password; + } + restoreBackup(): Observable { const url = '/pp/v1/endpoints/restore'; const fromObject = {}; 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 2611a539fd..c99ad6600f 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 @@ -31,7 +31,12 @@

Restore Endpoints

Provide the password that was given at the time the backup was created

- + Password + +
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 8762a498f2..bfd419aad2 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 @@ -5,6 +5,7 @@ import { Observable, of, Subject } from 'rxjs'; import { first, map, tap } from 'rxjs/operators'; import { getEventFiles } from '../../../../core/browser-helper'; +import { httpErrorResponseToSafeString } from '../../../../jetstream.helpers'; import { ConfirmationDialogConfig } from '../../../../shared/components/confirmation-dialog.config'; import { ConfirmationDialogService } from '../../../../shared/components/confirmation-dialog.service'; import { StepOnNextFunction, StepOnNextResult } from '../../../../shared/components/stepper/step/step.component'; @@ -24,6 +25,7 @@ export class RestoreEndpointsComponent { // Step 2 passwordValid$: Observable; passwordForm: FormGroup; + show = false; constructor( public service: RestoreEndpointsService, @@ -38,7 +40,7 @@ export class RestoreEndpointsComponent { }); this.passwordValid$ = this.passwordForm.statusChanges.pipe( map(() => { - this.service.password = this.passwordForm.controls.password.value; + this.service.setPassword(this.passwordForm.controls.password.value); return this.passwordForm.valid; }) ); @@ -80,18 +82,16 @@ export class RestoreEndpointsComponent { }; const backupFailure = err => { - const errorMessage = this.service.createError(err); + const errorMessage = httpErrorResponseToSafeString(err); result.next({ success: false, message: `Failed to restore backup` + (errorMessage ? `: ${errorMessage}` : '') }); return of(false); }; - // TODO: RC make generic in base const createBackup = () => this.service.restoreBackup().pipe(first()).subscribe(restoreSuccess, backupFailure); - // TODO: RC tie in progress indicator (not sure if possible) this.confirmDialog.openWithCancel(confirmation, createBackup, userCancelledDialog); // TODO: RC Remove console.log diff --git a/src/frontend/packages/core/src/jetstream.helpers.ts b/src/frontend/packages/core/src/jetstream.helpers.ts index 36bbac53ea..1a29978099 100644 --- a/src/frontend/packages/core/src/jetstream.helpers.ts +++ b/src/frontend/packages/core/src/jetstream.helpers.ts @@ -53,3 +53,20 @@ export function isHttpErrorResponse(obj: any): HttpErrorResponse { ) ? obj as HttpErrorResponse : null; } +/** + * Attempt to create a sensible string explaining the error object returned from a failed http request + * @param err The raw error from a http request + */ +export function httpErrorResponseToSafeString(err: any): string { + const httpResponse: HttpErrorResponse = isHttpErrorResponse(err); + if (httpResponse) { + if (httpResponse.error) { + if (typeof (httpResponse.error) === 'string') { + return httpResponse.error + ` (${httpResponse.status})`; + } + return httpResponse.error.error + ` (${httpResponse.status})`; + } + return JSON.stringify(httpResponse.error) + ` (${httpResponse.status})`; + } + return err.message; +} diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index d32e53fa6d..f5c6c501a3 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -3,9 +3,7 @@ package main import ( "crypto/x509" "encoding/json" - "errors" "fmt" - "io/ioutil" "net/http" "net/url" "strconv" @@ -638,340 +636,24 @@ func (p *portalProxy) updateEndpoint(c echo.Context) error { return nil } -type BackupConnectionType string - -const ( - BACKUP_CONNECTION_NONE BackupConnectionType = "NONE" - BACKUP_CONNECTION_CURRENT = "CURRENT" - BACKUP_CONNECTION_ALL = "ALL" -) - -// TODO: RC position -type BackupEndpointsState struct { - Endpoint bool `json:"endpoint"` - Connect BackupConnectionType `json:"connect"` -} - -// 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 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? func (p *portalProxy) backupEndpoints(c echo.Context) error { - log.Debug("backupEndpoints") - - // Check we can unmarshall the request - body, err := ioutil.ReadAll(c.Request().Body) - if err != nil { - return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body") - } - - data := &BackupRequest{} - if err = json.Unmarshal(body, data); err != nil { - return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body - could not parse JSON") - } - // log.Infof("BODY: %+v", data) - - if data.State == nil || len(data.State) == 0 { - return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body - no endpoints to backup") - } - - response, err := p.createBackup(data) - if err != nil { - 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) - if err != nil { - return interfaces.NewHTTPError(http.StatusInternalServerError, "Failed to serialize response") - } - - log.Infof("jsonString: %+v", jsonString) - - // Return data - c.Response().Header().Set("Content-Type", "application/json") - c.Response().Write(jsonString) - return nil -} - -func (p *portalProxy) createBackup(data *BackupRequest) (*BackupContent, error) { - log.Debug("createBackup") - allEndpoints, err := p.ListEndpoints() - if err != nil { - return nil, interfaces.NewHTTPError(http.StatusBadGateway, "Failed to fetch endpoints") - } - - // Fetch/Format required data - endpoints := make([]*interfaces.CNSIRecord, 0) - // allTokensFrom := make([]string, 0) - // userTokenFrom := make([]string, 0) - tokens := make([]interfaces.BackupTokenRecord, 0) - - for endpointID, endpoint := range data.State { - - if !endpoint.Endpoint { - continue - } - - for _, e := range allEndpoints { - if endpointID == e.GUID { - endpoints = append(endpoints, e) - break - } - } - - switch connectionType := endpoint.Connect; connectionType { - case BACKUP_CONNECTION_ALL: - // allTokensFrom = append(allTokensFrom, endpointID) - if tokenRecords, ok := p.getCNSITokenRecordsBackup(endpointID); ok { - log.Warn("tokens for AllConnect") - tokens = append(tokens, tokenRecords...) - } else { - log.Warn("No tokens for AllConnect") - // TODO: RC - } - case BACKUP_CONNECTION_CURRENT: - // userTokenFrom = append(userTokenFrom, endpointID) - if tokenRecord, ok := p.GetCNSITokenRecordWithDisconnected(endpointID, data.UserID); ok { - log.Warn("tokens for Connect") - // var btr BackupTokenRecord - // TODO: RC Q This will be the linked token as if it were the users token - var btr = interfaces.BackupTokenRecord{ - // tokenRecord: tokenRecord, - TokenRecord: tokenRecord, - EndpointGUID: endpointID, - TokenType: "CNSI", - UserGUID: data.UserID, - } - - tokens = append(tokens, btr) - } else { - log.Warnf("No tokens for Connect: %+v,%+v", endpointID, data.UserID) - // TODO: RC - // msg := "Unable to retrieve CNSI token record." - // log.Debug(msg) - // return nil, nil, false - } - } - - // if endpoint.Connect == BACKUP_CONNECTION_ALL { - // // allTokensFrom = append(allTokensFrom, endpointID) - // if tokenRecords, ok := p.getCNSITokenRecordsBackup(endpointID); ok { - // log.Warn("tokens for AllConnect") - // tokens = append(tokens, tokenRecords...) - // } else { - // log.Warn("No tokens for AllConnect") - // // TODO: RC - // } - // } else if endpoint.Connect { - // // userTokenFrom = append(userTokenFrom, endpointID) - // if tokenRecord, ok := p.GetCNSITokenRecordWithDisconnected(endpointID, data.UserID); ok { - // log.Warn("tokens for Connect") - // // var btr BackupTokenRecord - // // TODO: RC Q This will be the linked token as if it were the users token - // var btr = interfaces.BackupTokenRecord{ - // // tokenRecord: tokenRecord, - // TokenRecord: tokenRecord, - // EndpointGUID: endpointID, - // TokenType: "CNSI", - // UserGUID: data.UserID, - // } - - // tokens = append(tokens, btr) - // } else { - // log.Warnf("No tokens for Connect: %+v,%+v", endpointID, data.UserID) - // // TODO: RC - // // msg := "Unable to retrieve CNSI token record." - // // log.Debug(msg) - // // return nil, nil, false - // } - // } - } - - log.Infof("endpoints: %+v", endpoints) - // log.Infof("allTokensFrom: %+v", allTokensFrom) - // log.Infof("userTokenFrom: %+v", userTokenFrom) - log.Infof("tokens: %+v", tokens) - - payload := &BackupRequestEndpointsResponse{ - Endpoints: endpoints, - Tokens: tokens, - } + log.Debug("BackupEndpoints") - // 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 -} - -// func (p *portalProxy) GetCNSITokens(cnsiGUID string) ([]interfaces.TokenRecord, bool) { -// log.Debug("GetCNSITokens") -// tokenRepo, err := tokens.NewPgsqlTokenRepository(p.DatabaseConnectionPool) -// if err != nil { -// return make([]interfaces.TokenRecord, 0), false -// } - -// trs, err := tokenRepo.FindAllCNSITokenIncludeDisconnected(cnsiGUID, p.Config.EncryptionKeyInBytes) -// if err != nil { -// return make([]interfaces.TokenRecord, 0), false -// } - -// return trs, true -// } - -func (p *portalProxy) getCNSITokenRecordsBackup(endpointID string) ([]interfaces.BackupTokenRecord, bool) { - log.Debug("getCNSITokenRecordsBackup") - tokenRepo, err := tokens.NewPgsqlTokenRepository(p.DatabaseConnectionPool) - if err != nil { - return make([]interfaces.BackupTokenRecord, 0), false - } - - trs, err := tokenRepo.FindAllCNSITokenBackup(endpointID, p.Config.EncryptionKeyInBytes) - if err != nil { - return make([]interfaces.BackupTokenRecord, 0), false + ctb := &cnsiTokenBackup{ + databaseConnectionPool: p.DatabaseConnectionPool, + p: p, } - return trs, true + return ctb.BackupEndpoints(c) } func (p *portalProxy) restoreEndpoints(c echo.Context) error { log.Debug("restoreEndpoints") - // Check we can unmarshall the request - body, err := ioutil.ReadAll(c.Request().Body) - if err != nil { - return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body") - } - - data := &RestoreRequest{} - if err = json.Unmarshal(body, data); err != nil { - return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body - could not parse JSON") - } - - err = p.restoreBackup(data) - if err != nil { - // TODO: RC write error? - return err - } - - // log.Warnf("BACKUP DATA: %+v", backup) - c.Response().WriteHeader(http.StatusOK) - return nil - -} - -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 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) - } - } - - tokenRepo, err := tokens.NewPgsqlTokenRepository(p.DatabaseConnectionPool) - if err != nil { - return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to connect to db", "Failed to connect to db: %+v", err) + ctb := &cnsiTokenBackup{ + databaseConnectionPool: p.DatabaseConnectionPool, + p: p, } - 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) - } - } - - return nil + return ctb.RestoreEndpoints(c) } - -// find := func(a interfaces.CNSIRecord) bool { -// return endpointID == a.GUID -// } - -// endpointPos := sliceContainsFn(find, allEndpoints) -// if endpointPos >= 0 { -// endpoints = append(endpoints, endpoints[endpointPos]) -// } - -// // TODO:RC pos -// func sliceContains(what interface{}, where []interface{}) (idx int) { -// for i, v := range where { -// if v == what { -// return i -// } -// } -// return -1 -// } - -// func sliceContainsFn(is func(a interface{}) bool, where []interface{}) (idx int) { -// for i, v := range where { -// if is(v) { -// return i -// } -// } -// return -1 -// } diff --git a/src/jetstream/cnsi_token_backup.go b/src/jetstream/cnsi_token_backup.go new file mode 100644 index 0000000000..8e23318e48 --- /dev/null +++ b/src/jetstream/cnsi_token_backup.go @@ -0,0 +1,303 @@ +package main + +import ( + "database/sql" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/cnsis" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/tokens" + "github.com/labstack/echo" + log "github.com/sirupsen/logrus" +) + +type cnsiTokenBackup struct { + databaseConnectionPool *sql.DB + p *portalProxy +} + +// BackupConnectionType - Determine what kind of connection details are stored for an endpoint +type BackupConnectionType string + +const ( + BACKUP_CONNECTION_NONE BackupConnectionType = "NONE" + BACKUP_CONNECTION_CURRENT = "CURRENT" + BACKUP_CONNECTION_ALL = "ALL" +) + +// BackupEndpointsState - For a given endpoint define what's backed up +type BackupEndpointsState struct { + Endpoint bool `json:"endpoint"` + Connect BackupConnectionType `json:"connect"` +} + +// BackupRequest - Request from client to create a back up file +type BackupRequest struct { + State map[string]BackupEndpointsState `json:"state"` + UserID string `json:"userId"` + DBVersion string `json:"dbVersion"` + Password string `json:"password"` +} + +// BackupContentPayload - Encrypted part of the backup +type BackupContentPayload struct { + Endpoints []*interfaces.CNSIRecord + Tokens []interfaces.BackupTokenRecord +} + +// BackupContent - Everything that's backed up and stored in a file client side +type BackupContent struct { + Payload BackupContentPayload `json:"payload"` + DBVersion int64 `json:"dbVersion"` +} + +// RestoreRequest - Request from client to restore content from payload +type RestoreRequest struct { + // Payload - Encrypted version of BackupContent + Payload string `json:"data"` + Password string `json:"password"` + IgnoreDbVersion bool `json:"ignoreDbVersion"` +} + +func (ctb *cnsiTokenBackup) BackupEndpoints(c echo.Context) error { + log.Debug("BackupEndpoints") + + // Check we can unmarshall the request + body, err := ioutil.ReadAll(c.Request().Body) + if err != nil { + return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body") + } + + data := &BackupRequest{} + if err = json.Unmarshal(body, data); err != nil { + return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body - could not parse JSON") + } + + if data.State == nil || len(data.State) == 0 { + return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body - no endpoints to backup") + } + + response, err := ctb.createBackup(data) + if err != nil { + return err + } + + log.Infof("response: %+v", response) // TODO: RC REMOVE + + // Send back the response to the client + // TODO: RC Missing client_secret when serialised, `-` in definition + jsonString, err := json.Marshal(response) + if err != nil { + return interfaces.NewHTTPError(http.StatusInternalServerError, "Failed to serialize response") + } + + log.Infof("jsonString: %+v", jsonString) // TODO: RC REMOVE + + // Return data + c.Response().Header().Set("Content-Type", "application/json") + c.Response().Write(jsonString) + return nil +} + +func (ctb *cnsiTokenBackup) createBackup(data *BackupRequest) (*BackupContent, error) { + log.Debug("createBackup") + allEndpoints, err := ctb.p.ListEndpoints() + if err != nil { + return nil, interfaces.NewHTTPError(http.StatusBadGateway, "Failed to fetch endpoints") + } + + // Fetch/Format required data + endpoints := make([]*interfaces.CNSIRecord, 0) + tokens := make([]interfaces.BackupTokenRecord, 0) + + for endpointID, endpoint := range data.State { + + if !endpoint.Endpoint { + continue + } + + for _, e := range allEndpoints { + if endpointID == e.GUID { + endpoints = append(endpoints, e) + break + } + } + + switch connectionType := endpoint.Connect; connectionType { + case BACKUP_CONNECTION_ALL: + // allTokensFrom = append(allTokensFrom, endpointID) + if tokenRecords, ok := ctb.getCNSITokenRecordsBackup(endpointID); ok { + log.Warn("tokens for AllConnect") // TODO: RC REMOVE + tokens = append(tokens, tokenRecords...) + } else { + log.Warn("No tokens for AllConnect") // TODO: RC REMOVE + } + case BACKUP_CONNECTION_CURRENT: + // userTokenFrom = append(userTokenFrom, endpointID) + if tokenRecord, ok := ctb.p.GetCNSITokenRecordWithDisconnected(endpointID, data.UserID); ok { + log.Warn("tokens for Connect") + // TODO: RC Q This will be the linked token as if it were the users token + var btr = interfaces.BackupTokenRecord{ + // tokenRecord: tokenRecord, + TokenRecord: tokenRecord, + EndpointGUID: endpointID, + TokenType: "CNSI", + UserGUID: data.UserID, + } + + tokens = append(tokens, btr) + } else { + log.Infof("Request to back up connected user's (%+v) token for endpoint (%+v) failed. No token for user.", endpointID, data.UserID) + } + } + + } + + log.Infof("endpoints: %+v", endpoints) // TODO: RC REMOVE + log.Infof("tokens: %+v", tokens) // TODO: RC REMOVE + + payload := &BackupContentPayload{ + Endpoints: endpoints, + Tokens: tokens, + } + + // Encrypt data (see above) // TODO: RC leave until last + // encryptedPayload := payload + + versions, err := ctb.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 +} + +func (ctb *cnsiTokenBackup) getCNSITokenRecordsBackup(endpointID string) ([]interfaces.BackupTokenRecord, bool) { + log.Debug("getCNSITokenRecordsBackup") + tokenRepo, err := tokens.NewPgsqlTokenRepository(ctb.databaseConnectionPool) + if err != nil { + return make([]interfaces.BackupTokenRecord, 0), false + } + + trs, err := tokenRepo.FindAllCNSITokenBackup(endpointID, ctb.p.Config.EncryptionKeyInBytes) + if err != nil { + return make([]interfaces.BackupTokenRecord, 0), false + } + + return trs, true +} + +func (ctb *cnsiTokenBackup) RestoreEndpoints(c echo.Context) error { + log.Debug("RestoreEndpoints") + + // Check we can unmarshall the request + body, err := ioutil.ReadAll(c.Request().Body) + if err != nil { + return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body") + } + + data := &RestoreRequest{} + if err = json.Unmarshal(body, data); err != nil { + return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body - could not parse JSON") + } + + err = ctb.restoreBackup(data) + if err != nil { + return err + } + + // log.Warnf("BACKUP DATA: %+v", backup) // TODO: RC REMOVE + c.Response().WriteHeader(http.StatusOK) + return nil + +} + +func (ctb *cnsiTokenBackup) restoreBackup(backup *RestoreRequest) error { + log.Debug("restoreBackup") + + // TODO: RC Q all errors are NewHTTPError + + data := &BackupContent{} + if err := json.Unmarshal([]byte(backup.Payload), backup); err != nil { + return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid backup - could not parse JSON") + } + + if backup.IgnoreDbVersion == false { + versions, err := ctb.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(ctb.databaseConnectionPool) + if err != nil { + return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to connect to db", "Failed to connect to db: %+v", err) + } + + for _, endpoint := range unencryptedBackup.Endpoints { + if err := cnsiRepo.Overwrite(*endpoint, ctb.p.Config.EncryptionKeyInBytes); err != nil { + return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to overwrite endpoints", "Failed to overwrite endpoint: %+v", endpoint.Name) + } + } + + tokenRepo, err := tokens.NewPgsqlTokenRepository(ctb.databaseConnectionPool) + if err != nil { + return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to connect to db", "Failed to connect to db: %+v", err) + } + + for _, tr := range unencryptedBackup.Tokens { + if err := tokenRepo.SaveCNSIToken(tr.EndpointGUID, tr.UserGUID, tr.TokenRecord, ctb.p.Config.EncryptionKeyInBytes); err != nil { + return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to overwrite token", "Failed to overwrite token: %+v", tr.TokenRecord.TokenGUID) + } + } + + return nil +} + +// find := func(a interfaces.CNSIRecord) bool { +// return endpointID == a.GUID +// } + +// endpointPos := sliceContainsFn(find, allEndpoints) +// if endpointPos >= 0 { +// endpoints = append(endpoints, endpoints[endpointPos]) +// } + +// // TODO:RC pos +// func sliceContains(what interface{}, where []interface{}) (idx int) { +// for i, v := range where { +// if v == what { +// return i +// } +// } +// return -1 +// } + +// func sliceContainsFn(is func(a interface{}) bool, where []interface{}) (idx int) { +// for i, v := range where { +// if is(v) { +// return i +// } +// } +// return -1 +// } diff --git a/src/jetstream/repository/cnsis/pgsql_cnsis.go b/src/jetstream/repository/cnsis/pgsql_cnsis.go index 6ba5344004..9582f3e1a4 100644 --- a/src/jetstream/repository/cnsis/pgsql_cnsis.go +++ b/src/jetstream/repository/cnsis/pgsql_cnsis.go @@ -363,21 +363,10 @@ func (p *PostgresCNSIRepository) Overwrite(endpoint interfaces.CNSIRecord, encry log.Errorf("Unknown error attempting to find CNSI: %v", err) } - // if _, err := p.Find(endpoint.GUID, encryptionKey); err != nil { - // // Found, so update endpoint - // // TODO: RC ALL STRINGS? - // return p.Update(endpoint, encryptionKey) - // } else { - // // Not Found, create endpoint - // return p.Save(endpoint.GUID, endpoint, encryptionKey) - // // TODO: RC Q could actually be error - // } - switch count { case 0: return p.Save(endpoint.GUID, endpoint, encryptionKey) default: - return p.Update(endpoint, encryptionKey) } } diff --git a/src/jetstream/repository/tokens/pgsql_tokens.go b/src/jetstream/repository/tokens/pgsql_tokens.go index ddb7dfc3f3..427541747c 100644 --- a/src/jetstream/repository/tokens/pgsql_tokens.go +++ b/src/jetstream/repository/tokens/pgsql_tokens.go @@ -43,15 +43,6 @@ var findCNSITokenConnected = `SELECT token_guid, auth_token, refresh_token, toke FROM tokens WHERE cnsi_guid = $1 AND (user_guid = $2 OR user_guid = $3) AND token_type = 'cnsi' AND disconnected = '0'` -// TODO: RC -var findAllCNSIToken2 = `SELECT token_guid, auth_token, refresh_token, token_expiry, disconnected, auth_type, meta_data, user_guid, linked_token - FROM tokens - WHERE cnsi_guid = $1 AND token_type = 'cnsi'` - -var findAllCNSITokenConnected = `SELECT token_guid, auth_token, refresh_token, token_expiry, disconnected, auth_type, meta_data, user_guid, linked_token - FROM tokens - WHERE cnsi_guid = $1 AND token_type = 'cnsi' AND disconnected = '0'` - var findAllCNSIToken = `SELECT user_guid, token_guid, auth_token, refresh_token, token_expiry, disconnected, auth_type, meta_data, user_guid, linked_token FROM tokens WHERE cnsi_guid = $1 AND token_type = 'cnsi'` @@ -95,15 +86,12 @@ func InitRepositoryProvider(databaseProvider string) { updateAuthToken = datastore.ModifySQLStatement(updateAuthToken, databaseProvider) findCNSIToken = datastore.ModifySQLStatement(findCNSIToken, databaseProvider) findCNSITokenConnected = datastore.ModifySQLStatement(findCNSITokenConnected, databaseProvider) - findAllCNSIToken2 = datastore.ModifySQLStatement(findAllCNSIToken2, databaseProvider) - findAllCNSITokenConnected = datastore.ModifySQLStatement(findAllCNSITokenConnected, databaseProvider) countCNSITokens = datastore.ModifySQLStatement(countCNSITokens, databaseProvider) insertCNSIToken = datastore.ModifySQLStatement(insertCNSIToken, databaseProvider) updateCNSIToken = datastore.ModifySQLStatement(updateCNSIToken, databaseProvider) deleteCNSIToken = datastore.ModifySQLStatement(deleteCNSIToken, databaseProvider) deleteCNSITokens = datastore.ModifySQLStatement(deleteCNSITokens, databaseProvider) updateToken = datastore.ModifySQLStatement(updateToken, databaseProvider) - findAllCNSIToken = datastore.ModifySQLStatement(findAllCNSIToken, databaseProvider) } // saveAuthToken - Save the Auth token to the datastore @@ -329,129 +317,6 @@ func (p *PgsqlTokenRepository) SaveCNSIToken(cnsiGUID string, userGUID string, t return nil } -// TODO: RC remove all `all` -func (p *PgsqlTokenRepository) FindAllCNSIToken(cnsiGUID string, encryptionKey []byte) ([]interfaces.TokenRecord, error) { - log.Debug("FindAllCNSIToken") - return p.findAllCNSIToken(cnsiGUID, encryptionKey, false) -} - -func (p *PgsqlTokenRepository) FindAllCNSITokenIncludeDisconnected(cnsiGUID string, encryptionKey []byte) ([]interfaces.TokenRecord, error) { - log.Debug("FindAllCNSITokenIncludeDisconnected") - return p.findAllCNSIToken(cnsiGUID, encryptionKey, true) -} - -func (p *PgsqlTokenRepository) findAllCNSIToken(cnsiGUID string, encryptionKey []byte, includeDisconnected bool) ([]interfaces.TokenRecord, error) { - log.Debug("findAllCNSIToken") - if cnsiGUID == "" { - msg := "Unable to find CNSI Token without a valid CNSI GUID." - log.Debug(msg) - return make([]interfaces.TokenRecord, 0), errors.New(msg) - } - - var rows *sql.Rows - var err error - if includeDisconnected { - rows, err = p.db.Query(findAllCNSIToken, cnsiGUID) - } else { - rows, err = p.db.Query(findAllCNSITokenConnected, cnsiGUID) - } - if err != nil { - msg := "Unable to Find All CNSI tokens: %v" - if err == sql.ErrNoRows { - log.Debugf(msg, err) - } else { - log.Errorf(msg, err) - } - return make([]interfaces.TokenRecord, 0), fmt.Errorf(msg, err) - } - - // TODO: RC Q should this close come before returning? it doesn't in cnsi List(encryptionKey []byte) ([]*interfaces.CNSIRecord, error) { - defer rows.Close() - - trs := make([]interfaces.TokenRecord, 0) - for rows.Next() { - // temp vars to retrieve db data - var ( - tokenGUID sql.NullString - ciphertextAuthToken []byte - ciphertextRefreshToken []byte - tokenExpiry sql.NullInt64 - disconnected bool - authType string - metadata sql.NullString - tokenUserGUID sql.NullString - linkedTokenGUID sql.NullString - ) - err = rows.Scan(&tokenGUID, &ciphertextAuthToken, &ciphertextRefreshToken, &tokenExpiry, &disconnected, &authType, &metadata, &tokenUserGUID, &linkedTokenGUID) - if err != nil { - return nil, fmt.Errorf("Unable to scan CNSI records: %v", err) - } - - log.Debug("Decrypting Auth Token") - plaintextAuthToken, err := crypto.DecryptToken(encryptionKey, ciphertextAuthToken) - if err != nil { - return make([]interfaces.TokenRecord, 0), err - } - - log.Debug("Decrypting Refresh Token") - plaintextRefreshToken, err := crypto.DecryptToken(encryptionKey, ciphertextRefreshToken) - if err != nil { - return make([]interfaces.TokenRecord, 0), err - } - - // Build a new TokenRecord based on the decrypted tokens - tr := new(interfaces.TokenRecord) - if tokenGUID.Valid { - tr.TokenGUID = tokenGUID.String - } - tr.AuthToken = plaintextAuthToken - tr.RefreshToken = plaintextRefreshToken - if tokenExpiry.Valid { - tr.TokenExpiry = tokenExpiry.Int64 - } - tr.Disconnected = disconnected - tr.AuthType = authType - if metadata.Valid { - tr.Metadata = metadata.String - } - if tokenUserGUID.Valid { - tr.SystemShared = tokenUserGUID.String == SystemSharedUserGuid - } - if linkedTokenGUID.Valid { - tr.LinkedGUID = linkedTokenGUID.String - } - - trs = append(trs, *tr) - - } - - // TODO: RC merge with find single - // TODO: RC Use? - // // If this token is linked - fetch that token and use it instead - // // Currently we don't recurse - we only support one level of linked token - you can't link to another linked token - // if linkedTokenGUID.Valid { - // if includeDisconnected { - // err = p.db.QueryRow(getToken, userGUID, linkedTokenGUID.String).Scan(&tokenGUID, &ciphertextAuthToken, &ciphertextRefreshToken, &tokenExpiry, &disconnected, &authType, &metadata, &tokenUserGUID, &linkedTokenGUID) - // } else { - // err = p.db.QueryRow(getTokenConnected, userGUID, linkedTokenGUID.String).Scan(&tokenGUID, &ciphertextAuthToken, &ciphertextRefreshToken, &tokenExpiry, &disconnected, &authType, &metadata, &tokenUserGUID, &linkedTokenGUID) - // } - - // if err != nil { - // msg := "Unable to Find CNSI token: %v" - // if err == sql.ErrNoRows { - // log.Debugf(msg, err) - // } else { - // log.Errorf(msg, err) - // } - // return interfaces.TokenRecord{}, fmt.Errorf(msg, err) - // } - // } - - // TODO: RC Finish off - - return trs, nil -} - func (p *PgsqlTokenRepository) FindCNSIToken(cnsiGUID string, userGUID string, encryptionKey []byte) (interfaces.TokenRecord, error) { log.Debug("FindCNSIToken") return p.findCNSIToken(cnsiGUID, userGUID, encryptionKey, false) @@ -775,17 +640,3 @@ func (p *PgsqlTokenRepository) UpdateTokenAuth(userGUID string, tr interfaces.To return nil } - -// func (p *PgsqlTokenRepository) Overwrite(tr interfaces.TokenRecord, encryptionKey []byte) error { -// log.Debug("Overwrite Token") - -// if _, err := p.FindCNSITokenIncludeDisconnected(endpointGuid, userGuid, encryptionKey); err != nil { -// // Found, so update endpoint -// // TODO: RC ALL STRINGS? -// return p. Update(endpoint, encryptionKey) -// } else { -// // Not Found, create endpoint -// return p.SaveCNSIToken(endpointGuid, userGuid, tr, encryptionKey) -// // TODO: RC Q could actually be error -// } -// } diff --git a/src/jetstream/repository/tokens/tokens.go b/src/jetstream/repository/tokens/tokens.go index 354baa58b6..f473885e15 100644 --- a/src/jetstream/repository/tokens/tokens.go +++ b/src/jetstream/repository/tokens/tokens.go @@ -18,8 +18,6 @@ type Repository interface { FindCNSIToken(cnsiGUID string, userGUID string, encryptionKey []byte) (interfaces.TokenRecord, error) FindCNSITokenIncludeDisconnected(cnsiGUID string, userGUID string, encryptionKey []byte) (interfaces.TokenRecord, error) - FindAllCNSIToken(cnsiGUID string, encryptionKey []byte) ([]interfaces.TokenRecord, error) - FindAllCNSITokenIncludeDisconnected(cnsiGUID string, encryptionKey []byte) ([]interfaces.TokenRecord, error) FindAllCNSITokenBackup(cnsiGUID string, encryptionKey []byte) ([]interfaces.BackupTokenRecord, error) DeleteCNSIToken(cnsiGUID string, userGUID string) error DeleteCNSITokens(cnsiGUID string) error