From 8051743f1da58d052ed0e9af16db7489f56c85bc Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Mon, 6 Apr 2020 17:33:14 +0100 Subject: [PATCH 01/12] Add initial UI for backup/restore --- .../shared/components/components.module.ts | 6 +- .../backup-endpoints.service.ts | 198 ++++++++++++++ .../backup-endpoints.component.html | 32 +++ .../backup-endpoints.component.scss | 15 + .../backup-endpoints.component.spec.ts | 25 ++ .../backup-endpoints.component.ts | 184 +++++++++++++ .../backup-restore-cell.component.html | 2 + .../backup-restore-cell.component.scss | 0 .../backup-restore-cell.component.spec.ts | 25 ++ .../backup-restore-cell.component.ts | 25 ++ .../backup-restore-endpoints.service.ts | 213 +++++++++++++++ .../backup-restore-endpoints.component.html | 13 + .../backup-restore-endpoints.component.scss | 3 + ...backup-restore-endpoints.component.spec.ts | 25 ++ .../backup-restore-endpoints.component.ts | 48 ++++ .../restore-endpoints.service.ts | 76 ++++++ .../restore-endpoints.component.html | 27 ++ .../restore-endpoints.component.scss | 18 ++ .../restore-endpoints.component.spec.ts | 25 ++ .../restore-endpoints.component.ts | 176 ++++++++++++ .../features/endpoints/endpoints.module.ts | 17 +- .../features/endpoints/endpoints.routing.ts | 19 +- .../table-cell/table-cell.component.ts | 1 + .../endpoint/endpoints-list-config.service.ts | 25 +- .../store/src/actions/endpoint.actions.ts | 2 +- src/jetstream/cnsi.go | 257 +++++++++++++++++- src/jetstream/main.go | 4 + src/jetstream/repository/cnsis/cnsis.go | 1 + src/jetstream/repository/cnsis/pgsql_cnsis.go | 32 +++ .../repository/interfaces/portal_proxy.go | 2 +- .../repository/interfaces/structs.go | 22 +- .../repository/tokens/pgsql_tokens.go | 244 +++++++++++++++++ src/jetstream/repository/tokens/tokens.go | 3 + src/jetstream/stringutils/utils.go | 2 +- 34 files changed, 1744 insertions(+), 23 deletions(-) create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints.service.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.scss create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.spec.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.html create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.scss create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.spec.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints.service.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.html create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.scss create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.spec.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints.service.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.html create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.scss create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.spec.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.ts diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/components.module.ts b/src/frontend/packages/cloud-foundry/src/shared/components/components.module.ts index 197aca9173..0eab5d7bd5 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/components.module.ts +++ b/src/frontend/packages/cloud-foundry/src/shared/components/components.module.ts @@ -100,6 +100,9 @@ import { import { TableCellEventTypeComponent, } from './list/list-types/cf-events/table-cell-event-type/table-cell-event-type.component'; +import { + TableCellFeatureFlagDescriptionComponent, +} from './list/list-types/cf-feature-flags/table-cell-feature-flag-description/table-cell-feature-flag-description.component'; import { TableCellFeatureFlagStateComponent, } from './list/list-types/cf-feature-flags/table-cell-feature-flag-state/table-cell-feature-flag-state.component'; @@ -175,7 +178,6 @@ import { SelectServiceComponent } from './select-service/select-service.componen import { ServiceIconComponent } from './service-icon/service-icon.component'; import { ServicePlanPriceComponent } from './service-plan-price/service-plan-price.component'; import { ServicePlanPublicComponent } from './service-plan-public/service-plan-public.component'; -import { TableCellFeatureFlagDescriptionComponent } from './list/list-types/cf-feature-flags/table-cell-feature-flag-description/table-cell-feature-flag-description.component'; // tslint:disable:max-line-length // tslint:enable:max-line-length @@ -219,7 +221,7 @@ const cfListTableCells: Type>[] = [ TableCellServiceBindableComponent, TableCellServiceActiveComponent, TableCellServiceReferencesComponent, - TableCellServiceInstanceTagsComponent + TableCellServiceInstanceTagsComponent, ]; const cfListCards: Type>[] = [ 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 new file mode 100644 index 0000000000..80cc1c5f43 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints.service.ts @@ -0,0 +1,198 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { filter, first, map, switchMap } from 'rxjs/operators'; + +import { GeneralEntityAppState } from '../../../../../store/src/app-state'; +import { entityCatalog } from '../../../../../store/src/entity-catalog/entity-catalog.service'; +import { AuthState } from '../../../../../store/src/reducers/auth.reducer'; +import { SessionData } from '../../../../../store/src/types/auth.types'; +import { EndpointModel } from '../../../../../store/src/types/endpoint.types'; +import { BrowserStandardEncoder } from '../../../helper'; +import { + BackupEndpointConfigUI, + BackupEndpointsConfig, + BackupEndpointTypes, + BackupRestoreEndpointService, + BaseEndpointConfig, +} from './backup-restore-endpoints.service'; + +interface BackupEndpointConfigRequest extends BaseEndpointConfig { + // connectedUser: string; +} + +interface BackupEndpointRequestData { + state: BackupEndpointsConfig; + userId: string; + password: string; +} + +@Injectable() +export class BackupEndpointsService extends BackupRestoreEndpointService { + + hasChanges = new BehaviorSubject(false); + hasChanges$ = this.hasChanges.asObservable(); + allChanged = new BehaviorSubject(false); + allChanged$ = this.allChanged.asObservable(); + + state: BackupEndpointsConfig = {}; + password: string; + + constructor( + private store: Store, + private http: HttpClient + ) { + super(); + } + + // State Related + initialize(endpoints: EndpointModel[]) { + endpoints.forEach(entity => { + this.state[entity.guid] = { + [BackupEndpointTypes.ENDPOINT]: false, + [BackupEndpointTypes.CONNECT]: false, + [BackupEndpointTypes.ALL_CONNECT]: false, + entity + }; + }); + this.validate(); + } + + validate() { + const endpoints = Object.values(this.state); + endpoints.forEach(endpoint => { + if (!endpoint[BackupEndpointTypes.ENDPOINT]) { + endpoint[BackupEndpointTypes.CONNECT] = false; + endpoint[BackupEndpointTypes.ALL_CONNECT] = false; + } + if (endpoint[BackupEndpointTypes.ALL_CONNECT] && this.canBackup(endpoint.entity, BackupEndpointTypes.CONNECT)) { + endpoint[BackupEndpointTypes.CONNECT] = true; + } + }); + + const hasChanges = !!endpoints.find(endpoint => + endpoint[BackupEndpointTypes.ENDPOINT] || + endpoint[BackupEndpointTypes.CONNECT] || + endpoint[BackupEndpointTypes.ALL_CONNECT] + ); + this.hasChanges.next(hasChanges); + const allChanged = endpoints.every(endpoint => { + const e = !this.canBackup(endpoint.entity, BackupEndpointTypes.ENDPOINT) || endpoint[BackupEndpointTypes.ENDPOINT]; + const c = !this.canBackup(endpoint.entity, BackupEndpointTypes.CONNECT) || endpoint[BackupEndpointTypes.CONNECT]; + const aC = !this.canBackup(endpoint.entity, BackupEndpointTypes.ALL_CONNECT) || endpoint[BackupEndpointTypes.ALL_CONNECT]; + return e && c && aC; + } + + ); + this.allChanged.next(allChanged); + } + + canBackup(endpoint: EndpointModel, type: BackupEndpointTypes): boolean { + // Can always back up endpoint + if (type === BackupEndpointTypes.ENDPOINT) { + return true; + } + + // All other settings require endpoint to be backed up + if (!this.state[endpoint.guid][BackupEndpointTypes.ENDPOINT]) { + return false; + } + + const epType = entityCatalog.getEndpoint(endpoint.cnsi_type, endpoint.sub_type).definition; + // The endpoint type supports connection details + if (epType.unConnectable) { + return false; + } + + // Are all connection details backed up anyway? + // Does the user have connection details for this endpoint? + if (type === BackupEndpointTypes.CONNECT) { + return !this.state[endpoint.guid][BackupEndpointTypes.ALL_CONNECT] && + endpoint.connectionStatus === 'connected'; + // return !this.service.state[endpoint.guid][BackupRestoreTypes.CONNECT]; + } + + return true; + } + + selectAll() { + Object.values(this.state).forEach(endpoint => { + if (this.canBackup(endpoint.entity, BackupEndpointTypes.ENDPOINT)) { + endpoint[BackupEndpointTypes.ENDPOINT] = true; + } + if (this.canBackup(endpoint.entity, BackupEndpointTypes.CONNECT)) { + endpoint[BackupEndpointTypes.CONNECT] = true; + } + if (this.canBackup(endpoint.entity, BackupEndpointTypes.ALL_CONNECT)) { + endpoint[BackupEndpointTypes.ALL_CONNECT] = true; + } + }); + this.validate(); + } + + selectNone() { + Object.values(this.state).forEach(endpoint => { + endpoint[BackupEndpointTypes.ENDPOINT] = false; + endpoint[BackupEndpointTypes.CONNECT] = false; + endpoint[BackupEndpointTypes.ALL_CONNECT] = false; + }); + this.validate(); + } + + // Request Related + + createBackup(): Observable { + const url = '/pp/v1/endpoints/backup'; + const fromObject = {}; + const params: HttpParams = new HttpParams({ + fromObject, + encoder: new BrowserStandardEncoder() + }); + + return this.getSessionData().pipe( + switchMap(ses => this.http.post(url, this.createBodyToSend(ses), { + params + })), + map(res => { + console.log('Response: ', res); + return new Blob([JSON.stringify(res)]); + }), + first(), + ); + } + + private createBodyToSend(sd: SessionData): BackupEndpointRequestData { + const state: BackupEndpointsConfig = Object.entries(this.state).reduce((res, [endpointId, endpoint]) => { + const { entity, ...rest } = endpoint; + const requestConfig: BackupEndpointConfigRequest = { + ...rest, + }; + res[endpointId] = requestConfig; + return res; + }, {}); + return { + state, + userId: this.getUserIdFromSessionData(sd), + password: this.password + }; + } + + private getUserIdFromSessionData(sd: SessionData): string { + if (sd && sd.user) { + return sd.user.guid; + } + return null; + } + + private getSessionData(): Observable { + return this.store.select(s => s.auth).pipe( + filter(auth => !!(auth && auth.sessionData)), + map((auth: AuthState) => auth.sessionData), + first() + ); + } + + + +} diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html new file mode 100644 index 0000000000..e6dd32385c --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html @@ -0,0 +1,32 @@ + +

Backup Endpoints

+
+ + + +
+

Select the endpoints and connection details that you would like to backup

+
+ + +
+ +
+
+ +
+

Protect the backup by providing as password. You will need this password when restoring from this backup

+
+ + + +
+
+
+
+ + \ No newline at end of file diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.scss b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.scss new file mode 100644 index 0000000000..de139e1b04 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.scss @@ -0,0 +1,15 @@ +:host { + flex: 1; +} +.select-step { + display: flex; + flex: 1; + flex-direction: column; + + &__buttons { + padding-bottom: 12px; + button { + margin-right: 24px; + } + } +} diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.spec.ts new file mode 100644 index 0000000000..16cf213f81 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BackupEndpointsComponent } from './backup-endpoints.component'; + +describe('BackupEndpointsComponent', () => { + let component: BackupEndpointsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ BackupEndpointsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(BackupEndpointsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 0000000000..23f5216ab3 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.ts @@ -0,0 +1,184 @@ +import { Component, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import * as moment from 'moment'; +import { Observable, of, Subject } from 'rxjs'; +import { filter, first, map, tap } from 'rxjs/operators'; + +import { GetAllEndpoints } from '../../../../../../store/src/actions/endpoint.actions'; +import { AppState } from '../../../../../../store/src/app-state'; +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 { 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'; +import { ITableColumn } from '../../../../shared/components/list/list-table/table.types'; +import { StepOnNextFunction, StepOnNextResult } from '../../../../shared/components/stepper/step/step.component'; +import { BackupEndpointsService } from '../backup-endpoints.service'; +import { BackupRestoreCellComponent } from '../backup-restore-cell/backup-restore-cell.component'; +import { BackupEndpointTypes } from '../backup-restore-endpoints.service'; + +@Component({ + selector: 'app-backup-endpoints', + templateUrl: './backup-endpoints.component.html', + styleUrls: ['./backup-endpoints.component.scss'], + providers: [ + BackupEndpointsService + ] +}) +export class BackupEndpointsComponent implements OnInit { + + // Step 1 + columns: ITableColumn[] = [ + { + columnId: 'name', + headerCell: () => 'Name', + cellDefinition: { + valuePath: 'name' + } + }, + { + columnId: 'endpoint', + headerCell: () => 'Backup', + cellComponent: BackupRestoreCellComponent, + cellConfig: { + type: BackupEndpointTypes.ENDPOINT + } + }, + { + columnId: 'connect', + headerCell: () => 'Backup Your Connection Details', + cellComponent: BackupRestoreCellComponent, + cellConfig: { + type: BackupEndpointTypes.CONNECT + } + }, + { + columnId: 'all-connect', + headerCell: () => 'Backup All Users Connection Details', + cellComponent: BackupRestoreCellComponent, + cellConfig: { + type: BackupEndpointTypes.ALL_CONNECT + } + }, + ]; + endpointDataSource: ITableListDataSource; + disableSelectAll$: Observable; + disableSelectNone$: Observable; + selectValid$: Observable; + + // Step 2 + passwordValid$: Observable; + passwordForm: FormGroup; + + constructor( + public service: BackupEndpointsService, + private store: Store, + private paginationMonitorFactory: PaginationMonitorFactory, + private confirmDialog: ConfirmationDialogService, + ) { + this.setupSelectStep(); + this.setupPasswordStep(); + } + + setupSelectStep() { + const action = new GetAllEndpoints(); + const endpointObs = getPaginationObservables({ + store: this.store, + action, + paginationMonitor: this.paginationMonitorFactory.create( + action.paginationKey, + action, + true + ) + }, true); + + + const endpoints$ = endpointObs.entities$.pipe( + filter(entities => !!entities), + map(endpoints => endpoints.sort((a, b) => a.name.localeCompare(b.name))) + ); + + endpoints$.pipe(first()).subscribe(entities => this.service.initialize(entities)); + + this.endpointDataSource = { + isTableLoading$: endpointObs.fetchingEntities$, + connect: () => endpoints$, + disconnect: () => { }, + trackBy: (index, row) => row.guid + }; + + this.disableSelectAll$ = this.service.allChanged$; + this.disableSelectNone$ = this.service.hasChanges$.pipe( + map(hasChanges => !hasChanges) + ); + + this.selectValid$ = this.service.hasChanges$; + } + + setupPasswordStep() { + this.passwordForm = new FormGroup({ + password: new FormControl('', [Validators.required]), + }); + this.passwordValid$ = this.passwordForm.statusChanges.pipe( + map(() => { + this.service.password = this.passwordForm.controls.password.value; + return this.passwordForm.valid; + }) + ); + } + + ngOnInit() { + } + + onNext: StepOnNextFunction = () => { + // TODO: RC Complete/Finish token warning + const confirmation = new ConfirmationDialogConfig( + 'Backup', + 'Backing up connection details ?????????', + 'Continue', + true + ); + const result = new Subject(); + + const userCancelledDialog = () => { + result.next({ + success: false + }); + }; + + const backupSuccess = data => { + result.next({ + success: true, + redirect: true, + }); + + const downloadURL = window.URL.createObjectURL(data); + const link = document.createElement('a'); + link.href = downloadURL; + const dateTime = moment().format('YYYYMMDD-HHmmss'); // TODO: RC timezone? + link.download = `stratos_backup_${dateTime}.bk`; + link.click(); + }; + + const backupFailure = err => { + const errorMessage = this.service.createError(err); + result.next({ + success: false, + message: `Failed to create backup` + (errorMessage ? `: ${errorMessage}` : '') + }); + return of(false); + }; + + 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); + + // 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-cell/backup-restore-cell.component.html b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.html new file mode 100644 index 0000000000..3f240bbf9e --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.scss b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.spec.ts new file mode 100644 index 0000000000..1809ff8d89 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BackupRestoreCellComponent } from './backup-restore-cell.component'; + +describe('BackupRestoreCellComponent', () => { + let component: BackupRestoreCellComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ BackupRestoreCellComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(BackupRestoreCellComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.ts new file mode 100644 index 0000000000..97a05e5198 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.ts @@ -0,0 +1,25 @@ +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'; + +@Component({ + selector: 'app-backup-restore-cell', + templateUrl: './backup-restore-cell.component.html', + styleUrls: ['./backup-restore-cell.component.scss'] +}) +export class BackupRestoreCellComponent extends TableCellCustom { + + constructor(public service: BackupEndpointsService) { + super(); + } + + validate() { + this.service.validate(); + } + + disabled(): boolean { + return !this.service.canBackup(this.row, this.config.type); + } +} 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 new file mode 100644 index 0000000000..29b19d45f2 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints.service.ts @@ -0,0 +1,213 @@ +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', + ALL_CONNECT = 'all_connect' +} + +export interface BackupEndpointsConfig { + [endpointId: string]: T; +} + +export interface BaseEndpointConfig { + [BackupEndpointTypes.ENDPOINT]: boolean; + [BackupEndpointTypes.CONNECT]: boolean; + [BackupEndpointTypes.ALL_CONNECT]: boolean; +} + +// type BackupEndpoint = Omit; + +export interface BackupEndpointConfigUI extends BaseEndpointConfig { + entity: EndpointModel; +} + +// @Injectable() +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; + } + + // hasChanges = new BehaviorSubject(false); + // hasChanges$ = this.hasChanges.asObservable(); + // allChanged = new BehaviorSubject(false); + // allChanged$ = this.allChanged.asObservable(); + + // state: BackupEndpointsConfig = {}; + // password: string; + + + // constructor( + // private store: Store, + // private http: HttpClient + // ) { + + // } + + // // State Related + // initialize(endpoints: EndpointModel[]) { + // endpoints.forEach(entity => { + // this.state[entity.guid] = { + // [BackupRestoreTypes.ENDPOINT]: false, + // [BackupRestoreTypes.CONNECT]: false, + // [BackupRestoreTypes.ALL_CONNECT]: false, + // entity + // }; + // }); + // this.validate(); + // } + + // validate() { + // const endpoints = Object.values(this.state); + // endpoints.forEach(endpoint => { + // if (!endpoint[BackupRestoreTypes.ENDPOINT]) { + // endpoint[BackupRestoreTypes.CONNECT] = false; + // endpoint[BackupRestoreTypes.ALL_CONNECT] = false; + // } + // if (endpoint[BackupRestoreTypes.ALL_CONNECT] && this.canBackup(endpoint.entity, BackupRestoreTypes.CONNECT)) { + // endpoint[BackupRestoreTypes.CONNECT] = true; + // } + // }); + + // const hasChanges = !!endpoints.find(endpoint => + // endpoint[BackupRestoreTypes.ENDPOINT] || + // endpoint[BackupRestoreTypes.CONNECT] || + // endpoint[BackupRestoreTypes.ALL_CONNECT] + // ); + // this.hasChanges.next(hasChanges); + // const allChanged = endpoints.every(endpoint => { + // const e = !this.canBackup(endpoint.entity, BackupRestoreTypes.ENDPOINT) || endpoint[BackupRestoreTypes.ENDPOINT]; + // const c = !this.canBackup(endpoint.entity, BackupRestoreTypes.CONNECT) || endpoint[BackupRestoreTypes.CONNECT]; + // const aC = !this.canBackup(endpoint.entity, BackupRestoreTypes.ALL_CONNECT) || endpoint[BackupRestoreTypes.ALL_CONNECT]; + // return e && c && aC; + // } + + // ); + // this.allChanged.next(allChanged); + // } + + // canBackup(endpoint: EndpointModel, type: BackupRestoreTypes): boolean { + // // Can always back up endpoint + // if (type === BackupRestoreTypes.ENDPOINT) { + // return true; + // } + + // // All other settings require endpoint to be backed up + // if (!this.state[endpoint.guid][BackupRestoreTypes.ENDPOINT]) { + // return false; + // } + + // const epType = entityCatalog.getEndpoint(endpoint.cnsi_type, endpoint.sub_type).definition; + // // The endpoint type supports connection details + // if (epType.unConnectable) { + // return false; + // } + + // // Are all connection details backed up anyway? + // // Does the user have connection details for this endpoint? + // if (type === BackupRestoreTypes.CONNECT) { + // return !this.state[endpoint.guid][BackupRestoreTypes.ALL_CONNECT] && + // endpoint.connectionStatus === 'connected'; + // // return !this.service.state[endpoint.guid][BackupRestoreTypes.CONNECT]; + // } + + // return true; + // } + + // selectAll() { + // Object.values(this.state).forEach(endpoint => { + // if (this.canBackup(endpoint.entity, BackupRestoreTypes.ENDPOINT)) { + // endpoint[BackupRestoreTypes.ENDPOINT] = true; + // } + // if (this.canBackup(endpoint.entity, BackupRestoreTypes.CONNECT)) { + // endpoint[BackupRestoreTypes.CONNECT] = true; + // } + // if (this.canBackup(endpoint.entity, BackupRestoreTypes.ALL_CONNECT)) { + // endpoint[BackupRestoreTypes.ALL_CONNECT] = true; + // } + // }); + // this.validate(); + // } + + // selectNone() { + // Object.values(this.state).forEach(endpoint => { + // endpoint[BackupRestoreTypes.ENDPOINT] = false; + // endpoint[BackupRestoreTypes.CONNECT] = false; + // endpoint[BackupRestoreTypes.ALL_CONNECT] = false; + // }); + // this.validate(); + // } + + // // Request Related + + // createBackup(): Observable { + // const url = '/pp/v1/backup/endpoints'; + // const fromObject = {}; + // const params: HttpParams = new HttpParams({ + // fromObject, + // encoder: new BrowserStandardEncoder() + // }); + + // return this.getSessionData().pipe( + // switchMap(ses => this.http.post(url, this.createBodyToSend(ses), { + // params + // })), + // map(res => { + // console.log('Response: ', res); + // return new Blob([JSON.stringify(res)]); + // }), + // first(), + // ); + // } + + // private createBodyToSend(sd: SessionData): BackupEndpointRequestData { + // const state: BackupEndpointsConfig = Object.entries(this.state).reduce((res, [endpointId, endpoint]) => { + // const { entity, ...rest } = endpoint; + // const requestConfig: BackupEndpointConfigRequest = { + // ...rest, + // }; + // res[endpointId] = requestConfig; + // return res; + // }, {}); + // return { + // state, + // userId: this.getUserIdFromSessionData(sd), + // password: this.password + // }; + // } + + // private getUserIdFromSessionData(sd: SessionData): string { + // if (sd && sd.user) { + // return sd.user.guid; + // } + // return null; + // } + + // private getSessionData(): Observable { + // return this.store.select(s => s.auth).pipe( + // filter(auth => !!(auth && auth.sessionData)), + // map((auth: AuthState) => auth.sessionData), + // first() + // ); + // } + + + +} diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.html b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.html new file mode 100644 index 0000000000..d1ea1197f7 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.html @@ -0,0 +1,13 @@ + +

Backup/Restore Endpoints

+
+ + + +
+ +

Create a backup of Endpoints and their connection details or restore from an existing backup.

+ +
+
+
\ No newline at end of file diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.scss b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.scss new file mode 100644 index 0000000000..712926fb2c --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.scss @@ -0,0 +1,3 @@ +.tiles { + flex: 1; +} diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.spec.ts new file mode 100644 index 0000000000..587eefc846 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BackupRestoreEndpointsComponent } from './backup-restore-endpoints.component'; + +describe('BackupRestoreEndpointsComponent', () => { + let component: BackupRestoreEndpointsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ BackupRestoreEndpointsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(BackupRestoreEndpointsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.ts new file mode 100644 index 0000000000..90b1e4cc2d --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.ts @@ -0,0 +1,48 @@ +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; + +import { RouterNav } from '../../../../../../store/src/actions/router.actions'; +import { AppState } from '../../../../../../store/src/app-state'; +import { ITileConfig, ITileData } from '../../../../shared/components/tile/tile-selector.types'; + +interface IAppTileData extends ITileData { + type: string; +} + + +@Component({ + selector: 'app-backup-restore-endpoints', + templateUrl: './backup-restore-endpoints.component.html', + styleUrls: ['./backup-restore-endpoints.component.scss'], +}) +export class BackupRestoreEndpointsComponent { + + // static BACKUP_RESTORE_PARAM = 'backup'; + + public serviceType: string; + public tileSelectorConfig: ITileConfig[]; + + set selectedTile(tile: ITileConfig) { + if (tile) { + const url = 'endpoints/backup-restore/' + tile.data.type; + this.store.dispatch(new RouterNav({ path: url })); + } + } + + constructor( + private store: Store) { + this.tileSelectorConfig = [ + new ITileConfig( + 'Backup', + { matIcon: 'cloud_download' }, + { type: 'backup' } + ), + new ITileConfig( + 'Restore', + { matIcon: 'cloud_upload' }, + { type: 'restore' } + ) + ]; + } + +} 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 new file mode 100644 index 0000000000..ca3daa19b9 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints.service.ts @@ -0,0 +1,76 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { BehaviorSubject, Observable } from 'rxjs'; + +import { GeneralEntityAppState } from '../../../../../store/src/app-state'; +import { BrowserStandardEncoder } from '../../../helper'; +import { BackupRestoreEndpointService } from './backup-restore-endpoints.service'; + +interface RestoreEndpointsData { + data: string; + password: string; +} + +@Injectable() +export class RestoreEndpointsService extends BackupRestoreEndpointService { + + validFile = new BehaviorSubject(false); + validFile$ = this.validFile.asObservable(); + password: string; // TODO: RC use set password in both services + fileName: string; + private fileContent: string; + + constructor( + private store: Store, + private http: HttpClient + ) { + super(); + } + + setFile(file): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const res = reader.result as string; + this.setFileResult(res, file.name); + resolve(res); + }; + reader.onerror = () => this.setFileResult(null, null); + reader.onabort = () => this.setFileResult(null, null); + reader.readAsText(file); + }); + } + + private setFileResult(content: string, fileName: string) { + if (!!content) { + this.validFile.next(true); + this.fileName = fileName; + this.fileContent = content; + } else { + this.validFile.next(false); + this.fileName = ''; + this.fileContent = ''; + } + } + + + restoreBackup(): Observable { + const url = '/pp/v1/endpoints/restore'; + const fromObject = {}; + const params: HttpParams = new HttpParams({ + fromObject, + encoder: new BrowserStandardEncoder() + }); + return this.http.post(url, this.createBodyToSend(), { + params + }); + } + + createBodyToSend(): RestoreEndpointsData { + return { + data: this.fileContent, + password: this.password + }; + } +} 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 new file mode 100644 index 0000000000..7b9ef8f1f0 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.html @@ -0,0 +1,27 @@ + +

Restore Endpoints

+
+ + + +
+

Provide the backup file to restore from.

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

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

+
+ + + +
+
+
+
\ No newline at end of file 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 new file mode 100644 index 0000000000..89184f73a4 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.scss @@ -0,0 +1,18 @@ +:host { + flex: 1; +} +.file-step { + display: flex; + flex-direction: column; + &__input { + &--input { + height: 0; + visibility: hidden; + width: 0; + } + button { + margin-right: 5px; + } + } + +} diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.spec.ts new file mode 100644 index 0000000000..b260285638 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RestoreEndpointsComponent } from './restore-endpoints.component'; + +describe('RestoreEndpointsComponent', () => { + let component: RestoreEndpointsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RestoreEndpointsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RestoreEndpointsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 0000000000..0bfb63a132 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.ts @@ -0,0 +1,176 @@ +import { Component, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +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 { EndpointModel } from '../../../../../../store/src/types/endpoint.types'; +import { getEventFiles } from '../../../../core/browser-helper'; +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'; +import { ITableColumn } from '../../../../shared/components/list/list-table/table.types'; +import { StepOnNextFunction, StepOnNextResult } from '../../../../shared/components/stepper/step/step.component'; +import { BackupRestoreCellComponent } from '../backup-restore-cell/backup-restore-cell.component'; +import { BackupEndpointTypes } from '../backup-restore-endpoints.service'; +import { RestoreEndpointsService } from '../restore-endpoints.service'; + + +@Component({ + selector: 'app-restore-endpoints', + templateUrl: './restore-endpoints.component.html', + styleUrls: ['./restore-endpoints.component.scss'], + providers: [ + RestoreEndpointsService + ] +}) +export class RestoreEndpointsComponent implements OnInit { + + // Step 1 + columns: ITableColumn[] = [ + { + columnId: 'name', + headerCell: () => 'Name', + cellDefinition: { + valuePath: 'name' + } + }, + { + columnId: 'endpoint', + headerCell: () => 'Restore', + cellComponent: BackupRestoreCellComponent, + cellConfig: { + type: BackupEndpointTypes.ENDPOINT, + + } + }, + { + columnId: 'connect', + headerCell: () => 'Include Connection', + cellComponent: BackupRestoreCellComponent, + cellConfig: { + type: BackupEndpointTypes.CONNECT + } + }, + // TODO: RC disable backup token if unconnectable + ]; + endpointDataSource: ITableListDataSource; + fileValid$: Observable; + fileName: string; + + // Step 2 + passwordValid$: Observable; + passwordForm: FormGroup; + + 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() { + this.passwordForm = new FormGroup({ + password: new FormControl('', [Validators.required]), + }); + this.passwordValid$ = this.passwordForm.statusChanges.pipe( + map(() => { + this.service.password = this.passwordForm.controls.password.value; + return this.passwordForm.valid; + }) + ); + } + + 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); + // }); + } + + restore: StepOnNextFunction = () => { + const confirmation = new ConfirmationDialogConfig( + 'Restore', + 'This will overwrite any matching endpoints and connection details', + 'Continue', + true + ); + const result = new Subject(); + + const userCancelledDialog = () => { + result.next({ + success: false + }); + }; + + const restoreSuccess = data => { + result.next({ + success: true, + redirect: true, + }); + }; + + const backupFailure = err => { + const errorMessage = this.service.createError(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 + return result.asObservable().pipe(tap(console.log)); + } + +} diff --git a/src/frontend/packages/core/src/features/endpoints/endpoints.module.ts b/src/frontend/packages/core/src/features/endpoints/endpoints.module.ts index b68dec67b8..c3704479ff 100644 --- a/src/frontend/packages/core/src/features/endpoints/endpoints.module.ts +++ b/src/frontend/packages/core/src/features/endpoints/endpoints.module.ts @@ -1,16 +1,22 @@ -import { EditEndpointStepComponent } from './edit-endpoint/edit-endpoint-step/edit-endpoint-step.component'; import { NgModule } from '@angular/core'; import { CoreModule } from '../../core/core.module'; import { SharedModule } from '../../shared/shared.module'; +import { BackupEndpointsComponent } from './backup-restore/backup-endpoints/backup-endpoints.component'; +import { BackupRestoreCellComponent } from './backup-restore/backup-restore-cell/backup-restore-cell.component'; +import { + BackupRestoreEndpointsComponent, +} from './backup-restore/backup-restore-endpoints/backup-restore-endpoints.component'; +import { RestoreEndpointsComponent } from './backup-restore/restore-endpoints/restore-endpoints.component'; import { CredentialsAuthFormComponent } from './connect-endpoint-dialog/auth-forms/credentials-auth-form.component'; import { NoneAuthFormComponent } from './connect-endpoint-dialog/auth-forms/none-auth-form.component'; import { SSOAuthFormComponent } from './connect-endpoint-dialog/auth-forms/sso-auth-form.component'; import { ConnectEndpointDialogComponent } from './connect-endpoint-dialog/connect-endpoint-dialog.component'; import { CreateEndpointModule } from './create-endpoint/create-endpoint.module'; +import { EditEndpointStepComponent } from './edit-endpoint/edit-endpoint-step/edit-endpoint-step.component'; +import { EditEndpointComponent } from './edit-endpoint/edit-endpoint.component'; import { EndpointsPageComponent } from './endpoints-page/endpoints-page.component'; import { EndpointsRoutingModule } from './endpoints.routing'; -import { EditEndpointComponent } from './edit-endpoint/edit-endpoint.component'; @NgModule({ imports: [ @@ -27,12 +33,17 @@ import { EditEndpointComponent } from './edit-endpoint/edit-endpoint.component'; NoneAuthFormComponent, EditEndpointComponent, EditEndpointStepComponent, + BackupRestoreEndpointsComponent, + BackupEndpointsComponent, + RestoreEndpointsComponent, + BackupRestoreCellComponent, ], entryComponents: [ ConnectEndpointDialogComponent, CredentialsAuthFormComponent, SSOAuthFormComponent, - NoneAuthFormComponent + NoneAuthFormComponent, + BackupRestoreCellComponent ] }) export class EndpointsModule { } diff --git a/src/frontend/packages/core/src/features/endpoints/endpoints.routing.ts b/src/frontend/packages/core/src/features/endpoints/endpoints.routing.ts index 2b9fcb25c4..194357b894 100644 --- a/src/frontend/packages/core/src/features/endpoints/endpoints.routing.ts +++ b/src/frontend/packages/core/src/features/endpoints/endpoints.routing.ts @@ -4,12 +4,17 @@ import { RouterModule, Routes } from '@angular/router'; import { DynamicExtensionRoutes } from '../../core/extension/dynamic-extension-routes'; import { StratosActionType } from '../../core/extension/extension-service'; import { PageNotFoundComponentComponent } from '../../core/page-not-found-component/page-not-found-component.component'; +import { BackupEndpointsComponent } from './backup-restore/backup-endpoints/backup-endpoints.component'; +import { + BackupRestoreEndpointsComponent, +} from './backup-restore/backup-restore-endpoints/backup-restore-endpoints.component'; +import { RestoreEndpointsComponent } from './backup-restore/restore-endpoints/restore-endpoints.component'; import { CreateEndpointBaseStepComponent, } from './create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component'; import { CreateEndpointComponent } from './create-endpoint/create-endpoint.component'; -import { EndpointsPageComponent } from './endpoints-page/endpoints-page.component'; import { EditEndpointComponent } from './edit-endpoint/edit-endpoint.component'; +import { EndpointsPageComponent } from './endpoints-page/endpoints-page.component'; const endpointsRoutes: Routes = [ { @@ -34,6 +39,18 @@ const endpointsRoutes: Routes = [ path: 'edit/:id', component: EditEndpointComponent }, + { + path: 'backup-restore', + component: BackupRestoreEndpointsComponent + }, + { + path: 'backup-restore/backup', + component: BackupEndpointsComponent + }, + { + path: 'backup-restore/restore', + component: RestoreEndpointsComponent + }, { path: '**', component: PageNotFoundComponentComponent, diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell/table-cell.component.ts b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell/table-cell.component.ts index 2b101101aa..d27c1f175f 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell/table-cell.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell/table-cell.component.ts @@ -55,6 +55,7 @@ export const listTableCells: Type>[] = [ TableCellFavoriteComponent, TableCellEndpointDetailsComponent, TableCellSidePanelComponent, + // BackupRestoreCellComponent, ...coreEndpointListDetailsComponents ]; diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts index 6955532041..429f68fafd 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts @@ -1,18 +1,22 @@ import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; +import { of } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; import { CFAppState } from '../../../../../../../cloud-foundry/src/cf-app-state'; import { ListView } from '../../../../../../../store/src/actions/list.actions'; -import { EndpointModel } from '../../../../../../../store/src/types/endpoint.types'; import { entityCatalog } from '../../../../../../../store/src/entity-catalog/entity-catalog.service'; -import { getFullEndpointApiUrl } from '../../../../../features/endpoints/endpoint-helpers'; import { EntityMonitorFactory } from '../../../../../../../store/src/monitors/entity-monitor.factory.service'; import { InternalEventMonitorFactory } from '../../../../../../../store/src/monitors/internal-event-monitor.factory'; import { PaginationMonitorFactory } from '../../../../../../../store/src/monitors/pagination-monitor.factory'; +import { AuthState } from '../../../../../../../store/src/reducers/auth.reducer'; +import { EndpointModel } from '../../../../../../../store/src/types/endpoint.types'; +import { getFullEndpointApiUrl } from '../../../../../features/endpoints/endpoint-helpers'; import { FavoritesConfigMapper } from '../../../favorites-meta-card/favorite-config-mapper'; import { createTableColumnFavorite } from '../../list-table/table-cell-favorite/table-cell-favorite.component'; import { ITableColumn } from '../../list-table/table.types'; -import { IListAction, IListConfig, ListViewTypes } from '../../list.component.types'; +import { IGlobalListAction, IListAction, IListConfig, ListViewTypes } from '../../list.component.types'; import { EndpointCardComponent } from './endpoint-card/endpoint-card.component'; import { EndpointListHelper } from './endpoint-list.helpers'; import { EndpointsDataSource } from './endpoints-data-source'; @@ -28,7 +32,18 @@ export class EndpointsListConfigService implements IListConfig { private singleActions: IListAction[]; - private globalActions = []; + private globalActions: IGlobalListAction[] = [{ + action: () => this.router.navigate(['endpoints/backup-restore']), + icon: 'settings_backup_restore', + label: 'Backup/Restore Endpoints', + description: 'Backup or Restore Endpoints', + visible$: this.store.select(s => s.auth).pipe( + filter(auth => !!auth && !!auth.sessionData), + map((auth: AuthState) => auth.sessionData), + map(session => session.user && session.user.admin) + ), + enabled$: of(true), // Table is not shown when there's no endpoints, so this should always be true + }]; public readonly columns: ITableColumn[] = [ { @@ -109,7 +124,7 @@ export class EndpointsListConfigService implements IListConfig { internalEventMonitorFactory: InternalEventMonitorFactory, endpointListHelper: EndpointListHelper, favoritesConfigMapper: FavoritesConfigMapper, - + private router: Router ) { this.singleActions = endpointListHelper.endpointActions(); const favoriteCell = createTableColumnFavorite( diff --git a/src/frontend/packages/store/src/actions/endpoint.actions.ts b/src/frontend/packages/store/src/actions/endpoint.actions.ts index 21504086a8..c0a728eb06 100644 --- a/src/frontend/packages/store/src/actions/endpoint.actions.ts +++ b/src/frontend/packages/store/src/actions/endpoint.actions.ts @@ -160,4 +160,4 @@ export class UpdateEndpoint extends EndpointAction { ) { super(); } -} \ No newline at end of file +} diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 3e01345e77..048a912ce8 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -4,6 +4,7 @@ import ( "crypto/x509" "encoding/json" "fmt" + "io/ioutil" "net/http" "net/url" "strconv" @@ -278,8 +279,8 @@ func marshalClusterList(clusterList []*interfaces.ConnectedEndpoint) ([]byte, er return jsonString, nil } -func (p *portalProxy) UpdateEndointMetadata(guid string, metadata string) error { - log.Debug("UpdateEndointMetadata") +func (p *portalProxy) UpdateEndpointMetadata(guid string, metadata string) error { + log.Debug("UpdateEndpointMetadata") cnsiRepo, err := cnsis.NewPostgresCNSIRepository(p.DatabaseConnectionPool) if err != nil { @@ -635,3 +636,255 @@ func (p *portalProxy) updateEndpoint(c echo.Context) error { return nil } + +// 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 bool `json:"connect"` + AllConnect bool `json:"all_connect"` +} + +type BackupRestoreState struct { + Endpoints []*interfaces.CNSIRecord + Tokens []interfaces.BackupTokenRecord +} + +type RestoreDataRequest struct { + Data string `json:"data"` + Password string `json:"password"` +} + +// 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 := &BackupDataRequest{} + 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 + } + + // 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") + } + + // Encrypt data (see above) // TODO: RC leave until last + encryptedResponse := jsonString + + // Return data + c.Response().Header().Set("Content-Type", "application/json") + c.Response().Write(encryptedResponse) + return nil +} + +func (p *portalProxy) createBackup(data *BackupDataRequest) (*BackupRestoreState, 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 + } + } + + if endpoint.AllConnect { + // 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) + + response := &BackupRestoreState{ + Endpoints: endpoints, + Tokens: tokens, + } + + 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 + } + + return trs, true +} + +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 := &RestoreDataRequest{} + 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) + if err != nil { + return err + } + + // log.Warnf("BACKUP DATA: %+v", backup) + c.Response().WriteHeader(http.StatusOK) + return nil + +} + +func (p *portalProxy) restoreBackup(backup *BackupRestoreState) error { + log.Debug("restoreBackup") + 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 { + 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) + } + + for _, tr := range backup.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 +} + +// 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/main.go b/src/jetstream/main.go index 9b649da31d..ea8da57516 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -973,6 +973,10 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { // Apply edits for the given endpoint adminGroup.POST("/endpoint/:id", p.updateEndpoint) + // TODO: RC Q better as `/backup` & `/restore`? + adminGroup.POST("/endpoints/backup", p.backupEndpoints) + adminGroup.POST("/endpoints/restore", p.restoreEndpoints) + adminGroup.POST("/unregister", p.unregisterCluster) // sessionGroup.DELETE("/cnsis", p.removeCluster) diff --git a/src/jetstream/repository/cnsis/cnsis.go b/src/jetstream/repository/cnsis/cnsis.go index d18ddda5ef..b1e5bcc4bc 100644 --- a/src/jetstream/repository/cnsis/cnsis.go +++ b/src/jetstream/repository/cnsis/cnsis.go @@ -14,6 +14,7 @@ type Repository interface { Save(guid string, cnsiRecord interfaces.CNSIRecord, encryptionKey []byte) error Update(endpoint interfaces.CNSIRecord, encryptionKey []byte) error UpdateMetadata(guid string, metadata string) error + Overwrite(endpoint interfaces.CNSIRecord, encryptionKey []byte) error } type Endpoint interface { diff --git a/src/jetstream/repository/cnsis/pgsql_cnsis.go b/src/jetstream/repository/cnsis/pgsql_cnsis.go index 7d8db3b2ff..6ba5344004 100644 --- a/src/jetstream/repository/cnsis/pgsql_cnsis.go +++ b/src/jetstream/repository/cnsis/pgsql_cnsis.go @@ -38,6 +38,8 @@ var updateCNSI = `UPDATE cnsis SET name = $1, skip_ssl_validation = $2, sso_allo // Update the metadata var updateCNSIMetadata = `UPDATE cnsis SET meta_data = $1 WHERE guid = $2` +var countCNSI = `SELECT COUNT(*) FROM cnsis WHERE guid=$1` + // PostgresCNSIRepository is a PostgreSQL-backed CNSI repository type PostgresCNSIRepository struct { db *sql.DB @@ -349,3 +351,33 @@ func (p *PostgresCNSIRepository) UpdateMetadata(guid string, metadata string) er return nil } + +// Overwrite - Creates or Updates CNSI Record +func (p *PostgresCNSIRepository) Overwrite(endpoint interfaces.CNSIRecord, encryptionKey []byte) error { + log.Debug("Overwrite CNSI") + + // Is there an existing token? + var count int + err := p.db.QueryRow(countCNSI, endpoint.GUID).Scan(&count) + if err != nil { + 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/interfaces/portal_proxy.go b/src/jetstream/repository/interfaces/portal_proxy.go index d965601156..4f7ec4eefd 100644 --- a/src/jetstream/repository/interfaces/portal_proxy.go +++ b/src/jetstream/repository/interfaces/portal_proxy.go @@ -44,7 +44,7 @@ type PortalProxy interface { Env() *env.VarSet ListEndpointsByUser(userGUID string) ([]*ConnectedEndpoint, error) ListEndpoints() ([]*CNSIRecord, error) - UpdateEndointMetadata(guid string, metadata string) error + UpdateEndpointMetadata(guid string, metadata string) error // UAA Token GetUAATokenRecord(userGUID string) (TokenRecord, error) diff --git a/src/jetstream/repository/interfaces/structs.go b/src/jetstream/repository/interfaces/structs.go index 041f2ebea0..bdca5a0493 100644 --- a/src/jetstream/repository/interfaces/structs.go +++ b/src/jetstream/repository/interfaces/structs.go @@ -85,13 +85,21 @@ const ( AuthConnectTypeNone = "none" ) -// Token record for an endpoint (includes the Endpoint GUID) -type EndpointTokenRecord struct { - *TokenRecord - EndpointGUID string - EndpointType string - APIEndpint string - LoggingEndpoint string +// // Token record for an endpoint (includes the Endpoint GUID) +// type EndpointTokenRecord struct { +// *TokenRecord +// EndpointGUID string +// EndpointType string +// APIEndpint string +// LoggingEndpoint string +// } + +// BackupTokenRecord used when backing up tokens +type BackupTokenRecord struct { + TokenRecord TokenRecord + UserGUID string + EndpointGUID string + TokenType string } // TokenRecord repsrents and endpoint or uaa token diff --git a/src/jetstream/repository/tokens/pgsql_tokens.go b/src/jetstream/repository/tokens/pgsql_tokens.go index 37e715551c..ddb7dfc3f3 100644 --- a/src/jetstream/repository/tokens/pgsql_tokens.go +++ b/src/jetstream/repository/tokens/pgsql_tokens.go @@ -43,6 +43,19 @@ 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'` + var countCNSITokens = `SELECT COUNT(*) FROM tokens WHERE cnsi_guid=$1 AND user_guid = $2 AND token_type = 'cnsi'` @@ -82,12 +95,15 @@ 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 @@ -313,6 +329,129 @@ 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) @@ -323,6 +462,97 @@ func (p *PgsqlTokenRepository) FindCNSITokenIncludeDisconnected(cnsiGUID string, return p.findCNSIToken(cnsiGUID, userGUID, encryptionKey, true) } +func (p *PgsqlTokenRepository) FindAllCNSITokenBackup(cnsiGUID string, encryptionKey []byte) ([]interfaces.BackupTokenRecord, error) { + log.Debug("FindAllCNSITokenBackup") + if cnsiGUID == "" { + msg := "Unable to find CNSI Token without a valid CNSI GUID." + log.Debug(msg) + return make([]interfaces.BackupTokenRecord, 0), errors.New(msg) + } + + var rows *sql.Rows + var err error + rows, err = p.db.Query(findAllCNSIToken, 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.BackupTokenRecord, 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() + + btrs := make([]interfaces.BackupTokenRecord, 0) + for rows.Next() { + // temp vars to retrieve db data + var ( + userGUID string + 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(&userGUID, &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.BackupTokenRecord, 0), err + } + + log.Debug("Decrypting Refresh Token") + plaintextRefreshToken, err := crypto.DecryptToken(encryptionKey, ciphertextRefreshToken) + if err != nil { + return make([]interfaces.BackupTokenRecord, 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 + } + + btr := new(interfaces.BackupTokenRecord) + btr.TokenRecord = *tr + btr.EndpointGUID = cnsiGUID + btr.TokenType = "cnsi" + btr.UserGUID = userGUID + + btrs = append(btrs, *btr) + + } + + return btrs, nil +} + func (p *PgsqlTokenRepository) findCNSIToken(cnsiGUID string, userGUID string, encryptionKey []byte, includeDisconnected bool) (interfaces.TokenRecord, error) { log.Debug("findCNSIToken") if cnsiGUID == "" { @@ -545,3 +775,17 @@ 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 05384df430..354baa58b6 100644 --- a/src/jetstream/repository/tokens/tokens.go +++ b/src/jetstream/repository/tokens/tokens.go @@ -18,6 +18,9 @@ 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 SaveCNSIToken(cnsiGUID string, userGUID string, tokenRecord interfaces.TokenRecord, encryptionKey []byte) error diff --git a/src/jetstream/stringutils/utils.go b/src/jetstream/stringutils/utils.go index 2ea9baa5cb..b34c4c22ec 100644 --- a/src/jetstream/stringutils/utils.go +++ b/src/jetstream/stringutils/utils.go @@ -1,9 +1,9 @@ package stringutils import ( + "net/url" "strings" "unicode" - "net/url" ) // ArrayContainsString checks the string array to see if it contains the specifed value From c2189079c289ff960ea093ab721ada0197cbd12e Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Tue, 14 Apr 2020 15:55:34 +0100 Subject: [PATCH 02/12] Convert connection details checkbox to drop down --- .../backup-connection-cell.component.html | 8 + .../backup-connection-cell.component.scss | 0 .../backup-connection-cell.component.spec.ts | 25 +++ .../backup-connection-cell.component.ts | 22 +++ .../backup-endpoints.service.ts | 34 +--- .../backup-endpoints.component.html | 7 +- .../backup-endpoints.component.ts | 29 +-- .../backup-restore-endpoints.service.ts | 179 +----------------- .../restore-endpoints.component.ts | 33 ---- .../endpoints-page.component.html | 7 +- .../features/endpoints/endpoints.module.ts | 5 +- .../endpoint/endpoints-list-config.service.ts | 25 +-- src/jetstream/cnsi.go | 51 ++++- 13 files changed, 152 insertions(+), 273 deletions(-) create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.html create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.scss create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.spec.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.ts 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 new file mode 100644 index 0000000000..752618ca1a --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.html @@ -0,0 +1,8 @@ + + + None + 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.scss b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.spec.ts new file mode 100644 index 0000000000..9f1d8760e4 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BackupConnectionCellComponent } from './backup-connection-cell.component'; + +describe('BackupConnectionCellComponent', () => { + let component: BackupConnectionCellComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ BackupConnectionCellComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(BackupConnectionCellComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 0000000000..60a5190361 --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.ts @@ -0,0 +1,22 @@ +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'; + +@Component({ + selector: 'app-backup-connection-cell', + templateUrl: './backup-connection-cell.component.html', + styleUrls: ['./backup-connection-cell.component.scss'] +}) +export class BackupConnectionCellComponent extends TableCellCustom { + + backupType = BackupEndpointTypes; + connectionTypes = BackupEndpointConnectionTypes; + selected: BackupEndpointConnectionTypes; + + constructor(public service: BackupEndpointsService) { + super(); + } +} 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 80cc1c5f43..9b5479d444 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 @@ -12,6 +12,7 @@ import { EndpointModel } from '../../../../../store/src/types/endpoint.types'; import { BrowserStandardEncoder } from '../../../helper'; import { BackupEndpointConfigUI, + BackupEndpointConnectionTypes, BackupEndpointsConfig, BackupEndpointTypes, BackupRestoreEndpointService, @@ -51,8 +52,7 @@ export class BackupEndpointsService extends BackupRestoreEndpointService { endpoints.forEach(entity => { this.state[entity.guid] = { [BackupEndpointTypes.ENDPOINT]: false, - [BackupEndpointTypes.CONNECT]: false, - [BackupEndpointTypes.ALL_CONNECT]: false, + [BackupEndpointTypes.CONNECT]: BackupEndpointConnectionTypes.NONE, entity }; }); @@ -63,25 +63,19 @@ export class BackupEndpointsService extends BackupRestoreEndpointService { const endpoints = Object.values(this.state); endpoints.forEach(endpoint => { if (!endpoint[BackupEndpointTypes.ENDPOINT]) { - endpoint[BackupEndpointTypes.CONNECT] = false; - endpoint[BackupEndpointTypes.ALL_CONNECT] = false; - } - if (endpoint[BackupEndpointTypes.ALL_CONNECT] && this.canBackup(endpoint.entity, BackupEndpointTypes.CONNECT)) { - endpoint[BackupEndpointTypes.CONNECT] = true; + endpoint[BackupEndpointTypes.CONNECT] = BackupEndpointConnectionTypes.NONE; } }); const hasChanges = !!endpoints.find(endpoint => endpoint[BackupEndpointTypes.ENDPOINT] || - endpoint[BackupEndpointTypes.CONNECT] || - endpoint[BackupEndpointTypes.ALL_CONNECT] + endpoint[BackupEndpointTypes.CONNECT] !== BackupEndpointConnectionTypes.NONE ); this.hasChanges.next(hasChanges); const allChanged = endpoints.every(endpoint => { const e = !this.canBackup(endpoint.entity, BackupEndpointTypes.ENDPOINT) || endpoint[BackupEndpointTypes.ENDPOINT]; - const c = !this.canBackup(endpoint.entity, BackupEndpointTypes.CONNECT) || endpoint[BackupEndpointTypes.CONNECT]; - const aC = !this.canBackup(endpoint.entity, BackupEndpointTypes.ALL_CONNECT) || endpoint[BackupEndpointTypes.ALL_CONNECT]; - return e && c && aC; + const c = !this.canBackup(endpoint.entity, BackupEndpointTypes.CONNECT) || endpoint[BackupEndpointTypes.CONNECT] !== BackupEndpointConnectionTypes.NONE; + return e && c; } ); @@ -105,14 +99,6 @@ export class BackupEndpointsService extends BackupRestoreEndpointService { return false; } - // Are all connection details backed up anyway? - // Does the user have connection details for this endpoint? - if (type === BackupEndpointTypes.CONNECT) { - return !this.state[endpoint.guid][BackupEndpointTypes.ALL_CONNECT] && - endpoint.connectionStatus === 'connected'; - // return !this.service.state[endpoint.guid][BackupRestoreTypes.CONNECT]; - } - return true; } @@ -122,10 +108,7 @@ export class BackupEndpointsService extends BackupRestoreEndpointService { endpoint[BackupEndpointTypes.ENDPOINT] = true; } if (this.canBackup(endpoint.entity, BackupEndpointTypes.CONNECT)) { - endpoint[BackupEndpointTypes.CONNECT] = true; - } - if (this.canBackup(endpoint.entity, BackupEndpointTypes.ALL_CONNECT)) { - endpoint[BackupEndpointTypes.ALL_CONNECT] = true; + endpoint[BackupEndpointTypes.CONNECT] = BackupEndpointConnectionTypes.CURRENT; } }); this.validate(); @@ -134,8 +117,7 @@ export class BackupEndpointsService extends BackupRestoreEndpointService { selectNone() { Object.values(this.state).forEach(endpoint => { endpoint[BackupEndpointTypes.ENDPOINT] = false; - endpoint[BackupEndpointTypes.CONNECT] = false; - endpoint[BackupEndpointTypes.ALL_CONNECT] = false; + endpoint[BackupEndpointTypes.CONNECT] = BackupEndpointConnectionTypes.NONE; }); this.validate(); } diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html index e6dd32385c..14a8f6bf96 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html @@ -22,7 +22,12 @@

Backup Endpoints

Protect the backup by providing as password. You will need this password when restoring from this backup

- + Password + +
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 23f5216ab3..a32fa6f617 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 @@ -7,6 +7,7 @@ import { filter, first, map, tap } from 'rxjs/operators'; import { GetAllEndpoints } from '../../../../../../store/src/actions/endpoint.actions'; import { AppState } from '../../../../../../store/src/app-state'; +import { entityCatalog } from '../../../../../../store/src/entity-catalog/entity-catalog.service'; 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'; @@ -15,6 +16,7 @@ import { ConfirmationDialogService } from '../../../../shared/components/confirm import { ITableListDataSource } from '../../../../shared/components/list/data-sources-controllers/list-data-source-types'; import { ITableColumn } from '../../../../shared/components/list/list-table/table.types'; import { StepOnNextFunction, StepOnNextResult } from '../../../../shared/components/stepper/step/step.component'; +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'; @@ -38,6 +40,13 @@ export class BackupEndpointsComponent implements OnInit { valuePath: 'name' } }, + { + columnId: 'type', + headerCell: () => 'Type', + cellDefinition: { + getValue: this.getEndpointTypeString + }, + }, { columnId: 'endpoint', headerCell: () => 'Backup', @@ -48,19 +57,8 @@ export class BackupEndpointsComponent implements OnInit { }, { columnId: 'connect', - headerCell: () => 'Backup Your Connection Details', - cellComponent: BackupRestoreCellComponent, - cellConfig: { - type: BackupEndpointTypes.CONNECT - } - }, - { - columnId: 'all-connect', - headerCell: () => 'Backup All Users Connection Details', - cellComponent: BackupRestoreCellComponent, - cellConfig: { - type: BackupEndpointTypes.ALL_CONNECT - } + headerCell: () => 'Connection Details', + cellComponent: BackupConnectionCellComponent, }, ]; endpointDataSource: ITableListDataSource; @@ -71,6 +69,7 @@ export class BackupEndpointsComponent implements OnInit { // Step 2 passwordValid$: Observable; passwordForm: FormGroup; + show = false; constructor( public service: BackupEndpointsService, @@ -82,6 +81,7 @@ export class BackupEndpointsComponent implements OnInit { this.setupPasswordStep(); } + setupSelectStep() { const action = new GetAllEndpoints(); const endpointObs = getPaginationObservables({ @@ -181,4 +181,7 @@ export class BackupEndpointsComponent implements OnInit { } + private getEndpointTypeString(endpoint: EndpointModel): string { + return entityCatalog.getEndpoint(endpoint.cnsi_type, endpoint.sub_type).definition.label; + } } 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 index 29b19d45f2..abb46f3628 100644 --- 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 @@ -8,7 +8,12 @@ import { isHttpErrorResponse } from '../../../jetstream.helpers'; export enum BackupEndpointTypes { ENDPOINT = 'endpoint', CONNECT = 'connect', - ALL_CONNECT = 'all_connect' +} + +export enum BackupEndpointConnectionTypes { + NONE = 'NONE', + CURRENT = 'CURRENT', + ALL = 'ALL' } export interface BackupEndpointsConfig { @@ -17,17 +22,13 @@ export interface BackupEndpointsConfig { export interface BaseEndpointConfig { [BackupEndpointTypes.ENDPOINT]: boolean; - [BackupEndpointTypes.CONNECT]: boolean; - [BackupEndpointTypes.ALL_CONNECT]: boolean; + [BackupEndpointTypes.CONNECT]: BackupEndpointConnectionTypes; } -// type BackupEndpoint = Omit; - export interface BackupEndpointConfigUI extends BaseEndpointConfig { entity: EndpointModel; } -// @Injectable() export class BackupRestoreEndpointService { createError(err: any): string { @@ -44,170 +45,4 @@ export class BackupRestoreEndpointService { } return err.message; } - - // hasChanges = new BehaviorSubject(false); - // hasChanges$ = this.hasChanges.asObservable(); - // allChanged = new BehaviorSubject(false); - // allChanged$ = this.allChanged.asObservable(); - - // state: BackupEndpointsConfig = {}; - // password: string; - - - // constructor( - // private store: Store, - // private http: HttpClient - // ) { - - // } - - // // State Related - // initialize(endpoints: EndpointModel[]) { - // endpoints.forEach(entity => { - // this.state[entity.guid] = { - // [BackupRestoreTypes.ENDPOINT]: false, - // [BackupRestoreTypes.CONNECT]: false, - // [BackupRestoreTypes.ALL_CONNECT]: false, - // entity - // }; - // }); - // this.validate(); - // } - - // validate() { - // const endpoints = Object.values(this.state); - // endpoints.forEach(endpoint => { - // if (!endpoint[BackupRestoreTypes.ENDPOINT]) { - // endpoint[BackupRestoreTypes.CONNECT] = false; - // endpoint[BackupRestoreTypes.ALL_CONNECT] = false; - // } - // if (endpoint[BackupRestoreTypes.ALL_CONNECT] && this.canBackup(endpoint.entity, BackupRestoreTypes.CONNECT)) { - // endpoint[BackupRestoreTypes.CONNECT] = true; - // } - // }); - - // const hasChanges = !!endpoints.find(endpoint => - // endpoint[BackupRestoreTypes.ENDPOINT] || - // endpoint[BackupRestoreTypes.CONNECT] || - // endpoint[BackupRestoreTypes.ALL_CONNECT] - // ); - // this.hasChanges.next(hasChanges); - // const allChanged = endpoints.every(endpoint => { - // const e = !this.canBackup(endpoint.entity, BackupRestoreTypes.ENDPOINT) || endpoint[BackupRestoreTypes.ENDPOINT]; - // const c = !this.canBackup(endpoint.entity, BackupRestoreTypes.CONNECT) || endpoint[BackupRestoreTypes.CONNECT]; - // const aC = !this.canBackup(endpoint.entity, BackupRestoreTypes.ALL_CONNECT) || endpoint[BackupRestoreTypes.ALL_CONNECT]; - // return e && c && aC; - // } - - // ); - // this.allChanged.next(allChanged); - // } - - // canBackup(endpoint: EndpointModel, type: BackupRestoreTypes): boolean { - // // Can always back up endpoint - // if (type === BackupRestoreTypes.ENDPOINT) { - // return true; - // } - - // // All other settings require endpoint to be backed up - // if (!this.state[endpoint.guid][BackupRestoreTypes.ENDPOINT]) { - // return false; - // } - - // const epType = entityCatalog.getEndpoint(endpoint.cnsi_type, endpoint.sub_type).definition; - // // The endpoint type supports connection details - // if (epType.unConnectable) { - // return false; - // } - - // // Are all connection details backed up anyway? - // // Does the user have connection details for this endpoint? - // if (type === BackupRestoreTypes.CONNECT) { - // return !this.state[endpoint.guid][BackupRestoreTypes.ALL_CONNECT] && - // endpoint.connectionStatus === 'connected'; - // // return !this.service.state[endpoint.guid][BackupRestoreTypes.CONNECT]; - // } - - // return true; - // } - - // selectAll() { - // Object.values(this.state).forEach(endpoint => { - // if (this.canBackup(endpoint.entity, BackupRestoreTypes.ENDPOINT)) { - // endpoint[BackupRestoreTypes.ENDPOINT] = true; - // } - // if (this.canBackup(endpoint.entity, BackupRestoreTypes.CONNECT)) { - // endpoint[BackupRestoreTypes.CONNECT] = true; - // } - // if (this.canBackup(endpoint.entity, BackupRestoreTypes.ALL_CONNECT)) { - // endpoint[BackupRestoreTypes.ALL_CONNECT] = true; - // } - // }); - // this.validate(); - // } - - // selectNone() { - // Object.values(this.state).forEach(endpoint => { - // endpoint[BackupRestoreTypes.ENDPOINT] = false; - // endpoint[BackupRestoreTypes.CONNECT] = false; - // endpoint[BackupRestoreTypes.ALL_CONNECT] = false; - // }); - // this.validate(); - // } - - // // Request Related - - // createBackup(): Observable { - // const url = '/pp/v1/backup/endpoints'; - // const fromObject = {}; - // const params: HttpParams = new HttpParams({ - // fromObject, - // encoder: new BrowserStandardEncoder() - // }); - - // return this.getSessionData().pipe( - // switchMap(ses => this.http.post(url, this.createBodyToSend(ses), { - // params - // })), - // map(res => { - // console.log('Response: ', res); - // return new Blob([JSON.stringify(res)]); - // }), - // first(), - // ); - // } - - // private createBodyToSend(sd: SessionData): BackupEndpointRequestData { - // const state: BackupEndpointsConfig = Object.entries(this.state).reduce((res, [endpointId, endpoint]) => { - // const { entity, ...rest } = endpoint; - // const requestConfig: BackupEndpointConfigRequest = { - // ...rest, - // }; - // res[endpointId] = requestConfig; - // return res; - // }, {}); - // return { - // state, - // userId: this.getUserIdFromSessionData(sd), - // password: this.password - // }; - // } - - // private getUserIdFromSessionData(sd: SessionData): string { - // if (sd && sd.user) { - // return sd.user.guid; - // } - // return null; - // } - - // private getSessionData(): Observable { - // return this.store.select(s => s.auth).pipe( - // filter(auth => !!(auth && auth.sessionData)), - // map((auth: AuthState) => auth.sessionData), - // first() - // ); - // } - - - } 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 0bfb63a132..e35d5af48a 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 @@ -6,15 +6,10 @@ import { first, map, tap } from 'rxjs/operators'; import { AppState } from '../../../../../../store/src/app-state'; import { PaginationMonitorFactory } from '../../../../../../store/src/monitors/pagination-monitor.factory'; -import { EndpointModel } from '../../../../../../store/src/types/endpoint.types'; import { getEventFiles } from '../../../../core/browser-helper'; 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'; -import { ITableColumn } from '../../../../shared/components/list/list-table/table.types'; import { StepOnNextFunction, StepOnNextResult } from '../../../../shared/components/stepper/step/step.component'; -import { BackupRestoreCellComponent } from '../backup-restore-cell/backup-restore-cell.component'; -import { BackupEndpointTypes } from '../backup-restore-endpoints.service'; import { RestoreEndpointsService } from '../restore-endpoints.service'; @@ -29,34 +24,6 @@ import { RestoreEndpointsService } from '../restore-endpoints.service'; export class RestoreEndpointsComponent implements OnInit { // Step 1 - columns: ITableColumn[] = [ - { - columnId: 'name', - headerCell: () => 'Name', - cellDefinition: { - valuePath: 'name' - } - }, - { - columnId: 'endpoint', - headerCell: () => 'Restore', - cellComponent: BackupRestoreCellComponent, - cellConfig: { - type: BackupEndpointTypes.ENDPOINT, - - } - }, - { - columnId: 'connect', - headerCell: () => 'Include Connection', - cellComponent: BackupRestoreCellComponent, - cellConfig: { - type: BackupEndpointTypes.CONNECT - } - }, - // TODO: RC disable backup token if unconnectable - ]; - endpointDataSource: ITableListDataSource; fileValid$: Observable; fileName: string; diff --git a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html index ee90c52f27..c8e3f991c3 100644 --- a/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html +++ b/src/frontend/packages/core/src/features/endpoints/endpoints-page/endpoints-page.component.html @@ -1,9 +1,14 @@

Endpoints

- +
diff --git a/src/frontend/packages/core/src/features/endpoints/endpoints.module.ts b/src/frontend/packages/core/src/features/endpoints/endpoints.module.ts index c3704479ff..307379946a 100644 --- a/src/frontend/packages/core/src/features/endpoints/endpoints.module.ts +++ b/src/frontend/packages/core/src/features/endpoints/endpoints.module.ts @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core'; import { CoreModule } from '../../core/core.module'; import { SharedModule } from '../../shared/shared.module'; +import { BackupConnectionCellComponent } from './backup-restore/backup-connection-cell/backup-connection-cell.component'; import { BackupEndpointsComponent } from './backup-restore/backup-endpoints/backup-endpoints.component'; import { BackupRestoreCellComponent } from './backup-restore/backup-restore-cell/backup-restore-cell.component'; import { @@ -37,13 +38,15 @@ import { EndpointsRoutingModule } from './endpoints.routing'; BackupEndpointsComponent, RestoreEndpointsComponent, BackupRestoreCellComponent, + BackupConnectionCellComponent, ], entryComponents: [ ConnectEndpointDialogComponent, CredentialsAuthFormComponent, SSOAuthFormComponent, NoneAuthFormComponent, - BackupRestoreCellComponent + BackupRestoreCellComponent, + BackupConnectionCellComponent ] }) export class EndpointsModule { } diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts index 67ef1c43ca..44e7281b6a 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts @@ -1,8 +1,6 @@ import { Injectable } from '@angular/core'; -import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; -import { of } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; +import { filter } from 'rxjs/operators'; import { CFAppState } from '../../../../../../../cloud-foundry/src/cf-app-state'; import { ListView } from '../../../../../../../store/src/actions/list.actions'; @@ -10,13 +8,12 @@ import { entityCatalog } from '../../../../../../../store/src/entity-catalog/ent import { EntityMonitorFactory } from '../../../../../../../store/src/monitors/entity-monitor.factory.service'; import { InternalEventMonitorFactory } from '../../../../../../../store/src/monitors/internal-event-monitor.factory'; import { PaginationMonitorFactory } from '../../../../../../../store/src/monitors/pagination-monitor.factory'; -import { AuthState } from '../../../../../../../store/src/reducers/auth.reducer'; import { EndpointModel } from '../../../../../../../store/src/types/endpoint.types'; import { getFullEndpointApiUrl } from '../../../../../features/endpoints/endpoint-helpers'; import { FavoritesConfigMapper } from '../../../favorites-meta-card/favorite-config-mapper'; import { createTableColumnFavorite } from '../../list-table/table-cell-favorite/table-cell-favorite.component'; import { ITableColumn } from '../../list-table/table.types'; -import { IGlobalListAction, IListAction, IListConfig, ListViewTypes } from '../../list.component.types'; +import { IListAction, IListConfig, ListViewTypes } from '../../list.component.types'; import { EndpointCardComponent } from './endpoint-card/endpoint-card.component'; import { EndpointListHelper } from './endpoint-list.helpers'; import { EndpointsDataSource } from './endpoints-data-source'; @@ -32,19 +29,6 @@ export class EndpointsListConfigService implements IListConfig { private singleActions: IListAction[]; - private globalActions: IGlobalListAction[] = [{ - action: () => this.router.navigate(['endpoints/backup-restore']), - icon: 'settings_backup_restore', - label: 'Backup/Restore Endpoints', - description: 'Backup or Restore Endpoints', - visible$: this.store.select(s => s.auth).pipe( - filter(auth => !!auth && !!auth.sessionData), - map((auth: AuthState) => auth.sessionData), - map(session => session.user && session.user.admin) - ), - enabled$: of(true), // Table is not shown when there's no endpoints, so this should always be true - }]; - public readonly columns: ITableColumn[] = [ { columnId: 'name', @@ -105,7 +89,6 @@ export class EndpointsListConfigService implements IListConfig { } ]; - isLocal = true; dataSource: EndpointsDataSource; viewType = ListViewTypes.BOTH; @@ -123,7 +106,7 @@ export class EndpointsListConfigService implements IListConfig { internalEventMonitorFactory: InternalEventMonitorFactory, endpointListHelper: EndpointListHelper, favoritesConfigMapper: FavoritesConfigMapper, - private router: Router + // private router: Router ) { this.singleActions = endpointListHelper.endpointActions(); const favoriteCell = createTableColumnFavorite( @@ -139,7 +122,7 @@ export class EndpointsListConfigService implements IListConfig { ); } - public getGlobalActions = () => this.globalActions; + public getGlobalActions = () => []; public getMultiActions = () => []; public getSingleActions = () => this.singleActions; public getColumns = () => this.columns; diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 048a912ce8..779645b1ba 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -637,6 +637,14 @@ 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 BackupDataRequest struct { State map[string]BackupEndpointsState `json:"state"` @@ -645,9 +653,8 @@ type BackupDataRequest struct { } type BackupEndpointsState struct { - Endpoint bool `json:"endpoint"` - Connect bool `json:"connect"` - AllConnect bool `json:"all_connect"` + Endpoint bool `json:"endpoint"` + Connect BackupConnectionType `json:"connect"` } type BackupRestoreState struct { @@ -727,7 +734,8 @@ func (p *portalProxy) createBackup(data *BackupDataRequest) (*BackupRestoreState } } - if endpoint.AllConnect { + 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") @@ -736,7 +744,7 @@ func (p *portalProxy) createBackup(data *BackupDataRequest) (*BackupRestoreState log.Warn("No tokens for AllConnect") // TODO: RC } - } else if endpoint.Connect { + case BACKUP_CONNECTION_CURRENT: // userTokenFrom = append(userTokenFrom, endpointID) if tokenRecord, ok := p.GetCNSITokenRecordWithDisconnected(endpointID, data.UserID); ok { log.Warn("tokens for Connect") @@ -759,6 +767,39 @@ func (p *portalProxy) createBackup(data *BackupDataRequest) (*BackupRestoreState // 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) From ca7929bae59641d77cd4587f06118de019ea0bb5 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Tue, 14 Apr 2020 17:25:36 +0100 Subject: [PATCH 03/12] 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) } From 6b9e79a6b976af98ea282d52f28ff4eb6108df71 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Thu, 16 Apr 2020 16:08:04 +0100 Subject: [PATCH 04/12] Tidy up #1 --- .../backup-connection-cell.component.html | 2 +- .../backup-connection-cell.component.ts | 2 +- .../backup-endpoints.service.ts | 25 +- .../backup-endpoints.component.ts | 18 +- .../backup-restore-endpoints.service.ts | 48 --- .../backup-restore/backup-restore.types.ts | 25 ++ .../restore-endpoints.service.ts | 10 +- .../restore-endpoints.component.html | 7 +- .../restore-endpoints.component.ts | 8 +- .../packages/core/src/jetstream.helpers.ts | 17 + src/jetstream/cnsi.go | 336 +----------------- src/jetstream/cnsi_token_backup.go | 303 ++++++++++++++++ src/jetstream/repository/cnsis/pgsql_cnsis.go | 11 - .../repository/tokens/pgsql_tokens.go | 149 -------- src/jetstream/repository/tokens/tokens.go | 2 - 15 files changed, 397 insertions(+), 566 deletions(-) delete mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints.service.ts create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore.types.ts create mode 100644 src/jetstream/cnsi_token_backup.go 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 From d78b23c5d267b0f7eedc91236aa0a4121a217862 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Thu, 16 Apr 2020 18:08:49 +0100 Subject: [PATCH 05/12] Tidying & bug fixes. Handle client_secret --- .../backup-connection-cell.component.html | 5 +- .../backup-connection-cell.component.ts | 13 ++- .../backup-endpoints.service.ts | 14 ++- .../backup-endpoints.component.html | 5 +- .../backup-restore-cell.component.ts | 2 +- .../restore-endpoints.component.ts | 7 +- src/jetstream/cnsi_token_backup.go | 108 ++++++++++-------- 7 files changed, 95 insertions(+), 59 deletions(-) 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 818fadc297..c964d79408 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 @@ -1,8 +1,9 @@ - + None Current User All Users - \ No newline at end of file + +N/A \ 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 528b256c22..67c64ab19e 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 @@ -1,5 +1,6 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { entityCatalog } from '../../../../../../store/src/entity-catalog/entity-catalog.service'; import { EndpointModel } from '../../../../../../store/src/types/endpoint.types'; import { TableCellCustom } from '../../../../shared/components/list/list.types'; import { BackupEndpointsService } from '../backup-endpoints.service'; @@ -10,8 +11,9 @@ import { BackupEndpointConnectionTypes, BackupEndpointTypes } from '../backup-re templateUrl: './backup-connection-cell.component.html', styleUrls: ['./backup-connection-cell.component.scss'] }) -export class BackupConnectionCellComponent extends TableCellCustom { +export class BackupConnectionCellComponent extends TableCellCustom implements OnInit { + connectable = false; backupType = BackupEndpointTypes; connectionTypes = BackupEndpointConnectionTypes; selected: BackupEndpointConnectionTypes; @@ -19,4 +21,11 @@ export class BackupConnectionCellComponent extends TableCellCustom { - const e = !this.canBackup(endpoint.entity, BackupEndpointTypes.ENDPOINT) || endpoint[BackupEndpointTypes.ENDPOINT]; - const c = !this.canBackup(endpoint.entity, BackupEndpointTypes.CONNECT) || endpoint[BackupEndpointTypes.CONNECT] !== BackupEndpointConnectionTypes.NONE; + const e = !this.canBackupEndpoint(endpoint.entity, BackupEndpointTypes.ENDPOINT) || endpoint[BackupEndpointTypes.ENDPOINT]; + const c = !this.canBackupEndpoint(endpoint.entity, BackupEndpointTypes.CONNECT) || endpoint[BackupEndpointTypes.CONNECT] !== BackupEndpointConnectionTypes.NONE; return e && c; } @@ -77,7 +77,7 @@ export class BackupEndpointsService { this.allChanged.next(allChanged); } - canBackup(endpoint: EndpointModel, type: BackupEndpointTypes): boolean { + canBackupEndpoint(endpoint: EndpointModel, type: BackupEndpointTypes): boolean { // Can always back up endpoint if (type === BackupEndpointTypes.ENDPOINT) { return true; @@ -97,12 +97,16 @@ export class BackupEndpointsService { return true; } + canBackup(): boolean { + return !!Object.values(this.state).length; + } + selectAll() { Object.values(this.state).forEach(endpoint => { - if (this.canBackup(endpoint.entity, BackupEndpointTypes.ENDPOINT)) { + if (this.canBackupEndpoint(endpoint.entity, BackupEndpointTypes.ENDPOINT)) { endpoint[BackupEndpointTypes.ENDPOINT] = true; } - if (this.canBackup(endpoint.entity, BackupEndpointTypes.CONNECT)) { + if (this.canBackupEndpoint(endpoint.entity, BackupEndpointTypes.CONNECT)) { endpoint[BackupEndpointTypes.CONNECT] = BackupEndpointConnectionTypes.ALL; } }); diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html index 14a8f6bf96..5344aaa34a 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html @@ -6,7 +6,7 @@

Backup Endpoints

path: '/endpoints/backup-restore' }"> -
+

Select the endpoints and connection details that you would like to backup

+ +

There are no endpoints to backup

+
diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.ts index 97a05e5198..0a1b80d52d 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.ts @@ -20,6 +20,6 @@ export class BackupRestoreCellComponent extends TableCellCustom { } disabled(): boolean { - return !this.service.canBackup(this.row, this.config.type); + return !this.service.canBackupEndpoint(this.row, this.config.type); } } 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 bfd419aad2..53fca1dcc1 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,9 +1,12 @@ import { Component } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatCheckboxChange } from '@angular/material'; +import { Store } from '@ngrx/store'; import { Observable, of, Subject } from 'rxjs'; import { first, map, tap } from 'rxjs/operators'; +import { GetAllEndpoints } from '../../../../../../store/src/actions/endpoint.actions'; +import { GeneralEntityAppState } from '../../../../../../store/src/app-state'; import { getEventFiles } from '../../../../core/browser-helper'; import { httpErrorResponseToSafeString } from '../../../../jetstream.helpers'; import { ConfirmationDialogConfig } from '../../../../shared/components/confirmation-dialog.config'; @@ -28,6 +31,7 @@ export class RestoreEndpointsComponent { show = false; constructor( + private store: Store, public service: RestoreEndpointsService, private confirmDialog: ConfirmationDialogService, ) { @@ -62,7 +66,7 @@ export class RestoreEndpointsComponent { restore: StepOnNextFunction = () => { const confirmation = new ConfirmationDialogConfig( 'Restore', - 'This will overwrite any matching endpoints and connection details', + 'This will overwrite any matching endpoints and connection details.', 'Continue', true ); @@ -75,6 +79,7 @@ export class RestoreEndpointsComponent { }; const restoreSuccess = data => { + this.store.dispatch(new GetAllEndpoints()); result.next({ success: true, redirect: true, diff --git a/src/jetstream/cnsi_token_backup.go b/src/jetstream/cnsi_token_backup.go index 8e23318e48..b3d25e6543 100644 --- a/src/jetstream/cnsi_token_backup.go +++ b/src/jetstream/cnsi_token_backup.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "net/http" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/crypto" "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" @@ -45,20 +46,20 @@ type BackupRequest struct { // BackupContentPayload - Encrypted part of the backup type BackupContentPayload struct { - Endpoints []*interfaces.CNSIRecord + Endpoints []map[string]interface{} 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"` + Payload []byte `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"` + // Data - Content of backup file. This should be of type BackupContent (//TODO: RC test as BackupContent) + Data string `json:"data"` Password string `json:"password"` IgnoreDbVersion bool `json:"ignoreDbVersion"` } @@ -89,7 +90,6 @@ func (ctb *cnsiTokenBackup) BackupEndpoints(c echo.Context) error { 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") @@ -111,7 +111,7 @@ func (ctb *cnsiTokenBackup) createBackup(data *BackupRequest) (*BackupContent, e } // Fetch/Format required data - endpoints := make([]*interfaces.CNSIRecord, 0) + endpoints := make([]map[string]interface{}, 0) tokens := make([]interfaces.BackupTokenRecord, 0) for endpointID, endpoint := range data.State { @@ -122,19 +122,19 @@ func (ctb *cnsiTokenBackup) createBackup(data *BackupRequest) (*BackupContent, e for _, e := range allEndpoints { if endpointID == e.GUID { - endpoints = append(endpoints, e) + endpoints = append(endpoints, serializeEndpoint(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 + text := fmt.Sprintf("Failed to fetch tokens for endpoint %+v", endpointID) + return nil, interfaces.NewHTTPError(http.StatusBadGateway, text) } case BACKUP_CONNECTION_CURRENT: // userTokenFrom = append(userTokenFrom, endpointID) @@ -151,10 +151,10 @@ func (ctb *cnsiTokenBackup) createBackup(data *BackupRequest) (*BackupContent, e 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) + text := fmt.Sprintf("Request to back up connected user's (%+v) token for endpoint (%+v) failed.", endpointID, data.UserID) + return nil, interfaces.NewHTTPError(http.StatusBadGateway, text) } } - } log.Infof("endpoints: %+v", endpoints) // TODO: RC REMOVE @@ -166,7 +166,9 @@ func (ctb *cnsiTokenBackup) createBackup(data *BackupRequest) (*BackupContent, e } // Encrypt data (see above) // TODO: RC leave until last - // encryptedPayload := payload + + bPayload, _ := json.Marshal(payload) + encryptedPayload, err := crypto.EncryptToken(encryptionKey, fmt.Sprintf("%+v", bPayload)) //TODO: RC error handling versions, err := ctb.p.getVersionsData() if err != nil { @@ -176,7 +178,7 @@ func (ctb *cnsiTokenBackup) createBackup(data *BackupRequest) (*BackupContent, e // log.Infof("payload: %+v", payload) response := &BackupContent{ - Payload: *payload, + Payload: encryptedPayload, DBVersion: versions.DatabaseVersion, } @@ -231,7 +233,7 @@ func (ctb *cnsiTokenBackup) restoreBackup(backup *RestoreRequest) error { // TODO: RC Q all errors are NewHTTPError data := &BackupContent{} - if err := json.Unmarshal([]byte(backup.Payload), backup); err != nil { + if err := json.Unmarshal([]byte(backup.Data), data); err != nil { return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid backup - could not parse JSON") } @@ -247,16 +249,25 @@ func (ctb *cnsiTokenBackup) restoreBackup(backup *RestoreRequest) error { } } - unencryptedBackup := data.Payload + unencryptedBackup, err := crypto.DecryptToken(encryptionKey, data.Payload) //TODO: RC error handling, comments + if err != nil { + return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to decrypt payload", "Failed to decrypt payload: %+v", err) + } + + payload := &BackupContentPayload{} + if err = json.Unmarshal([]byte(unencryptedBackup), payload); err != nil { + return interfaces.NewHTTPError(http.StatusBadRequest, "Could not parse 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) + for _, endpoint := range payload.Endpoints { + e := deSerializeEndpoint(endpoint) + if err := cnsiRepo.Overwrite(e, ctb.p.Config.EncryptionKeyInBytes); err != nil { + return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to overwrite endpoints", "Failed to overwrite endpoint: %+v", e.Name) } } @@ -265,7 +276,7 @@ func (ctb *cnsiTokenBackup) restoreBackup(backup *RestoreRequest) error { return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to connect to db", "Failed to connect to db: %+v", err) } - for _, tr := range unencryptedBackup.Tokens { + for _, tr := range payload.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) } @@ -274,30 +285,33 @@ func (ctb *cnsiTokenBackup) restoreBackup(backup *RestoreRequest) error { 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 -// } +// Work around the omission of the client secret when serialising the cnsi record +func serializeEndpoint(endpoint *interfaces.CNSIRecord) map[string]interface{} { + // encode the original + m, _ := json.Marshal(endpoint) + + // decode it back to get a map + var a interface{} + json.Unmarshal(m, &a) + newEndpoint := a.(map[string]interface{}) + + // Replace the map key + newEndpoint["client_secret"] = endpoint.ClientSecret + + return newEndpoint +} + +// Work around the omission of the client secret when serialising the cnsi record +func deSerializeEndpoint(endpoint map[string]interface{}) interfaces.CNSIRecord { + // encode the endpoint map + m, _ := json.Marshal(endpoint) + + // decode it back to get a record with all values except client secret + var a interfaces.CNSIRecord + json.Unmarshal(m, &a) + + // manually add the client secret + a.ClientSecret = fmt.Sprintf("%v", endpoint["client_secret"]) + log.Errorf("CLIENT SECRET: %+v", a.ClientSecret) // TODO: RC REMOVE + return a +} From 2191b38b14292ad0bbecbf875b90716e8185ae14 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Fri, 17 Apr 2020 11:46:36 +0100 Subject: [PATCH 06/12] Add minimum password length --- .../backup-endpoints/backup-endpoints.component.html | 11 +++++++---- .../backup-endpoints/backup-endpoints.component.ts | 2 +- .../restore-endpoints.component.html | 5 +++++ .../restore-endpoints/restore-endpoints.component.ts | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html index 5344aaa34a..7d237225f7 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html @@ -26,15 +26,18 @@

Backup Endpoints

Password - + + + Password is required + + Password must be at least {{passwordForm.controls.password.errors.minlength.requiredLength}} characters +
- - - \ No newline at end of file + \ No newline at end of file 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 882d6113f8..b0a0c1cd9c 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 @@ -120,7 +120,7 @@ export class BackupEndpointsComponent implements OnInit { setupPasswordStep() { this.passwordForm = new FormGroup({ - password: new FormControl('', [Validators.required]), + password: new FormControl('', [Validators.required, Validators.minLength(6)]), }); this.passwordValid$ = this.passwordForm.statusChanges.pipe( map(() => { 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 c99ad6600f..722d3a5e87 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 @@ -37,6 +37,11 @@

Restore Endpoints

[attr.aria-pressed]="!show"> {{!show ? 'visibility_off' : 'visibility'}} + + Password is required + + Password must be at least {{passwordForm.controls.password.errors.minlength.requiredLength}} characters +
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 53fca1dcc1..88430d66e8 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 @@ -40,7 +40,7 @@ export class RestoreEndpointsComponent { setupPasswordStep() { this.passwordForm = new FormGroup({ - password: new FormControl('', [Validators.required]), + password: new FormControl('', [Validators.required, Validators.minLength(6)]), }); this.passwordValid$ = this.passwordForm.statusChanges.pipe( map(() => { From 19fd37a2cd7f2a6c41f2e071493245b5dbfcff4b Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Fri, 17 Apr 2020 11:47:24 +0100 Subject: [PATCH 07/12] First pass encrypt/decrypt --- src/jetstream/cnsi.go | 2 + src/jetstream/cnsi_token_backup.go | 122 +++++++++++++++++++++-------- 2 files changed, 92 insertions(+), 32 deletions(-) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index f5c6c501a3..afaaebf9cf 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -641,6 +641,7 @@ func (p *portalProxy) backupEndpoints(c echo.Context) error { ctb := &cnsiTokenBackup{ databaseConnectionPool: p.DatabaseConnectionPool, + encryptionKey: p.Config.EncryptionKeyInBytes, p: p, } @@ -652,6 +653,7 @@ func (p *portalProxy) restoreEndpoints(c echo.Context) error { ctb := &cnsiTokenBackup{ databaseConnectionPool: p.DatabaseConnectionPool, + encryptionKey: p.Config.EncryptionKeyInBytes, p: p, } diff --git a/src/jetstream/cnsi_token_backup.go b/src/jetstream/cnsi_token_backup.go index b3d25e6543..8eca908ec0 100644 --- a/src/jetstream/cnsi_token_backup.go +++ b/src/jetstream/cnsi_token_backup.go @@ -1,9 +1,9 @@ package main import ( + "crypto/md5" "database/sql" "encoding/json" - "errors" "fmt" "io/ioutil" "net/http" @@ -18,6 +18,7 @@ import ( type cnsiTokenBackup struct { databaseConnectionPool *sql.DB + encryptionKey []byte p *portalProxy } @@ -58,7 +59,7 @@ type BackupContent struct { // RestoreRequest - Request from client to restore content from payload type RestoreRequest struct { - // Data - Content of backup file. This should be of type BackupContent (//TODO: RC test as BackupContent) + // Data - Content of backup file. This should be of type BackupContent Data string `json:"data"` Password string `json:"password"` IgnoreDbVersion bool `json:"ignoreDbVersion"` @@ -70,12 +71,12 @@ func (ctb *cnsiTokenBackup) BackupEndpoints(c echo.Context) error { // Check we can unmarshall the request body, err := ioutil.ReadAll(c.Request().Body) if err != nil { - return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body") + return interfaces.NewHTTPShadowError(http.StatusBadRequest, "Invalid request body", "Invalid request body: %+v", err) } data := &BackupRequest{} if err = json.Unmarshal(body, data); err != nil { - return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body - could not parse JSON") + return interfaces.NewHTTPShadowError(http.StatusBadRequest, "Invalid request body - could not parse JSON", "Invalid request body - could not parse JSON: %+v", err) } if data.State == nil || len(data.State) == 0 { @@ -92,7 +93,7 @@ func (ctb *cnsiTokenBackup) BackupEndpoints(c echo.Context) error { // Send back the response to the client jsonString, err := json.Marshal(response) if err != nil { - return interfaces.NewHTTPError(http.StatusInternalServerError, "Failed to serialize response") + return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to serialize response", "Failed to serialize response: %+v", err) } log.Infof("jsonString: %+v", jsonString) // TODO: RC REMOVE @@ -107,7 +108,7 @@ func (ctb *cnsiTokenBackup) createBackup(data *BackupRequest) (*BackupContent, e log.Debug("createBackup") allEndpoints, err := ctb.p.ListEndpoints() if err != nil { - return nil, interfaces.NewHTTPError(http.StatusBadGateway, "Failed to fetch endpoints") + return nil, interfaces.NewHTTPShadowError(http.StatusBadGateway, "Failed to fetch endpoints", "Failed to fetch endpoints: %+v", err) } // Fetch/Format required data @@ -157,32 +158,30 @@ func (ctb *cnsiTokenBackup) createBackup(data *BackupRequest) (*BackupContent, e } } - 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 - - bPayload, _ := json.Marshal(payload) - encryptedPayload, err := crypto.EncryptToken(encryptionKey, fmt.Sprintf("%+v", bPayload)) //TODO: RC error handling + // Encrypt the entire payload + encryptedPayload, err := encryptPayload(payload, data.Password, ctb.encryptionKey) + if err != nil { + return nil, interfaces.NewHTTPShadowError(http.StatusBadGateway, "Could not encrypt payload", "Could not encrypt payload: %+v", err) + } versions, err := ctb.p.getVersionsData() if err != nil { - return nil, errors.New("Could not find database version") + return nil, interfaces.NewHTTPShadowError(http.StatusBadGateway, "Could not find database version", "Could not find database version: %+v", err) } - // log.Infof("payload: %+v", payload) + // log.Infof("payload: %+v", payload) // TODO: RC REMOVE response := &BackupContent{ Payload: encryptedPayload, DBVersion: versions.DatabaseVersion, } - // log.Infof("response: %+v", response) + // log.Infof("response: %+v", response) // TODO: RC REMOVE return response, nil } @@ -194,7 +193,7 @@ func (ctb *cnsiTokenBackup) getCNSITokenRecordsBackup(endpointID string) ([]inte return make([]interfaces.BackupTokenRecord, 0), false } - trs, err := tokenRepo.FindAllCNSITokenBackup(endpointID, ctb.p.Config.EncryptionKeyInBytes) + trs, err := tokenRepo.FindAllCNSITokenBackup(endpointID, ctb.encryptionKey) if err != nil { return make([]interfaces.BackupTokenRecord, 0), false } @@ -208,12 +207,12 @@ func (ctb *cnsiTokenBackup) RestoreEndpoints(c echo.Context) error { // Check we can unmarshall the request body, err := ioutil.ReadAll(c.Request().Body) if err != nil { - return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body") + return interfaces.NewHTTPShadowError(http.StatusBadRequest, "Invalid request body", "Invalid request body: %+v", err) } data := &RestoreRequest{} if err = json.Unmarshal(body, data); err != nil { - return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body - could not parse JSON") + return interfaces.NewHTTPShadowError(http.StatusBadRequest, "Invalid request body - could not parse JSON", "Invalid request body - could not parse JSON: %+v", err) } err = ctb.restoreBackup(data) @@ -224,41 +223,40 @@ func (ctb *cnsiTokenBackup) RestoreEndpoints(c echo.Context) error { // 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.Data), data); err != nil { - return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid backup - could not parse JSON") + return interfaces.NewHTTPShadowError(http.StatusBadRequest, "Invalid backup - could not parse JSON", "Invalid backup - could not parse JSON: %+v", err) } + // Check that the db version of backup file matches the stratos db version if backup.IgnoreDbVersion == false { versions, err := ctb.p.getVersionsData() if err != nil { - return errors.New("Could not find database version") + return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Could not find database version", "Could not find database version: %+v", err) } 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) + return interfaces.NewHTTPError(http.StatusBadRequest, errorStr) } } - unencryptedBackup, err := crypto.DecryptToken(encryptionKey, data.Payload) //TODO: RC error handling, comments + // Get the actual, unencrypted set of endpoints and tokens + payloadString, err := decryptPayload(data.Payload, backup.Password, ctb.encryptionKey) if err != nil { return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to decrypt payload", "Failed to decrypt payload: %+v", err) } - payload := &BackupContentPayload{} - if err = json.Unmarshal([]byte(unencryptedBackup), payload); err != nil { - return interfaces.NewHTTPError(http.StatusBadRequest, "Could not parse payload") + if err = json.Unmarshal([]byte(*payloadString), payload); err != nil { + return interfaces.NewHTTPShadowError(http.StatusBadRequest, "Failed to parse payload. This could be due to an incorrect password", "Failed to decrypt payload, possible incorrect password: %+v", err) } + // Insert/Update the endpoints and tokens 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) @@ -266,7 +264,7 @@ func (ctb *cnsiTokenBackup) restoreBackup(backup *RestoreRequest) error { for _, endpoint := range payload.Endpoints { e := deSerializeEndpoint(endpoint) - if err := cnsiRepo.Overwrite(e, ctb.p.Config.EncryptionKeyInBytes); err != nil { + if err := cnsiRepo.Overwrite(e, ctb.encryptionKey); err != nil { return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to overwrite endpoints", "Failed to overwrite endpoint: %+v", e.Name) } } @@ -277,7 +275,7 @@ func (ctb *cnsiTokenBackup) restoreBackup(backup *RestoreRequest) error { } for _, tr := range payload.Tokens { - if err := tokenRepo.SaveCNSIToken(tr.EndpointGUID, tr.UserGUID, tr.TokenRecord, ctb.p.Config.EncryptionKeyInBytes); err != nil { + if err := tokenRepo.SaveCNSIToken(tr.EndpointGUID, tr.UserGUID, tr.TokenRecord, ctb.encryptionKey); err != nil { return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to overwrite token", "Failed to overwrite token: %+v", tr.TokenRecord.TokenGUID) } } @@ -312,6 +310,66 @@ func deSerializeEndpoint(endpoint map[string]interface{}) interfaces.CNSIRecord // manually add the client secret a.ClientSecret = fmt.Sprintf("%v", endpoint["client_secret"]) - log.Errorf("CLIENT SECRET: %+v", a.ClientSecret) // TODO: RC REMOVE + // log.Errorf("CLIENT SECRET: %+v", a.ClientSecret) // TODO: RC REMOVE return a } + +func encryptPayload(payload *BackupContentPayload, password string, encryptionKey []byte) ([]byte, error) { + // First ensure the password is an ok length + secret, err := createHash(password, encryptionKey) + if err != nil { + log.Warningf("Could not create hash: %+v", err) + return nil, fmt.Errorf("Could not create hash") + } + + // log.Errorf("password: %+v", password) // TODO: RC REMOVE + // log.Errorf("secret: %+v", secret) // TODO: RC REMOVE + // log.Errorf("payload: %+v", payload) // TODO: RC REMOVE + // Create the text that will be encrypted + payloadBytes, err := json.Marshal(payload) + // log.Errorf("payloadBytes: %+v", string(payloadBytes)) // TODO: RC REMOVE + if err != nil { + return nil, fmt.Errorf("Could not marshal payload: %+v", err) + } + + // Encrypt + payloadEncrypted, err := crypto.EncryptToken(secret, string(payloadBytes)) + // log.Errorf("payloadEncrypted: %+v", string(payloadEncrypted)) // TODO: RC REMOVE + if err != nil { + return nil, fmt.Errorf("Could not encrypt payload: %+v", err) + } + + return payloadEncrypted, nil +} + +func decryptPayload(payloadEncrypted []byte, password string, encryptionKey []byte) (*string, error) { + // First ensure the password is an ok length + secret, err := createHash(password, encryptionKey) + if err != nil { + log.Warningf("Could not create hash: %+v", err) + return nil, fmt.Errorf("Could not create hash") + } + + // log.Errorf("password: %+v", password) // TODO: RC REMOVE + // log.Errorf("secret: %+v", secret) // TODO: RC REMOVE + // log.Errorf("payloadEncrypted: %+v", payloadEncrypted) // TODO: RC REMOVE + payloadUnencrypted, err := crypto.DecryptToken(secret, payloadEncrypted) + if err != nil { + return nil, fmt.Errorf("Failed to decrypt payload: %+v", err) + } + // log.Errorf("payloadUnencrypted: %+v", string(payloadUnencrypted)) // TODO: RC REMOVE + + return &payloadUnencrypted, nil +} + +// createHash - Ensure the token used by crypto is at an acceptable length +func createHash(password string, salt []byte) ([]byte, error) { + hasher := md5.New() + if _, err := hasher.Write([]byte(password)); err != nil { + return nil, fmt.Errorf("Failed to write password to hash") + } + if _, err := hasher.Write(salt); err != nil { + return nil, fmt.Errorf("Failed to write salt to hash") + } + return hasher.Sum(nil), nil +} From 5de414d55581b539faf2e2d08a8af28d582a077e Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Fri, 17 Apr 2020 14:02:45 +0100 Subject: [PATCH 08/12] Tidy up --- .../backup-endpoints.component.html | 2 +- .../backup-endpoints.component.ts | 7 ++-- .../restore-endpoints.component.ts | 5 ++- src/jetstream/cnsi_token_backup.go | 34 ++++--------------- src/jetstream/main.go | 1 - .../repository/tokens/pgsql_tokens.go | 1 - 6 files changed, 12 insertions(+), 38 deletions(-) diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html index 7d237225f7..bfbec70bda 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.html @@ -22,7 +22,7 @@

Backup Endpoints

-

Protect the backup by providing as password. You will need this password when restoring from this backup

+

Protect the backup by providing a password. You will need this password when restoring from this backup

Password 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 b0a0c1cd9c..6a64e5d9f1 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 @@ -3,7 +3,7 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import * as moment from 'moment'; import { Observable, of, Subject } from 'rxjs'; -import { filter, first, map, tap } from 'rxjs/operators'; +import { filter, first, map } from 'rxjs/operators'; import { GetAllEndpoints } from '../../../../../../store/src/actions/endpoint.actions'; import { AppState } from '../../../../../../store/src/app-state'; @@ -136,7 +136,7 @@ export class BackupEndpointsComponent implements OnInit { onNext: StepOnNextFunction = () => { const confirmation = new ConfirmationDialogConfig( 'Backup', - 'This backup contains endpoint connection details. The contents will be encrypted, but please still ensure the safety of the file', + 'The backup that is about to be created may contain credentials, tokens and other sensitive information. Although it is encrypted, you should take the appropriate steps to secure it. ', 'Continue', true ); @@ -180,8 +180,7 @@ export class BackupEndpointsComponent implements OnInit { createBackup(); } - // TODO: RC Remove console.log - return result.asObservable().pipe(tap(console.log)); + return result.asObservable(); } 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 88430d66e8..64b2e5e592 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 @@ -3,7 +3,7 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatCheckboxChange } from '@angular/material'; import { Store } from '@ngrx/store'; import { Observable, of, Subject } from 'rxjs'; -import { first, map, tap } from 'rxjs/operators'; +import { first, map } from 'rxjs/operators'; import { GetAllEndpoints } from '../../../../../../store/src/actions/endpoint.actions'; import { GeneralEntityAppState } from '../../../../../../store/src/app-state'; @@ -99,8 +99,7 @@ export class RestoreEndpointsComponent { this.confirmDialog.openWithCancel(confirmation, createBackup, userCancelledDialog); - // TODO: RC Remove console.log - return result.asObservable().pipe(tap(console.log)); + return result.asObservable(); } } diff --git a/src/jetstream/cnsi_token_backup.go b/src/jetstream/cnsi_token_backup.go index 8eca908ec0..3f4c4f77b9 100644 --- a/src/jetstream/cnsi_token_backup.go +++ b/src/jetstream/cnsi_token_backup.go @@ -68,7 +68,7 @@ type RestoreRequest struct { func (ctb *cnsiTokenBackup) BackupEndpoints(c echo.Context) error { log.Debug("BackupEndpoints") - // Check we can unmarshall the request + // Create the backup request struct from the body body, err := ioutil.ReadAll(c.Request().Body) if err != nil { return interfaces.NewHTTPShadowError(http.StatusBadRequest, "Invalid request body", "Invalid request body: %+v", err) @@ -83,22 +83,18 @@ func (ctb *cnsiTokenBackup) BackupEndpoints(c echo.Context) error { return interfaces.NewHTTPError(http.StatusBadRequest, "Invalid request body - no endpoints to backup") } + // Create 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 + // Send the response back to the client jsonString, err := json.Marshal(response) if err != nil { return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to serialize response", "Failed to serialize response: %+v", err) } - log.Infof("jsonString: %+v", jsonString) // TODO: RC REMOVE - - // Return data c.Response().Header().Set("Content-Type", "application/json") c.Response().Write(jsonString) return nil @@ -131,25 +127,20 @@ func (ctb *cnsiTokenBackup) createBackup(data *BackupRequest) (*BackupContent, e switch connectionType := endpoint.Connect; connectionType { case BACKUP_CONNECTION_ALL: if tokenRecords, ok := ctb.getCNSITokenRecordsBackup(endpointID); ok { - log.Warn("tokens for AllConnect") // TODO: RC REMOVE tokens = append(tokens, tokenRecords...) } else { text := fmt.Sprintf("Failed to fetch tokens for endpoint %+v", endpointID) return nil, interfaces.NewHTTPError(http.StatusBadGateway, text) } 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 { text := fmt.Sprintf("Request to back up connected user's (%+v) token for endpoint (%+v) failed.", endpointID, data.UserID) @@ -158,6 +149,7 @@ func (ctb *cnsiTokenBackup) createBackup(data *BackupRequest) (*BackupContent, e } } + // Create the payload, this will be encrypted payload := &BackupContentPayload{ Endpoints: endpoints, Tokens: tokens, @@ -169,20 +161,17 @@ func (ctb *cnsiTokenBackup) createBackup(data *BackupRequest) (*BackupContent, e return nil, interfaces.NewHTTPShadowError(http.StatusBadGateway, "Could not encrypt payload", "Could not encrypt payload: %+v", err) } + // Add the db version to the response, this will allow client side up front validation versions, err := ctb.p.getVersionsData() if err != nil { return nil, interfaces.NewHTTPShadowError(http.StatusBadGateway, "Could not find database version", "Could not find database version: %+v", err) } - // log.Infof("payload: %+v", payload) // TODO: RC REMOVE - response := &BackupContent{ Payload: encryptedPayload, DBVersion: versions.DatabaseVersion, } - // log.Infof("response: %+v", response) // TODO: RC REMOVE - return response, nil } @@ -204,7 +193,7 @@ func (ctb *cnsiTokenBackup) getCNSITokenRecordsBackup(endpointID string) ([]inte func (ctb *cnsiTokenBackup) RestoreEndpoints(c echo.Context) error { log.Debug("RestoreEndpoints") - // Check we can unmarshall the request + // Create the restore request struct from the body body, err := ioutil.ReadAll(c.Request().Body) if err != nil { return interfaces.NewHTTPShadowError(http.StatusBadRequest, "Invalid request body", "Invalid request body: %+v", err) @@ -220,7 +209,6 @@ func (ctb *cnsiTokenBackup) RestoreEndpoints(c echo.Context) error { return err } - // log.Warnf("BACKUP DATA: %+v", backup) // TODO: RC REMOVE c.Response().WriteHeader(http.StatusOK) return nil } @@ -310,7 +298,6 @@ func deSerializeEndpoint(endpoint map[string]interface{}) interfaces.CNSIRecord // manually add the client secret a.ClientSecret = fmt.Sprintf("%v", endpoint["client_secret"]) - // log.Errorf("CLIENT SECRET: %+v", a.ClientSecret) // TODO: RC REMOVE return a } @@ -322,19 +309,14 @@ func encryptPayload(payload *BackupContentPayload, password string, encryptionKe return nil, fmt.Errorf("Could not create hash") } - // log.Errorf("password: %+v", password) // TODO: RC REMOVE - // log.Errorf("secret: %+v", secret) // TODO: RC REMOVE - // log.Errorf("payload: %+v", payload) // TODO: RC REMOVE // Create the text that will be encrypted payloadBytes, err := json.Marshal(payload) - // log.Errorf("payloadBytes: %+v", string(payloadBytes)) // TODO: RC REMOVE if err != nil { return nil, fmt.Errorf("Could not marshal payload: %+v", err) } // Encrypt payloadEncrypted, err := crypto.EncryptToken(secret, string(payloadBytes)) - // log.Errorf("payloadEncrypted: %+v", string(payloadEncrypted)) // TODO: RC REMOVE if err != nil { return nil, fmt.Errorf("Could not encrypt payload: %+v", err) } @@ -350,14 +332,10 @@ func decryptPayload(payloadEncrypted []byte, password string, encryptionKey []by return nil, fmt.Errorf("Could not create hash") } - // log.Errorf("password: %+v", password) // TODO: RC REMOVE - // log.Errorf("secret: %+v", secret) // TODO: RC REMOVE - // log.Errorf("payloadEncrypted: %+v", payloadEncrypted) // TODO: RC REMOVE payloadUnencrypted, err := crypto.DecryptToken(secret, payloadEncrypted) if err != nil { return nil, fmt.Errorf("Failed to decrypt payload: %+v", err) } - // log.Errorf("payloadUnencrypted: %+v", string(payloadUnencrypted)) // TODO: RC REMOVE return &payloadUnencrypted, nil } diff --git a/src/jetstream/main.go b/src/jetstream/main.go index ea8da57516..53c3045e2b 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -973,7 +973,6 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { // Apply edits for the given endpoint adminGroup.POST("/endpoint/:id", p.updateEndpoint) - // TODO: RC Q better as `/backup` & `/restore`? adminGroup.POST("/endpoints/backup", p.backupEndpoints) adminGroup.POST("/endpoints/restore", p.restoreEndpoints) diff --git a/src/jetstream/repository/tokens/pgsql_tokens.go b/src/jetstream/repository/tokens/pgsql_tokens.go index 427541747c..32c779e355 100644 --- a/src/jetstream/repository/tokens/pgsql_tokens.go +++ b/src/jetstream/repository/tokens/pgsql_tokens.go @@ -348,7 +348,6 @@ func (p *PgsqlTokenRepository) FindAllCNSITokenBackup(cnsiGUID string, encryptio return make([]interfaces.BackupTokenRecord, 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() btrs := make([]interfaces.BackupTokenRecord, 0) From 68ef3c350dca9f7dd0db989168baeadb64a223b4 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Mon, 20 Apr 2020 13:48:09 +0100 Subject: [PATCH 09/12] Fix unit tests --- .../backup-connection-cell.component.spec.ts | 22 +++++++++++++++-- .../backup-endpoints.service.ts | 2 +- .../backup-endpoints.component.spec.ts | 14 +++++++++-- .../backup-restore-cell.component.spec.ts | 24 +++++++++++++++++-- ...backup-restore-endpoints.component.spec.ts | 14 +++++++++-- .../restore-endpoints.service.ts | 3 ++- .../restore-endpoints.component.spec.ts | 14 +++++++++-- 7 files changed, 81 insertions(+), 12 deletions(-) diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.spec.ts index 9f1d8760e4..09028f8d35 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.spec.ts +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-connection-cell/backup-connection-cell.component.spec.ts @@ -1,5 +1,8 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { EndpointModel } from '../../../../../../store/src/types/endpoint.types'; +import { BaseTestModulesNoShared } from '../../../../../test-framework/core-test.helper'; +import { BackupEndpointsService } from '../backup-endpoints.service'; import { BackupConnectionCellComponent } from './backup-connection-cell.component'; describe('BackupConnectionCellComponent', () => { @@ -8,14 +11,29 @@ describe('BackupConnectionCellComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ BackupConnectionCellComponent ] + declarations: [ + BackupConnectionCellComponent, + ], + imports: [ + ...BaseTestModulesNoShared + ], + providers: [ + BackupEndpointsService + ] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(BackupConnectionCellComponent); component = fixture.componentInstance; + component.row = { + guid: 'test', + cnsi_type: 'metrics', + } as EndpointModel; + component.service.initialize([{ + guid: 'test' + } as EndpointModel]); fixture.detectChanges(); }); 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 faffbeeef1..adff809338 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 @@ -84,7 +84,7 @@ export class BackupEndpointsService { } // All other settings require endpoint to be backed up - if (!this.state[endpoint.guid][BackupEndpointTypes.ENDPOINT]) { + if (!this.state[endpoint.guid] || !this.state[endpoint.guid][BackupEndpointTypes.ENDPOINT]) { return false; } diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.spec.ts index 16cf213f81..3ccb5308ed 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.spec.ts +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-endpoints/backup-endpoints.component.spec.ts @@ -1,5 +1,8 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TabNavService } from '../../../../../tab-nav.service'; +import { BaseTestModulesNoShared } from '../../../../../test-framework/core-test.helper'; +import { SharedModule } from '../../../../shared/shared.module'; import { BackupEndpointsComponent } from './backup-endpoints.component'; describe('BackupEndpointsComponent', () => { @@ -8,9 +11,16 @@ describe('BackupEndpointsComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ BackupEndpointsComponent ] + declarations: [BackupEndpointsComponent], + imports: [ + ...BaseTestModulesNoShared, + SharedModule + ], + providers: [ + TabNavService + ] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.spec.ts index 1809ff8d89..4a8ded6022 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.spec.ts +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.spec.ts @@ -1,5 +1,9 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { EndpointModel } from '../../../../../../store/src/types/endpoint.types'; +import { BaseTestModulesNoShared } from '../../../../../test-framework/core-test.helper'; +import { BackupEndpointsService } from '../backup-endpoints.service'; +import { BackupEndpointTypes } from '../backup-restore.types'; import { BackupRestoreCellComponent } from './backup-restore-cell.component'; describe('BackupRestoreCellComponent', () => { @@ -8,14 +12,30 @@ describe('BackupRestoreCellComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ BackupRestoreCellComponent ] + declarations: [BackupRestoreCellComponent], + imports: [ + ...BaseTestModulesNoShared + ], + providers: [ + BackupEndpointsService + ] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(BackupRestoreCellComponent); component = fixture.componentInstance; + component.config = { + type: BackupEndpointTypes.ENDPOINT + }; + component.row = { + guid: 'test', + cnsi_type: 'metrics', + } as EndpointModel; + component.service.initialize([{ + guid: 'test' + } as EndpointModel]); fixture.detectChanges(); }); diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.spec.ts index 587eefc846..528a5ee7a9 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.spec.ts +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.spec.ts @@ -1,5 +1,8 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TabNavService } from '../../../../../tab-nav.service'; +import { BaseTestModulesNoShared } from '../../../../../test-framework/core-test.helper'; +import { SharedModule } from '../../../../shared/shared.module'; import { BackupRestoreEndpointsComponent } from './backup-restore-endpoints.component'; describe('BackupRestoreEndpointsComponent', () => { @@ -8,9 +11,16 @@ describe('BackupRestoreEndpointsComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ BackupRestoreEndpointsComponent ] + declarations: [BackupRestoreEndpointsComponent], + imports: [ + ...BaseTestModulesNoShared, + SharedModule + ], + providers: [ + TabNavService + ], }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { 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 50d678f33d..074111bbac 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 @@ -2,7 +2,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; +import { filter, map, switchMap } from 'rxjs/operators'; import { GeneralEntityAppState } from '../../../../../store/src/app-state'; import { selectSessionData } from '../../../../../store/src/reducers/auth.reducer'; @@ -53,6 +53,7 @@ export class RestoreEndpointsService { private setupStep1() { this.currentDbVersion$ = this.store.select(selectSessionData()).pipe( + filter(sd => !!sd), map((sd: SessionData) => sd.version.database_version) ); diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.spec.ts index b260285638..ff141f7953 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.spec.ts +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.spec.ts @@ -1,5 +1,8 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TabNavService } from '../../../../../tab-nav.service'; +import { BaseTestModulesNoShared } from '../../../../../test-framework/core-test.helper'; +import { SharedModule } from '../../../../shared/shared.module'; import { RestoreEndpointsComponent } from './restore-endpoints.component'; describe('RestoreEndpointsComponent', () => { @@ -8,9 +11,16 @@ describe('RestoreEndpointsComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ RestoreEndpointsComponent ] + declarations: [RestoreEndpointsComponent], + imports: [ + ...BaseTestModulesNoShared, + SharedModule + ], + providers: [ + TabNavService + ] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { From 5736f8477ef0a561d9424b17a7ee8654b4584aed Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Mon, 20 Apr 2020 13:49:37 +0100 Subject: [PATCH 10/12] Re-add tests that now work --- .../autoscaler-metric-page.component.spec.ts | 10 ++-------- .../autoscaler-scale-history-page.component.spec.ts | 9 ++------- .../application-instance-chart.component.spec.ts | 3 +-- .../tabs/metrics-tab/metrics-tab.component.spec.ts | 3 +-- .../cloud-foundry-cell-apps.component.spec.ts | 7 ++----- .../cloud-foundry-cell-base.component.spec.ts | 11 +++++------ .../cloud-foundry-cell-charts.component.spec.ts | 7 ++----- .../cloud-foundry-cell-summary.component.spec.ts | 9 +++------ .../connect-endpoint-dialog.component.spec.ts | 9 +++++---- .../connect-endpoint.component.spec.ts | 5 ++--- .../create-endpoint-cf-step-1.component.spec.ts | 5 ++--- .../create-endpoint.component.spec.ts | 12 +++++++----- .../metrics/metrics/metrics.component.spec.ts | 5 ++--- .../metrics-chart/metrics-chart.component.spec.ts | 4 ++-- 14 files changed, 38 insertions(+), 61 deletions(-) diff --git a/src/frontend/packages/cf-autoscaler/src/features/autoscaler-metric-page/autoscaler-metric-page.component.spec.ts b/src/frontend/packages/cf-autoscaler/src/features/autoscaler-metric-page/autoscaler-metric-page.component.spec.ts index 0c897ce7f2..e59ab50816 100644 --- a/src/frontend/packages/cf-autoscaler/src/features/autoscaler-metric-page/autoscaler-metric-page.component.spec.ts +++ b/src/frontend/packages/cf-autoscaler/src/features/autoscaler-metric-page/autoscaler-metric-page.component.spec.ts @@ -2,18 +2,17 @@ import { DatePipe } from '@angular/common'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; +import { createEmptyStoreModule } from '@stratos/store/testing'; import { ApplicationService } from '../../../../cloud-foundry/src/features/applications/application.service'; import { CoreModule } from '../../../../core/src/core/core.module'; import { SharedModule } from '../../../../core/src/shared/shared.module'; import { TabNavService } from '../../../../core/tab-nav.service'; import { ApplicationServiceMock } from '../../../../core/test-framework/application-service-helper'; -import { createEmptyStoreModule } from '@stratos/store/testing'; import { CfAutoscalerTestingModule } from '../../cf-autoscaler-testing.module'; import { AutoscalerMetricPageComponent } from './autoscaler-metric-page.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('AutoscalerMetricPageComponent', () => { +describe('AutoscalerMetricPageComponent', () => { let component: AutoscalerMetricPageComponent; let fixture: ComponentFixture; @@ -47,10 +46,5 @@ xdescribe('AutoscalerMetricPageComponent', () => { expect(component).toBeTruthy(); }); - // TODO: Fix after metrics has been sorted - STRAT-152 (cause of `Cannot read property 'getEntityMonitor' of undefined` test failure) - it('Blocked', () => { - fail('Blocked: Requires metrics to be working (specifically metrics entities)'); - }); - afterAll(() => { }); }); diff --git a/src/frontend/packages/cf-autoscaler/src/features/autoscaler-scale-history-page/autoscaler-scale-history-page.component.spec.ts b/src/frontend/packages/cf-autoscaler/src/features/autoscaler-scale-history-page/autoscaler-scale-history-page.component.spec.ts index 55eead24ce..043a399200 100644 --- a/src/frontend/packages/cf-autoscaler/src/features/autoscaler-scale-history-page/autoscaler-scale-history-page.component.spec.ts +++ b/src/frontend/packages/cf-autoscaler/src/features/autoscaler-scale-history-page/autoscaler-scale-history-page.component.spec.ts @@ -2,18 +2,17 @@ import { DatePipe } from '@angular/common'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; +import { createEmptyStoreModule } from '@stratos/store/testing'; import { ApplicationService } from '../../../../cloud-foundry/src/features/applications/application.service'; import { CoreModule } from '../../../../core/src/core/core.module'; import { SharedModule } from '../../../../core/src/shared/shared.module'; import { TabNavService } from '../../../../core/tab-nav.service'; import { ApplicationServiceMock } from '../../../../core/test-framework/application-service-helper'; -import { createEmptyStoreModule } from '@stratos/store/testing'; import { CfAutoscalerTestingModule } from '../../cf-autoscaler-testing.module'; import { AutoscalerScaleHistoryPageComponent } from './autoscaler-scale-history-page.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('AutoscalerScaleHistoryPageComponent', () => { +describe('AutoscalerScaleHistoryPageComponent', () => { let component: AutoscalerScaleHistoryPageComponent; let fixture: ComponentFixture; @@ -47,9 +46,5 @@ xdescribe('AutoscalerScaleHistoryPageComponent', () => { expect(component).toBeTruthy(); }); - // TODO: Fix after metrics has been sorted - STRAT-152 (cause of `Cannot read property 'getEntityMonitor' of undefined` test failure) - it('Blocked', () => { - fail('Blocked: Requires metrics to be working (specifically metrics entities)'); - }); }); diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/application/application-instance-chart/application-instance-chart.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/applications/application/application-instance-chart/application-instance-chart.component.spec.ts index 4680ae8b5c..ebdde598a6 100644 --- a/src/frontend/packages/cloud-foundry/src/features/applications/application/application-instance-chart/application-instance-chart.component.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/features/applications/application/application-instance-chart/application-instance-chart.component.spec.ts @@ -7,8 +7,7 @@ import { SharedModule } from '../../../../../../core/src/shared/shared.module'; import { generateCfStoreModules } from '../../../../../test-framework/cloud-foundry-endpoint-service.helper'; import { ApplicationInstanceChartComponent } from './application-instance-chart.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('ApplicationInstanceChartComponent', () => { +describe('ApplicationInstanceChartComponent', () => { let component: ApplicationInstanceChartComponent; let fixture: ComponentFixture; diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/tabs/metrics-tab/metrics-tab.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/tabs/metrics-tab/metrics-tab.component.spec.ts index bb8752a65f..fdc73fff41 100644 --- a/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/tabs/metrics-tab/metrics-tab.component.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/tabs/metrics-tab/metrics-tab.component.spec.ts @@ -17,8 +17,7 @@ import { applicationEntityType } from '../../../../../../cf-entity-types'; import { ApplicationEnvVarsHelper } from '../build-tab/application-env-vars.service'; import { MetricsTabComponent } from './metrics-tab.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('MetricsTabComponent', () => { +describe('MetricsTabComponent', () => { let component: MetricsTabComponent; let fixture: ComponentFixture; const appId = '1'; diff --git a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-apps/cloud-foundry-cell-apps.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-apps/cloud-foundry-cell-apps.component.spec.ts index ba1472e058..af4480e9dc 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-apps/cloud-foundry-cell-apps.component.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-apps/cloud-foundry-cell-apps.component.spec.ts @@ -1,13 +1,10 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { - generateCfBaseTestModules, -} from '../../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; +import { generateCfBaseTestModules } from '../../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; import { ActiveRouteCfCell } from '../../../../cf-page.types'; import { CloudFoundryCellAppsComponent } from './cloud-foundry-cell-apps.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('CloudFoundryCellAppsComponent', () => { +describe('CloudFoundryCellAppsComponent', () => { let component: CloudFoundryCellAppsComponent; let fixture: ComponentFixture; diff --git a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-base/cloud-foundry-cell-base.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-base/cloud-foundry-cell-base.component.spec.ts index 93d7b9a131..205c8a732e 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-base/cloud-foundry-cell-base.component.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-base/cloud-foundry-cell-base.component.spec.ts @@ -1,16 +1,14 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TabNavService } from '../../../../../../../../core/tab-nav.service'; -import { - generateCfBaseTestModules, -} from '../../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; +import { generateCfBaseTestModules } from '../../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; +import { CfUserService } from '../../../../../../shared/data-services/cf-user.service'; import { ActiveRouteCfOrgSpace } from '../../../../cf-page.types'; import { CloudFoundryEndpointService } from '../../../../services/cloud-foundry-endpoint.service'; import { CloudFoundryCellService } from '../cloud-foundry-cell.service'; import { CloudFoundryCellBaseComponent } from './cloud-foundry-cell-base.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('CloudFoundryCellBaseComponent', () => { +describe('CloudFoundryCellBaseComponent', () => { let component: CloudFoundryCellBaseComponent; let fixture: ComponentFixture; @@ -22,7 +20,8 @@ xdescribe('CloudFoundryCellBaseComponent', () => { CloudFoundryEndpointService, CloudFoundryCellService, ActiveRouteCfOrgSpace, - TabNavService + TabNavService, + CfUserService ] }) .compileComponents(); diff --git a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-charts/cloud-foundry-cell-charts.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-charts/cloud-foundry-cell-charts.component.spec.ts index bf10e45c66..047523224d 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-charts/cloud-foundry-cell-charts.component.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-charts/cloud-foundry-cell-charts.component.spec.ts @@ -1,14 +1,11 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { - generateCfBaseTestModules, -} from '../../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; +import { generateCfBaseTestModules } from '../../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; import { ActiveRouteCfCell } from '../../../../cf-page.types'; import { CloudFoundryCellService } from '../cloud-foundry-cell.service'; import { CloudFoundryCellChartsComponent } from './cloud-foundry-cell-charts.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('CloudFoundryCellChartsComponent', () => { +describe('CloudFoundryCellChartsComponent', () => { let component: CloudFoundryCellChartsComponent; let fixture: ComponentFixture; diff --git a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-summary/cloud-foundry-cell-summary.component.spec.ts b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-summary/cloud-foundry-cell-summary.component.spec.ts index 8bf289dfb4..e8c160cbcf 100644 --- a/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-summary/cloud-foundry-cell-summary.component.spec.ts +++ b/src/frontend/packages/cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-cells/cloud-foundry-cell/cloud-foundry-cell-summary/cloud-foundry-cell-summary.component.spec.ts @@ -11,14 +11,12 @@ import { MetricsChartHelpers, } from '../../../../../../../../core/src/shared/components/metrics-chart/metrics.component.helpers'; import { MetricQueryType } from '../../../../../../../../core/src/shared/services/metrics-range-selector.types'; -import { - generateCfBaseTestModules, -} from '../../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; import { MetricQueryConfig } from '../../../../../../../../store/src/actions/metrics.actions'; +import { generateCfBaseTestModules } from '../../../../../../../test-framework/cloud-foundry-endpoint-service.helper'; +import { FetchCFCellMetricsAction } from '../../../../../../actions/cf-metrics.actions'; import { ActiveRouteCfCell } from '../../../../cf-page.types'; import { CloudFoundryCellService } from '../cloud-foundry-cell.service'; import { CloudFoundryCellSummaryComponent } from './cloud-foundry-cell-summary.component'; -import { FetchCFCellMetricsAction } from '../../../../../../actions/cf-metrics.actions'; class MockCloudFoundryCellService { cfGuid = 'cfGuid'; @@ -60,8 +58,7 @@ class MockCloudFoundryCellService { } -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('CloudFoundryCellSummaryComponent', () => { +describe('CloudFoundryCellSummaryComponent', () => { let component: CloudFoundryCellSummaryComponent; let fixture: ComponentFixture; diff --git a/src/frontend/packages/core/src/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.spec.ts index 5f93a9992b..93034184a4 100644 --- a/src/frontend/packages/core/src/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.spec.ts +++ b/src/frontend/packages/core/src/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.spec.ts @@ -4,10 +4,11 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; +import { createBasicStoreModule } from '@stratos/store/testing'; import { CoreTestingModule } from '../../../../test-framework/core-test.modules'; -import { createBasicStoreModule } from '@stratos/store/testing'; import { CoreModule } from '../../../core/core.module'; +import { SidePanelService } from '../../../shared/services/side-panel.service'; import { SharedModule } from '../../../shared/shared.module'; import { ConnectEndpointComponent } from '../connect-endpoint/connect-endpoint.component'; import { ConnectEndpointConfig } from '../connect.service'; @@ -25,8 +26,7 @@ class MatDialogDataMock implements ConnectEndpointConfig { ssoAllowed = false; } -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('ConnectEndpointDialogComponent', () => { +describe('ConnectEndpointDialogComponent', () => { let component: ConnectEndpointDialogComponent; let fixture: ComponentFixture; @@ -34,7 +34,8 @@ xdescribe('ConnectEndpointDialogComponent', () => { const testingModule = TestBed.configureTestingModule({ providers: [ { provide: MatDialogRef, useClass: MatDialogRefMock }, - { provide: MAT_DIALOG_DATA, useClass: MatDialogDataMock } + { provide: MAT_DIALOG_DATA, useClass: MatDialogDataMock }, + SidePanelService ], declarations: [ ConnectEndpointDialogComponent, diff --git a/src/frontend/packages/core/src/features/endpoints/connect-endpoint/connect-endpoint.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/connect-endpoint/connect-endpoint.component.spec.ts index 3097c898b3..d265f5b75b 100644 --- a/src/frontend/packages/core/src/features/endpoints/connect-endpoint/connect-endpoint.component.spec.ts +++ b/src/frontend/packages/core/src/features/endpoints/connect-endpoint/connect-endpoint.component.spec.ts @@ -1,14 +1,13 @@ import { CommonModule } from '@angular/common'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { createBasicStoreModule } from '@stratos/store/testing'; import { CoreTestingModule } from '../../../../test-framework/core-test.modules'; -import { createBasicStoreModule } from '@stratos/store/testing'; import { CoreModule } from '../../../core/core.module'; import { SharedModule } from '../../../shared/shared.module'; import { ConnectEndpointComponent } from './connect-endpoint.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('ConnectEndpointComponent', () => { +describe('ConnectEndpointComponent', () => { let component: ConnectEndpointComponent; let fixture: ComponentFixture; diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.spec.ts index 0c9396218b..0d1c1e7a2c 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.spec.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-cf-step-1/create-endpoint-cf-step-1.component.spec.ts @@ -1,15 +1,14 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ActivatedRoute } from '@angular/router'; +import { createBasicStoreModule } from '@stratos/store/testing'; import { CoreTestingModule } from '../../../../../test-framework/core-test.modules'; -import { createBasicStoreModule } from '@stratos/store/testing'; import { CoreModule } from '../../../../core/core.module'; import { SharedModule } from '../../../../shared/shared.module'; import { CreateEndpointCfStep1Component } from './create-endpoint-cf-step-1.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('CreateEndpointCfStep1Component', () => { +describe('CreateEndpointCfStep1Component', () => { let component: CreateEndpointCfStep1Component; let fixture: ComponentFixture; diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.spec.ts index 36294cefcf..dda3a78ace 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.spec.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.spec.ts @@ -1,21 +1,21 @@ +import { HttpClientModule } from '@angular/common/http'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; +import { createBasicStoreModule } from '@stratos/store/testing'; import { TabNavService } from '../../../../tab-nav.service'; import { CoreTestingModule } from '../../../../test-framework/core-test.modules'; -import { createBasicStoreModule } from '@stratos/store/testing'; import { CoreModule } from '../../../core/core.module'; +import { SidePanelService } from '../../../shared/services/side-panel.service'; import { SharedModule } from '../../../shared/shared.module'; import { ConnectEndpointComponent } from '../connect-endpoint/connect-endpoint.component'; import { CreateEndpointCfStep1Component } from './create-endpoint-cf-step-1/create-endpoint-cf-step-1.component'; import { CreateEndpointConnectComponent } from './create-endpoint-connect/create-endpoint-connect.component'; import { CreateEndpointComponent } from './create-endpoint.component'; -import { HttpClientModule } from '@angular/common/http'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('CreateEndpointComponent', () => { +describe('CreateEndpointComponent', () => { let component: CreateEndpointComponent; let fixture: ComponentFixture; @@ -47,7 +47,9 @@ xdescribe('CreateEndpointComponent', () => { } } } - }, TabNavService], + }, + TabNavService, + SidePanelService], }) .compileComponents(); })); diff --git a/src/frontend/packages/core/src/features/metrics/metrics/metrics.component.spec.ts b/src/frontend/packages/core/src/features/metrics/metrics/metrics.component.spec.ts index 34602f97ab..f0097b1e8e 100644 --- a/src/frontend/packages/core/src/features/metrics/metrics/metrics.component.spec.ts +++ b/src/frontend/packages/core/src/features/metrics/metrics/metrics.component.spec.ts @@ -2,17 +2,16 @@ import { CommonModule } from '@angular/common'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; +import { createBasicStoreModule } from '@stratos/store/testing'; import { TabNavService } from '../../../../tab-nav.service'; import { CoreTestingModule } from '../../../../test-framework/core-test.modules'; -import { createBasicStoreModule } from '@stratos/store/testing'; import { CoreModule } from '../../../core/core.module'; import { SharedModule } from '../../../shared/shared.module'; import { MetricsService } from '../services/metrics-service'; import { MetricsComponent } from './metrics.component'; -// TODO: Fix after metrics has been sorted - STRAT-152 -xdescribe('MetricsComponent', () => { +describe('MetricsComponent', () => { let component: MetricsComponent; let fixture: ComponentFixture; diff --git a/src/frontend/packages/core/src/shared/components/metrics-chart/metrics-chart.component.spec.ts b/src/frontend/packages/core/src/shared/components/metrics-chart/metrics-chart.component.spec.ts index 36029be8ae..98fa58a275 100644 --- a/src/frontend/packages/core/src/shared/components/metrics-chart/metrics-chart.component.spec.ts +++ b/src/frontend/packages/core/src/shared/components/metrics-chart/metrics-chart.component.spec.ts @@ -1,15 +1,15 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { createBasicStoreModule } from '@stratos/store/testing'; +import { FetchApplicationMetricsAction } from '../../../../../cloud-foundry/src/actions/cf-metrics.actions'; import { MetricQueryConfig } from '../../../../../store/src/actions/metrics.actions'; import { CoreTestingModule } from '../../../../test-framework/core-test.modules'; -import { createBasicStoreModule } from '@stratos/store/testing'; import { CoreModule } from '../../../core/core.module'; import { MDAppModule } from '../../../core/md.module'; import { SharedModule } from '../../shared.module'; import { MetricsChartComponent } from './metrics-chart.component'; import { MetricsLineChartConfig } from './metrics-chart.types'; -import { FetchApplicationMetricsAction } from '../../../../../cloud-foundry/src/actions/cf-metrics.actions'; // TODO: Fix after metrics has been sorted - STRAT-152 xdescribe('MetricsChartComponent', () => { From de03a0131ea651060b41359615e623b075883007 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Mon, 20 Apr 2020 15:12:58 +0100 Subject: [PATCH 11/12] Changes following review --- .../packages/core/sass/_all-theme.scss | 4 +- .../backup-checkbox-cell.component.html} | 0 .../backup-checkbox-cell.component.scss} | 0 .../backup-checkbox-cell.component.spec.ts} | 12 ++-- .../backup-checkbox-cell.component.ts} | 10 +-- .../backup-connection-cell.component.html | 3 +- .../backup-connection-cell.component.ts | 11 +++- .../backup-endpoints.service.ts | 63 +++++++++---------- .../backup-endpoints.component.ts | 21 +++---- .../backup-restore-endpoints.component.html | 3 +- .../backup-restore-endpoints.component.ts | 2 - .../restore-endpoints.service.ts | 25 ++++---- .../restore-endpoints.component.html | 24 ++++--- .../restore-endpoints.component.scss | 9 +++ .../restore-endpoints.component.theme.scss | 11 ++++ .../restore-endpoints.component.ts | 6 +- .../features/endpoints/endpoints.module.ts | 6 +- .../endpoint/endpoints-list-config.service.ts | 1 - .../store/src/types/endpoint.types.ts | 2 + src/jetstream/authcnsi.go | 2 +- src/jetstream/cnsi.go | 14 ++++- src/jetstream/cnsi_token_backup.go | 60 ++++++++---------- src/jetstream/repository/cnsis/cnsis.go | 2 +- src/jetstream/repository/cnsis/pgsql_cnsis.go | 5 +- .../repository/tokens/pgsql_tokens.go | 1 + src/jetstream/repository/tokens/tokens.go | 3 +- 26 files changed, 168 insertions(+), 132 deletions(-) rename src/frontend/packages/core/src/features/endpoints/backup-restore/{backup-restore-cell/backup-restore-cell.component.html => backup-checkbox-cell/backup-checkbox-cell.component.html} (100%) rename src/frontend/packages/core/src/features/endpoints/backup-restore/{backup-restore-cell/backup-restore-cell.component.scss => backup-checkbox-cell/backup-checkbox-cell.component.scss} (100%) rename src/frontend/packages/core/src/features/endpoints/backup-restore/{backup-restore-cell/backup-restore-cell.component.spec.ts => backup-checkbox-cell/backup-checkbox-cell.component.spec.ts} (74%) rename src/frontend/packages/core/src/features/endpoints/backup-restore/{backup-restore-cell/backup-restore-cell.component.ts => backup-checkbox-cell/backup-checkbox-cell.component.ts} (64%) create mode 100644 src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.theme.scss diff --git a/src/frontend/packages/core/sass/_all-theme.scss b/src/frontend/packages/core/sass/_all-theme.scss index 3daaa41180..a6fdef57e5 100644 --- a/src/frontend/packages/core/sass/_all-theme.scss +++ b/src/frontend/packages/core/sass/_all-theme.scss @@ -65,7 +65,8 @@ @import '../../cloud-foundry/src/features/cloud-foundry/tabs/cloud-foundry-firehose/cloud-foundry-firehose.component.theme'; @import '../../cloud-foundry/src/features/service-catalog/service-catalog-page/service-catalog-page.component.theme'; @import '../../cloud-foundry/src/features/applications/application-wall/application-wall.component.theme'; -@import '../../core/src/features/error-page/error-page/error-page.component.theme.scss'; +@import '../../core/src/features/error-page/error-page/error-page.component.theme'; +@import '../../core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.theme'; // Defaults $side-nav-light-text: #fff; @@ -159,6 +160,7 @@ $side-nav-light-active: #484848; @include code-block-theme($theme, $app-theme); @include copy-to-clipboard-theme($theme, $app-theme); @include app-user-avatar-theme($theme, $app-theme); + @include restore-endpoints-theme($theme, $app-theme) } @function app-generate-nav-theme($theme, $nav-theme: null) { diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.html b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.html similarity index 100% rename from src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.html rename to src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.html diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.scss b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.scss similarity index 100% rename from src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.scss rename to src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.scss diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.spec.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.spec.ts similarity index 74% rename from src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.spec.ts rename to src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.spec.ts index 4a8ded6022..448a1eac17 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.spec.ts +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.spec.ts @@ -4,15 +4,15 @@ import { EndpointModel } from '../../../../../../store/src/types/endpoint.types' import { BaseTestModulesNoShared } from '../../../../../test-framework/core-test.helper'; import { BackupEndpointsService } from '../backup-endpoints.service'; import { BackupEndpointTypes } from '../backup-restore.types'; -import { BackupRestoreCellComponent } from './backup-restore-cell.component'; +import { BackupCheckboxCellComponent } from './backup-checkbox-cell.component'; -describe('BackupRestoreCellComponent', () => { - let component: BackupRestoreCellComponent; - let fixture: ComponentFixture; +describe('BackupCheckboxCellComponent', () => { + let component: BackupCheckboxCellComponent; + let fixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [BackupRestoreCellComponent], + declarations: [BackupCheckboxCellComponent], imports: [ ...BaseTestModulesNoShared ], @@ -24,7 +24,7 @@ describe('BackupRestoreCellComponent', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(BackupRestoreCellComponent); + fixture = TestBed.createComponent(BackupCheckboxCellComponent); component = fixture.componentInstance; component.config = { type: BackupEndpointTypes.ENDPOINT diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.ts similarity index 64% rename from src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.ts rename to src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.ts index 0a1b80d52d..0098d10c9b 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-cell/backup-restore-cell.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-checkbox-cell/backup-checkbox-cell.component.ts @@ -5,18 +5,18 @@ import { TableCellCustom } from '../../../../shared/components/list/list.types'; import { BackupEndpointsService } from '../backup-endpoints.service'; @Component({ - selector: 'app-backup-restore-cell', - templateUrl: './backup-restore-cell.component.html', - styleUrls: ['./backup-restore-cell.component.scss'] + selector: 'app-backup-checkbox-cell', + templateUrl: './backup-checkbox-cell.component.html', + styleUrls: ['./backup-checkbox-cell.component.scss'] }) -export class BackupRestoreCellComponent extends TableCellCustom { +export class BackupCheckboxCellComponent extends TableCellCustom { constructor(public service: BackupEndpointsService) { super(); } validate() { - this.service.validate(); + this.service.stateUpdated(); } disabled(): boolean { 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 c964d79408..43e7eee490 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,8 @@ None - Current User + Current User All Users 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 67c64ab19e..978f098196 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 @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { entityCatalog } from '../../../../../../store/src/entity-catalog/entity-catalog.service'; -import { EndpointModel } from '../../../../../../store/src/types/endpoint.types'; +import { EndpointModel, SystemSharedUserGuid } 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.types'; @@ -17,15 +17,20 @@ export class BackupConnectionCellComponent extends TableCellCustom; - userId: string; password: string; } @@ -51,10 +48,10 @@ export class BackupEndpointsService { entity }; }); - this.validate(); + this.stateUpdated(); } - validate() { + stateUpdated() { const endpoints = Object.values(this.state); endpoints.forEach(endpoint => { if (!endpoint[BackupEndpointTypes.ENDPOINT]) { @@ -69,7 +66,8 @@ export class BackupEndpointsService { this.hasChanges.next(hasChanges); const allChanged = endpoints.every(endpoint => { const e = !this.canBackupEndpoint(endpoint.entity, BackupEndpointTypes.ENDPOINT) || endpoint[BackupEndpointTypes.ENDPOINT]; - const c = !this.canBackupEndpoint(endpoint.entity, BackupEndpointTypes.CONNECT) || endpoint[BackupEndpointTypes.CONNECT] !== BackupEndpointConnectionTypes.NONE; + const c = !this.canBackupEndpoint(endpoint.entity, BackupEndpointTypes.CONNECT) || + endpoint[BackupEndpointTypes.CONNECT] !== BackupEndpointConnectionTypes.NONE; return e && c; } @@ -110,7 +108,7 @@ export class BackupEndpointsService { endpoint[BackupEndpointTypes.CONNECT] = BackupEndpointConnectionTypes.ALL; } }); - this.validate(); + this.stateUpdated(); } selectNone() { @@ -118,7 +116,7 @@ export class BackupEndpointsService { endpoint[BackupEndpointTypes.ENDPOINT] = false; endpoint[BackupEndpointTypes.CONNECT] = BackupEndpointConnectionTypes.NONE; }); - this.validate(); + this.stateUpdated(); } hasConnectionDetails(): boolean { @@ -135,19 +133,18 @@ export class BackupEndpointsService { encoder: new BrowserStandardEncoder() }); - return this.getSessionData().pipe( - switchMap(ses => this.http.post(url, this.createBodyToSend(ses), { - params - })), - map(res => { - console.log('Response: ', res); - return new Blob([JSON.stringify(res)]); - }), + // return this.getSessionData().pipe( + // switchMap(ses => this.http.post(url, this.createBodyToSend(ses), { params })), + // map(res => new Blob([JSON.stringify(res)])), + // first(), + // ); + return this.http.post(url, this.createBodyToSend(), { params }).pipe( + map(res => new Blob([JSON.stringify(res)])), first(), ); } - private createBodyToSend(sd: SessionData): BackupRequest { + private createBodyToSend(): BackupRequest { const state: BackupEndpointsConfig = Object.entries(this.state).reduce((res, [endpointId, endpoint]) => { if (endpoint[BackupEndpointTypes.ENDPOINT]) { const { entity, ...rest } = endpoint; @@ -160,25 +157,25 @@ export class BackupEndpointsService { }, {}); return { state, - userId: this.getUserIdFromSessionData(sd), + // userId: this.getUserIdFromSessionData(sd), password: this.password, }; } - private getUserIdFromSessionData(sd: SessionData): string { - if (sd && sd.user) { - return sd.user.guid; - } - return null; - } - - private getSessionData(): Observable { - return this.store.select(s => s.auth).pipe( - filter(auth => !!(auth && auth.sessionData)), - map((auth: AuthState) => auth.sessionData), - first() - ); - } + // private getUserIdFromSessionData(sd: SessionData): string { + // if (sd && sd.user) { + // return sd.user.guid; + // } + // return null; + // } + + // private getSessionData(): Observable { + // return this.store.select(s => s.auth).pipe( + // filter(auth => !!(auth && auth.sessionData)), + // map((auth: AuthState) => auth.sessionData), + // first() + // ); + // } 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 6a64e5d9f1..1d24656803 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 @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import * as moment from 'moment'; @@ -17,9 +17,9 @@ import { ConfirmationDialogService } from '../../../../shared/components/confirm import { ITableListDataSource } from '../../../../shared/components/list/data-sources-controllers/list-data-source-types'; import { ITableColumn } from '../../../../shared/components/list/list-table/table.types'; import { StepOnNextFunction, StepOnNextResult } from '../../../../shared/components/stepper/step/step.component'; +import { BackupCheckboxCellComponent } from '../backup-checkbox-cell/backup-checkbox-cell.component'; 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.types'; @Component({ @@ -30,7 +30,7 @@ import { BackupEndpointTypes } from '../backup-restore.types'; BackupEndpointsService ] }) -export class BackupEndpointsComponent implements OnInit { +export class BackupEndpointsComponent { // Step 1 columns: ITableColumn[] = [ @@ -51,7 +51,7 @@ export class BackupEndpointsComponent implements OnInit { { columnId: 'endpoint', headerCell: () => 'Backup', - cellComponent: BackupRestoreCellComponent, + cellComponent: BackupCheckboxCellComponent, cellConfig: { type: BackupEndpointTypes.ENDPOINT } @@ -130,9 +130,6 @@ export class BackupEndpointsComponent implements OnInit { ); } - ngOnInit() { - } - onNext: StepOnNextFunction = () => { const confirmation = new ConfirmationDialogConfig( 'Backup', @@ -149,11 +146,6 @@ export class BackupEndpointsComponent implements OnInit { }; const backupSuccess = data => { - result.next({ - success: true, - redirect: true, - }); - const downloadURL = window.URL.createObjectURL(data); const link = document.createElement('a'); link.href = downloadURL; @@ -161,6 +153,11 @@ export class BackupEndpointsComponent implements OnInit { const dateTime = moment().format('YYYYMMDD-HHmmss'); link.download = `stratos_backup_${dateTime}.bk`; link.click(); + + result.next({ + success: true, + redirect: true, + }); }; const backupFailure = err => { diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.html b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.html index d1ea1197f7..089175d985 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.html +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.html @@ -5,8 +5,7 @@

Backup/Restore Endpoints

- -

Create a backup of Endpoints and their connection details or restore from an existing backup.

+

Create a backup of endpoints and their connection details or restore from an existing backup.

diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.ts b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.ts index 90b1e4cc2d..615c1d2021 100644 --- a/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/backup-restore-endpoints/backup-restore-endpoints.component.ts @@ -17,8 +17,6 @@ interface IAppTileData extends ITileData { }) export class BackupRestoreEndpointsComponent { - // static BACKUP_RESTORE_PARAM = 'backup'; - public serviceType: string; public tileSelectorConfig: ITileConfig[]; 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 074111bbac..8fa35048c1 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 @@ -36,6 +36,7 @@ export class RestoreEndpointsService { validDb = new BehaviorSubject(false); validDb$: Observable; + unparsableFileContent: string = null; currentDbVersion$: Observable; ignoreDbVersion = new BehaviorSubject(false); ignoreDbVersion$ = this.ignoreDbVersion.asObservable(); @@ -61,8 +62,9 @@ export class RestoreEndpointsService { this.file$, this.currentDbVersion$ ]).pipe( + filter(([file,]) => !!file && !!file.content), map(([file, currentDbVersion]) => { - return file && file.content.dbVersion === currentDbVersion; + return file && file.content && file.content.dbVersion === currentDbVersion; }) ); @@ -93,20 +95,17 @@ export class RestoreEndpointsService { let parsedContent: BackupContent; try { parsedContent = JSON.parse(content); - } catch (e) { - this.logger.warn('Failed to parse file contents: ', e); + this.unparsableFileContent = null; + } catch (err) { + this.logger.warn('Failed to parse file contents: ', err); + parsedContent = null; + this.unparsableFileContent = `${err instanceof Error ? err.message : String(err)}`; } - if (!!parsedContent) { - this.file.next({ - name: fileName, - content: parsedContent - }); - } else { - this.file.next(null); - } - - // this.updateFileContentValidation(); + this.file.next({ + name: fileName, + content: parsedContent + }); } setIgnoreDbVersion(ignore: boolean) { 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 722d3a5e87..4e8e0bfbaf 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 @@ -15,13 +15,23 @@

Restore Endpoints

-
-

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

+ warning 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 + +
+
+

+ warning Unable to parse file contents. Reason: + {{service.unparsableFileContent}} +

+
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 60b6ddd949..7fb53ed32c 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 @@ -20,4 +20,13 @@ } } + &__error { + p { + align-items: center; + display: flex; + } + mat-icon { + margin-right: 10px; + } + } } diff --git a/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.theme.scss b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.theme.scss new file mode 100644 index 0000000000..335545c7ad --- /dev/null +++ b/src/frontend/packages/core/src/features/endpoints/backup-restore/restore-endpoints/restore-endpoints.component.theme.scss @@ -0,0 +1,11 @@ +@mixin restore-endpoints-theme($theme, $app-theme) { + $status-colors: map-get($app-theme, status); + $warn-color: map-get($status-colors, warning); + .file-step { + &__error { + mat-icon { + color: $warn-color; + } + } + } +} 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 64b2e5e592..2904f6024d 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 @@ -78,7 +78,7 @@ export class RestoreEndpointsComponent { }); }; - const restoreSuccess = data => { + const restoreSuccess = () => { this.store.dispatch(new GetAllEndpoints()); result.next({ success: true, @@ -95,9 +95,9 @@ export class RestoreEndpointsComponent { return of(false); }; - const createBackup = () => this.service.restoreBackup().pipe(first()).subscribe(restoreSuccess, backupFailure); + const restoreBackup = () => this.service.restoreBackup().pipe(first()).subscribe(restoreSuccess, backupFailure); - this.confirmDialog.openWithCancel(confirmation, createBackup, userCancelledDialog); + this.confirmDialog.openWithCancel(confirmation, restoreBackup, userCancelledDialog); return result.asObservable(); } diff --git a/src/frontend/packages/core/src/features/endpoints/endpoints.module.ts b/src/frontend/packages/core/src/features/endpoints/endpoints.module.ts index 307379946a..44a05e8e2a 100644 --- a/src/frontend/packages/core/src/features/endpoints/endpoints.module.ts +++ b/src/frontend/packages/core/src/features/endpoints/endpoints.module.ts @@ -2,9 +2,9 @@ import { NgModule } from '@angular/core'; import { CoreModule } from '../../core/core.module'; import { SharedModule } from '../../shared/shared.module'; +import { BackupCheckboxCellComponent } from './backup-restore/backup-checkbox-cell/backup-checkbox-cell.component'; import { BackupConnectionCellComponent } from './backup-restore/backup-connection-cell/backup-connection-cell.component'; import { BackupEndpointsComponent } from './backup-restore/backup-endpoints/backup-endpoints.component'; -import { BackupRestoreCellComponent } from './backup-restore/backup-restore-cell/backup-restore-cell.component'; import { BackupRestoreEndpointsComponent, } from './backup-restore/backup-restore-endpoints/backup-restore-endpoints.component'; @@ -37,7 +37,7 @@ import { EndpointsRoutingModule } from './endpoints.routing'; BackupRestoreEndpointsComponent, BackupEndpointsComponent, RestoreEndpointsComponent, - BackupRestoreCellComponent, + BackupCheckboxCellComponent, BackupConnectionCellComponent, ], entryComponents: [ @@ -45,7 +45,7 @@ import { EndpointsRoutingModule } from './endpoints.routing'; CredentialsAuthFormComponent, SSOAuthFormComponent, NoneAuthFormComponent, - BackupRestoreCellComponent, + BackupCheckboxCellComponent, BackupConnectionCellComponent ] }) diff --git a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts index 44e7281b6a..8483e01383 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-types/endpoint/endpoints-list-config.service.ts @@ -106,7 +106,6 @@ export class EndpointsListConfigService implements IListConfig { internalEventMonitorFactory: InternalEventMonitorFactory, endpointListHelper: EndpointListHelper, favoritesConfigMapper: FavoritesConfigMapper, - // private router: Router ) { this.singleActions = endpointListHelper.endpointActions(); const favoriteCell = createTableColumnFavorite( diff --git a/src/frontend/packages/store/src/types/endpoint.types.ts b/src/frontend/packages/store/src/types/endpoint.types.ts index 6ec7c8e09a..83bf97984a 100644 --- a/src/frontend/packages/store/src/types/endpoint.types.ts +++ b/src/frontend/packages/store/src/types/endpoint.types.ts @@ -60,6 +60,8 @@ export interface EndpointModel { metricsAvailable: boolean; } +export const SystemSharedUserGuid = '00000000-1111-2222-3333-444444444444'; + // Metadata for the user connected to an endpoint export interface EndpointUser { guid: string; diff --git a/src/jetstream/authcnsi.go b/src/jetstream/authcnsi.go index 39cf03db4b..55809904d3 100644 --- a/src/jetstream/authcnsi.go +++ b/src/jetstream/authcnsi.go @@ -261,7 +261,7 @@ func (p *portalProxy) DoLoginToCNSIwithConsoleUAAtoken(c echo.Context, theCNSIre func santizeInfoForSystemSharedTokenUser(cnsiUser *interfaces.ConnectedUser, isSysystemShared bool) { if isSysystemShared { - cnsiUser.GUID = tokens.SystemSharedUserGuid + cnsiUser.GUID = tokens.SystemSharedUserGuid // Used by front end also cnsiUser.Scopes = make([]string, 0) cnsiUser.Name = "system_shared" } diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index afaaebf9cf..3520a06ccf 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -637,11 +637,17 @@ func (p *portalProxy) updateEndpoint(c echo.Context) error { } func (p *portalProxy) backupEndpoints(c echo.Context) error { - log.Debug("BackupEndpoints") + log.Debug("backupEndpoints") + + userID, err := p.GetSessionStringValue(c, "user_id") + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, "Could not find correct session value") + } ctb := &cnsiTokenBackup{ databaseConnectionPool: p.DatabaseConnectionPool, encryptionKey: p.Config.EncryptionKeyInBytes, + userID: userID, p: p, } @@ -651,9 +657,15 @@ func (p *portalProxy) backupEndpoints(c echo.Context) error { func (p *portalProxy) restoreEndpoints(c echo.Context) error { log.Debug("restoreEndpoints") + userID, err := p.GetSessionStringValue(c, "user_id") + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, "Could not find correct session value") + } + ctb := &cnsiTokenBackup{ databaseConnectionPool: p.DatabaseConnectionPool, encryptionKey: p.Config.EncryptionKeyInBytes, + userID: userID, p: p, } diff --git a/src/jetstream/cnsi_token_backup.go b/src/jetstream/cnsi_token_backup.go index 3f4c4f77b9..0666493f47 100644 --- a/src/jetstream/cnsi_token_backup.go +++ b/src/jetstream/cnsi_token_backup.go @@ -1,7 +1,7 @@ package main import ( - "crypto/md5" + "crypto/sha256" "database/sql" "encoding/json" "fmt" @@ -19,6 +19,7 @@ import ( type cnsiTokenBackup struct { databaseConnectionPool *sql.DB encryptionKey []byte + userID string p *portalProxy } @@ -39,10 +40,8 @@ type BackupEndpointsState struct { // 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"` + State map[string]BackupEndpointsState `json:"state"` + Password string `json:"password"` } // BackupContentPayload - Encrypted part of the backup @@ -133,17 +132,16 @@ func (ctb *cnsiTokenBackup) createBackup(data *BackupRequest) (*BackupContent, e return nil, interfaces.NewHTTPError(http.StatusBadGateway, text) } case BACKUP_CONNECTION_CURRENT: - if tokenRecord, ok := ctb.p.GetCNSITokenRecordWithDisconnected(endpointID, data.UserID); ok { - log.Warn("tokens for Connect") + if tokenRecord, ok := ctb.p.GetCNSITokenRecordWithDisconnected(endpointID, ctb.userID); ok { var btr = interfaces.BackupTokenRecord{ TokenRecord: tokenRecord, EndpointGUID: endpointID, - TokenType: "CNSI", - UserGUID: data.UserID, + TokenType: "cnsi", + UserGUID: ctb.userID, } tokens = append(tokens, btr) } else { - text := fmt.Sprintf("Request to back up connected user's (%+v) token for endpoint (%+v) failed.", endpointID, data.UserID) + text := fmt.Sprintf("Request to back up connected user's (%+v) token for endpoint (%+v) failed.", endpointID, ctb.userID) return nil, interfaces.NewHTTPError(http.StatusBadGateway, text) } } @@ -156,7 +154,7 @@ func (ctb *cnsiTokenBackup) createBackup(data *BackupRequest) (*BackupContent, e } // Encrypt the entire payload - encryptedPayload, err := encryptPayload(payload, data.Password, ctb.encryptionKey) + encryptedPayload, err := encryptPayload(payload, data.Password) if err != nil { return nil, interfaces.NewHTTPShadowError(http.StatusBadGateway, "Could not encrypt payload", "Could not encrypt payload: %+v", err) } @@ -235,7 +233,7 @@ func (ctb *cnsiTokenBackup) restoreBackup(backup *RestoreRequest) error { } // Get the actual, unencrypted set of endpoints and tokens - payloadString, err := decryptPayload(data.Payload, backup.Password, ctb.encryptionKey) + payloadString, err := decryptPayload(data.Payload, backup.Password) if err != nil { return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to decrypt payload", "Failed to decrypt payload: %+v", err) } @@ -252,7 +250,7 @@ func (ctb *cnsiTokenBackup) restoreBackup(backup *RestoreRequest) error { for _, endpoint := range payload.Endpoints { e := deSerializeEndpoint(endpoint) - if err := cnsiRepo.Overwrite(e, ctb.encryptionKey); err != nil { + if err := cnsiRepo.SaveOrUpdate(e, ctb.encryptionKey); err != nil { return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Failed to overwrite endpoints", "Failed to overwrite endpoint: %+v", e.Name) } } @@ -273,15 +271,13 @@ func (ctb *cnsiTokenBackup) restoreBackup(backup *RestoreRequest) error { // Work around the omission of the client secret when serialising the cnsi record func serializeEndpoint(endpoint *interfaces.CNSIRecord) map[string]interface{} { - // encode the original + // Convert struct to generic map m, _ := json.Marshal(endpoint) - - // decode it back to get a map var a interface{} json.Unmarshal(m, &a) newEndpoint := a.(map[string]interface{}) - // Replace the map key + // Apply the correct client secret newEndpoint["client_secret"] = endpoint.ClientSecret return newEndpoint @@ -289,21 +285,19 @@ func serializeEndpoint(endpoint *interfaces.CNSIRecord) map[string]interface{} { // Work around the omission of the client secret when serialising the cnsi record func deSerializeEndpoint(endpoint map[string]interface{}) interfaces.CNSIRecord { - // encode the endpoint map + // Convert struct to endpoint m, _ := json.Marshal(endpoint) + var cnsi interfaces.CNSIRecord + json.Unmarshal(m, &cnsi) - // decode it back to get a record with all values except client secret - var a interfaces.CNSIRecord - json.Unmarshal(m, &a) - - // manually add the client secret - a.ClientSecret = fmt.Sprintf("%v", endpoint["client_secret"]) - return a + // Apply the correct client secret + cnsi.ClientSecret = fmt.Sprintf("%v", endpoint["client_secret"]) + return cnsi } -func encryptPayload(payload *BackupContentPayload, password string, encryptionKey []byte) ([]byte, error) { +func encryptPayload(payload *BackupContentPayload, password string) ([]byte, error) { // First ensure the password is an ok length - secret, err := createHash(password, encryptionKey) + secret, err := createHash(password) if err != nil { log.Warningf("Could not create hash: %+v", err) return nil, fmt.Errorf("Could not create hash") @@ -324,9 +318,9 @@ func encryptPayload(payload *BackupContentPayload, password string, encryptionKe return payloadEncrypted, nil } -func decryptPayload(payloadEncrypted []byte, password string, encryptionKey []byte) (*string, error) { +func decryptPayload(payloadEncrypted []byte, password string) (*string, error) { // First ensure the password is an ok length - secret, err := createHash(password, encryptionKey) + secret, err := createHash(password) if err != nil { log.Warningf("Could not create hash: %+v", err) return nil, fmt.Errorf("Could not create hash") @@ -341,13 +335,11 @@ func decryptPayload(payloadEncrypted []byte, password string, encryptionKey []by } // createHash - Ensure the token used by crypto is at an acceptable length -func createHash(password string, salt []byte) ([]byte, error) { - hasher := md5.New() +func createHash(password string) ([]byte, error) { + // Create a hash long enough to ensure with use AES-256 + hasher := sha256.New() if _, err := hasher.Write([]byte(password)); err != nil { return nil, fmt.Errorf("Failed to write password to hash") } - if _, err := hasher.Write(salt); err != nil { - return nil, fmt.Errorf("Failed to write salt to hash") - } return hasher.Sum(nil), nil } diff --git a/src/jetstream/repository/cnsis/cnsis.go b/src/jetstream/repository/cnsis/cnsis.go index b1e5bcc4bc..0605361d72 100644 --- a/src/jetstream/repository/cnsis/cnsis.go +++ b/src/jetstream/repository/cnsis/cnsis.go @@ -14,7 +14,7 @@ type Repository interface { Save(guid string, cnsiRecord interfaces.CNSIRecord, encryptionKey []byte) error Update(endpoint interfaces.CNSIRecord, encryptionKey []byte) error UpdateMetadata(guid string, metadata string) error - Overwrite(endpoint interfaces.CNSIRecord, encryptionKey []byte) error + SaveOrUpdate(endpoint interfaces.CNSIRecord, encryptionKey []byte) error } type Endpoint interface { diff --git a/src/jetstream/repository/cnsis/pgsql_cnsis.go b/src/jetstream/repository/cnsis/pgsql_cnsis.go index 9582f3e1a4..b68bf4933f 100644 --- a/src/jetstream/repository/cnsis/pgsql_cnsis.go +++ b/src/jetstream/repository/cnsis/pgsql_cnsis.go @@ -61,6 +61,7 @@ func InitRepositoryProvider(databaseProvider string) { deleteCNSI = datastore.ModifySQLStatement(deleteCNSI, databaseProvider) updateCNSI = datastore.ModifySQLStatement(updateCNSI, databaseProvider) updateCNSIMetadata = datastore.ModifySQLStatement(updateCNSIMetadata, databaseProvider) + countCNSI = datastore.ModifySQLStatement(countCNSI, databaseProvider) } // List - Returns a list of CNSI Records @@ -352,8 +353,8 @@ func (p *PostgresCNSIRepository) UpdateMetadata(guid string, metadata string) er return nil } -// Overwrite - Creates or Updates CNSI Record -func (p *PostgresCNSIRepository) Overwrite(endpoint interfaces.CNSIRecord, encryptionKey []byte) error { +// SaveOrUpdate - Creates or Updates CNSI Record +func (p *PostgresCNSIRepository) SaveOrUpdate(endpoint interfaces.CNSIRecord, encryptionKey []byte) error { log.Debug("Overwrite CNSI") // Is there an existing token? diff --git a/src/jetstream/repository/tokens/pgsql_tokens.go b/src/jetstream/repository/tokens/pgsql_tokens.go index 32c779e355..43a45459f9 100644 --- a/src/jetstream/repository/tokens/pgsql_tokens.go +++ b/src/jetstream/repository/tokens/pgsql_tokens.go @@ -86,6 +86,7 @@ func InitRepositoryProvider(databaseProvider string) { updateAuthToken = datastore.ModifySQLStatement(updateAuthToken, databaseProvider) findCNSIToken = datastore.ModifySQLStatement(findCNSIToken, databaseProvider) findCNSITokenConnected = datastore.ModifySQLStatement(findCNSITokenConnected, databaseProvider) + findAllCNSIToken = datastore.ModifySQLStatement(findAllCNSIToken, databaseProvider) countCNSITokens = datastore.ModifySQLStatement(countCNSITokens, databaseProvider) insertCNSIToken = datastore.ModifySQLStatement(insertCNSIToken, databaseProvider) updateCNSIToken = datastore.ModifySQLStatement(updateCNSIToken, databaseProvider) diff --git a/src/jetstream/repository/tokens/tokens.go b/src/jetstream/repository/tokens/tokens.go index f473885e15..114a77a9b7 100644 --- a/src/jetstream/repository/tokens/tokens.go +++ b/src/jetstream/repository/tokens/tokens.go @@ -9,7 +9,8 @@ type Token struct { Record interfaces.TokenRecord } -const SystemSharedUserGuid = "00000000-1111-2222-3333-444444444444" // User ID for the system shared user for endpoints +// SystemSharedUserGuid - User ID for the system shared user for endpoints. Also used by front end +const SystemSharedUserGuid = "00000000-1111-2222-3333-444444444444" // Repository is an application of the repository pattern for storing tokens type Repository interface { From efeca9fe44cad501abdf7c7b345e18b13d4f686b Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Mon, 20 Apr 2020 16:04:30 +0100 Subject: [PATCH 12/12] Move backup into it's own module --- .../restore-endpoints.component.html | 2 +- src/jetstream/cnsi.go | 36 ------ src/jetstream/load_plugins.go | 2 + src/jetstream/main.go | 3 - .../backup/backup_restore.go} | 35 ++---- src/jetstream/plugins/backup/main.go | 111 ++++++++++++++++++ src/jetstream/version_info.go | 2 +- 7 files changed, 128 insertions(+), 63 deletions(-) rename src/jetstream/{cnsi_token_backup.go => plugins/backup/backup_restore.go} (91%) create mode 100644 src/jetstream/plugins/backup/main.go 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 4e8e0bfbaf..1682c9b572 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 @@ -28,7 +28,7 @@

Restore Endpoints

- warning Unable to parse file contents. Reason: + warningUnable to parse file contents. Reason:  {{service.unparsableFileContent}}

diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index 3520a06ccf..daf0ea3446 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -635,39 +635,3 @@ func (p *portalProxy) updateEndpoint(c echo.Context) error { return nil } - -func (p *portalProxy) backupEndpoints(c echo.Context) error { - log.Debug("backupEndpoints") - - userID, err := p.GetSessionStringValue(c, "user_id") - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, "Could not find correct session value") - } - - ctb := &cnsiTokenBackup{ - databaseConnectionPool: p.DatabaseConnectionPool, - encryptionKey: p.Config.EncryptionKeyInBytes, - userID: userID, - p: p, - } - - return ctb.BackupEndpoints(c) -} - -func (p *portalProxy) restoreEndpoints(c echo.Context) error { - log.Debug("restoreEndpoints") - - userID, err := p.GetSessionStringValue(c, "user_id") - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, "Could not find correct session value") - } - - ctb := &cnsiTokenBackup{ - databaseConnectionPool: p.DatabaseConnectionPool, - encryptionKey: p.Config.EncryptionKeyInBytes, - userID: userID, - p: p, - } - - return ctb.RestoreEndpoints(c) -} diff --git a/src/jetstream/load_plugins.go b/src/jetstream/load_plugins.go index 0c8b9fe639..c76b421c4f 100644 --- a/src/jetstream/load_plugins.go +++ b/src/jetstream/load_plugins.go @@ -2,6 +2,7 @@ package main import ( "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/autoscaler" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/backup" "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/cfapppush" "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/cfappssh" "github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/cloudfoundry" @@ -34,6 +35,7 @@ func (pp *portalProxy) loadPlugins() { {"userinvite", userinvite.Init}, {"userfavorites", userfavorites.Init}, {"autoscaler", autoscaler.Init}, + {"backup", backup.Init}, } { plugin, err := p.Init(pp) pp.Plugins[p.Name] = plugin diff --git a/src/jetstream/main.go b/src/jetstream/main.go index 53c3045e2b..9b649da31d 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -973,9 +973,6 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { // Apply edits for the given endpoint adminGroup.POST("/endpoint/:id", p.updateEndpoint) - adminGroup.POST("/endpoints/backup", p.backupEndpoints) - adminGroup.POST("/endpoints/restore", p.restoreEndpoints) - adminGroup.POST("/unregister", p.unregisterCluster) // sessionGroup.DELETE("/cnsis", p.removeCluster) diff --git a/src/jetstream/cnsi_token_backup.go b/src/jetstream/plugins/backup/backup_restore.go similarity index 91% rename from src/jetstream/cnsi_token_backup.go rename to src/jetstream/plugins/backup/backup_restore.go index 0666493f47..7e6fd8df59 100644 --- a/src/jetstream/cnsi_token_backup.go +++ b/src/jetstream/plugins/backup/backup_restore.go @@ -1,4 +1,4 @@ -package main +package backup import ( "crypto/sha256" @@ -20,22 +20,23 @@ type cnsiTokenBackup struct { databaseConnectionPool *sql.DB encryptionKey []byte userID string - p *portalProxy + dbVersion int64 + p interfaces.PortalProxy } -// BackupConnectionType - Determine what kind of connection details are stored for an endpoint -type BackupConnectionType string +// ConnectionType - Determine what kind of connection details are stored for an endpoint +type ConnectionType string const ( - BACKUP_CONNECTION_NONE BackupConnectionType = "NONE" - BACKUP_CONNECTION_CURRENT = "CURRENT" - BACKUP_CONNECTION_ALL = "ALL" + BACKUP_CONNECTION_NONE ConnectionType = "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"` + Endpoint bool `json:"endpoint"` + Connect ConnectionType `json:"connect"` } // BackupRequest - Request from client to create a back up file @@ -160,14 +161,9 @@ func (ctb *cnsiTokenBackup) createBackup(data *BackupRequest) (*BackupContent, e } // Add the db version to the response, this will allow client side up front validation - versions, err := ctb.p.getVersionsData() - if err != nil { - return nil, interfaces.NewHTTPShadowError(http.StatusBadGateway, "Could not find database version", "Could not find database version: %+v", err) - } - response := &BackupContent{ Payload: encryptedPayload, - DBVersion: versions.DatabaseVersion, + DBVersion: ctb.dbVersion, } return response, nil @@ -221,13 +217,8 @@ func (ctb *cnsiTokenBackup) restoreBackup(backup *RestoreRequest) error { // Check that the db version of backup file matches the stratos db version if backup.IgnoreDbVersion == false { - versions, err := ctb.p.getVersionsData() - if err != nil { - return interfaces.NewHTTPShadowError(http.StatusInternalServerError, "Could not find database version", "Could not find database version: %+v", err) - } - - if versions.DatabaseVersion != data.DBVersion { - errorStr := fmt.Sprintf("Incompatible database versions. Expected %+v but got %+v", versions.DatabaseVersion, data.DBVersion) + if ctb.dbVersion != data.DBVersion { + errorStr := fmt.Sprintf("Incompatible database versions. Expected %+v but got %+v", ctb.dbVersion, data.DBVersion) return interfaces.NewHTTPError(http.StatusBadRequest, errorStr) } } diff --git a/src/jetstream/plugins/backup/main.go b/src/jetstream/plugins/backup/main.go new file mode 100644 index 0000000000..d2ddcf5dec --- /dev/null +++ b/src/jetstream/plugins/backup/main.go @@ -0,0 +1,111 @@ +package backup + +import ( + "database/sql" + "errors" + "net/http" + + goosedbversion "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/goose-db-version" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" + "github.com/labstack/echo" + log "github.com/sirupsen/logrus" +) + +// BackupRestore - Backup or restore endpoints and tokens +type BackupRestore struct { + portalProxy interfaces.PortalProxy +} + +// Init creates a new Autoscaler +func Init(portalProxy interfaces.PortalProxy) (interfaces.StratosPlugin, error) { + return &BackupRestore{portalProxy: portalProxy}, nil +} + +// GetMiddlewarePlugin gets the middleware plugin for this plugin +func (br *BackupRestore) GetMiddlewarePlugin() (interfaces.MiddlewarePlugin, error) { + return nil, errors.New("Not implemented") +} + +// GetEndpointPlugin gets the endpoint plugin for this plugin +func (br *BackupRestore) GetEndpointPlugin() (interfaces.EndpointPlugin, error) { + return nil, errors.New("Not implemented") +} + +// GetRoutePlugin gets the route plugin for this plugin +func (br *BackupRestore) GetRoutePlugin() (interfaces.RoutePlugin, error) { + return br, nil +} + +// AddAdminGroupRoutes adds the admin routes for this plugin to the Echo server +func (br *BackupRestore) AddAdminGroupRoutes(echoGroup *echo.Group) { + echoGroup.POST("/endpoints/backup", br.backupEndpoints) + echoGroup.POST("/endpoints/restore", br.restoreEndpoints) +} + +// AddSessionGroupRoutes adds the session routes for this plugin to the Echo server +func (br *BackupRestore) AddSessionGroupRoutes(echoGroup *echo.Group) { + // no-op +} + +// Init performs plugin initialization +func (br *BackupRestore) Init() error { + return nil +} + +func (br *BackupRestore) backupEndpoints(c echo.Context) error { + log.Debug("backupEndpoints") + + userID, err := br.portalProxy.GetSessionStringValue(c, "user_id") + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, "Could not find correct session value") + } + + version, err := getDBVersion(br.portalProxy.GetDatabaseConnection()) + if err != nil { + return interfaces.NewHTTPShadowError(http.StatusBadGateway, "Could not find database version", "Could not find database version: %+v", err) + } + + ctb := &cnsiTokenBackup{ + databaseConnectionPool: br.portalProxy.GetDatabaseConnection(), + encryptionKey: br.portalProxy.GetConfig().EncryptionKeyInBytes, + userID: userID, + dbVersion: version, + p: br.portalProxy, + } + + return ctb.BackupEndpoints(c) +} + +func (br *BackupRestore) restoreEndpoints(c echo.Context) error { + log.Debug("restoreEndpoints") + + userID, err := br.portalProxy.GetSessionStringValue(c, "user_id") + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, "Could not find correct session value") + } + + version, err := getDBVersion(br.portalProxy.GetDatabaseConnection()) + if err != nil { + return interfaces.NewHTTPShadowError(http.StatusBadGateway, "Could not find database version", "Could not find database version: %+v", err) + } + + ctb := &cnsiTokenBackup{ + databaseConnectionPool: br.portalProxy.GetDatabaseConnection(), + encryptionKey: br.portalProxy.GetConfig().EncryptionKeyInBytes, + userID: userID, + dbVersion: version, + p: br.portalProxy, + } + + return ctb.RestoreEndpoints(c) +} + +func getDBVersion(databaseConnectionPool *sql.DB) (int64, error) { + dbVersionRepo, _ := goosedbversion.NewPostgresGooseDBVersionRepository(databaseConnectionPool) + databaseVersionRec, err := dbVersionRepo.GetCurrentVersion() + if err != nil { + return 0, errors.New("Error trying to get current database version") + } + + return databaseVersionRec.VersionID, nil +} diff --git a/src/jetstream/version_info.go b/src/jetstream/version_info.go index c62257d785..08fd2bd9c8 100644 --- a/src/jetstream/version_info.go +++ b/src/jetstream/version_info.go @@ -4,7 +4,7 @@ import ( "errors" "net/http" - "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/goose-db-version" + goosedbversion "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/goose-db-version" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" "github.com/labstack/echo" log "github.com/sirupsen/logrus"