mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
feat(auth): Add option for metadata server application default credentials without project override (#12948)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user