Skip to content

Commit 8813369

Browse files
authored
fix: Requests Respect config.projectIdRequired === false (#1988)
* fix: Requests Respect `config.projectIdRequired` === `false` * chore: `needs authentication` -> `authentication`
1 parent 71a61ec commit 8813369

File tree

4 files changed

+169
-34
lines changed

4 files changed

+169
-34
lines changed

src/nodejs-common/service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
util,
2929
} from './util';
3030

31-
const PROJECT_ID_TOKEN = '{{projectId}}';
31+
export const DEFAULT_PROJECT_ID_TOKEN = '{{projectId}}';
3232

3333
export interface StreamRequestOptions extends DecorateRequestOptions {
3434
shouldReturnStream: true;
@@ -106,7 +106,7 @@ export class Service {
106106
this.globalInterceptors = arrify(options.interceptors_!);
107107
this.interceptors = [];
108108
this.packageJson = config.packageJson;
109-
this.projectId = options.projectId || PROJECT_ID_TOKEN;
109+
this.projectId = options.projectId || DEFAULT_PROJECT_ID_TOKEN;
110110
this.projectIdRequired = config.projectIdRequired !== false;
111111
this.providedUserAgent = options.userAgent;
112112

@@ -167,7 +167,7 @@ export class Service {
167167

168168
protected async getProjectIdAsync(): Promise<string> {
169169
const projectId = await this.authClient.getProjectId();
170-
if (this.projectId === PROJECT_ID_TOKEN && projectId) {
170+
if (this.projectId === DEFAULT_PROJECT_ID_TOKEN && projectId) {
171171
this.projectId = projectId;
172172
}
173173
return this.projectId;

src/nodejs-common/util.ts

Lines changed: 92 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
* @module common/util
1919
*/
2020

21-
import {replaceProjectIdToken} from '@google-cloud/projectify';
21+
import {
22+
replaceProjectIdToken,
23+
MissingProjectIdError,
24+
} from '@google-cloud/projectify';
2225
import * as ent from 'ent';
2326
import * as extend from 'extend';
2427
import {AuthClient, GoogleAuth, GoogleAuthOptions} from 'google-auth-library';
@@ -29,6 +32,8 @@ import {Duplex, DuplexOptions, Readable, Transform, Writable} from 'stream';
2932
import {teenyRequest} from 'teeny-request';
3033
import {Interceptor} from './service-object';
3134
import * as uuid from 'uuid';
35+
import {DEFAULT_PROJECT_ID_TOKEN} from './service';
36+
3237
const packageJson = require('../../../package.json');
3338

3439
// eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -164,6 +169,11 @@ export interface MakeAuthenticatedRequestFactoryConfig
164169
* A new will be created if this is not set.
165170
*/
166171
authClient?: AuthClient | GoogleAuth;
172+
173+
/**
174+
* Determines if a projectId is required for authenticated requests. Defaults to `true`.
175+
*/
176+
projectIdRequired?: boolean;
167177
}
168178

169179
export interface MakeAuthenticatedRequestOptions {
@@ -592,7 +602,7 @@ export class Util {
592602
config: MakeAuthenticatedRequestFactoryConfig
593603
) {
594604
const googleAutoAuthConfig = extend({}, config);
595-
if (googleAutoAuthConfig.projectId === '{{projectId}}') {
605+
if (googleAutoAuthConfig.projectId === DEFAULT_PROJECT_ID_TOKEN) {
596606
delete googleAutoAuthConfig.projectId;
597607
}
598608

@@ -650,7 +660,11 @@ export class Util {
650660
const callback =
651661
typeof optionsOrCallback === 'function' ? optionsOrCallback : undefined;
652662

653-
const onAuthenticated = (
663+
async function setProjectId() {
664+
projectId = await authClient.getProjectId();
665+
}
666+
667+
const onAuthenticated = async (
654668
err: Error | null,
655669
authenticatedReqOpts?: DecorateRequestOptions
656670
) => {
@@ -667,16 +681,35 @@ export class Util {
667681

668682
if (!err || autoAuthFailed) {
669683
try {
684+
// Try with existing `projectId` value
670685
authenticatedReqOpts = util.decorateRequest(
671686
authenticatedReqOpts!,
672687
projectId
673688
);
689+
674690
err = null;
675691
} catch (e) {
676-
// A projectId was required, but we don't have one.
677-
// Re-use the "Could not load the default credentials error" if
678-
// auto auth failed.
679-
err = err || (e as Error);
692+
if (e instanceof MissingProjectIdError) {
693+
// A `projectId` was required, but we don't have one.
694+
try {
695+
// Attempt to get the `projectId`
696+
await setProjectId();
697+
698+
authenticatedReqOpts = util.decorateRequest(
699+
authenticatedReqOpts!,
700+
projectId
701+
);
702+
703+
err = null;
704+
} catch (e) {
705+
// Re-use the "Could not load the default credentials error" if
706+
// auto auth failed.
707+
err = err || (e as Error);
708+
}
709+
} else {
710+
// Some other error unrelated to missing `projectId`
711+
err = err || (e as Error);
712+
}
680713
}
681714
}
682715

@@ -715,23 +748,58 @@ export class Util {
715748
}
716749
};
717750

718-
Promise.all([
719-
config.projectId && config.projectId !== '{{projectId}}'
720-
? // The user provided a project ID. We don't need to check with the
721-
// auth client, it could be incorrect.
722-
new Promise(resolve => resolve(config.projectId))
723-
: authClient.getProjectId(),
724-
reqConfig.customEndpoint && reqConfig.useAuthWithCustomEndpoint !== true
725-
? // Using a custom API override. Do not use `google-auth-library` for
726-
// authentication. (ex: connecting to a local Datastore server)
727-
new Promise(resolve => resolve(reqOpts))
728-
: authClient.authorizeRequest(reqOpts),
729-
])
730-
.then(([_projectId, authorizedReqOpts]) => {
731-
projectId = _projectId as string;
732-
onAuthenticated(null, authorizedReqOpts as DecorateRequestOptions);
733-
})
734-
.catch(onAuthenticated);
751+
const prepareRequest = async () => {
752+
try {
753+
const getProjectId = async () => {
754+
if (
755+
config.projectId &&
756+
config.projectId !== DEFAULT_PROJECT_ID_TOKEN
757+
) {
758+
// The user provided a project ID. We don't need to check with the
759+
// auth client, it could be incorrect.
760+
return config.projectId;
761+
}
762+
763+
if (config.projectIdRequired === false) {
764+
// A projectId is not required. Return the default.
765+
return DEFAULT_PROJECT_ID_TOKEN;
766+
}
767+
768+
return setProjectId();
769+
};
770+
771+
const authorizeRequest = async () => {
772+
if (
773+
reqConfig.customEndpoint &&
774+
!reqConfig.useAuthWithCustomEndpoint
775+
) {
776+
// Using a custom API override. Do not use `google-auth-library` for
777+
// authentication. (ex: connecting to a local Datastore server)
778+
return reqOpts;
779+
} else {
780+
return authClient.authorizeRequest(reqOpts);
781+
}
782+
};
783+
784+
const [_projectId, authorizedReqOpts] = await Promise.all([
785+
getProjectId(),
786+
authorizeRequest(),
787+
]);
788+
789+
if (_projectId) {
790+
projectId = _projectId;
791+
}
792+
793+
return onAuthenticated(
794+
null,
795+
authorizedReqOpts as DecorateRequestOptions
796+
);
797+
} catch (e) {
798+
return onAuthenticated(e as Error);
799+
}
800+
};
801+
802+
prepareRequest();
735803

736804
if (stream!) {
737805
return stream!;

test/nodejs-common/service.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ import {Request} from 'teeny-request';
2222
import {AuthClient, GoogleAuth, OAuth2Client} from 'google-auth-library';
2323

2424
import {Interceptor} from '../../src/nodejs-common';
25-
import {ServiceConfig, ServiceOptions} from '../../src/nodejs-common/service';
25+
import {
26+
DEFAULT_PROJECT_ID_TOKEN,
27+
ServiceConfig,
28+
ServiceOptions,
29+
} from '../../src/nodejs-common/service';
2630
import {
2731
BodyResponseCallback,
2832
DecorateRequestOptions,
@@ -228,7 +232,7 @@ describe('Service', () => {
228232

229233
it('should default projectId with placeholder', () => {
230234
const service = new Service(fakeCfg, {});
231-
assert.strictEqual(service.projectId, '{{projectId}}');
235+
assert.strictEqual(service.projectId, DEFAULT_PROJECT_ID_TOKEN);
232236
});
233237

234238
it('should localize the projectIdRequired', () => {

test/nodejs-common/util.ts

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
* limitations under the License.
1515
*/
1616

17-
import {replaceProjectIdToken} from '@google-cloud/projectify';
17+
import {
18+
MissingProjectIdError,
19+
replaceProjectIdToken,
20+
} from '@google-cloud/projectify';
1821
import * as assert from 'assert';
1922
import {describe, it, before, beforeEach, afterEach} from 'mocha';
2023
import * as extend from 'extend';
@@ -45,6 +48,7 @@ import {
4548
ParsedHttpRespMessage,
4649
Util,
4750
} from '../../src/nodejs-common/util';
51+
import {DEFAULT_PROJECT_ID_TOKEN} from '../../src/nodejs-common/service';
4852

4953
// eslint-disable-next-line @typescript-eslint/no-var-requires
5054
const duplexify: DuplexifyConstructor = require('duplexify');
@@ -756,7 +760,7 @@ describe('common/util', () => {
756760
});
757761

758762
it('should not pass projectId token to google-auth-library', done => {
759-
const config = {projectId: '{{projectId}}'};
763+
const config = {projectId: DEFAULT_PROJECT_ID_TOKEN};
760764

761765
sandbox.stub(fakeGoogleAuth, 'GoogleAuth').callsFake(config_ => {
762766
assert.strictEqual(config_.projectId, undefined);
@@ -768,10 +772,10 @@ describe('common/util', () => {
768772
});
769773

770774
it('should not remove projectId from config object', done => {
771-
const config = {projectId: '{{projectId}}'};
775+
const config = {projectId: DEFAULT_PROJECT_ID_TOKEN};
772776

773777
sandbox.stub(fakeGoogleAuth, 'GoogleAuth').callsFake(() => {
774-
assert.strictEqual(config.projectId, '{{projectId}}');
778+
assert.strictEqual(config.projectId, DEFAULT_PROJECT_ID_TOKEN);
775779
setImmediate(done);
776780
return authClient;
777781
});
@@ -901,7 +905,7 @@ describe('common/util', () => {
901905
});
902906
});
903907

904-
describe('needs authentication', () => {
908+
describe('authentication', () => {
905909
it('should pass correct args to authorizeRequest', done => {
906910
const fake = extend(true, authClient, {
907911
authorizeRequest: async (rOpts: {}) => {
@@ -973,6 +977,65 @@ describe('common/util', () => {
973977
onAuthenticated: assert.ifError,
974978
});
975979
});
980+
981+
it('should use default `projectId` and not call `authClient#getProjectId` when !`projectIdRequired`', done => {
982+
const getProjectIdSpy = sandbox.spy(authClient, 'getProjectId');
983+
984+
sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient);
985+
986+
const config = {
987+
customEndpoint: true,
988+
projectIdRequired: false,
989+
};
990+
991+
stub('decorateRequest', (reqOpts, projectId) => {
992+
assert.strictEqual(projectId, DEFAULT_PROJECT_ID_TOKEN);
993+
});
994+
995+
const makeAuthenticatedRequest =
996+
util.makeAuthenticatedRequestFactory(config);
997+
998+
makeAuthenticatedRequest(reqOpts, {
999+
onAuthenticated: e => {
1000+
assert.ifError(e);
1001+
assert(getProjectIdSpy.notCalled);
1002+
done(e);
1003+
},
1004+
});
1005+
});
1006+
1007+
it('should fallback to checking for a `projectId` on when missing a `projectId` when !`projectIdRequired`', done => {
1008+
const getProjectIdSpy = sandbox.spy(authClient, 'getProjectId');
1009+
1010+
sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient);
1011+
1012+
const config = {
1013+
customEndpoint: true,
1014+
projectIdRequired: false,
1015+
};
1016+
1017+
const decorateRequestStub = sandbox.stub(util, 'decorateRequest');
1018+
1019+
decorateRequestStub.onFirstCall().callsFake(() => {
1020+
throw new MissingProjectIdError();
1021+
});
1022+
1023+
decorateRequestStub.onSecondCall().callsFake((reqOpts, projectId) => {
1024+
assert.strictEqual(projectId, AUTH_CLIENT_PROJECT_ID);
1025+
return reqOpts;
1026+
});
1027+
1028+
const makeAuthenticatedRequest =
1029+
util.makeAuthenticatedRequestFactory(config);
1030+
1031+
makeAuthenticatedRequest(reqOpts, {
1032+
onAuthenticated: e => {
1033+
assert.ifError(e);
1034+
assert(getProjectIdSpy.calledOnce);
1035+
done(e);
1036+
},
1037+
});
1038+
});
9761039
});
9771040

9781041
describe('authentication errors', () => {

0 commit comments

Comments
 (0)