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: implement support for Trusted Partner Cloud #1552

Merged
merged 6 commits into from
Jan 31, 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
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."
);
}
}
Comment on lines +414 to +425

Choose a reason for hiding this comment

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

await this.auth.getUniverseDomain() is called even if there is no universe domain configured in the options (because it then falls back to googleapis.com) which takes a considerable amount of time. In my case around 300ms. Should it really work that way? @alexander-fenster

This change slows down my project testing setup a lot because each test-bigtable creation takes around 300ms longer.

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