Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add universe support to googleapis libraries #548

Merged
merged 1 commit into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unrelated, webpack and karma-webpack v5 require Node 18 and it breaks tests. These are dev dependencies, we don't really care.

"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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an unrelated fix: without this, the existing code does not compile on the new TypeScript version.

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;
}
);
});
});
});
Loading