diff --git a/package.json b/package.json index 7664c20afad..b3400730077 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "packages-version": "lerna version --allow-branch main --no-push --no-private -m \"chore(release): Bump package versions\"", "release": "npm run release --workspace mongodb-compass --", "reformat": "lerna run reformat --stream --no-bail", + "reformat-changed": "lerna run reformat --stream --no-bail --since origin/HEAD --exclude-dependents", "package-compass": "npm run package-compass --workspace=mongodb-compass --", "prestart": "npm run compile --workspace=@mongodb-js/webpack-config-compass", "start": "npm run start --workspace=mongodb-compass", diff --git a/packages/connect-form/src/components/advanced-options-tabs/advanced-tab/advanced-tab.tsx b/packages/connect-form/src/components/advanced-options-tabs/advanced-tab/advanced-tab.tsx index ddf23c17b22..0342dafe173 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/advanced-tab/advanced-tab.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/advanced-tab/advanced-tab.tsx @@ -50,7 +50,7 @@ function AdvancedTab({ : pathname; const handleFieldChanged = useCallback( - (key: keyof MongoClientOptions, value: unknown) => { + (key: keyof MongoClientOptions, value?: string) => { if (!value) { return updateConnectionFormField({ type: 'delete-search-param', diff --git a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.spec.tsx b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.spec.tsx index 650f9e8684a..0194e9bb644 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.spec.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.spec.tsx @@ -156,12 +156,12 @@ describe('AuthenticationDefault Component', function () { it('decodes the password as a uri component before rendering', function () { renderComponent({ connectionStringUrl: new ConnectionStringUrl( - 'mongodb://C%3BIb86n5b8%7BAnExew%5BTU%25XZy%2C)E6G!dk:password@outerspace:12345' + 'mongodb://username:C%3BIb86n5b8%7BAnExew%5BTU%25XZy%2C)E6G!dk@outerspace:12345' ), updateConnectionFormField: updateConnectionFormFieldSpy, }); - expect(screen.getByLabelText('Username').getAttribute('value')).to.equal( + expect(screen.getByLabelText('Password').getAttribute('value')).to.equal( 'C;Ib86n5b8{AnExew[TU%XZy,)E6G!dk' ); }); @@ -171,12 +171,26 @@ describe('AuthenticationDefault Component', function () { errors: [ { fieldName: 'username', - message: 'pineapples', + message: 'username error', + }, + ], + updateConnectionFormField: updateConnectionFormFieldSpy, + }); + + expect(screen.getByText('username error')).to.be.visible; + }); + + it('renders a password error when there is a password error', function () { + renderComponent({ + errors: [ + { + fieldName: 'password', + message: 'password error', }, ], updateConnectionFormField: updateConnectionFormFieldSpy, }); - expect(screen.getByText('pineapples')).to.be.visible; + expect(screen.getByText('password error')).to.be.visible; }); }); diff --git a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.tsx b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.tsx index d615f171a04..bbe126ae46f 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.tsx @@ -14,7 +14,14 @@ import { AuthMechanism } from 'mongodb'; import { UpdateConnectionFormField } from '../../../hooks/use-connect-form'; import FormFieldContainer from '../../form-field-container'; -import { ConnectionFormError } from '../../../utils/validation'; +import { + ConnectionFormError, + errorMessageByFieldName, +} from '../../../utils/validation'; +import { + getConnectionStringPassword, + getConnectionStringUsername, +} from '../../../utils/connection-string-helpers'; const authSourceLabelStyles = css({ padding: 0, @@ -54,8 +61,8 @@ function AuthenticationDefault({ errors: ConnectionFormError[]; updateConnectionFormField: UpdateConnectionFormField; }): React.ReactElement { - const password = decodeURIComponent(connectionStringUrl.password); - const username = decodeURIComponent(connectionStringUrl.username); + const password = getConnectionStringPassword(connectionStringUrl); + const username = getConnectionStringUsername(connectionStringUrl); const selectedAuthMechanism = connectionStringUrl.searchParams.get('authMechanism') ?? ''; @@ -76,7 +83,8 @@ function AuthenticationDefault({ [updateConnectionFormField] ); - const usernameError = errors?.find((error) => error.fieldName === 'username'); + const usernameError = errorMessageByFieldName(errors, 'username'); + const passwordError = errorMessageByFieldName(errors, 'password'); return ( <> @@ -91,7 +99,7 @@ function AuthenticationDefault({ }); }} label="Username" - errorMessage={usernameError?.message} + errorMessage={usernameError} state={usernameError ? 'error' : undefined} value={username || ''} /> @@ -109,6 +117,8 @@ function AuthenticationDefault({ label="Password" type="password" value={password || ''} + errorMessage={passwordError} + state={passwordError ? 'error' : undefined} /> diff --git a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx new file mode 100644 index 00000000000..aafcc909271 --- /dev/null +++ b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import ConnectionStringUrl from 'mongodb-connection-string-url'; + +import AuthenticationGssapi from './authentication-gssapi'; +import { ConnectionFormError } from '../../../utils/validation'; +import { UpdateConnectionFormField } from '../../../hooks/use-connect-form'; + +function renderComponent({ + errors = [], + connectionStringUrl = new ConnectionStringUrl('mongodb://localhost:27017'), + updateConnectionFormField, +}: { + connectionStringUrl?: ConnectionStringUrl; + errors?: ConnectionFormError[]; + updateConnectionFormField: UpdateConnectionFormField; +}) { + render( + + ); +} + +describe('AuthenticationGssapi Component', function () { + let updateConnectionFormFieldSpy: sinon.SinonSpy; + beforeEach(function () { + updateConnectionFormFieldSpy = sinon.spy(); + }); + + describe('when the kerberosPrincipal input is changed', function () { + beforeEach(function () { + renderComponent({ + updateConnectionFormField: updateConnectionFormFieldSpy, + }); + expect(updateConnectionFormFieldSpy.callCount).to.equal(0); + + fireEvent.change(screen.getByLabelText('Principal'), { + target: { value: 'good sandwich' }, + }); + }); + + it('calls to update the form field', function () { + expect(updateConnectionFormFieldSpy.callCount).to.equal(1); + expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({ + type: 'update-username', + username: 'good sandwich', + }); + }); + }); + + describe('when the serviceName input is changed', function () { + beforeEach(function () { + renderComponent({ + updateConnectionFormField: updateConnectionFormFieldSpy, + }); + expect(updateConnectionFormFieldSpy.callCount).to.equal(0); + + fireEvent.change(screen.getByLabelText('Service Name'), { + target: { value: 'good sandwich' }, + }); + }); + + it('calls to update the form field', function () { + expect(updateConnectionFormFieldSpy.callCount).to.equal(1); + expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({ + key: 'SERVICE_NAME', + type: 'update-auth-mechanism-property', + value: 'good sandwich', + }); + }); + }); + + describe('when the serviceRealm input is changed', function () { + beforeEach(function () { + renderComponent({ + updateConnectionFormField: updateConnectionFormFieldSpy, + }); + expect(updateConnectionFormFieldSpy.callCount).to.equal(0); + + fireEvent.change(screen.getByLabelText('Service Realm'), { + target: { value: 'good sandwich' }, + }); + }); + + it('calls to update the form field', function () { + expect(updateConnectionFormFieldSpy.callCount).to.equal(1); + expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({ + key: 'SERVICE_REALM', + type: 'update-auth-mechanism-property', + value: 'good sandwich', + }); + }); + }); + + describe('when the canoncalize hostname is changed', function () { + beforeEach(function () { + renderComponent({ + updateConnectionFormField: updateConnectionFormFieldSpy, + }); + + expect(updateConnectionFormFieldSpy.callCount).to.equal(0); + const checkbox = screen.getByLabelText('Canonicalize Host Name'); + fireEvent.click(checkbox); + }); + + it('calls to update the form field', function () { + expect(updateConnectionFormFieldSpy.callCount).to.equal(1); + expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({ + key: 'CANONICALIZE_HOST_NAME', + type: 'update-auth-mechanism-property', + value: 'true', + }); + }); + }); + + it('renders an error when there is a kerberosPrincipal error', function () { + renderComponent({ + errors: [ + { + fieldName: 'kerberosPrincipal', + message: 'kerberosPrincipal error', + }, + ], + updateConnectionFormField: updateConnectionFormFieldSpy, + }); + + expect(screen.getByText('kerberosPrincipal error')).to.be.visible; + }); +}); diff --git a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx index 4c1cb74e23d..873e2a6a902 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.tsx @@ -1,9 +1,105 @@ import React from 'react'; +import { Checkbox, TextInput } from '@mongodb-js/compass-components'; + +import ConnectionStringUrl from 'mongodb-connection-string-url'; +import { UpdateConnectionFormField } from '../../../hooks/use-connect-form'; +import FormFieldContainer from '../../form-field-container'; +import { + ConnectionFormError, + errorMessageByFieldName, +} from '../../../utils/validation'; +import { + getConnectionStringUsername, + parseAuthMechanismProperties, +} from '../../../utils/connection-string-helpers'; + +function AuthenticationGSSAPI({ + errors, + connectionStringUrl, + updateConnectionFormField, +}: { + connectionStringUrl: ConnectionStringUrl; + errors: ConnectionFormError[]; + updateConnectionFormField: UpdateConnectionFormField; +}): React.ReactElement { + const kerberosPrincipalError = errorMessageByFieldName( + errors, + 'kerberosPrincipal' + ); + const principal = getConnectionStringUsername(connectionStringUrl); + const authMechanismProperties = + parseAuthMechanismProperties(connectionStringUrl); + const serviceName = authMechanismProperties.get('SERVICE_NAME'); + const serviceRealm = authMechanismProperties.get('SERVICE_REALM'); + const canonicalizeHostname = authMechanismProperties.get( + 'CANONICALIZE_HOST_NAME' + ); -function AuthenticationGSSAPI(): React.ReactElement { return ( <> -

Kerberos

+ + ) => { + updateConnectionFormField({ + type: 'update-username', + username: value, + }); + }} + label="Principal" + errorMessage={kerberosPrincipalError} + state={kerberosPrincipalError ? 'error' : undefined} + value={principal || ''} + /> + + + + ) => { + updateConnectionFormField({ + type: 'update-auth-mechanism-property', + key: 'SERVICE_NAME', + value: value, + }); + }} + label="Service Name" + value={serviceName || ''} + /> + + + + ) => { + updateConnectionFormField({ + type: 'update-auth-mechanism-property', + key: 'CANONICALIZE_HOST_NAME', + value: event.target.checked ? 'true' : '', + }); + }} + label="Canonicalize Host Name" + checked={canonicalizeHostname === 'true'} + bold={false} + /> + + + + ) => { + updateConnectionFormField({ + type: 'update-auth-mechanism-property', + key: 'SERVICE_REALM', + value: value, + }); + }} + label="Service Realm" + value={serviceRealm || ''} + /> + ); } diff --git a/packages/connect-form/src/components/advanced-options-tabs/general-tab/direct-connection-input.spec.tsx b/packages/connect-form/src/components/advanced-options-tabs/general-tab/direct-connection-input.spec.tsx index 0a0b0cc8949..0ad2b20e4d6 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/general-tab/direct-connection-input.spec.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/general-tab/direct-connection-input.spec.tsx @@ -78,7 +78,7 @@ describe('DirectConnectionInput', function () { expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({ type: 'update-search-param', currentKey: 'directConnection', - value: true, + value: 'true', }); }); }); @@ -113,7 +113,7 @@ describe('DirectConnectionInput', function () { expect(updateConnectionFormFieldSpy.firstCall.args[0]).to.deep.equal({ type: 'update-search-param', currentKey: 'directConnection', - value: true, + value: 'true', }); }); }); diff --git a/packages/connect-form/src/components/advanced-options-tabs/general-tab/direct-connection-input.tsx b/packages/connect-form/src/components/advanced-options-tabs/general-tab/direct-connection-input.tsx index cf65e85b27c..00f1bebecc2 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/general-tab/direct-connection-input.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/general-tab/direct-connection-input.tsx @@ -28,7 +28,7 @@ function DirectConnectionInput({ return updateConnectionFormField({ type: 'update-search-param', currentKey: 'directConnection', - value: event.target.checked, + value: event.target.checked ? 'true' : 'false', }); }, [updateConnectionFormField] diff --git a/packages/connect-form/src/components/advanced-options-tabs/ssh-tunnel-tab/socks.tsx b/packages/connect-form/src/components/advanced-options-tabs/ssh-tunnel-tab/socks.tsx index b8b519fb9d3..a3707f9659f 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/ssh-tunnel-tab/socks.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/ssh-tunnel-tab/socks.tsx @@ -38,7 +38,7 @@ function Socks({ connectionStringUrl.typedSearchParams(); const handleFieldChanged = useCallback( - (key: keyof MongoClientOptions, value: unknown) => { + (key: keyof MongoClientOptions, value?: string) => { if (!value) { return updateConnectionFormField({ type: 'delete-search-param', diff --git a/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.spec.tsx b/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.spec.tsx index 3e1bf3b59b4..63794ee7cb1 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.spec.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.spec.tsx @@ -248,7 +248,7 @@ describe('SchemaInput', function () { ).to.deep.equal({ type: 'update-search-param', currentKey: connectionStringTlsParam, - value: true, + value: 'true', }); }); }); diff --git a/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.tsx b/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.tsx index 4ea2bf545f1..a8858b7f4be 100644 --- a/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.tsx +++ b/packages/connect-form/src/components/advanced-options-tabs/tls-ssl-tab/tls-ssl-tab.tsx @@ -144,7 +144,7 @@ function TLSTab({ const tlsOptionsDisabled = tlsOption !== 'ON'; const handleFieldChanged = useCallback( - (key: keyof MongoClientOptions, value: unknown) => { + (key: keyof MongoClientOptions, value?: string | null) => { if (!value) { return updateConnectionFormField({ type: 'delete-search-param', @@ -212,7 +212,10 @@ function TLSTab({ ) => { - handleFieldChanged(tlsOptionField.name, event.target.checked); + handleFieldChanged( + tlsOptionField.name, + event.target.checked ? 'true' : '' + ); }} data-testid={`${tlsOptionField.name}-input`} label={tlsOptionField.name} diff --git a/packages/connect-form/src/hooks/use-connect-form.ts b/packages/connect-form/src/hooks/use-connect-form.ts index c937679eb7b..8ba1331c314 100644 --- a/packages/connect-form/src/hooks/use-connect-form.ts +++ b/packages/connect-form/src/hooks/use-connect-form.ts @@ -1,14 +1,11 @@ import { Dispatch, useCallback, useEffect, useReducer } from 'react'; -import ConnectionStringUrl from 'mongodb-connection-string-url'; import { ConnectionInfo, ConnectionOptions } from 'mongodb-data-service'; import type { MongoClientOptions, ProxyOptions } from 'mongodb'; import { cloneDeep } from 'lodash'; -import ConnectionString from 'mongodb-connection-string-url'; - +import ConnectionStringUrl from 'mongodb-connection-string-url'; import { ConnectionFormError, ConnectionFormWarning, - tryToParseConnectionString, validateConnectionOptionsWarnings, } from '../utils/validation'; import { getNextHost } from '../utils/get-next-host'; @@ -35,6 +32,11 @@ import { UpdatePasswordAction, UpdateUsernameAction, } from '../utils/authentication-handler'; +import { + AuthMechanismProperties, + parseAuthMechanismProperties, + tryToParseConnectionString, +} from '../utils/connection-string-helpers'; export interface ConnectFormState { connectionOptions: ConnectionOptions; @@ -117,12 +119,17 @@ type ConnectionFormFieldActions = type: 'update-search-param'; currentKey: keyof MongoClientOptions; newKey?: keyof MongoClientOptions; - value?: unknown; + value?: string; } | { type: 'delete-search-param'; key: keyof MongoClientOptions; } + | { + type: 'update-auth-mechanism-property'; + key: keyof AuthMechanismProperties; + value?: string; + } | { type: 'update-connection-path'; value: string; @@ -140,7 +147,7 @@ export type UpdateConnectionFormField = ( function parseConnectionString( connectionString: string -): [ConnectionString | undefined, ConnectionFormError[]] { +): [ConnectionStringUrl | undefined, ConnectionFormError[]] { const [parsedConnectionString, parsingError] = tryToParseConnectionString(connectionString); @@ -412,6 +419,29 @@ export function handleConnectionFormFieldUpdate( }, }; } + case 'update-auth-mechanism-property': { + const authMechanismProperties = parseAuthMechanismProperties( + parsedConnectionStringUrl + ); + + if (action.value) { + authMechanismProperties.set(action.key, action.value); + } else { + authMechanismProperties.delete(action.key); + } + + updatedSearchParams.set( + 'authMechanismProperties', + authMechanismProperties.toString() + ); + + return { + connectionOptions: { + ...currentConnectionOptions, + connectionString: parsedConnectionStringUrl.toString(), + }, + }; + } case 'delete-search-param': { updatedSearchParams.delete(action.key); return { diff --git a/packages/connect-form/src/utils/authentication-handler.spec.ts b/packages/connect-form/src/utils/authentication-handler.spec.ts index 2b08f5d3b54..67871bf7c11 100644 --- a/packages/connect-form/src/utils/authentication-handler.spec.ts +++ b/packages/connect-form/src/utils/authentication-handler.spec.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { MongoClientOptions } from 'mongodb'; +import { AuthMechanism, MongoClientOptions } from 'mongodb'; import ConnectionString from 'mongodb-connection-string-url'; import { @@ -230,11 +230,63 @@ describe('Authentication Handler', function () { }); expect(res.connectionOptions.connectionString).to.equal( - 'mongodb://localhost/?authMechanism=PLAIN' + 'mongodb://localhost/?authMechanism=PLAIN&authSource=%24external' ); expect(res.errors).to.equal(undefined); }); + it('should set authSource=$external when needed', function () { + const externalAuthMechanisms: AuthMechanism[] = [ + 'MONGODB-AWS', + 'GSSAPI', + 'PLAIN', + 'MONGODB-X509', + ]; + + for (const authMechanism of externalAuthMechanisms) { + const res = handleUpdateAuthMechanism({ + action: { + type: 'update-auth-mechanism', + authMechanism, + }, + connectionStringUrl: new ConnectionString(testConnectionString), + connectionOptions: { + connectionString: testConnectionString, + }, + }); + + expect(res.connectionOptions.connectionString).to.equal( + `mongodb://localhost/?authMechanism=${authMechanism}&authSource=%24external` + ); + } + }); + + it('should not set authSource=$external when not needed', function () { + const externalAuthMechanisms: AuthMechanism[] = [ + 'MONGODB-CR', + 'DEFAULT', + 'SCRAM-SHA-1', + 'SCRAM-SHA-256', + ]; + + for (const authMechanism of externalAuthMechanisms) { + const res = handleUpdateAuthMechanism({ + action: { + type: 'update-auth-mechanism', + authMechanism, + }, + connectionStringUrl: new ConnectionString(testConnectionString), + connectionOptions: { + connectionString: testConnectionString, + }, + }); + + expect(res.connectionOptions.connectionString).to.equal( + `mongodb://localhost/?authMechanism=${authMechanism}` + ); + } + }); + it('should remove the username/password field when being set to no auth', function () { const res = handleUpdateAuthMechanism({ action: { diff --git a/packages/connect-form/src/utils/authentication-handler.ts b/packages/connect-form/src/utils/authentication-handler.ts index 6b7ce770e62..2be3950b879 100644 --- a/packages/connect-form/src/utils/authentication-handler.ts +++ b/packages/connect-form/src/utils/authentication-handler.ts @@ -3,7 +3,12 @@ import ConnectionStringUrl from 'mongodb-connection-string-url'; import { ConnectionOptions } from 'mongodb-data-service'; import type { MongoClientOptions } from 'mongodb'; -import { ConnectionFormError, tryToParseConnectionString } from './validation'; +import { ConnectionFormError } from './validation'; +import { + setConnectionStringPassword, + setConnectionStringUsername, + tryToParseConnectionString, +} from './connection-string-helpers'; export type UpdateAuthMechanismAction = { type: 'update-auth-mechanism'; @@ -60,6 +65,13 @@ export function handleUpdateAuthMechanism({ if (action.authMechanism) { updatedSearchParams.set('authMechanism', action.authMechanism); + if ( + ['MONGODB-AWS', 'GSSAPI', 'PLAIN', 'MONGODB-X509'].includes( + action.authMechanism + ) + ) { + updatedSearchParams.set('authSource', '$external'); + } } return { @@ -82,9 +94,10 @@ export function handleUpdateUsername({ connectionOptions: ConnectionOptions; errors?: ConnectionFormError[]; } { - const updatedConnectionString = connectionStringUrl.clone(); - - updatedConnectionString.username = encodeURIComponent(action.username); + const updatedConnectionString = setConnectionStringUsername( + connectionStringUrl, + action.username + ); const [, parsingError] = tryToParseConnectionString( updatedConnectionString.toString() @@ -124,9 +137,10 @@ export function handleUpdatePassword({ connectionOptions: ConnectionOptions; errors?: ConnectionFormError[]; } { - const updatedConnectionString = connectionStringUrl.clone(); - - updatedConnectionString.password = encodeURIComponent(action.password); + const updatedConnectionString = setConnectionStringPassword( + connectionStringUrl, + action.password + ); const [, parsingError] = tryToParseConnectionString( updatedConnectionString.toString() diff --git a/packages/connect-form/src/utils/connection-string-helpers.spec.ts b/packages/connect-form/src/utils/connection-string-helpers.spec.ts new file mode 100644 index 00000000000..af47fd49410 --- /dev/null +++ b/packages/connect-form/src/utils/connection-string-helpers.spec.ts @@ -0,0 +1,133 @@ +import { expect } from 'chai'; +import ConnectionStringUrl from 'mongodb-connection-string-url'; +import { + getConnectionStringPassword, + getConnectionStringUsername, + parseAuthMechanismProperties, + setConnectionStringPassword, + setConnectionStringUsername, + tryToParseConnectionString, +} from './connection-string-helpers'; + +describe('connection-string-helpers', function () { + describe('#parseAuthMechanismProperties', function () { + it('parses legit authMechanismProperties string', function () { + const props = parseAuthMechanismProperties( + new ConnectionStringUrl( + 'mongodb://localhost?authMechanismProperties=SERVICE_NAME:serviceName' + ) + ); + + expect(props.get('SERVICE_NAME')).to.equal('serviceName'); + }); + + it('parses broken authMechanismProperties string as empty', function () { + const props = parseAuthMechanismProperties( + new ConnectionStringUrl( + 'mongodb://localhost?authMechanismProperties=broken' + ) + ); + + expect(props.get('SERVICE_NAME')).to.equal(undefined); + }); + }); + + describe('#tryToParseConnectionString', function () { + it('should return the connection string when successfully parsed', function () { + const [connectionString] = tryToParseConnectionString( + 'mongodb://outerspace:27099?directConnection=true' + ); + + expect(connectionString.toString()).to.equal( + 'mongodb://outerspace:27099/?directConnection=true' + ); + expect(connectionString.hosts[0]).to.equal('outerspace:27099'); + }); + + it('should return without an error when successfully parsed', function () { + const [connectionString, error] = tryToParseConnectionString( + 'mongodb://outerspace:27099/?directConnection=true' + ); + + expect(connectionString).to.not.equal(undefined); + expect(error).to.equal(undefined); + }); + + it('should return an error when it cannot be parsed', function () { + const [connectionString, error] = tryToParseConnectionString( + 'mangos://pineapple:27099/?directConnection=true' + ); + + expect(connectionString).to.equal(undefined); + expect(error.message).to.equal( + 'Invalid connection string "mangos://pineapple:27099/?directConnection=true"' + ); + }); + }); + + describe('#getConnectionStringUsername', function () { + it('returns a decoded username', function () { + const username = getConnectionStringUsername( + new ConnectionStringUrl( + 'mongodb://C%3BIb86n5b8%7BAnExew%5BTU%25XZy%2C)E6G!dk:password@outerspace:12345' + ) + ); + + expect(username).to.equal('C;Ib86n5b8{AnExew[TU%XZy,)E6G!dk'); + }); + }); + + describe('#getConnectionStringPassword', function () { + it('returns a decoded password', function () { + const password = getConnectionStringPassword( + new ConnectionStringUrl( + 'mongodb://username:C%3BIb86n5b8%7BAnExew%5BTU%25XZy%2C)E6G!dk@outerspace:12345' + ) + ); + + expect(password).to.equal('C;Ib86n5b8{AnExew[TU%XZy,)E6G!dk'); + }); + }); + + describe('#setConnectionStringUsername', function () { + it('sets an encoded username and does not mutate the param', function () { + const connectionString = new ConnectionStringUrl( + 'mongodb://username:password@outerspace:12345' + ); + + expect( + setConnectionStringUsername( + connectionString, + 'C;Ib86n5b8{AnExew[TU%XZy,)E6G!dk' + ).href + ).to.equal( + 'mongodb://C%3BIb86n5b8%7BAnExew%5BTU%25XZy%2C)E6G!dk:password@outerspace:12345/' + ); + + expect(connectionString.href).to.equal( + 'mongodb://username:password@outerspace:12345/' + ); + }); + }); + + describe('#setConnectionStringPassword', function () { + it('sets an encoded password and does not mutate the param', function () { + const connectionString = new ConnectionStringUrl( + 'mongodb://username:password@outerspace:12345' + ); + + expect( + setConnectionStringPassword( + connectionString, + 'C;Ib86n5b8{AnExew[TU%XZy,)E6G!dk' + ).href + ).to.equal( + 'mongodb://username:C%3BIb86n5b8%7BAnExew%5BTU%25XZy%2C)E6G!dk@outerspace:12345/' + ); + + expect(connectionString.href).to.equal( + 'mongodb://username:password@outerspace:12345/' + ); + }); + }); +}); diff --git a/packages/connect-form/src/utils/connection-string-helpers.ts b/packages/connect-form/src/utils/connection-string-helpers.ts new file mode 100644 index 00000000000..3ee1e319a0b --- /dev/null +++ b/packages/connect-form/src/utils/connection-string-helpers.ts @@ -0,0 +1,68 @@ +import type { MongoClientOptions } from 'mongodb'; +import ConnectionStringUrl, { + CommaAndColonSeparatedRecord, +} from 'mongodb-connection-string-url'; + +export interface AuthMechanismProperties { + SERVICE_NAME?: string; + SERVICE_REALM?: string; + CANONICALIZE_HOST_NAME?: boolean; + AWS_SESSION_TOKEN?: string; +} + +export function parseAuthMechanismProperties( + connectionString: ConnectionStringUrl +): CommaAndColonSeparatedRecord { + const searchParams = connectionString.typedSearchParams(); + const authMechanismPropertiesString = searchParams.get( + 'authMechanismProperties' + ); + try { + return new CommaAndColonSeparatedRecord( + authMechanismPropertiesString + ); + } catch (e) { + return new CommaAndColonSeparatedRecord(); + } +} + +export function tryToParseConnectionString( + connectionString: string +): [ConnectionStringUrl | undefined, Error | undefined] { + try { + const connectionStringUrl = new ConnectionStringUrl(connectionString); + return [connectionStringUrl, undefined]; + } catch (err) { + return [undefined, err as Error]; + } +} + +export function getConnectionStringUsername( + connectionStringUrl: ConnectionStringUrl +): string { + return decodeURIComponent(connectionStringUrl.username); +} + +export function getConnectionStringPassword( + connectionStringUrl: ConnectionStringUrl +): string { + return decodeURIComponent(connectionStringUrl.password); +} + +export function setConnectionStringUsername( + connectionStringUrl: ConnectionStringUrl, + username: string +): ConnectionStringUrl { + const updated = connectionStringUrl.clone(); + updated.username = encodeURIComponent(username); + return updated; +} + +export function setConnectionStringPassword( + connectionStringUrl: ConnectionStringUrl, + password: string +): ConnectionStringUrl { + const updated = connectionStringUrl.clone(); + updated.password = encodeURIComponent(password); + return updated; +} diff --git a/packages/connect-form/src/utils/url-options.ts b/packages/connect-form/src/utils/url-options.ts index 67e839ff3e7..558a2c30180 100644 --- a/packages/connect-form/src/utils/url-options.ts +++ b/packages/connect-form/src/utils/url-options.ts @@ -36,10 +36,6 @@ export const editableUrlOptions = [ title: 'Read Preferences Options', values: ['maxStalenessSeconds', 'readPreferenceTags'], }, - { - title: 'Authentication Options', - values: ['authMechanismProperties', 'gssapiServiceName'], - }, { title: 'Server Options', values: [ diff --git a/packages/connect-form/src/utils/validation.spec.ts b/packages/connect-form/src/utils/validation.spec.ts index 51f88952a8b..95ef2d0970f 100644 --- a/packages/connect-form/src/utils/validation.spec.ts +++ b/packages/connect-form/src/utils/validation.spec.ts @@ -1,7 +1,6 @@ import { expect } from 'chai'; import { - tryToParseConnectionString, validateConnectionOptionsErrors, validateConnectionOptionsWarnings, } from './validation'; @@ -484,37 +483,4 @@ describe('validation', function () { }); }); }); - - describe('#tryToParseConnectionString', function () { - it('should return the connection string when successfully parsed', function () { - const [connectionString] = tryToParseConnectionString( - 'mongodb://outerspace:27099?directConnection=true' - ); - - expect(connectionString.toString()).to.equal( - 'mongodb://outerspace:27099/?directConnection=true' - ); - expect(connectionString.hosts[0]).to.equal('outerspace:27099'); - }); - - it('should return without an error when successfully parsed', function () { - const [connectionString, error] = tryToParseConnectionString( - 'mongodb://outerspace:27099/?directConnection=true' - ); - - expect(connectionString).to.not.equal(undefined); - expect(error).to.equal(undefined); - }); - - it('should return an error when it cannot be parsed', function () { - const [connectionString, error] = tryToParseConnectionString( - 'mangos://pineapple:27099/?directConnection=true' - ); - - expect(connectionString).to.equal(undefined); - expect(error.message).to.equal( - 'Invalid connection string "mangos://pineapple:27099/?directConnection=true"' - ); - }); - }); }); diff --git a/packages/connect-form/src/utils/validation.ts b/packages/connect-form/src/utils/validation.ts index d2dfdcb176b..25898e3e07f 100644 --- a/packages/connect-form/src/utils/validation.ts +++ b/packages/connect-form/src/utils/validation.ts @@ -30,17 +30,6 @@ export interface ConnectionFormWarning { message: string; } -export function tryToParseConnectionString( - connectionString: string -): [ConnectionString | undefined, Error | undefined] { - try { - const connectionStringUrl = new ConnectionString(connectionString); - return [connectionStringUrl, undefined]; - } catch (err) { - return [undefined, err as Error]; - } -} - export function errorMessageByFieldName( errors: ConnectionFormError[], fieldName: FieldName