diff --git a/package.json b/package.json index 67b7962..0822413 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "dependencies": { "extend": "^3.0.2", "gaxios": "^6.0.3", - "google-auth-library": "^9.0.0", + "google-auth-library": "^9.7.0", "qs": "^6.7.0", "url-template": "^2.0.8", "uuid": "^9.0.0" @@ -69,7 +69,7 @@ "karma-mocha": "^2.0.0", "karma-remap-coverage": "^0.1.5", "karma-sourcemap-loader": "^0.4.0", - "karma-webpack": "^5.0.0", + "karma-webpack": "^4.0.0", "linkinator": "^3.1.0", "mocha": "^9.2.2", "mv": "^2.1.1", @@ -82,7 +82,7 @@ "tmp": "^0.2.0", "ts-loader": "^8.0.0", "typescript": "5.1.6", - "webpack": "^5.30.0", + "webpack": "^4.0.0", "webpack-cli": "^4.0.0" }, "engines": { diff --git a/src/api.ts b/src/api.ts index 4710a63..4562d3a 100644 --- a/src/api.ts +++ b/src/api.ts @@ -45,6 +45,8 @@ export interface APIRequestContext { */ export interface GlobalOptions extends MethodOptions { auth?: GoogleAuth | OAuth2Client | BaseExternalAccountClient | string; + universeDomain?: string; + universe_domain?: string; } export interface MethodOptions extends GaxiosOptions { diff --git a/src/apirequest.ts b/src/apirequest.ts index 00ed3c3..1387be2 100644 --- a/src/apirequest.ts +++ b/src/apirequest.ts @@ -160,7 +160,11 @@ async function createAPIRequestAsync(parameters: APIRequestParams) { // Parse urls if (options.url) { - options.url = urlTemplate.parse(options.url).expand(params); + let url = options.url; + if (typeof url === 'object') { + url = url.toString(); + } + options.url = urlTemplate.parse(url).expand(params); } if (parameters.mediaUrl) { parameters.mediaUrl = urlTemplate.parse(parameters.mediaUrl).expand(params); @@ -312,11 +316,52 @@ async function createAPIRequestAsync(parameters: APIRequestParams) { options.retry = options.retry === undefined ? true : options.retry; delete options.auth; // is overridden by our auth code + // Determine TPC universe + if ( + options.universeDomain && + options.universe_domain && + options.universeDomain !== options.universe_domain + ) { + throw new Error( + 'Please set either universe_domain or universeDomain, but not both.' + ); + } + const universeDomainEnvVar = + typeof process === 'object' && typeof process.env === 'object' + ? process.env['GOOGLE_CLOUD_UNIVERSE_DOMAIN'] + : undefined; + const universeDomain = + options.universeDomain ?? + options.universe_domain ?? + universeDomainEnvVar ?? + 'googleapis.com'; + + // Update URL to point to the given TPC universe + if (universeDomain !== 'googleapis.com' && options.url) { + const url = new URL(options.url); + if (url.hostname.endsWith('.googleapis.com')) { + url.hostname = url.hostname.replace(/googleapis\.com$/, universeDomain); + options.url = url.toString(); + } + } + // Perform the HTTP request. NOTE: this function used to return a // mikeal/request object. Since the transition to Axios, the method is // now void. This may be a source of confusion for users upgrading from // version 24.0 -> 25.0 or up. if (authClient && typeof authClient === 'object') { + // Validate TPC universe + const universeFromAuth = + typeof authClient.getUniverseDomain === 'function' + ? await authClient.getUniverseDomain() + : undefined; + if (universeFromAuth && universeDomain !== universeFromAuth) { + throw new Error( + `The configured universe domain (${universeDomain}) does not match the universe domain found in the credentials (${universeFromAuth}). ` + + "If you haven't configured the universe domain explicitly, googleapis.com is the default." + ); + } + if (options.http2) { const authHeaders = await authClient.getRequestHeaders(options.url); const mooOpts = Object.assign({}, options); diff --git a/src/authplus.ts b/src/authplus.ts index cfd3ce4..19e9e4e 100644 --- a/src/authplus.ts +++ b/src/authplus.ts @@ -47,6 +47,7 @@ export class AuthPlus extends GoogleAuth { Compute | JWT | UserRefreshClient | BaseExternalAccountClient | Impersonated > { this._cachedAuth = new GoogleAuth(options); + // eslint-disable-next-line @typescript-eslint/no-explicit-any return this._cachedAuth.getClient() as any; } diff --git a/test/test.apirequest.ts b/test/test.apirequest.ts index 3a6add8..31a0f66 100644 --- a/test/test.apirequest.ts +++ b/test/test.apirequest.ts @@ -485,4 +485,265 @@ describe('createAPIRequest', () => { assert.ok(stub.calledOnce); }); }); + + describe('TPC', () => { + it('should allow setting universeDomain', async () => { + const gduUrl = 'https://api.googleapis.com/path?param=value#extra'; + const expectedUniverseUrl = + 'https://api.universe.com/path?param=value#extra'; + const auth = new GoogleAuth(); + const getUniverseDomainStub = sandbox + .stub(auth, 'getUniverseDomain') + .resolves('universe.com'); + sandbox.stub(auth, 'getRequestHeaders').resolves({}); + const requestStub = sandbox + .stub(auth, 'request') + .resolves({data: fakeResponse} as GaxiosResponse); + const result = await createAPIRequest({ + options: {url: gduUrl}, + params: {}, + requiredParams: [], + pathParams: [], + context: { + _options: { + universeDomain: 'universe.com', + auth, + }, + }, + }); + assert.strictEqual(result.data, fakeResponse as {}); + assert.ok(getUniverseDomainStub.calledOnce); + assert.ok(requestStub.calledOnce); + assert.strictEqual( + requestStub.getCall(0).args[0].url, + expectedUniverseUrl + ); + assert(result); + }); + + it('should allow setting universe_domain', async () => { + const gduUrl = 'https://api.googleapis.com/path?param=value#extra'; + const expectedUniverseUrl = + 'https://api.universe.com/path?param=value#extra'; + const auth = new GoogleAuth(); + const getUniverseDomainStub = sandbox + .stub(auth, 'getUniverseDomain') + .resolves('universe.com'); + sandbox.stub(auth, 'getRequestHeaders').resolves({}); + const requestStub = sandbox + .stub(auth, 'request') + .resolves({data: fakeResponse} as GaxiosResponse); + const result = await createAPIRequest({ + options: {url: gduUrl}, + params: {}, + requiredParams: [], + pathParams: [], + context: { + _options: { + universe_domain: 'universe.com', + auth, + }, + }, + }); + assert.strictEqual(result.data, fakeResponse as {}); + assert.ok(getUniverseDomainStub.calledOnce); + assert.ok(requestStub.calledOnce); + assert.strictEqual( + requestStub.getCall(0).args[0].url, + expectedUniverseUrl + ); + assert(result); + }); + + it('should disallow setting both universeDomain and universe_domain', async () => { + const gduUrl = 'https://api.googleapis.com/path?param=value#extra'; + assert.rejects( + createAPIRequest({ + options: {url: gduUrl}, + params: {}, + requiredParams: [], + pathParams: [], + context: { + _options: { + universe_domain: 'universe1.com', + universeDomain: 'universe2.com', + }, + }, + }), + (err: Error) => { + assert.ok(err.message.includes('but not both')); + return true; + } + ); + }); + + if (typeof process === 'object' && typeof process.env === 'object') { + it('should allow setting GOOGLE_CLOUD_UNIVERSE_DOMAIN environment variable', async () => { + const saved = process.env['GOOGLE_CLOUD_UNIVERSE_DOMAIN']; + process.env['GOOGLE_CLOUD_UNIVERSE_DOMAIN'] = 'universe.com'; + const gduUrl = 'https://api.googleapis.com/path?param=value#extra'; + const expectedUniverseUrl = + 'https://api.universe.com/path?param=value#extra'; + const auth = new GoogleAuth(); + const getUniverseDomainStub = sandbox + .stub(auth, 'getUniverseDomain') + .resolves('universe.com'); + sandbox.stub(auth, 'getRequestHeaders').resolves({}); + const requestStub = sandbox + .stub(auth, 'request') + .resolves({data: fakeResponse} as GaxiosResponse); + const result = await createAPIRequest({ + options: {url: gduUrl}, + params: {}, + requiredParams: [], + pathParams: [], + context: { + _options: { + auth, + }, + }, + }); + if (saved) { + process.env['GOOGLE_CLOUD_UNIVERSE_DOMAIN'] = saved; + } else { + delete process.env['GOOGLE_CLOUD_UNIVERSE_DOMAIN']; + } + assert.strictEqual(result.data, fakeResponse as {}); + assert.ok(getUniverseDomainStub.calledOnce); + assert.ok(requestStub.calledOnce); + assert.strictEqual( + requestStub.getCall(0).args[0].url, + expectedUniverseUrl + ); + assert(result); + }); + + it('configuration in code has priority over GOOGLE_CLOUD_UNIVERSE_DOMAIN environment variable', async () => { + const saved = process.env['GOOGLE_CLOUD_UNIVERSE_DOMAIN']; + process.env['GOOGLE_CLOUD_UNIVERSE_DOMAIN'] = 'wrong-universe.com'; + const gduUrl = 'https://api.googleapis.com/path?param=value#extra'; + const expectedUniverseUrl = + 'https://api.universe.com/path?param=value#extra'; + const auth = new GoogleAuth(); + const getUniverseDomainStub = sandbox + .stub(auth, 'getUniverseDomain') + .resolves('universe.com'); + sandbox.stub(auth, 'getRequestHeaders').resolves({}); + const requestStub = sandbox + .stub(auth, 'request') + .resolves({data: fakeResponse} as GaxiosResponse); + const result = await createAPIRequest({ + options: {url: gduUrl}, + params: {}, + requiredParams: [], + pathParams: [], + context: { + _options: { + universeDomain: 'universe.com', + auth, + }, + }, + }); + if (saved) { + process.env['GOOGLE_CLOUD_UNIVERSE_DOMAIN'] = saved; + } else { + delete process.env['GOOGLE_CLOUD_UNIVERSE_DOMAIN']; + } + assert.strictEqual(result.data, fakeResponse as {}); + assert.ok(getUniverseDomainStub.calledOnce); + assert.ok(requestStub.calledOnce); + assert.strictEqual( + requestStub.getCall(0).args[0].url, + expectedUniverseUrl + ); + assert(result); + }); + } + + it('should validate universe domain received from auth library', async () => { + const gduUrl = 'https://api.googleapis.com/path?param=value#extra'; + const auth = new GoogleAuth(); + sandbox.stub(auth, 'getUniverseDomain').resolves('wrong-universe.com'); + await assert.rejects( + createAPIRequest({ + options: {url: gduUrl}, + params: {}, + requiredParams: [], + pathParams: [], + context: { + _options: { + universeDomain: 'universe.com', + auth, + }, + }, + }), + (err: Error) => { + assert.ok( + err.message.includes( + 'The configured universe domain (universe.com) does not match the universe domain ' + + 'found in the credentials (wrong-universe.com)' + ) + ); + return true; + } + ); + }); + + it('should not leak TPC universe credentials to googleapis.com universe', async () => { + const gduUrl = 'https://api.googleapis.com/path?param=value#extra'; + const auth = new GoogleAuth(); + sandbox.stub(auth, 'getUniverseDomain').resolves('wrong-universe.com'); + await assert.rejects( + createAPIRequest({ + options: {url: gduUrl}, + params: {}, + requiredParams: [], + pathParams: [], + context: { + _options: { + auth, + }, + }, + }), + (err: Error) => { + assert.ok( + err.message.includes( + 'The configured universe domain (googleapis.com) does not match the universe domain ' + + 'found in the credentials (wrong-universe.com)' + ) + ); + return true; + } + ); + }); + + it('should not leak googleapis.com credentials to TPC universe', async () => { + const gduUrl = 'https://api.googleapis.com/path?param=value#extra'; + const auth = new GoogleAuth(); + sandbox.stub(auth, 'getUniverseDomain').resolves('googleapis.com'); + await assert.rejects( + createAPIRequest({ + options: {url: gduUrl}, + params: {}, + requiredParams: [], + pathParams: [], + context: { + _options: { + universe_domain: 'wrong-universe.com', + auth, + }, + }, + }), + (err: Error) => { + assert.ok( + err.message.includes( + 'The configured universe domain (wrong-universe.com) does not match the universe domain ' + + 'found in the credentials (googleapis.com)' + ) + ); + return true; + } + ); + }); + }); });