Skip to content

Commit c9c3ca9

Browse files
authored
feat(compass-settings): add OIDC browser setting COMPASS-6849 (#4469)
1 parent 5e0eb3b commit c9c3ca9

File tree

9 files changed

+252
-31
lines changed

9 files changed

+252
-31
lines changed

packages/compass-connections/src/components/connections.spec.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ describe('Connections Component', function () {
206206
expect(mockConnectFn.firstCall.args[0]).to.deep.equal({
207207
connectionString:
208208
'mongodb://localhost:27018/?readPreference=primary&ssl=false&appName=Test+App+Name',
209+
oidc: {},
209210
});
210211
});
211212

@@ -258,6 +259,7 @@ describe('Connections Component', function () {
258259
expect(mockConnectFn.callCount).to.equal(1);
259260
expect(mockConnectFn.firstCall.args[0]).to.deep.equal({
260261
connectionString: 'mongodb://localhost:27019/?appName=Some+App+Name',
262+
oidc: {},
261263
});
262264
});
263265
});
@@ -380,6 +382,7 @@ describe('Connections Component', function () {
380382
expect(mockConnectFn.firstCall.args[0]).to.deep.equal({
381383
connectionString:
382384
'mongodb://localhost:27099/?connectTimeoutMS=5000&serverSelectionTimeoutMS=5000&appName=Test+App+Name',
385+
oidc: {},
383386
});
384387
});
385388

@@ -416,6 +419,7 @@ describe('Connections Component', function () {
416419
expect(mockConnectFn.secondCall.args[0]).to.deep.equal({
417420
connectionString:
418421
'mongodb://localhost:27018/?readPreference=primary&ssl=false&appName=Test+App+Name',
422+
oidc: {},
419423
});
420424
});
421425

packages/compass-e2e-tests/tests/oidc.test.ts

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ const DEFAULT_AUTH_INFO = {
3434
authenticatedUserRoles: [{ role: 'dev/testgroup', db: 'admin' }],
3535
};
3636

37+
const host = '127.0.0.1';
38+
39+
function getTestBrowserShellCommand() {
40+
return `${process.execPath} ${path.resolve(
41+
__dirname,
42+
'..',
43+
'fixtures',
44+
'curl.js'
45+
)}`;
46+
}
47+
3748
describe('OIDC integration', function () {
3849
let compass: Compass;
3950
let browser: CompassBrowser;
@@ -44,6 +55,7 @@ describe('OIDC integration', function () {
4455
let oidcMockProvider: OIDCMockProvider;
4556

4657
let i = 0;
58+
let port: number;
4759
let tmpdir: string;
4860
let server: ChildProcess;
4961
let serverExit: Promise<unknown>;
@@ -121,7 +133,7 @@ describe('OIDC integration', function () {
121133

122134
serverExit = once(server, 'exit');
123135

124-
const port = await Promise.race([
136+
port = await Promise.race([
125137
serverExit.then((code) => {
126138
throw new Error(`mongod exited with code ${code}`);
127139
}),
@@ -141,28 +153,33 @@ describe('OIDC integration', function () {
141153
})(),
142154
]);
143155

144-
connectionString = `mongodb://127.0.0.1:${port}/?authMechanism=MONGODB-OIDC`;
156+
connectionString = `mongodb://${host}:${port}/?authMechanism=MONGODB-OIDC`;
145157
}
146-
147-
process.env.COMPASS_TEST_OIDC_BROWSER_DUMMY = `${
148-
process.execPath
149-
} ${path.resolve(__dirname, '..', 'fixtures', 'curl.js')}`;
150158
});
151159

152160
beforeEach(async function () {
153161
getTokenPayload = () => DEFAULT_TOKEN_PAYLOAD;
154162
overrideRequestHandler = () => {};
155-
compass = await beforeTests();
163+
compass = await beforeTests({
164+
// TODO(COMPASS-6803): Remove feature flag: enableOidc.
165+
// Note: This isn't needed to connect, but shows the oidc options in the
166+
// connect form and settings.
167+
extraSpawnArgs: ['--enable-oidc'],
168+
});
156169
browser = compass.browser;
170+
await browser.setFeature(
171+
'browserCommandForOIDCAuth',
172+
getTestBrowserShellCommand()
173+
);
157174
});
158175

159176
afterEach(async function () {
177+
await browser.setFeature('browserCommandForOIDCAuth', undefined);
160178
await afterTest(compass, this.currentTest);
161179
await afterTests(compass, this.currentTest);
162180
});
163181

164182
after(async function () {
165-
delete process.env.COMPASS_TEST_OIDC_BROWSER_DUMMY;
166183
server?.kill();
167184
await serverExit;
168185
await oidcMockProvider?.close();
@@ -223,4 +240,28 @@ describe('OIDC integration', function () {
223240

224241
expect(result).to.deep.equal(DEFAULT_AUTH_INFO);
225242
});
243+
244+
it('can successfully connect with the connection form', async function () {
245+
let tokenFetchCalls = 0;
246+
getTokenPayload = () => {
247+
tokenFetchCalls++;
248+
return DEFAULT_TOKEN_PAYLOAD;
249+
};
250+
251+
await browser.setConnectFormState({
252+
hosts: [`${host}:${port}`],
253+
authMethod: 'MONGODB-OIDC',
254+
});
255+
await browser.clickVisible(Selectors.ConnectButton);
256+
257+
await browser.waitForConnectionResult('success');
258+
259+
const result: any = await browser.shellEval(
260+
'db.runCommand({ connectionStatus: 1 }).authInfo',
261+
true
262+
);
263+
264+
expect(tokenFetchCalls).to.equal(1); // No separate request from the shell.
265+
expect(result).to.deep.equal(DEFAULT_AUTH_INFO);
266+
});
226267
});

packages/compass-preferences-model/src/preferences.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export type UserConfigurablePreferences = PermanentFeatureFlags &
3737
forceConnectionOptions?: [key: string, value: string][];
3838
showKerberosPasswordField: boolean;
3939
showOIDCDeviceAuthFlow: boolean;
40+
browserCommandForOIDCAuth?: string;
4041
enableDevTools: boolean;
4142
theme: THEMES;
4243
maxTimeMS?: number;
@@ -532,6 +533,21 @@ const modelPreferencesProps: Required<{
532533
long: 'Show a checkbox on the connection form to enable device auth flow authentication. This enables a less secure authentication flow that can be used as a fallback when browser-based authentication is unavailable.',
533534
},
534535
},
536+
/**
537+
* Input to change the browser command used for OIDC authentication.
538+
*/
539+
browserCommandForOIDCAuth: {
540+
type: 'string',
541+
required: false,
542+
default: undefined,
543+
ui: true,
544+
cli: true,
545+
global: true,
546+
description: {
547+
short: 'Browser command to use for OIDC Authentication',
548+
long: 'Specify a shell command that is run to start the browser for authenticating with the OIDC identity provider. Leave this empty for default browser.',
549+
},
550+
},
535551
/**
536552
* Override certain connection string properties.
537553
*/

packages/compass-settings/src/components/settings/oidc-settings.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import type { SettingsListProps } from './settings-list';
77
import { SettingsList } from './settings-list';
88
import { pick } from '../../utils/pick';
99

10-
const oidcFields = ['showOIDCDeviceAuthFlow'] as const;
10+
const oidcFields = [
11+
'browserCommandForOIDCAuth',
12+
'showOIDCDeviceAuthFlow',
13+
] as const;
1114
type OIDCFields = typeof oidcFields[number];
1215
type OIDCSettingsProps = Omit<SettingsListProps<OIDCFields>, 'fields'>;
1316

packages/compass-settings/src/components/settings/settings-list.tsx

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
type KeysMatching<T, V> = keyof {
2020
[P in keyof T as T[P] extends V ? P : never]: P;
2121
};
22-
// Currently, only boolean and numeric options are supported in the UI.
22+
// Currently, boolean, numeric, and string options are supported in the UI.
2323
type BooleanPreferences = KeysMatching<
2424
UserConfigurablePreferences,
2525
boolean | undefined
@@ -28,7 +28,14 @@ type NumericPreferences = KeysMatching<
2828
UserConfigurablePreferences,
2929
number | undefined
3030
>;
31-
type SupportedPreferences = BooleanPreferences | NumericPreferences;
31+
type StringPreferences = KeysMatching<
32+
UserConfigurablePreferences,
33+
string | undefined
34+
>;
35+
type SupportedPreferences =
36+
| BooleanPreferences
37+
| NumericPreferences
38+
| StringPreferences;
3239

3340
const inputStyles = css({
3441
marginTop: spacing[3],
@@ -148,6 +155,51 @@ function NumericSetting<PreferenceName extends NumericPreferences>({
148155
</>
149156
);
150157
}
158+
function StringSetting<PreferenceName extends StringPreferences>({
159+
name,
160+
handleChange,
161+
value,
162+
disabled,
163+
required,
164+
}: {
165+
name: PreferenceName;
166+
handleChange: HandleChange<PreferenceName>;
167+
value: string | undefined;
168+
disabled: boolean;
169+
required: boolean;
170+
}) {
171+
const handleChangeEvent = useCallback(
172+
(event: React.ChangeEvent<HTMLInputElement>) => {
173+
const { value } = event.target;
174+
handleChange(
175+
name,
176+
(value === ''
177+
? required
178+
? ''
179+
: undefined
180+
: value) as UserConfigurablePreferences[PreferenceName]
181+
);
182+
},
183+
[name, handleChange, required]
184+
);
185+
186+
return (
187+
<>
188+
<SettingLabel name={name} />
189+
<TextInput
190+
className={inputStyles}
191+
aria-labelledby={`${name}-label`}
192+
id={name}
193+
name={name}
194+
data-testid={name}
195+
value={value === undefined ? '' : `${value}`}
196+
onChange={handleChangeEvent}
197+
disabled={disabled}
198+
optional={!required}
199+
/>
200+
</>
201+
);
202+
}
151203

152204
export function SettingsList<PreferenceName extends SupportedPreferences>({
153205
fields,
@@ -159,7 +211,7 @@ export function SettingsList<PreferenceName extends SupportedPreferences>({
159211
<>
160212
{fields.map((name) => {
161213
const { type, required } = getSettingDescription(name);
162-
if (type !== 'boolean' && type !== 'number') {
214+
if (type !== 'boolean' && type !== 'number' && type !== 'string') {
163215
throw new Error(
164216
`do not know how to render type ${
165217
type as string
@@ -186,6 +238,16 @@ export function SettingsList<PreferenceName extends SupportedPreferences>({
186238
required={required}
187239
disabled={!!preferenceStates[name]}
188240
/>
241+
) : type === 'string' ? (
242+
<StringSetting
243+
name={name as StringPreferences}
244+
handleChange={handleChange}
245+
value={
246+
currentValues[name as StringPreferences & PreferenceName]
247+
}
248+
required={required}
249+
disabled={!!preferenceStates[name]}
250+
/>
189251
) : null}
190252
{settingStateLabels[preferenceStates[name] ?? '']}
191253
</FormFieldContainer>

packages/connection-form/src/hooks/use-connect-form.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ import type {
5656
} from '../utils/csfle-handler';
5757
import {
5858
handleUpdateOIDCParam,
59-
setOIDCNotifyDeviceFlow,
59+
adjustOIDCConnectionOptionsBeforeConnect,
6060
} from '../utils/oidc-handler';
6161
import type { UpdateOIDCAction } from '../utils/oidc-handler';
6262
import { setAppNameParamIfMissing } from '../utils/set-app-name-if-missing';
@@ -708,7 +708,7 @@ export function adjustConnectionOptionsBeforeConnect({
708708
) => ConnectionOptions)[] = [
709709
adjustCSFLEParams,
710710
setAppNameParamIfMissing(defaultAppName),
711-
setOIDCNotifyDeviceFlow(notifyDeviceFlow),
711+
adjustOIDCConnectionOptionsBeforeConnect(notifyDeviceFlow),
712712
applyForceConnectionOptions,
713713
];
714714
for (const transformer of transformers) {

0 commit comments

Comments
 (0)