Skip to content

Commit

Permalink
feat: add protectConnectionStrings option COMPASS-6066 (#3660)
Browse files Browse the repository at this point in the history
  • Loading branch information
addaleax authored Nov 2, 2022
1 parent 3485820 commit beb1ccb
Show file tree
Hide file tree
Showing 29 changed files with 992 additions and 54 deletions.
581 changes: 581 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/compass-connections/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
},
"devDependencies": {
"@mongodb-js/compass-components": "^1.3.0",
"@mongodb-js/compass-maybe-protect-connection-string": "^0.1.0",
"@mongodb-js/eslint-config-compass": "^1.0.1",
"@mongodb-js/mocha-config-compass": "^1.0.1",
"@mongodb-js/prettier-config-compass": "^1.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { getConnectionTitle } from 'mongodb-data-service';

import ConnectionIcon from './connection-icon';
import { useConnectionColor } from '@mongodb-js/connection-form';
import { maybeProtectConnectionString } from '@mongodb-js/compass-maybe-protect-connection-string';

const TOAST_TIMEOUT_MS = 5000; // 5 seconds.

Expand Down Expand Up @@ -254,7 +255,9 @@ function Connection({

if (action === 'copy-connection-string') {
void copyConnectionString(
connectionInfo.connectionOptions.connectionString
maybeProtectConnectionString(
connectionInfo.connectionOptions.connectionString
)
);
return;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/compass-e2e-tests/.depcheckrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ ignores:
'@mongodb-js/tsconfig-compass',
'@wdio/types',
'mongodb-compass',
'ps-list'
'ps-list',
'compass-preferences-model'
]
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export async function selectConnectionMenuItem(
// workaround for weirdness in the ItemActionControls menu opener icon
await browser.clickVisible(Selectors.ConnectionsTitle);

// Hover over an arbitrary other element to ensure that the second hover will
// actually be a fresh one. This otherwise breaks if this function is called
// twice in a row.
await browser.hover(`*:not(${selector}, ${selector} *)`);
await browser.hover(selector);

await browser.clickVisible(Selectors.sidebarFavoriteMenuButton(favoriteName));
Expand Down
21 changes: 17 additions & 4 deletions packages/compass-e2e-tests/tests/import-export-connections.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('Connection Import / Export', function () {
await telemetry.stop();
});

for (const variant of ['plaintext', 'encrypted'] as const) {
for (const variant of ['plaintext', 'encrypted', 'protected'] as const) {
it(`can export and import connections through the CLI, ${variant}`, async function () {
if (process.platform === 'win32') {
// TODO(COMPASS-6269): these tests are very flaky on windows
Expand All @@ -53,7 +53,11 @@ describe('Connection Import / Export', function () {
const favoriteName = 'Fav for export';
const passphrase = 'pässwörd';
const passphraseArgs =
variant === 'encrypted' ? [`--passphrase=${passphrase}`] : [];
variant === 'encrypted'
? [`--passphrase=${passphrase}`]
: variant === 'protected'
? ['--protectConnectionStrings']
: [];
const connectionString = 'mongodb://foo:bar@host:1234/';
const connectionStringWithoutCredentials = 'mongodb://foo@host:1234/';

Expand All @@ -76,6 +80,7 @@ describe('Connection Import / Export', function () {
await runCompassOnce([
`--export-connections=${file}`,
...passphraseArgs,
'--trackUsageStatistics',
]);

const contents = JSON.parse(await fs.readFile(file, 'utf8'));
Expand All @@ -90,12 +95,15 @@ describe('Connection Import / Export', function () {
expect(conn.connectionOptions.connectionString).to.equal(
connectionString
);
expect(conn.connectionSecrets).to.not.exist;
} else {
expect(conn.connectionOptions.connectionString).to.equal(
connectionStringWithoutCredentials
);
}
if (variant === 'encrypted') {
expect(conn.connectionSecrets).to.be.a('string');
} else {
expect(conn.connectionSecrets).to.not.exist;
}

const newEvents = telemetry.events().slice(existingEventsCount);
Expand Down Expand Up @@ -124,6 +132,7 @@ describe('Connection Import / Export', function () {
await runCompassOnce([
`--import-connections=${file}`,
...passphraseArgs,
'--trackUsageStatistics',
]);

const newEvents = telemetry.events().slice(existingEventsCount);
Expand All @@ -147,7 +156,11 @@ describe('Connection Import / Export', function () {
);
expect(
await browser.$(Selectors.ConnectionStringInput).getValue()
).to.equal(connectionString);
).to.equal(
variant === 'protected'
? connectionStringWithoutCredentials
: connectionString
);
await browser.selectFavorite(favoriteName);
await browser.selectConnectionMenuItem(
favoriteName,
Expand Down
103 changes: 103 additions & 0 deletions packages/compass-e2e-tests/tests/protect-connection-strings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import type { CompassBrowser } from '../helpers/compass-browser';
import { beforeTests, afterTests, afterTest } from '../helpers/compass';
import type { Compass } from '../helpers/compass';
import clipboard from 'clipboardy';
import { expect } from 'chai';
import * as Selectors from '../helpers/selectors';
import type { ConnectFormState } from '../helpers/connect-form-state';

async function expectCopyConnectionStringToClipboard(
browser: CompassBrowser,
favoriteName: string,
expected: string
): Promise<void> {
if (process.env.COMPASS_E2E_DISABLE_CLIPBOARD_USAGE !== 'true') {
await browser.selectConnectionMenuItem(
favoriteName,
Selectors.CopyConnectionStringItem
);
await browser.waitUntil(
async () => {
return (await clipboard.read()) === expected;
},
{ timeoutMsg: 'Expected copy to clipboard to work' }
);
}
}

describe('protectConnectionStrings', function () {
let compass: Compass;
let browser: CompassBrowser;

before(async function () {
compass = await beforeTests();
browser = compass.browser;
await browser.setFeature('protectConnectionStrings', false);
});

after(async function () {
await browser.setFeature('protectConnectionStrings', false);
await afterTests(compass, this.currentTest);
});

afterEach(async function () {
await afterTest(compass, this.currentTest);
});

it('hides connection string credentials from users', async function () {
const favoriteName = 'protected fave';
const state: ConnectFormState = {
hosts: ['localhost:12345'],
authMethod: 'DEFAULT',
defaultUsername: 'foo',
defaultPassword: 'bar',
};
await browser.setConnectFormState(state);
await browser.saveFavorite(favoriteName, 'color4');
await browser.selectFavorite(favoriteName);

expect(
await browser.$(Selectors.ConnectionStringInput).getValue()
).to.equal('mongodb://foo:*****@localhost:12345/?authMechanism=DEFAULT');

// Enter edit connection string mode
await browser.clickVisible(Selectors.EditConnectionStringToggle);
const confirmModal = await browser.$(Selectors.EditConnectionStringModal);
await confirmModal.waitForDisplayed();
await browser.clickVisible(
Selectors.EditConnectionStringModalConfirmButton
);

expect(
await browser.$(Selectors.ConnectionStringInput).getValue()
).to.equal('mongodb://foo:bar@localhost:12345/?authMechanism=DEFAULT');
await expectCopyConnectionStringToClipboard(
browser,
favoriteName,
'mongodb://foo:bar@localhost:12345/?authMechanism=DEFAULT'
);
await browser
.$(Selectors.EditConnectionStringToggle)
.waitForExist({ reverse: false });
await browser
.$(Selectors.ShowConnectionFormButton)
.waitForExist({ reverse: false });

await browser.setFeature('protectConnectionStrings', true);

expect(
await browser.$(Selectors.ConnectionStringInput).getValue()
).to.equal('mongodb://foo:*****@localhost:12345/?authMechanism=DEFAULT');
await expectCopyConnectionStringToClipboard(
browser,
favoriteName,
'mongodb://<credentials>@localhost:12345/?authMechanism=DEFAULT'
);
await browser
.$(Selectors.EditConnectionStringToggle)
.waitForExist({ reverse: true });
await browser
.$(Selectors.ShowConnectionFormButton)
.waitForExist({ reverse: true });
});
});
3 changes: 3 additions & 0 deletions packages/compass-export-to-language/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"peerDependencies": {
"@mongodb-js/compass-editor": "*",
"@mongodb-js/compass-logging": "*",
"@mongodb-js/compass-maybe-protect-connection-string": "^0.1.0",
"@mongodb-js/mongodb-redux-common": "*",
"bson-transpilers": "*",
"hadron-react-buttons": "*",
Expand All @@ -64,6 +65,7 @@
"dependencies": {
"@mongodb-js/compass-editor": "^0.2.0",
"@mongodb-js/compass-logging": "^1.1.1",
"@mongodb-js/compass-maybe-protect-connection-string": "^0.1.0",
"@mongodb-js/mongodb-redux-common": "^2.0.2",
"bson-transpilers": "^2.0.1",
"hadron-react-buttons": "^6.0.2",
Expand All @@ -77,6 +79,7 @@
"@mongodb-js/webpack-config-compass": "^1.0.3",
"chai": "^4.3.6",
"classnames": "^2.2.6",
"compass-preferences-model": "^2.2.0",
"depcheck": "^1.4.1",
"enzyme": "^3.11.0",
"eslint": "^7.25.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { globalAppRegistryEmit } from '@mongodb-js/mongodb-redux-common/app-regi
import { transpiledExpressionChanged } from './transpiled-expression';
import { importsChanged } from './imports';
import { errorChanged } from './error';
import { maybeProtectConnectionString } from '@mongodb-js/compass-maybe-protect-connection-string';

export const runTranspiler = (input) => {
return (dispatch, getState) => {
Expand All @@ -22,7 +23,7 @@ export const runTranspiler = (input) => {
options: {
collection: ns.collection,
database: ns.database,
uri: state.uri,
uri: maybeProtectConnectionString(state.uri),
},
},
input
Expand Down
36 changes: 36 additions & 0 deletions packages/compass-export-to-language/src/stores/store.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import preferences from 'compass-preferences-model';
import AppRegistry from 'hadron-app-registry';
import { expect } from 'chai';
import compiler from 'bson-transpilers';
import configureStore from './';
import { uriChanged } from '../modules/uri';
import { driverChanged } from '../modules/driver';
import sinon from 'sinon';

const subscribeCheck = (s, pipeline, check, done) => {
const unsubscribe = s.subscribe(function () {
Expand Down Expand Up @@ -89,6 +93,38 @@ describe('ExportToLanguage Store', function () {
);
appRegistry.emit('open-aggregation-export-to-language', agg);
});

for (const protectConnectionStrings of [false, true]) {
context(
`when protect connection strings is ${protectConnectionStrings}`,
function () {
let sandbox;
beforeEach(function () {
sandbox = sinon.createSandbox();
sandbox
.stub(preferences, 'getPreferences')
.returns({ protectConnectionStrings });
});
afterEach(function () {
return sandbox.restore();
});

it('showes/hides the connection string as appropriate', function (done) {
unsubscribe = subscribeCheck(
store,
agg,
(s) =>
s.transpiledExpression.includes('foo:bar') ===
!protectConnectionStrings,
done
);
store.dispatch(uriChanged('mongodb://foo:[email protected]'));
store.dispatch(driverChanged(true));
appRegistry.emit('open-aggregation-export-to-language', agg);
});
}
);
}
});

describe('when query opens export to language with imperfect fields', function () {
Expand Down
8 changes: 8 additions & 0 deletions packages/compass-maybe-protect-connection-string/.depcheckrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ignores:
- '@mongodb-js/prettier-config-compass'
- '@mongodb-js/tsconfig-compass'
- '@types/chai'
- '@types/sinon-chai'
- 'sinon'
ignore-patterns:
- 'dist'
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.nyc-output
dist
8 changes: 8 additions & 0 deletions packages/compass-maybe-protect-connection-string/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
root: true,
extends: ['@mongodb-js/eslint-config-compass'],
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig-lint.json'],
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@mongodb-js/mocha-config-compass');
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.nyc_output
dist
coverage
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"@mongodb-js/prettier-config-compass"
Loading

0 comments on commit beb1ccb

Please sign in to comment.