Skip to content
This repository was archived by the owner on Nov 18, 2025. It is now read-only.

Commit d51218c

Browse files
feat: implement support for Trusted Partner Cloud (#1552)
* feat: TPC support * test: add unit tests for TPC * fix: typo * fix: formatting * fix: lint in tests
1 parent 2f39306 commit d51218c

5 files changed

Lines changed: 115 additions & 10 deletions

File tree

gax/src/clientInterface.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ export interface ClientOptions
3838
fallback?: boolean | 'rest' | 'proto';
3939
apiEndpoint?: string;
4040
gaxServerStreamingRetries?: boolean;
41+
// We support both camelCase and snake_case for the universe domain.
42+
// No preference; exception will be thrown if both are set to different values.
43+
universeDomain?: string;
44+
universe_domain?: string;
4145
}
4246

4347
export interface Descriptors {

gax/src/fallback.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,18 @@ export class GrpcClient {
290290
if (!this.authClient) {
291291
throw new Error('No authentication was provided');
292292
}
293+
if (!opts.universeDomain) {
294+
opts.universeDomain = 'googleapis.com';
295+
}
296+
if (opts.universeDomain) {
297+
const universeFromAuth = this.authClient.universeDomain;
298+
if (universeFromAuth && opts.universeDomain !== universeFromAuth) {
299+
throw new Error(
300+
`The configured universe domain (${opts.universeDomain}) does not match the universe domain found in the credentials (${universeFromAuth}). ` +
301+
"If you haven't configured the universe domain explicitly, googleapis.com is the default."
302+
);
303+
}
304+
}
293305
service.resolveAll();
294306
const methods = GrpcClient.getServiceMethods(service);
295307

gax/src/grpc.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export interface GrpcClientOptions extends GoogleAuthOptions {
5050
protoJson?: protobuf.Root;
5151
httpRules?: Array<google.api.IHttpRule>;
5252
numericEnums?: boolean;
53+
universeDomain?: string;
5354
}
5455

5556
export interface MetadataValue {
@@ -104,6 +105,7 @@ export interface ClientStubOptions {
104105
// For mtls:
105106
cert?: string;
106107
key?: string;
108+
universeDomain?: string;
107109
}
108110

109111
export class ClientStub extends grpc.Client {
@@ -398,14 +400,29 @@ export class GrpcClient {
398400
'grpc.channelFactoryOverride',
399401
'grpc.gcpApiConfig',
400402
];
401-
const [cert, key] = await this._detectClientCertificate(options);
403+
const [cert, key] = await this._detectClientCertificate(
404+
options,
405+
options.universeDomain
406+
);
402407
const servicePath = this._mtlsServicePath(
403408
options.servicePath,
404409
customServicePath,
405410
cert && key
406411
);
407412
const opts = Object.assign({}, options, {cert, key, servicePath});
408413
const serviceAddress = servicePath + ':' + opts.port;
414+
if (!options.universeDomain) {
415+
options.universeDomain = 'googleapis.com';
416+
}
417+
if (options.universeDomain) {
418+
const universeFromAuth = await this.auth.getUniverseDomain();
419+
if (universeFromAuth && options.universeDomain !== universeFromAuth) {
420+
throw new Error(
421+
`The configured universe domain (${options.universeDomain}) does not match the universe domain found in the credentials (${universeFromAuth}). ` +
422+
"If you haven't configured the universe domain explicitly, googleapis.com is the default."
423+
);
424+
}
425+
}
409426
const creds = await this._getCredentials(opts);
410427
const grpcOptions: ClientOptions = {};
411428
// @grpc/grpc-js limits max receive/send message length starting from v0.8.0
@@ -447,7 +464,10 @@ export class GrpcClient {
447464
* @param {object} [options] - The configuration object.
448465
* @returns {Promise} Resolves array of strings representing cert and key.
449466
*/
450-
async _detectClientCertificate(opts?: ClientOptions) {
467+
async _detectClientCertificate(
468+
opts?: ClientOptions,
469+
universeDomain?: string
470+
) {
451471
const certRegex =
452472
/(?<cert>-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----)/s;
453473
const keyRegex =
@@ -457,6 +477,11 @@ export class GrpcClient {
457477
typeof process !== 'undefined' &&
458478
process?.env?.GOOGLE_API_USE_CLIENT_CERTIFICATE === 'true'
459479
) {
480+
if (universeDomain && universeDomain !== 'googleapis.com') {
481+
throw new Error(
482+
'mTLS is not supported outside of googleapis.com universe domain.'
483+
);
484+
}
460485
if (opts?.cert && opts?.key) {
461486
return [opts.cert, opts.key];
462487
}

gax/test/unit/grpc-fallback.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {GoogleError} from '../../src';
3131
const hasAbortController = typeof AbortController !== 'undefined';
3232

3333
const authClient = {
34+
universeDomain: 'googleapis.com',
3435
async getRequestHeaders() {
3536
return {Authorization: 'Bearer SOME_TOKEN'};
3637
},
@@ -135,6 +136,24 @@ describe('createStub', () => {
135136
assert.strictEqual(echoStub.echo.length, 4);
136137
});
137138

139+
it('validates universe domain if set', async () => {
140+
const opts = {...stubOptions, universeDomain: 'example.com'};
141+
assert.rejects(
142+
gaxGrpc.createStub(echoService, opts),
143+
/configured universe domain/
144+
);
145+
});
146+
147+
it('validates universe domain if unset', async () => {
148+
authClient.universeDomain = 'example.com';
149+
assert.rejects(
150+
gaxGrpc.createStub(echoService, stubOptions),
151+
/configured universe domain/
152+
);
153+
// reset to default value
154+
authClient.universeDomain = 'googleapis.com';
155+
});
156+
138157
it('should support optional parameters', async () => {
139158
const echoStub = await gaxGrpc.createStub(echoService, stubExtraOptions);
140159

gax/test/unit/grpc.ts

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -119,17 +119,18 @@ describe('grpc', () => {
119119
});
120120
});
121121

122+
class DummyStub {
123+
constructor(
124+
public address: {},
125+
public creds: {},
126+
public options: {[index: string]: string | number | Function}
127+
) {}
128+
}
129+
122130
describe('createStub', () => {
123-
class DummyStub {
124-
constructor(
125-
public address: {},
126-
public creds: {},
127-
public options: {[index: string]: string | number | Function}
128-
) {}
129-
}
130131
let grpcClient: GrpcClient;
131132
const dummyChannelCreds = {channelCreds: 'dummyChannelCreds'};
132-
const stubAuth = {getClient: sinon.stub()};
133+
const stubAuth = {getClient: sinon.stub(), getUniverseDomain: sinon.stub()};
133134
const stubGrpc = {
134135
credentials: {
135136
createSsl: sinon.stub(),
@@ -148,6 +149,7 @@ describe('grpc', () => {
148149
stubGrpc.credentials.createFromGoogleCredential.reset();
149150

150151
stubAuth.getClient.resolves(dummyAuth);
152+
stubAuth.getUniverseDomain.resolves('googleapis.com');
151153
stubGrpc.credentials.createSsl.returns(dummySslCreds);
152154
stubGrpc.credentials.createFromGoogleCredential
153155
.withArgs(dummyAuth)
@@ -176,6 +178,30 @@ describe('grpc', () => {
176178
});
177179
});
178180

181+
it('validates universe domain if set', async () => {
182+
const opts = {
183+
servicePath: 'foo.example.com',
184+
port: 443,
185+
universeDomain: 'example.com',
186+
};
187+
assert.rejects(
188+
// @ts-ignore
189+
grpcClient.createStub(DummyStub, opts),
190+
/configured universe domain/
191+
);
192+
});
193+
194+
it('validates universe domain if unset', async () => {
195+
const opts = {servicePath: 'foo.example.com', port: 443};
196+
stubAuth.getUniverseDomain.reset();
197+
stubAuth.getUniverseDomain.resolves('example.com');
198+
assert.rejects(
199+
// @ts-ignore
200+
grpcClient.createStub(DummyStub, opts),
201+
/configured universe domain/
202+
);
203+
});
204+
179205
it('supports optional parameters', () => {
180206
const opts = {
181207
servicePath: 'foo.example.com',
@@ -659,5 +685,24 @@ dvorak
659685
assert.ok(key.includes('dvorak'));
660686
rimrafSync(tmpFolder); // Cleanup.
661687
});
688+
it('throws if attempted to use mTLS in non-default universe', async () => {
689+
// Pretend that "tmp-secure-context" in the current folder is the
690+
// home directory, so that we can test logic for loading
691+
// context_aware_metadata.json from well known location:
692+
const tmpdir = path.join(tmpFolder, '.secureConnect');
693+
mkdirSync(tmpdir, {recursive: true});
694+
const metadataFile = path.join(tmpdir, 'context_aware_metadata.json');
695+
writeFileSync(metadataFile, JSON.stringify(metadataFileContents), 'utf8');
696+
sandbox.stub(os, 'homedir').returns(tmpFolder);
697+
// Create a client and test the certificate detection flow:
698+
process.env.GOOGLE_API_USE_CLIENT_CERTIFICATE = 'true';
699+
const client = gaxGrpc();
700+
assert.rejects(
701+
// @ts-ignore
702+
client.createStub(DummyStub, {universeDomain: 'example.com'}),
703+
/configured universe domain/
704+
);
705+
rimrafSync(tmpFolder); // Cleanup.
706+
});
662707
});
663708
});

0 commit comments

Comments
 (0)