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
+2 -2
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', () => {
+1 -1
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;
}
+10 -4
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,
);
}
}
+35 -2
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,
});
});
+11 -3
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,
@@ -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);
+1 -1
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);
+5 -5
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',
);
});
});
+9 -6
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,
)}`,
);
@@ -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,
+4 -3
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(
+2 -2
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',
},
});
});
+1 -1
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: