feat(auth): Add option for metadata server application default credentials without project override (#12948)

This commit is contained in:
Caroline Rose
2025-11-14 11:39:11 -05:00
committed by GitHub
parent 016b5b42e2
commit 9d74b7c0e8
14 changed files with 113 additions and 35 deletions
+11 -1
View File
@@ -8,13 +8,23 @@ CLI, configure **one** of the following authentication methods:
- Use Gemini API key - Use Gemini API key
- Use Vertex AI - Use Vertex AI
- Headless (non-interactive) mode - Headless (non-interactive) mode
- Google Cloud Shell - Google Cloud Environments (Cloud Shell, Compute Engine, etc.)
## Quick Check: Running in Google Cloud Shell? ## Quick Check: Running in Google Cloud Shell?
If you are running the Gemini CLI within a Google Cloud Shell environment, If you are running the Gemini CLI within a Google Cloud Shell environment,
authentication is typically automatic using your Cloud Shell credentials. authentication is typically automatic using your Cloud Shell credentials.
### Other Google Cloud Environments (e.g., Compute Engine)
Some other Google Cloud environments, such as Compute Engine VMs, might also
support automatic authentication. In these environments, Gemini CLI can
automatically use Application Default Credentials (ADC) sourced from the
environment's metadata server.
If automatic authentication does not occur in your environment, you will need to
use one of the interactive methods described below.
## Authenticate in Interactive mode ## Authenticate in Interactive mode
When you run Gemini CLI through the command-line, Gemini CLI will provide the When you run Gemini CLI through the command-line, Gemini CLI will provide the
+2 -2
View File
@@ -32,8 +32,8 @@ describe('validateAuthMethod', () => {
expect(validateAuthMethod(AuthType.LOGIN_WITH_GOOGLE)).toBeNull(); expect(validateAuthMethod(AuthType.LOGIN_WITH_GOOGLE)).toBeNull();
}); });
it('should return null for CLOUD_SHELL', () => { it('should return null for COMPUTE_ADC', () => {
expect(validateAuthMethod(AuthType.CLOUD_SHELL)).toBeNull(); expect(validateAuthMethod(AuthType.COMPUTE_ADC)).toBeNull();
}); });
describe('USE_GEMINI', () => { describe('USE_GEMINI', () => {
+1 -1
View File
@@ -11,7 +11,7 @@ export function validateAuthMethod(authMethod: string): string | null {
loadEnvironment(loadSettings().merged); loadEnvironment(loadSettings().merged);
if ( if (
authMethod === AuthType.LOGIN_WITH_GOOGLE || authMethod === AuthType.LOGIN_WITH_GOOGLE ||
authMethod === AuthType.CLOUD_SHELL authMethod === AuthType.COMPUTE_ADC
) { ) {
return null; return null;
} }
+10 -4
View File
@@ -284,13 +284,19 @@ export async function main() {
validateDnsResolutionOrder(settings.merged.advanced?.dnsResolutionOrder), validateDnsResolutionOrder(settings.merged.advanced?.dnsResolutionOrder),
); );
// Set a default auth type if one isn't set. // Set a default auth type if one isn't set or is set to a legacy type
if (!settings.merged.security?.auth?.selectedType) { if (
if (process.env['CLOUD_SHELL'] === 'true') { !settings.merged.security?.auth?.selectedType ||
settings.merged.security?.auth?.selectedType === AuthType.LEGACY_CLOUD_SHELL
) {
if (
process.env['CLOUD_SHELL'] === 'true' ||
process.env['GEMINI_CLI_USE_COMPUTE_ADC'] === 'true'
) {
settings.setValue( settings.setValue(
SettingScope.User, SettingScope.User,
'selectedAuthType', 'selectedAuthType',
AuthType.CLOUD_SHELL, AuthType.COMPUTE_ADC,
); );
} }
} }
+35 -2
View File
@@ -109,8 +109,41 @@ describe('AuthDialog', () => {
const items = mockedRadioButtonSelect.mock.calls[0][0].items; const items = mockedRadioButtonSelect.mock.calls[0][0].items;
expect(items).toContainEqual({ expect(items).toContainEqual({
label: 'Use Cloud Shell user credentials', label: 'Use Cloud Shell user credentials',
value: AuthType.CLOUD_SHELL, value: AuthType.COMPUTE_ADC,
key: AuthType.CLOUD_SHELL, key: AuthType.COMPUTE_ADC,
});
});
it('does not show metadata server application default credentials option in Cloud Shell environment', () => {
process.env['CLOUD_SHELL'] = 'true';
renderWithProviders(<AuthDialog {...props} />);
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
expect(items).not.toContainEqual({
label: 'Use metadata server application default credentials',
value: AuthType.COMPUTE_ADC,
key: AuthType.COMPUTE_ADC,
});
});
it('shows metadata server application default credentials option when GEMINI_CLI_USE_COMPUTE_ADC env var is true', () => {
process.env['GEMINI_CLI_USE_COMPUTE_ADC'] = 'true';
renderWithProviders(<AuthDialog {...props} />);
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
expect(items).toContainEqual({
label: 'Use metadata server application default credentials',
value: AuthType.COMPUTE_ADC,
key: AuthType.COMPUTE_ADC,
});
});
it('does not show Cloud Shell option when when GEMINI_CLI_USE_COMPUTE_ADC env var is true', () => {
process.env['GEMINI_CLI_USE_COMPUTE_ADC'] = 'true';
renderWithProviders(<AuthDialog {...props} />);
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
expect(items).not.toContainEqual({
label: 'Use Cloud Shell user credentials',
value: AuthType.COMPUTE_ADC,
key: AuthType.COMPUTE_ADC,
}); });
}); });
+10 -2
View File
@@ -50,8 +50,16 @@ export function AuthDialog({
? [ ? [
{ {
label: 'Use Cloud Shell user credentials', label: 'Use Cloud Shell user credentials',
value: AuthType.CLOUD_SHELL, value: AuthType.COMPUTE_ADC,
key: AuthType.CLOUD_SHELL, key: AuthType.COMPUTE_ADC,
},
]
: process.env['GEMINI_CLI_USE_COMPUTE_ADC'] === 'true'
? [
{
label: 'Use metadata server application default credentials',
value: AuthType.COMPUTE_ADC,
key: AuthType.COMPUTE_ADC,
}, },
] ]
: []), : []),
@@ -68,18 +68,18 @@ describe('codeAssist', () => {
expect(generator).toBeInstanceOf(MockedCodeAssistServer); expect(generator).toBeInstanceOf(MockedCodeAssistServer);
}); });
it('should create a server for CLOUD_SHELL', async () => { it('should create a server for COMPUTE_ADC', async () => {
mockedGetOauthClient.mockResolvedValue(mockAuthClient as never); mockedGetOauthClient.mockResolvedValue(mockAuthClient as never);
mockedSetupUser.mockResolvedValue(mockUserData); mockedSetupUser.mockResolvedValue(mockUserData);
const generator = await createCodeAssistContentGenerator( const generator = await createCodeAssistContentGenerator(
httpOptions, httpOptions,
AuthType.CLOUD_SHELL, AuthType.COMPUTE_ADC,
mockConfig, mockConfig,
); );
expect(getOauthClient).toHaveBeenCalledWith( expect(getOauthClient).toHaveBeenCalledWith(
AuthType.CLOUD_SHELL, AuthType.COMPUTE_ADC,
mockConfig, mockConfig,
); );
expect(setupUser).toHaveBeenCalledWith(mockAuthClient); expect(setupUser).toHaveBeenCalledWith(mockAuthClient);
+1 -1
View File
@@ -21,7 +21,7 @@ export async function createCodeAssistContentGenerator(
): Promise<ContentGenerator> { ): Promise<ContentGenerator> {
if ( if (
authType === AuthType.LOGIN_WITH_GOOGLE || authType === AuthType.LOGIN_WITH_GOOGLE ||
authType === AuthType.CLOUD_SHELL authType === AuthType.COMPUTE_ADC
) { ) {
const authClient = await getOauthClient(authType, config); const authClient = await getOauthClient(authType, config);
const userData = await setupUser(authClient); const userData = await setupUser(authClient);
+5 -5
View File
@@ -318,7 +318,7 @@ describe('oauth2', () => {
}); });
it('should use Compute to get a client if no cached credentials exist', async () => { it('should use Compute to get a client if no cached credentials exist', async () => {
await getOauthClient(AuthType.CLOUD_SHELL, mockConfig); await getOauthClient(AuthType.COMPUTE_ADC, mockConfig);
expect(Compute).toHaveBeenCalledWith({}); expect(Compute).toHaveBeenCalledWith({});
expect(mockGetAccessToken).toHaveBeenCalled(); expect(mockGetAccessToken).toHaveBeenCalled();
@@ -329,7 +329,7 @@ describe('oauth2', () => {
mockComputeClient.credentials = newCredentials; mockComputeClient.credentials = newCredentials;
mockGetAccessToken.mockResolvedValue({ token: 'new-adc-token' }); mockGetAccessToken.mockResolvedValue({ token: 'new-adc-token' });
await getOauthClient(AuthType.CLOUD_SHELL, mockConfig); await getOauthClient(AuthType.COMPUTE_ADC, mockConfig);
const credsPath = path.join( const credsPath = path.join(
tempHomeDir, tempHomeDir,
@@ -340,7 +340,7 @@ describe('oauth2', () => {
}); });
it('should return the Compute client on successful ADC authentication', async () => { it('should return the Compute client on successful ADC authentication', async () => {
const client = await getOauthClient(AuthType.CLOUD_SHELL, mockConfig); const client = await getOauthClient(AuthType.COMPUTE_ADC, mockConfig);
expect(client).toBe(mockComputeClient); expect(client).toBe(mockComputeClient);
}); });
@@ -349,9 +349,9 @@ describe('oauth2', () => {
mockGetAccessToken.mockRejectedValue(testError); mockGetAccessToken.mockRejectedValue(testError);
await expect( await expect(
getOauthClient(AuthType.CLOUD_SHELL, mockConfig), getOauthClient(AuthType.COMPUTE_ADC, mockConfig),
).rejects.toThrow( ).rejects.toThrow(
'Could not authenticate using Cloud Shell credentials. Please select a different authentication method or ensure you are in a properly configured environment. Error: ADC Failed', 'Could not authenticate using metadata server application default credentials. Please select a different authentication method or ensure you are in a properly configured environment. Error: ADC Failed',
); );
}); });
}); });
+9 -6
View File
@@ -155,12 +155,15 @@ async function initOauthClient(
} }
} }
// In Google Cloud Shell, we can use Application Default Credentials (ADC) // In Google Compute Engine based environments (including Cloud Shell), we can
// provided via its metadata server to authenticate non-interactively using // use Application Default Credentials (ADC) provided via its metadata server
// the identity of the user logged into Cloud Shell. // to authenticate non-interactively using the identity of the logged-in user.
if (authType === AuthType.CLOUD_SHELL) { if (authType === AuthType.COMPUTE_ADC) {
try { try {
debugLogger.log("Attempting to authenticate via Cloud Shell VM's ADC."); debugLogger.log(
'Attempting to authenticate via metadata server application default credentials.',
);
const computeClient = new Compute({ const computeClient = new Compute({
// We can leave this empty, since the metadata server will provide // We can leave this empty, since the metadata server will provide
// the service account email. // the service account email.
@@ -172,7 +175,7 @@ async function initOauthClient(
return computeClient; return computeClient;
} catch (e) { } catch (e) {
throw new Error( throw new Error(
`Could not authenticate using Cloud Shell credentials. Please select a different authentication method or ensure you are in a properly configured environment. Error: ${getErrorMessage( `Could not authenticate using metadata server application default credentials. Please select a different authentication method or ensure you are in a properly configured environment. Error: ${getErrorMessage(
e, e,
)}`, )}`,
); );
@@ -67,7 +67,7 @@ describe('createContentGenerator', () => {
expect(generator).toBeInstanceOf(RecordingContentGenerator); expect(generator).toBeInstanceOf(RecordingContentGenerator);
}); });
it('should create a CodeAssistContentGenerator', async () => { it('should create a CodeAssistContentGenerator when AuthType is LOGIN_WITH_GOOGLE', async () => {
const mockGenerator = {} as unknown as ContentGenerator; const mockGenerator = {} as unknown as ContentGenerator;
vi.mocked(createCodeAssistContentGenerator).mockResolvedValue( vi.mocked(createCodeAssistContentGenerator).mockResolvedValue(
mockGenerator as never, mockGenerator as never,
@@ -84,6 +84,23 @@ describe('createContentGenerator', () => {
); );
}); });
it('should create a CodeAssistContentGenerator when AuthType is COMPUTE_ADC', async () => {
const mockGenerator = {} as unknown as ContentGenerator;
vi.mocked(createCodeAssistContentGenerator).mockResolvedValue(
mockGenerator as never,
);
const generator = await createContentGenerator(
{
authType: AuthType.COMPUTE_ADC,
},
mockConfig,
);
expect(createCodeAssistContentGenerator).toHaveBeenCalled();
expect(generator).toEqual(
new LoggingContentGenerator(mockGenerator, mockConfig),
);
});
it('should create a GoogleGenAI content generator', async () => { it('should create a GoogleGenAI content generator', async () => {
const mockConfig = { const mockConfig = {
getUsageStatisticsEnabled: () => true, getUsageStatisticsEnabled: () => true,
+4 -3
View File
@@ -48,7 +48,8 @@ export enum AuthType {
LOGIN_WITH_GOOGLE = 'oauth-personal', LOGIN_WITH_GOOGLE = 'oauth-personal',
USE_GEMINI = 'gemini-api-key', USE_GEMINI = 'gemini-api-key',
USE_VERTEX_AI = 'vertex-ai', USE_VERTEX_AI = 'vertex-ai',
CLOUD_SHELL = 'cloud-shell', LEGACY_CLOUD_SHELL = 'cloud-shell',
COMPUTE_ADC = 'compute-default-credentials',
} }
export type ContentGeneratorConfig = { export type ContentGeneratorConfig = {
@@ -79,7 +80,7 @@ export async function createContentGeneratorConfig(
// If we are using Google auth or we are in Cloud Shell, there is nothing else to validate for now // If we are using Google auth or we are in Cloud Shell, there is nothing else to validate for now
if ( if (
authType === AuthType.LOGIN_WITH_GOOGLE || authType === AuthType.LOGIN_WITH_GOOGLE ||
authType === AuthType.CLOUD_SHELL authType === AuthType.COMPUTE_ADC
) { ) {
return contentGeneratorConfig; return contentGeneratorConfig;
} }
@@ -120,7 +121,7 @@ export async function createContentGenerator(
}; };
if ( if (
config.authType === AuthType.LOGIN_WITH_GOOGLE || config.authType === AuthType.LOGIN_WITH_GOOGLE ||
config.authType === AuthType.CLOUD_SHELL config.authType === AuthType.COMPUTE_ADC
) { ) {
const httpOptions = { headers: baseHeaders }; const httpOptions = { headers: baseHeaders };
return new LoggingContentGenerator( return new LoggingContentGenerator(
+2 -2
View File
@@ -298,7 +298,7 @@ describe('loggers', () => {
const event = new UserPromptEvent( const event = new UserPromptEvent(
11, 11,
'prompt-id-9', 'prompt-id-9',
AuthType.CLOUD_SHELL, AuthType.COMPUTE_ADC,
'test-prompt', 'test-prompt',
); );
@@ -315,7 +315,7 @@ describe('loggers', () => {
interactive: false, interactive: false,
prompt_length: 11, prompt_length: 11,
prompt_id: 'prompt-id-9', prompt_id: 'prompt-id-9',
auth_type: 'cloud-shell', auth_type: 'compute-default-credentials',
}, },
}); });
}); });
+1 -1
View File
@@ -886,7 +886,7 @@ export function getConventionAttributes(event: {
function getGenAiProvider(authType?: string): GenAiProviderName { function getGenAiProvider(authType?: string): GenAiProviderName {
switch (authType) { switch (authType) {
case AuthType.USE_VERTEX_AI: case AuthType.USE_VERTEX_AI:
case AuthType.CLOUD_SHELL: case AuthType.COMPUTE_ADC:
case AuthType.LOGIN_WITH_GOOGLE: case AuthType.LOGIN_WITH_GOOGLE:
return GenAiProviderName.GCP_VERTEX_AI; return GenAiProviderName.GCP_VERTEX_AI;
case AuthType.USE_GEMINI: case AuthType.USE_GEMINI: