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

View File

@@ -8,13 +8,23 @@ CLI, configure **one** of the following authentication methods:
- Use Gemini API key
- Use Vertex AI
- Headless (non-interactive) mode
- Google Cloud Shell
- Google Cloud Environments (Cloud Shell, Compute Engine, etc.)
## Quick Check: Running in Google Cloud Shell?
If you are running the Gemini CLI within a Google Cloud Shell environment,
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
When you run Gemini CLI through the command-line, Gemini CLI will provide the

View File

@@ -32,8 +32,8 @@ describe('validateAuthMethod', () => {
expect(validateAuthMethod(AuthType.LOGIN_WITH_GOOGLE)).toBeNull();
});
it('should return null for CLOUD_SHELL', () => {
expect(validateAuthMethod(AuthType.CLOUD_SHELL)).toBeNull();
it('should return null for COMPUTE_ADC', () => {
expect(validateAuthMethod(AuthType.COMPUTE_ADC)).toBeNull();
});
describe('USE_GEMINI', () => {

View File

@@ -11,7 +11,7 @@ export function validateAuthMethod(authMethod: string): string | null {
loadEnvironment(loadSettings().merged);
if (
authMethod === AuthType.LOGIN_WITH_GOOGLE ||
authMethod === AuthType.CLOUD_SHELL
authMethod === AuthType.COMPUTE_ADC
) {
return null;
}

View File

@@ -284,13 +284,19 @@ export async function main() {
validateDnsResolutionOrder(settings.merged.advanced?.dnsResolutionOrder),
);
// Set a default auth type if one isn't set.
if (!settings.merged.security?.auth?.selectedType) {
if (process.env['CLOUD_SHELL'] === 'true') {
// Set a default auth type if one isn't set or is set to a legacy type
if (
!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(
SettingScope.User,
'selectedAuthType',
AuthType.CLOUD_SHELL,
AuthType.COMPUTE_ADC,
);
}
}

View File

@@ -109,8 +109,41 @@ describe('AuthDialog', () => {
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
expect(items).toContainEqual({
label: 'Use Cloud Shell user credentials',
value: AuthType.CLOUD_SHELL,
key: AuthType.CLOUD_SHELL,
value: AuthType.COMPUTE_ADC,
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,
});
});

View File

@@ -50,11 +50,19 @@ export function AuthDialog({
? [
{
label: 'Use Cloud Shell user credentials',
value: AuthType.CLOUD_SHELL,
key: AuthType.CLOUD_SHELL,
value: AuthType.COMPUTE_ADC,
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,
},
]
: []),
{
label: 'Use Gemini API Key',
value: AuthType.USE_GEMINI,

View File

@@ -68,18 +68,18 @@ describe('codeAssist', () => {
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);
mockedSetupUser.mockResolvedValue(mockUserData);
const generator = await createCodeAssistContentGenerator(
httpOptions,
AuthType.CLOUD_SHELL,
AuthType.COMPUTE_ADC,
mockConfig,
);
expect(getOauthClient).toHaveBeenCalledWith(
AuthType.CLOUD_SHELL,
AuthType.COMPUTE_ADC,
mockConfig,
);
expect(setupUser).toHaveBeenCalledWith(mockAuthClient);

View File

@@ -21,7 +21,7 @@ export async function createCodeAssistContentGenerator(
): Promise<ContentGenerator> {
if (
authType === AuthType.LOGIN_WITH_GOOGLE ||
authType === AuthType.CLOUD_SHELL
authType === AuthType.COMPUTE_ADC
) {
const authClient = await getOauthClient(authType, config);
const userData = await setupUser(authClient);

View File

@@ -318,7 +318,7 @@ describe('oauth2', () => {
});
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(mockGetAccessToken).toHaveBeenCalled();
@@ -329,7 +329,7 @@ describe('oauth2', () => {
mockComputeClient.credentials = newCredentials;
mockGetAccessToken.mockResolvedValue({ token: 'new-adc-token' });
await getOauthClient(AuthType.CLOUD_SHELL, mockConfig);
await getOauthClient(AuthType.COMPUTE_ADC, mockConfig);
const credsPath = path.join(
tempHomeDir,
@@ -340,7 +340,7 @@ describe('oauth2', () => {
});
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);
});
@@ -349,9 +349,9 @@ describe('oauth2', () => {
mockGetAccessToken.mockRejectedValue(testError);
await expect(
getOauthClient(AuthType.CLOUD_SHELL, mockConfig),
getOauthClient(AuthType.COMPUTE_ADC, mockConfig),
).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',
);
});
});

View File

@@ -155,12 +155,15 @@ async function initOauthClient(
}
}
// In Google Cloud Shell, we can use Application Default Credentials (ADC)
// provided via its metadata server to authenticate non-interactively using
// the identity of the user logged into Cloud Shell.
if (authType === AuthType.CLOUD_SHELL) {
// In Google Compute Engine based environments (including Cloud Shell), we can
// use Application Default Credentials (ADC) provided via its metadata server
// to authenticate non-interactively using the identity of the logged-in user.
if (authType === AuthType.COMPUTE_ADC) {
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({
// We can leave this empty, since the metadata server will provide
// the service account email.
@@ -172,7 +175,7 @@ async function initOauthClient(
return computeClient;
} catch (e) {
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,
)}`,
);

View File

@@ -67,7 +67,7 @@ describe('createContentGenerator', () => {
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;
vi.mocked(createCodeAssistContentGenerator).mockResolvedValue(
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 () => {
const mockConfig = {
getUsageStatisticsEnabled: () => true,

View File

@@ -48,7 +48,8 @@ export enum AuthType {
LOGIN_WITH_GOOGLE = 'oauth-personal',
USE_GEMINI = 'gemini-api-key',
USE_VERTEX_AI = 'vertex-ai',
CLOUD_SHELL = 'cloud-shell',
LEGACY_CLOUD_SHELL = 'cloud-shell',
COMPUTE_ADC = 'compute-default-credentials',
}
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 (
authType === AuthType.LOGIN_WITH_GOOGLE ||
authType === AuthType.CLOUD_SHELL
authType === AuthType.COMPUTE_ADC
) {
return contentGeneratorConfig;
}
@@ -120,7 +121,7 @@ export async function createContentGenerator(
};
if (
config.authType === AuthType.LOGIN_WITH_GOOGLE ||
config.authType === AuthType.CLOUD_SHELL
config.authType === AuthType.COMPUTE_ADC
) {
const httpOptions = { headers: baseHeaders };
return new LoggingContentGenerator(

View File

@@ -298,7 +298,7 @@ describe('loggers', () => {
const event = new UserPromptEvent(
11,
'prompt-id-9',
AuthType.CLOUD_SHELL,
AuthType.COMPUTE_ADC,
'test-prompt',
);
@@ -315,7 +315,7 @@ describe('loggers', () => {
interactive: false,
prompt_length: 11,
prompt_id: 'prompt-id-9',
auth_type: 'cloud-shell',
auth_type: 'compute-default-credentials',
},
});
});

View File

@@ -886,7 +886,7 @@ export function getConventionAttributes(event: {
function getGenAiProvider(authType?: string): GenAiProviderName {
switch (authType) {
case AuthType.USE_VERTEX_AI:
case AuthType.CLOUD_SHELL:
case AuthType.COMPUTE_ADC:
case AuthType.LOGIN_WITH_GOOGLE:
return GenAiProviderName.GCP_VERTEX_AI;
case AuthType.USE_GEMINI: