Skip to content

Commit a735ec5

Browse files
authored
feat: Retrieve universe_domain for Compute clients (googleapis#1692)
* feat: Get `universe_domain` for Compute clients * refactor: streamline * refactor: Fallback to returning default universe * fix: transparent error message * chore: cleanup * docs: Minor doc change
1 parent bf219c8 commit a735ec5

File tree

3 files changed

+118
-68
lines changed

3 files changed

+118
-68
lines changed

src/auth/credentials.ts

+2
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export interface JWTInput {
7676
client_secret?: string;
7777
refresh_token?: string;
7878
quota_project_id?: string;
79+
universe_domain?: string;
7980
}
8081

8182
export interface ImpersonatedJWTInput {
@@ -88,4 +89,5 @@ export interface ImpersonatedJWTInput {
8889
export interface CredentialBody {
8990
client_email?: string;
9091
private_key?: string;
92+
universe_domain?: string;
9193
}

src/auth/googleauth.ts

+77-40
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import {exec} from 'child_process';
1616
import * as fs from 'fs';
17-
import {GaxiosOptions, GaxiosResponse} from 'gaxios';
17+
import {GaxiosError, GaxiosOptions, GaxiosResponse} from 'gaxios';
1818
import * as gcpMetadata from 'gcp-metadata';
1919
import * as os from 'os';
2020
import * as path from 'path';
@@ -47,12 +47,13 @@ import {
4747
EXTERNAL_ACCOUNT_TYPE,
4848
BaseExternalAccountClient,
4949
} from './baseexternalclient';
50-
import {AuthClient, AuthClientOptions} from './authclient';
50+
import {AuthClient, AuthClientOptions, DEFAULT_UNIVERSE} from './authclient';
5151
import {
5252
EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE,
5353
ExternalAccountAuthorizedUserClient,
5454
ExternalAccountAuthorizedUserClientOptions,
5555
} from './externalAccountAuthorizedUserClient';
56+
import {originalOrCamelOptions} from '../util';
5657

5758
/**
5859
* Defines all types of explicit clients that are determined via ADC JSON
@@ -131,6 +132,14 @@ const GoogleAuthExceptionMessages = {
131132
'Unable to detect a Project Id in the current environment. \n' +
132133
'To learn more about authentication and Google APIs, visit: \n' +
133134
'https://cloud.google.com/docs/authentication/getting-started',
135+
NO_CREDENTIALS_FOUND:
136+
'Unable to find credentials in current environment. \n' +
137+
'To learn more about authentication and Google APIs, visit: \n' +
138+
'https://cloud.google.com/docs/authentication/getting-started',
139+
NO_UNIVERSE_DOMAIN_FOUND:
140+
'Unable to detect a Universe Domain in the current environment.\n' +
141+
'To learn more about Universe Domain retrieval, visit: \n' +
142+
'https://cloud.google.com/compute/docs/metadata/predefined-metadata-keys',
134143
} as const;
135144

136145
export class GoogleAuth<T extends AuthClient = JSONClient> {
@@ -168,6 +177,13 @@ export class GoogleAuth<T extends AuthClient = JSONClient> {
168177
private scopes?: string | string[];
169178
private clientOptions?: AuthClientOptions;
170179

180+
/**
181+
* The cached universe domain.
182+
*
183+
* @see {@link GoogleAuth.getUniverseDomain}
184+
*/
185+
#universeDomain?: string = undefined;
186+
171187
/**
172188
* Export DefaultTransporter as a static property of the class.
173189
*/
@@ -286,6 +302,42 @@ export class GoogleAuth<T extends AuthClient = JSONClient> {
286302
return this._findProjectIdPromise;
287303
}
288304

305+
async #getUniverseFromMetadataServer() {
306+
if (!(await this._checkIsGCE())) return;
307+
308+
let universeDomain: string;
309+
310+
try {
311+
universeDomain = await gcpMetadata.universe('universe_domain');
312+
universeDomain ||= DEFAULT_UNIVERSE;
313+
} catch (e) {
314+
if (e instanceof GaxiosError && e.status === 404) {
315+
universeDomain = DEFAULT_UNIVERSE;
316+
} else {
317+
throw e;
318+
}
319+
}
320+
321+
return universeDomain;
322+
}
323+
324+
/**
325+
* Retrieves, caches, and returns the universe domain in the following order
326+
* of precedence:
327+
* - The universe domain in {@link GoogleAuth.clientOptions}
328+
* - {@link gcpMetadata.universe}
329+
*
330+
* @returns The universe domain
331+
*/
332+
async getUniverseDomain(): Promise<string> {
333+
this.#universeDomain ??= originalOrCamelOptions(this.clientOptions).get(
334+
'universe_domain'
335+
);
336+
this.#universeDomain ??= await this.#getUniverseFromMetadataServer();
337+
338+
return this.#universeDomain || DEFAULT_UNIVERSE;
339+
}
340+
289341
/**
290342
* @returns Any scopes (user-specified or default scopes specified by the
291343
* client library) that need to be set on the current Auth client.
@@ -370,30 +422,21 @@ export class GoogleAuth<T extends AuthClient = JSONClient> {
370422
}
371423

372424
// Determine if we're running on GCE.
373-
let isGCE;
374-
try {
375-
isGCE = await this._checkIsGCE();
376-
} catch (e) {
377-
if (e instanceof Error) {
378-
e.message = `Unexpected error determining execution environment: ${e.message}`;
425+
if (await this._checkIsGCE()) {
426+
// set universe domain for Compute client
427+
if (!originalOrCamelOptions(options).get('universe_domain')) {
428+
options.universeDomain = await this.getUniverseDomain();
379429
}
380430

381-
throw e;
382-
}
383-
384-
if (!isGCE) {
385-
// We failed to find the default credentials. Bail out with an error.
386-
throw new Error(
387-
'Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.'
431+
(options as ComputeOptions).scopes = this.getAnyScopes();
432+
return await this.prepareAndCacheADC(
433+
new Compute(options),
434+
quotaProjectIdOverride
388435
);
389436
}
390437

391-
// For GCE, just return a default ComputeClient. It will take care of
392-
// the rest.
393-
(options as ComputeOptions).scopes = this.getAnyScopes();
394-
return await this.prepareAndCacheADC(
395-
new Compute(options),
396-
quotaProjectIdOverride
438+
throw new Error(
439+
'Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.'
397440
);
398441
}
399442

@@ -893,37 +936,31 @@ export class GoogleAuth<T extends AuthClient = JSONClient> {
893936
if (client instanceof BaseExternalAccountClient) {
894937
const serviceAccountEmail = client.getServiceAccountEmail();
895938
if (serviceAccountEmail) {
896-
return {client_email: serviceAccountEmail};
939+
return {
940+
client_email: serviceAccountEmail,
941+
universe_domain: client.universeDomain,
942+
};
897943
}
898944
}
899945

900946
if (this.jsonContent) {
901-
const credential: CredentialBody = {
947+
return {
902948
client_email: (this.jsonContent as JWTInput).client_email,
903949
private_key: (this.jsonContent as JWTInput).private_key,
950+
universe_domain: this.jsonContent.universe_domain,
904951
};
905-
return credential;
906-
}
907-
908-
const isGCE = await this._checkIsGCE();
909-
if (!isGCE) {
910-
throw new Error('Unknown error.');
911952
}
912953

913-
// For GCE, return the service account details from the metadata server
914-
// NOTE: The trailing '/' at the end of service-accounts/ is very important!
915-
// The GCF metadata server doesn't respect querystring params if this / is
916-
// not included.
917-
const data = await gcpMetadata.instance({
918-
property: 'service-accounts/',
919-
params: {recursive: 'true'},
920-
});
954+
if (await this._checkIsGCE()) {
955+
const [client_email, universe_domain] = await Promise.all([
956+
gcpMetadata.instance('service-accounts/default/email'),
957+
this.getUniverseDomain(),
958+
]);
921959

922-
if (!data || !data.default || !data.default.email) {
923-
throw new Error('Failure from metadata server.');
960+
return {client_email, universe_domain};
924961
}
925962

926-
return {client_email: data.default.email};
963+
throw new Error(GoogleAuthExceptionMessages.NO_CREDENTIALS_FOUND);
927964
}
928965

929966
/**

test/test.googleauth.ts

+39-28
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ describe('googleauth', () => {
6464
const tokenPath = `${BASE_PATH}/instance/service-accounts/default/token`;
6565
const host = HOST_ADDRESS;
6666
const instancePath = `${BASE_PATH}/instance`;
67-
const svcAccountPath = `${instancePath}/service-accounts/?recursive=true`;
67+
const svcAccountPath = `${instancePath}/service-accounts/default/email`;
68+
const universeDomainPath = `${BASE_PATH}/universe/universe_domain`;
6869
const API_KEY = 'test-123';
6970
const PEM_PATH = './test/fixtures/private.pem';
7071
const STUB_PROJECT = 'my-awesome-project';
@@ -199,20 +200,22 @@ describe('googleauth', () => {
199200
createLinuxWellKnownStream = () => fs.createReadStream(filePath);
200201
}
201202

202-
function nockIsGCE() {
203+
function nockIsGCE(opts = {universeDomain: 'my-universe.com'}) {
203204
const primary = nock(host).get(instancePath).reply(200, {}, HEADERS);
204205
const secondary = nock(SECONDARY_HOST_ADDRESS)
205206
.get(instancePath)
206207
.reply(200, {}, HEADERS);
208+
const universeDomain = nock(HOST_ADDRESS)
209+
.get(universeDomainPath)
210+
.reply(200, opts.universeDomain, HEADERS);
207211

208212
return {
209213
done: () => {
210-
try {
211-
primary.done();
212-
secondary.done();
213-
} catch (_err) {
214-
// secondary can sometimes complete prior to primary.
215-
}
214+
return Promise.allSettled([
215+
primary.done(),
216+
secondary.done(),
217+
universeDomain.done(),
218+
]);
216219
},
217220
};
218221
}
@@ -1085,11 +1088,10 @@ describe('googleauth', () => {
10851088
// * Well-known file is not set.
10861089
// * Running on GCE is set to true.
10871090
mockWindows();
1088-
sandbox.stub(auth, '_checkIsGCE').rejects('🤮');
1089-
await assert.rejects(
1090-
auth.getApplicationDefault(),
1091-
/Unexpected error determining execution environment/
1092-
);
1091+
const e = new Error('abc');
1092+
1093+
sandbox.stub(auth, '_checkIsGCE').rejects(e);
1094+
await assert.rejects(auth.getApplicationDefault(), e);
10931095
});
10941096

10951097
it('getApplicationDefault should also get project ID', async () => {
@@ -1128,25 +1130,19 @@ describe('googleauth', () => {
11281130
});
11291131

11301132
it('getCredentials should get metadata from the server when running on GCE', async () => {
1131-
const response = {
1132-
default: {
1133-
1134-
private_key: null,
1135-
},
1136-
};
1133+
const clientEmail = '[email protected]';
1134+
const universeDomain = 'my-amazing-universe.com';
11371135
const scopes = [
1138-
nockIsGCE(),
1136+
nockIsGCE({universeDomain}),
11391137
createGetProjectIdNock(),
1140-
nock(host).get(svcAccountPath).reply(200, response, HEADERS),
1138+
nock(host).get(svcAccountPath).reply(200, clientEmail, HEADERS),
11411139
];
11421140
await auth._checkIsGCE();
11431141
assert.strictEqual(true, auth.isGCE);
11441142
const body = await auth.getCredentials();
11451143
assert.ok(body);
1146-
assert.strictEqual(
1147-
body.client_email,
1148-
1149-
);
1144+
assert.strictEqual(body.client_email, clientEmail);
1145+
assert.strictEqual(body.universe_domain, universeDomain);
11501146
assert.strictEqual(body.private_key, undefined);
11511147
scopes.forEach(s => s.done());
11521148
});
@@ -1415,9 +1411,7 @@ describe('googleauth', () => {
14151411
const data = 'abc123';
14161412
scopes.push(
14171413
nock(iamUri).post(iamPath).reply(200, {signedBlob}),
1418-
nock(host)
1419-
.get(svcAccountPath)
1420-
.reply(200, {default: {email, private_key: privateKey}}, HEADERS)
1414+
nock(host).get(svcAccountPath).reply(200, email, HEADERS)
14211415
);
14221416
const value = await auth.sign(data);
14231417
scopes.forEach(x => x.done());
@@ -1556,6 +1550,23 @@ describe('googleauth', () => {
15561550
assert.fail('failed to throw');
15571551
});
15581552

1553+
describe('getUniverseDomain', () => {
1554+
it('should prefer `clientOptions` > metadata service when available', async () => {
1555+
const universeDomain = 'my.universe.com';
1556+
const auth = new GoogleAuth({clientOptions: {universeDomain}});
1557+
1558+
assert.equal(await auth.getUniverseDomain(), universeDomain);
1559+
});
1560+
1561+
it('should use the metadata service if on GCP', async () => {
1562+
const universeDomain = 'my.universe.com';
1563+
const scope = nockIsGCE({universeDomain});
1564+
1565+
assert.equal(await auth.getUniverseDomain(), universeDomain);
1566+
await scope.done();
1567+
});
1568+
});
1569+
15591570
function mockApplicationDefaultCredentials(path: string) {
15601571
// Fake a home directory in our fixtures path.
15611572
mockEnvVar('GCLOUD_PROJECT', 'my-fake-project');

0 commit comments

Comments
 (0)