Add a setting to disable auth mode validation on startup (#5358)
This commit is contained in:
parent
c725e258c6
commit
24c5a15d7a
|
@ -60,6 +60,7 @@ export interface Settings {
|
||||||
theme?: string;
|
theme?: string;
|
||||||
customThemes?: Record<string, CustomTheme>;
|
customThemes?: Record<string, CustomTheme>;
|
||||||
selectedAuthType?: AuthType;
|
selectedAuthType?: AuthType;
|
||||||
|
useExternalAuth?: boolean;
|
||||||
sandbox?: boolean | string;
|
sandbox?: boolean | string;
|
||||||
coreTools?: string[];
|
coreTools?: string[];
|
||||||
excludeTools?: string[];
|
excludeTools?: string[];
|
||||||
|
|
|
@ -186,7 +186,10 @@ export async function main() {
|
||||||
: [];
|
: [];
|
||||||
const sandboxConfig = config.getSandbox();
|
const sandboxConfig = config.getSandbox();
|
||||||
if (sandboxConfig) {
|
if (sandboxConfig) {
|
||||||
if (settings.merged.selectedAuthType) {
|
if (
|
||||||
|
settings.merged.selectedAuthType &&
|
||||||
|
!settings.merged.useExternalAuth
|
||||||
|
) {
|
||||||
// Validate authentication here because the sandbox will interfere with the Oauth2 web redirect.
|
// Validate authentication here because the sandbox will interfere with the Oauth2 web redirect.
|
||||||
try {
|
try {
|
||||||
const err = validateAuthMethod(settings.merged.selectedAuthType);
|
const err = validateAuthMethod(settings.merged.selectedAuthType);
|
||||||
|
@ -344,6 +347,7 @@ async function loadNonInteractiveConfig(
|
||||||
|
|
||||||
return await validateNonInteractiveAuth(
|
return await validateNonInteractiveAuth(
|
||||||
settings.merged.selectedAuthType,
|
settings.merged.selectedAuthType,
|
||||||
|
settings.merged.useExternalAuth,
|
||||||
finalConfig,
|
finalConfig,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { Tips } from './components/Tips.js';
|
||||||
import { checkForUpdates, UpdateObject } from './utils/updateCheck.js';
|
import { checkForUpdates, UpdateObject } from './utils/updateCheck.js';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { updateEventEmitter } from '../utils/updateEventEmitter.js';
|
import { updateEventEmitter } from '../utils/updateEventEmitter.js';
|
||||||
|
import * as auth from '../config/auth.js';
|
||||||
|
|
||||||
// Define a more complete mock server config based on actual Config
|
// Define a more complete mock server config based on actual Config
|
||||||
interface MockServerConfig {
|
interface MockServerConfig {
|
||||||
|
@ -232,6 +233,10 @@ vi.mock('./utils/updateCheck.js', () => ({
|
||||||
checkForUpdates: vi.fn(),
|
checkForUpdates: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('./config/auth.js', () => ({
|
||||||
|
validateAuthMethod: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
const mockedCheckForUpdates = vi.mocked(checkForUpdates);
|
const mockedCheckForUpdates = vi.mocked(checkForUpdates);
|
||||||
const { isGitRepository: mockedIsGitRepository } = vi.mocked(
|
const { isGitRepository: mockedIsGitRepository } = vi.mocked(
|
||||||
await import('@google/gemini-cli-core'),
|
await import('@google/gemini-cli-core'),
|
||||||
|
@ -1005,4 +1010,50 @@ describe('App UI', () => {
|
||||||
expect(lastFrame()).toContain('5 errors');
|
expect(lastFrame()).toContain('5 errors');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('auth validation', () => {
|
||||||
|
it('should call validateAuthMethod when useExternalAuth is false', async () => {
|
||||||
|
const validateAuthMethodSpy = vi.spyOn(auth, 'validateAuthMethod');
|
||||||
|
mockSettings = createMockSettings({
|
||||||
|
workspace: {
|
||||||
|
selectedAuthType: 'USE_GEMINI' as AuthType,
|
||||||
|
useExternalAuth: false,
|
||||||
|
theme: 'Default',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { unmount } = render(
|
||||||
|
<App
|
||||||
|
config={mockConfig as unknown as ServerConfig}
|
||||||
|
settings={mockSettings}
|
||||||
|
version={mockVersion}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
currentUnmount = unmount;
|
||||||
|
|
||||||
|
expect(validateAuthMethodSpy).toHaveBeenCalledWith('USE_GEMINI');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT call validateAuthMethod when useExternalAuth is true', async () => {
|
||||||
|
const validateAuthMethodSpy = vi.spyOn(auth, 'validateAuthMethod');
|
||||||
|
mockSettings = createMockSettings({
|
||||||
|
workspace: {
|
||||||
|
selectedAuthType: 'USE_GEMINI' as AuthType,
|
||||||
|
useExternalAuth: true,
|
||||||
|
theme: 'Default',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { unmount } = render(
|
||||||
|
<App
|
||||||
|
config={mockConfig as unknown as ServerConfig}
|
||||||
|
settings={mockSettings}
|
||||||
|
version={mockVersion}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
currentUnmount = unmount;
|
||||||
|
|
||||||
|
expect(validateAuthMethodSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -234,14 +234,19 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||||
} = useAuthCommand(settings, setAuthError, config);
|
} = useAuthCommand(settings, setAuthError, config);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (settings.merged.selectedAuthType) {
|
if (settings.merged.selectedAuthType && !settings.merged.useExternalAuth) {
|
||||||
const error = validateAuthMethod(settings.merged.selectedAuthType);
|
const error = validateAuthMethod(settings.merged.selectedAuthType);
|
||||||
if (error) {
|
if (error) {
|
||||||
setAuthError(error);
|
setAuthError(error);
|
||||||
openAuthDialog();
|
openAuthDialog();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [settings.merged.selectedAuthType, openAuthDialog, setAuthError]);
|
}, [
|
||||||
|
settings.merged.selectedAuthType,
|
||||||
|
settings.merged.useExternalAuth,
|
||||||
|
openAuthDialog,
|
||||||
|
setAuthError,
|
||||||
|
]);
|
||||||
|
|
||||||
// Sync user tier from config when authentication changes
|
// Sync user tier from config when authentication changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
NonInteractiveConfig,
|
NonInteractiveConfig,
|
||||||
} from './validateNonInterActiveAuth.js';
|
} from './validateNonInterActiveAuth.js';
|
||||||
import { AuthType } from '@google/gemini-cli-core';
|
import { AuthType } from '@google/gemini-cli-core';
|
||||||
|
import * as auth from './config/auth.js';
|
||||||
|
|
||||||
describe('validateNonInterActiveAuth', () => {
|
describe('validateNonInterActiveAuth', () => {
|
||||||
let originalEnvGeminiApiKey: string | undefined;
|
let originalEnvGeminiApiKey: string | undefined;
|
||||||
|
@ -59,7 +60,11 @@ describe('validateNonInterActiveAuth', () => {
|
||||||
refreshAuth: refreshAuthMock,
|
refreshAuth: refreshAuthMock,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await validateNonInteractiveAuth(undefined, nonInteractiveConfig);
|
await validateNonInteractiveAuth(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
nonInteractiveConfig,
|
||||||
|
);
|
||||||
expect.fail('Should have exited');
|
expect.fail('Should have exited');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect((e as Error).message).toContain('process.exit(1) called');
|
expect((e as Error).message).toContain('process.exit(1) called');
|
||||||
|
@ -75,7 +80,11 @@ describe('validateNonInterActiveAuth', () => {
|
||||||
const nonInteractiveConfig: NonInteractiveConfig = {
|
const nonInteractiveConfig: NonInteractiveConfig = {
|
||||||
refreshAuth: refreshAuthMock,
|
refreshAuth: refreshAuthMock,
|
||||||
};
|
};
|
||||||
await validateNonInteractiveAuth(undefined, nonInteractiveConfig);
|
await validateNonInteractiveAuth(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
nonInteractiveConfig,
|
||||||
|
);
|
||||||
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.LOGIN_WITH_GOOGLE);
|
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.LOGIN_WITH_GOOGLE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -84,7 +93,11 @@ describe('validateNonInterActiveAuth', () => {
|
||||||
const nonInteractiveConfig: NonInteractiveConfig = {
|
const nonInteractiveConfig: NonInteractiveConfig = {
|
||||||
refreshAuth: refreshAuthMock,
|
refreshAuth: refreshAuthMock,
|
||||||
};
|
};
|
||||||
await validateNonInteractiveAuth(undefined, nonInteractiveConfig);
|
await validateNonInteractiveAuth(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
nonInteractiveConfig,
|
||||||
|
);
|
||||||
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
|
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -95,7 +108,11 @@ describe('validateNonInterActiveAuth', () => {
|
||||||
const nonInteractiveConfig: NonInteractiveConfig = {
|
const nonInteractiveConfig: NonInteractiveConfig = {
|
||||||
refreshAuth: refreshAuthMock,
|
refreshAuth: refreshAuthMock,
|
||||||
};
|
};
|
||||||
await validateNonInteractiveAuth(undefined, nonInteractiveConfig);
|
await validateNonInteractiveAuth(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
nonInteractiveConfig,
|
||||||
|
);
|
||||||
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI);
|
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -105,7 +122,11 @@ describe('validateNonInterActiveAuth', () => {
|
||||||
const nonInteractiveConfig: NonInteractiveConfig = {
|
const nonInteractiveConfig: NonInteractiveConfig = {
|
||||||
refreshAuth: refreshAuthMock,
|
refreshAuth: refreshAuthMock,
|
||||||
};
|
};
|
||||||
await validateNonInteractiveAuth(undefined, nonInteractiveConfig);
|
await validateNonInteractiveAuth(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
nonInteractiveConfig,
|
||||||
|
);
|
||||||
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI);
|
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -118,7 +139,11 @@ describe('validateNonInterActiveAuth', () => {
|
||||||
const nonInteractiveConfig: NonInteractiveConfig = {
|
const nonInteractiveConfig: NonInteractiveConfig = {
|
||||||
refreshAuth: refreshAuthMock,
|
refreshAuth: refreshAuthMock,
|
||||||
};
|
};
|
||||||
await validateNonInteractiveAuth(undefined, nonInteractiveConfig);
|
await validateNonInteractiveAuth(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
nonInteractiveConfig,
|
||||||
|
);
|
||||||
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.LOGIN_WITH_GOOGLE);
|
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.LOGIN_WITH_GOOGLE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -130,7 +155,11 @@ describe('validateNonInterActiveAuth', () => {
|
||||||
const nonInteractiveConfig: NonInteractiveConfig = {
|
const nonInteractiveConfig: NonInteractiveConfig = {
|
||||||
refreshAuth: refreshAuthMock,
|
refreshAuth: refreshAuthMock,
|
||||||
};
|
};
|
||||||
await validateNonInteractiveAuth(undefined, nonInteractiveConfig);
|
await validateNonInteractiveAuth(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
nonInteractiveConfig,
|
||||||
|
);
|
||||||
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI);
|
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -142,7 +171,11 @@ describe('validateNonInterActiveAuth', () => {
|
||||||
const nonInteractiveConfig: NonInteractiveConfig = {
|
const nonInteractiveConfig: NonInteractiveConfig = {
|
||||||
refreshAuth: refreshAuthMock,
|
refreshAuth: refreshAuthMock,
|
||||||
};
|
};
|
||||||
await validateNonInteractiveAuth(undefined, nonInteractiveConfig);
|
await validateNonInteractiveAuth(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
nonInteractiveConfig,
|
||||||
|
);
|
||||||
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
|
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -152,20 +185,24 @@ describe('validateNonInterActiveAuth', () => {
|
||||||
const nonInteractiveConfig: NonInteractiveConfig = {
|
const nonInteractiveConfig: NonInteractiveConfig = {
|
||||||
refreshAuth: refreshAuthMock,
|
refreshAuth: refreshAuthMock,
|
||||||
};
|
};
|
||||||
await validateNonInteractiveAuth(AuthType.USE_GEMINI, nonInteractiveConfig);
|
await validateNonInteractiveAuth(
|
||||||
|
AuthType.USE_GEMINI,
|
||||||
|
undefined,
|
||||||
|
nonInteractiveConfig,
|
||||||
|
);
|
||||||
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
|
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('exits if validateAuthMethod returns error', async () => {
|
it('exits if validateAuthMethod returns error', async () => {
|
||||||
// Mock validateAuthMethod to return error
|
// Mock validateAuthMethod to return error
|
||||||
const mod = await import('./config/auth.js');
|
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
|
||||||
vi.spyOn(mod, 'validateAuthMethod').mockReturnValue('Auth error!');
|
|
||||||
const nonInteractiveConfig: NonInteractiveConfig = {
|
const nonInteractiveConfig: NonInteractiveConfig = {
|
||||||
refreshAuth: refreshAuthMock,
|
refreshAuth: refreshAuthMock,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await validateNonInteractiveAuth(
|
await validateNonInteractiveAuth(
|
||||||
AuthType.USE_GEMINI,
|
AuthType.USE_GEMINI,
|
||||||
|
undefined,
|
||||||
nonInteractiveConfig,
|
nonInteractiveConfig,
|
||||||
);
|
);
|
||||||
expect.fail('Should have exited');
|
expect.fail('Should have exited');
|
||||||
|
@ -175,4 +212,28 @@ describe('validateNonInterActiveAuth', () => {
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Auth error!');
|
expect(consoleErrorSpy).toHaveBeenCalledWith('Auth error!');
|
||||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('skips validation if useExternalAuth is true', async () => {
|
||||||
|
// Mock validateAuthMethod to return error to ensure it's not being called
|
||||||
|
const validateAuthMethodSpy = vi
|
||||||
|
.spyOn(auth, 'validateAuthMethod')
|
||||||
|
.mockReturnValue('Auth error!');
|
||||||
|
const nonInteractiveConfig: NonInteractiveConfig = {
|
||||||
|
refreshAuth: refreshAuthMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Even with an invalid auth type, it should not exit
|
||||||
|
// because validation is skipped.
|
||||||
|
await validateNonInteractiveAuth(
|
||||||
|
'invalid-auth-type' as AuthType,
|
||||||
|
true, // useExternalAuth = true
|
||||||
|
nonInteractiveConfig,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(validateAuthMethodSpy).not.toHaveBeenCalled();
|
||||||
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||||
|
expect(processExitSpy).not.toHaveBeenCalled();
|
||||||
|
// We still expect refreshAuth to be called with the (invalid) type
|
||||||
|
expect(refreshAuthMock).toHaveBeenCalledWith('invalid-auth-type');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,6 +23,7 @@ function getAuthTypeFromEnv(): AuthType | undefined {
|
||||||
|
|
||||||
export async function validateNonInteractiveAuth(
|
export async function validateNonInteractiveAuth(
|
||||||
configuredAuthType: AuthType | undefined,
|
configuredAuthType: AuthType | undefined,
|
||||||
|
useExternalAuth: boolean | undefined,
|
||||||
nonInteractiveConfig: Config,
|
nonInteractiveConfig: Config,
|
||||||
) {
|
) {
|
||||||
const effectiveAuthType = configuredAuthType || getAuthTypeFromEnv();
|
const effectiveAuthType = configuredAuthType || getAuthTypeFromEnv();
|
||||||
|
@ -34,10 +35,12 @@ export async function validateNonInteractiveAuth(
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const err = validateAuthMethod(effectiveAuthType);
|
if (!useExternalAuth) {
|
||||||
if (err != null) {
|
const err = validateAuthMethod(effectiveAuthType);
|
||||||
console.error(err);
|
if (err != null) {
|
||||||
process.exit(1);
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await nonInteractiveConfig.refreshAuth(effectiveAuthType);
|
await nonInteractiveConfig.refreshAuth(effectiveAuthType);
|
||||||
|
|
Loading…
Reference in New Issue