Skip to content

Commit

Permalink
feat: add universe support to googleapis libraries (#548)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-fenster committed Mar 21, 2024
1 parent 4d22220 commit dfd4b0d
Show file tree
Hide file tree
Showing 5 changed files with 313 additions and 4 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand All @@ -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": {
Expand Down
2 changes: 2 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
47 changes: 46 additions & 1 deletion src/apirequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,11 @@ async function createAPIRequestAsync<T>(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);
Expand Down Expand Up @@ -312,11 +316,52 @@ async function createAPIRequestAsync<T>(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);
Expand Down
1 change: 1 addition & 0 deletions src/authplus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
261 changes: 261 additions & 0 deletions test/test.apirequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<FakeParams>({
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<FakeParams>({
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<FakeParams>({
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<FakeParams>({
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<FakeParams>({
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<FakeParams>({
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<FakeParams>({
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<FakeParams>({
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;
}
);
});
});
});

0 comments on commit dfd4b0d

Please sign in to comment.