Skip to content

Commit

Permalink
feat: implement support for Trusted Partner Cloud (#1552)
Browse files Browse the repository at this point in the history
* feat: TPC support

* test: add unit tests for TPC

* fix: typo

* fix: formatting

* fix: lint in tests
  • Loading branch information
alexander-fenster authored Jan 31, 2024
1 parent 2f39306 commit d51218c
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 10 deletions.
4 changes: 4 additions & 0 deletions gax/src/clientInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export interface ClientOptions
fallback?: boolean | 'rest' | 'proto';
apiEndpoint?: string;
gaxServerStreamingRetries?: boolean;
// We support both camelCase and snake_case for the universe domain.
// No preference; exception will be thrown if both are set to different values.
universeDomain?: string;
universe_domain?: string;
}

export interface Descriptors {
Expand Down
12 changes: 12 additions & 0 deletions gax/src/fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,18 @@ export class GrpcClient {
if (!this.authClient) {
throw new Error('No authentication was provided');
}
if (!opts.universeDomain) {
opts.universeDomain = 'googleapis.com';
}
if (opts.universeDomain) {
const universeFromAuth = this.authClient.universeDomain;
if (universeFromAuth && opts.universeDomain !== universeFromAuth) {
throw new Error(
`The configured universe domain (${opts.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."
);
}
}
service.resolveAll();
const methods = GrpcClient.getServiceMethods(service);

Expand Down
29 changes: 27 additions & 2 deletions gax/src/grpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export interface GrpcClientOptions extends GoogleAuthOptions {
protoJson?: protobuf.Root;
httpRules?: Array<google.api.IHttpRule>;
numericEnums?: boolean;
universeDomain?: string;
}

export interface MetadataValue {
Expand Down Expand Up @@ -104,6 +105,7 @@ export interface ClientStubOptions {
// For mtls:
cert?: string;
key?: string;
universeDomain?: string;
}

export class ClientStub extends grpc.Client {
Expand Down Expand Up @@ -398,14 +400,29 @@ export class GrpcClient {
'grpc.channelFactoryOverride',
'grpc.gcpApiConfig',
];
const [cert, key] = await this._detectClientCertificate(options);
const [cert, key] = await this._detectClientCertificate(
options,
options.universeDomain
);
const servicePath = this._mtlsServicePath(
options.servicePath,
customServicePath,
cert && key
);
const opts = Object.assign({}, options, {cert, key, servicePath});
const serviceAddress = servicePath + ':' + opts.port;
if (!options.universeDomain) {
options.universeDomain = 'googleapis.com';
}
if (options.universeDomain) {
const universeFromAuth = await this.auth.getUniverseDomain();
if (universeFromAuth && options.universeDomain !== universeFromAuth) {
throw new Error(
`The configured universe domain (${options.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."
);
}
}
const creds = await this._getCredentials(opts);
const grpcOptions: ClientOptions = {};
// @grpc/grpc-js limits max receive/send message length starting from v0.8.0
Expand Down Expand Up @@ -447,7 +464,10 @@ export class GrpcClient {
* @param {object} [options] - The configuration object.
* @returns {Promise} Resolves array of strings representing cert and key.
*/
async _detectClientCertificate(opts?: ClientOptions) {
async _detectClientCertificate(
opts?: ClientOptions,
universeDomain?: string
) {
const certRegex =
/(?<cert>-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----)/s;
const keyRegex =
Expand All @@ -457,6 +477,11 @@ export class GrpcClient {
typeof process !== 'undefined' &&
process?.env?.GOOGLE_API_USE_CLIENT_CERTIFICATE === 'true'
) {
if (universeDomain && universeDomain !== 'googleapis.com') {
throw new Error(
'mTLS is not supported outside of googleapis.com universe domain.'
);
}
if (opts?.cert && opts?.key) {
return [opts.cert, opts.key];
}
Expand Down
19 changes: 19 additions & 0 deletions gax/test/unit/grpc-fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {GoogleError} from '../../src';
const hasAbortController = typeof AbortController !== 'undefined';

const authClient = {
universeDomain: 'googleapis.com',
async getRequestHeaders() {
return {Authorization: 'Bearer SOME_TOKEN'};
},
Expand Down Expand Up @@ -135,6 +136,24 @@ describe('createStub', () => {
assert.strictEqual(echoStub.echo.length, 4);
});

it('validates universe domain if set', async () => {
const opts = {...stubOptions, universeDomain: 'example.com'};
assert.rejects(
gaxGrpc.createStub(echoService, opts),
/configured universe domain/
);
});

it('validates universe domain if unset', async () => {
authClient.universeDomain = 'example.com';
assert.rejects(
gaxGrpc.createStub(echoService, stubOptions),
/configured universe domain/
);
// reset to default value
authClient.universeDomain = 'googleapis.com';
});

it('should support optional parameters', async () => {
const echoStub = await gaxGrpc.createStub(echoService, stubExtraOptions);

Expand Down
61 changes: 53 additions & 8 deletions gax/test/unit/grpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,17 +119,18 @@ describe('grpc', () => {
});
});

class DummyStub {
constructor(
public address: {},
public creds: {},
public options: {[index: string]: string | number | Function}
) {}
}

describe('createStub', () => {
class DummyStub {
constructor(
public address: {},
public creds: {},
public options: {[index: string]: string | number | Function}
) {}
}
let grpcClient: GrpcClient;
const dummyChannelCreds = {channelCreds: 'dummyChannelCreds'};
const stubAuth = {getClient: sinon.stub()};
const stubAuth = {getClient: sinon.stub(), getUniverseDomain: sinon.stub()};
const stubGrpc = {
credentials: {
createSsl: sinon.stub(),
Expand All @@ -148,6 +149,7 @@ describe('grpc', () => {
stubGrpc.credentials.createFromGoogleCredential.reset();

stubAuth.getClient.resolves(dummyAuth);
stubAuth.getUniverseDomain.resolves('googleapis.com');
stubGrpc.credentials.createSsl.returns(dummySslCreds);
stubGrpc.credentials.createFromGoogleCredential
.withArgs(dummyAuth)
Expand Down Expand Up @@ -176,6 +178,30 @@ describe('grpc', () => {
});
});

it('validates universe domain if set', async () => {
const opts = {
servicePath: 'foo.example.com',
port: 443,
universeDomain: 'example.com',
};
assert.rejects(
// @ts-ignore
grpcClient.createStub(DummyStub, opts),
/configured universe domain/
);
});

it('validates universe domain if unset', async () => {
const opts = {servicePath: 'foo.example.com', port: 443};
stubAuth.getUniverseDomain.reset();
stubAuth.getUniverseDomain.resolves('example.com');
assert.rejects(
// @ts-ignore
grpcClient.createStub(DummyStub, opts),
/configured universe domain/
);
});

it('supports optional parameters', () => {
const opts = {
servicePath: 'foo.example.com',
Expand Down Expand Up @@ -659,5 +685,24 @@ dvorak
assert.ok(key.includes('dvorak'));
rimrafSync(tmpFolder); // Cleanup.
});
it('throws if attempted to use mTLS in non-default universe', async () => {
// Pretend that "tmp-secure-context" in the current folder is the
// home directory, so that we can test logic for loading
// context_aware_metadata.json from well known location:
const tmpdir = path.join(tmpFolder, '.secureConnect');
mkdirSync(tmpdir, {recursive: true});
const metadataFile = path.join(tmpdir, 'context_aware_metadata.json');
writeFileSync(metadataFile, JSON.stringify(metadataFileContents), 'utf8');
sandbox.stub(os, 'homedir').returns(tmpFolder);
// Create a client and test the certificate detection flow:
process.env.GOOGLE_API_USE_CLIENT_CERTIFICATE = 'true';
const client = gaxGrpc();
assert.rejects(
// @ts-ignore
client.createStub(DummyStub, {universeDomain: 'example.com'}),
/configured universe domain/
);
rimrafSync(tmpFolder); // Cleanup.
});
});
});

0 comments on commit d51218c

Please sign in to comment.