Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/modules/ROOT/pages/operations.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,10 @@ Separate multiple commands with a semicolon (`;`).
| `browser.retain_connection_credentials`
| `true`
| Configure the Neo4j Browser to store or not store user credentials.

| `browser.retain_editor_history`
| `true`
| Configure the Neo4j Browser to store or not store editor history.
|===


Expand Down
4 changes: 2 additions & 2 deletions src/browser/AppInit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import { APP_START } from 'shared/modules/app/appDuck'
import { detectRuntimeEnv } from 'services/utils'
import { NEO4J_CLOUD_DOMAINS } from 'shared/modules/settings/settingsDuck'
import { version } from 'project-root/package.json'
import { allowOutgoingConnections } from 'shared/modules/dbMeta/dbMetaDuck'
import { shouldAllowOutgoingConnections } from 'shared/modules/dbMeta/dbMetaDuck'
import { getUuid } from 'shared/modules/udc/udcDuck'
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
Expand Down Expand Up @@ -167,7 +167,7 @@ export function setupSentry(): void {
}
},
beforeSend: event =>
allowOutgoingConnections(store.getState())
shouldAllowOutgoingConnections(store.getState())
? scrubQueryParamsAndUrl(event)
: null,
environment: 'unset'
Expand Down
4 changes: 2 additions & 2 deletions src/browser/modules/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { utilizeBrowserSync } from 'shared/modules/features/featuresDuck'
import { getOpenDrawer } from 'shared/modules/sidebar/sidebarDuck'
import { getErrorMessage } from 'shared/modules/commands/commandsDuck'
import {
allowOutgoingConnections,
shouldAllowOutgoingConnections,
getDatabases
} from 'shared/modules/dbMeta/dbMetaDuck'
import {
Expand Down Expand Up @@ -244,7 +244,7 @@ const mapStateToProps = (state: any) => {
lastConnectionUpdate: getLastConnectionUpdate(state),
errorMessage: getErrorMessage(state),
loadExternalScripts:
allowOutgoingConnections(state) !== false && isConnected(state),
shouldAllowOutgoingConnections(state) !== false && isConnected(state),
titleString: asTitleString(connectionData),
defaultConnectionData: getConnectionData(state, CONNECTION_ID),
syncConsent: state.syncConsent.consented,
Expand Down
4 changes: 2 additions & 2 deletions src/browser/modules/Sync/BrowserSync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import TimeAgo from 'react-timeago'
import { allowOutgoingConnections } from 'shared/modules/dbMeta/dbMetaDuck'
import { shouldAllowOutgoingConnections } from 'shared/modules/dbMeta/dbMetaDuck'
import {
getConnectionState,
DISCONNECTED_STATE
Expand Down Expand Up @@ -282,7 +282,7 @@ const mapStateToProps = (state: any) => {
browserSyncConfig: getBrowserSyncConfig(state),
syncConsent: state.syncConsent.consented,
connectionState: getConnectionState(state),
isAllowed: allowOutgoingConnections(state) !== false
isAllowed: shouldAllowOutgoingConnections(state) !== false
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Object {
"browser.allow_outgoing_connections": false,
"browser.remote_content_hostname_allowlist": "guides.neo4j.com, localhost",
"browser.retain_connection_credentials": false,
"browser.retain_editor_history": false,
},
}
`;
Expand All @@ -45,6 +46,7 @@ Object {
"browser.allow_outgoing_connections": false,
"browser.remote_content_hostname_allowlist": "guides.neo4j.com, localhost",
"browser.retain_connection_credentials": false,
"browser.retain_editor_history": false,
},
}
`;
Expand Down
45 changes: 42 additions & 3 deletions src/shared/modules/dbMeta/dbMetaDuck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import neo4j from 'neo4j-driver'
import Rx from 'rxjs/Rx'
import semver from 'semver'
import bolt from 'services/bolt/bolt'
import { isConfigValFalsy } from 'services/bolt/boltHelpers'
import { APP_START } from 'shared/modules/app/appDuck'
Expand Down Expand Up @@ -59,6 +60,7 @@ import {
isACausalCluster,
setClientConfig
} from '../features/featuresDuck'
import { clearHistory } from 'shared/modules/history/historyDuck'

export const NAME = 'meta'
export const UPDATE = 'meta/UPDATE'
Expand Down Expand Up @@ -106,20 +108,26 @@ export const getStoreId = (state: any) =>

export const getAvailableSettings = (state: any) =>
(state[NAME] || initialState).settings
export const allowOutgoingConnections = (state: any) =>
export const getAllowOutgoingConnections = (state: any) =>
getAvailableSettings(state)['browser.allow_outgoing_connections']
export const credentialsTimeout = (state: any) =>
getAvailableSettings(state)['browser.credential_timeout'] || 0
export const getRemoteContentHostnameAllowlist = (state: any) =>
getAvailableSettings(state)['browser.remote_content_hostname_allowlist']
export const getDefaultRemoteContentHostnameAllowlist = (_state: any) =>
initialState.settings['browser.remote_content_hostname_allowlist']
export const shouldRetainConnectionCredentials = (state: any) => {
export const getRetainConnectionCredentials = (state: any) => {
const settings = getAvailableSettings(state)
const conf = settings['browser.retain_connection_credentials']
if (conf === null || typeof conf === 'undefined') return false
return !isConfigValFalsy(conf)
}
export const getRetainEditorHistory = (state: any) => {
const settings = getAvailableSettings(state)
const conf = settings['browser.retain_editor_history']
if (conf === null || typeof conf === 'undefined') return false
return !isConfigValFalsy(conf)
}
export const getDatabases = (state: any) =>
(state[NAME] || initialState).databases
export const getActiveDbName = (state: any) =>
Expand All @@ -128,6 +136,23 @@ export const getActiveDbName = (state: any) =>
* Helpers
*/

export const VERSION_FOR_EDITOR_HISTORY_SETTING = '4.3.0'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this need to be exported


export const versionHasEditorHistorySetting = (version: string) =>
semver.gte(version, VERSION_FOR_EDITOR_HISTORY_SETTING)

export const supportsEditorHistorySetting = (state: any) =>
isEnterprise(state) && versionHasEditorHistorySetting(getVersion(state))

export const shouldAllowOutgoingConnections = (state: any) =>
!isEnterprise(state) || getAllowOutgoingConnections(state)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would send outgoing connections before we connect to a database? I think that's a no-go in airgapped deployments


export const shouldRetainConnectionCredentials = (state: any) =>
!isEnterprise(state) || getRetainConnectionCredentials(state)

export const shouldRetainEditorHistory = (state: any) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a nit but could we move this to by be the other history getters? (so to line 146). Feel free to disregard this comment, just find it slightly more readable

!supportsEditorHistorySetting(state) || getRetainEditorHistory(state)

function updateMetaForContext(state: any, meta: any, context: any) {
if (!meta || !meta.records || !meta.records.length) {
return {
Expand Down Expand Up @@ -216,7 +241,8 @@ export const initialState = {
settings: {
'browser.allow_outgoing_connections': false,
'browser.remote_content_hostname_allowlist': 'guides.neo4j.com, localhost',
'browser.retain_connection_credentials': false
'browser.retain_connection_credentials': false,
'browser.retain_editor_history': false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it not make sense to default this to true?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As that's the current behaviour?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth noting that the !isEnterprise stuff defaults to true since edition default/before connect is null

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it's better to explicitly look for "community" rather than not enterprise?

Copy link
Contributor Author

@jharris4 jharris4 May 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server does default this setting to true, but for safety it was decided to default to false on the client side.

The settings are only ever loaded after initial discovery is done, so by that point we know the version/edition.

Also, only enterprise supports the setting, so that's why I decided to code it as "not enterprise"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Canny and Sentry both check allow shouldAllowOutgoing connections before connecting

}
}

Expand Down Expand Up @@ -605,6 +631,13 @@ export const serverConfigEpic = (some$: any, store: any) =>
}
store.dispatch(setRetainCredentials(retainCredentials))
value = retainCredentials
} else if (name === 'browser.retain_editor_history') {
let retainHistory = true
// Check if we should wipe user history from localstorage
if (typeof value !== 'undefined' && isConfigValFalsy(value)) {
retainHistory = false
}
value = retainHistory
} else if (name === 'browser.allow_outgoing_connections') {
// Use isConfigValFalsy to cast undefined to true
value = !isConfigValFalsy(value)
Expand All @@ -623,6 +656,12 @@ export const serverConfigEpic = (some$: any, store: any) =>
updateUserCapability(USER_CAPABILITIES.serverConfigReadable, true)
)
store.dispatch(updateSettings(settings))
if (
supportsEditorHistorySetting(store.getState()) &&
!settings['browser.retain_editor_history']
) {
store.dispatch(clearHistory())
}
return Rx.Observable.of(null)
})
})
Expand Down
114 changes: 111 additions & 3 deletions src/shared/services/localstorage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ describe('localstorage', () => {
})

describe('localstorage redux middleware', () => {
const createAndInvokeMiddlewareWithRetainFlag = (retain: boolean) => {
const createAndInvokeMiddlewareWithRetainConnectionFlag = (
retain: boolean,
edition = 'enterprise'
) => {
const setItemMock = jest.fn()
ls.applyKeys('connections')
ls.setStorage(({
Expand All @@ -109,6 +112,9 @@ describe('localstorage', () => {
connectionsById: { $$discovery: { password: 'secret password' } }
},
meta: {
server: {
edition
},
settings: {
'browser.retain_connection_credentials': retain
}
Expand All @@ -128,7 +134,9 @@ describe('localstorage', () => {
}

it('removes passwords from connection data if browser.retain_connection_credentials is false', () => {
const setItemMock = createAndInvokeMiddlewareWithRetainFlag(false)
const setItemMock = createAndInvokeMiddlewareWithRetainConnectionFlag(
false
)

expect(setItemMock).toHaveBeenCalledWith(
'neo4j.connections',
Expand All @@ -139,7 +147,9 @@ describe('localstorage', () => {
})

it('retains passwords in connection data if browser.retain_connection_credentials is true', () => {
const setItemMock = createAndInvokeMiddlewareWithRetainFlag(true)
const setItemMock = createAndInvokeMiddlewareWithRetainConnectionFlag(
true
)

expect(setItemMock).toHaveBeenCalledWith(
'neo4j.connections',
Expand All @@ -148,5 +158,103 @@ describe('localstorage', () => {
})
)
})

const existingHistory = ['history item']

const createAndInvokeMiddlewareWithRetainHistoryFlag = ({
retain,
edition = 'enterprise',
version = '4.3.0'
}: {
retain?: boolean
edition?: string
version?: string
}) => {
const setItemMock = jest.fn()
ls.applyKeys('history')
ls.setStorage(({
setItem: setItemMock
} as Partial<Storage>) as Storage)

const state = {
history: existingHistory,
meta: {
server: {
version,
edition
},
settings: {
'browser.retain_editor_history': retain
}
},
connections: {
connectionsById: { $$discovery: { password: 'secret password' } }
}
}

const store = {
getState: () => state,
dispatch: jest.fn()
}
const next = jest.fn(action => action)
const action = { type: 'some action' }

ls.createReduxMiddleware()(store)(next)(action)

return setItemMock
}

it('removes history from data if browser.retain_editor_history is false', () => {
const setItemMock = createAndInvokeMiddlewareWithRetainHistoryFlag({
retain: false
})

expect(setItemMock).toHaveBeenCalledWith(
'neo4j.history',
JSON.stringify([])
)
})

it('retains history from data if browser.retain_editor_history is true', () => {
const setItemMock = createAndInvokeMiddlewareWithRetainHistoryFlag({
retain: true
})

expect(setItemMock).toHaveBeenCalledWith(
'neo4j.history',
JSON.stringify(existingHistory)
)
})

it('removes history from data if browser.retain_editor_history is undefined and server version is >= 4.3.0', () => {
const setItemMock = createAndInvokeMiddlewareWithRetainHistoryFlag({})

expect(setItemMock).toHaveBeenCalledWith(
'neo4j.history',
JSON.stringify([])
)
})

it('retains history from data if browser.retain_editor_history is undefined and server version is < 4.3.0', () => {
const setItemMock = createAndInvokeMiddlewareWithRetainHistoryFlag({
version: '4.2.0'
})

expect(setItemMock).toHaveBeenCalledWith(
'neo4j.history',
JSON.stringify(existingHistory)
)
})

it('retains history from data if browser.retain_editor_history is undefined and server edition is not enterprise', () => {
const setItemMock = createAndInvokeMiddlewareWithRetainHistoryFlag({
edition: 'community'
})

expect(setItemMock).toHaveBeenCalledWith(
'neo4j.history',
JSON.stringify(existingHistory)
)
})
})
})
7 changes: 6 additions & 1 deletion src/shared/services/localstorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@

import { Middleware } from 'redux'
import { GlobalState } from 'shared/globalState'
import { shouldRetainConnectionCredentials } from '../modules/dbMeta/dbMetaDuck'
import {
shouldRetainConnectionCredentials,
shouldRetainEditorHistory
} from '../modules/dbMeta/dbMetaDuck'
import { initialState as settingsInitialState } from '../modules/settings/settingsDuck'

export const keyPrefix = 'neo4j.'
Expand Down Expand Up @@ -99,6 +102,8 @@ export function createReduxMiddleware(): Middleware {
)
)
})
} else if (key === 'history' && !shouldRetainEditorHistory(state)) {
setItem(key, [])
} else {
setItem(key, state[key])
}
Expand Down