diff --git a/package-lock.json b/package-lock.json index 6747d837..2d64d670 100644 --- a/package-lock.json +++ b/package-lock.json @@ -274,9 +274,9 @@ } }, "@types/node": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.3.0.tgz", - "integrity": "sha512-hWzNviaVFIr1TqcRA8ou49JaSHp+Rfabmnqg2kNvusKqLhPU0rIsGPUj5WJJ7ld4Bb7qdgLmIhLfCD1qS08IVA==", + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.5.1.tgz", + "integrity": "sha512-AFLl1IALIuyt6oK4AYZsgWVJ/5rnyzQWud7IebaZWWV3YmgtPZkQmYio9R5Ze/2pdd7XfqF5bP+hWS11mAKoOQ==", "dev": true }, "@types/pify": { @@ -285,6 +285,12 @@ "integrity": "sha512-a5AKF1/9pCU3HGMkesgY6LsBdXHUY3WU+I2qgpU0J+I8XuJA1aFr59eS84/HP0+dxsyBSNbt+4yGI2adUpHwSg==", "dev": true }, + "@types/semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", + "dev": true + }, "@types/shelljs": { "version": "0.7.7", "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.7.7.tgz", @@ -4612,8 +4618,7 @@ "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" }, "semver-diff": { "version": "2.1.0", diff --git a/package.json b/package.json index 966b5369..a3dadaf9 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,13 @@ ], "dependencies": { "axios": "^0.18.0", + "gcp-metadata": "^0.6.3", "gtoken": "^2.3.0", "jws": "^3.1.5", "lodash.isstring": "^4.0.1", "lru-cache": "^4.1.3", "retry-axios": "^0.3.2", - "gcp-metadata": "^0.6.3" + "semver": "^5.5.0" }, "devDependencies": { "@justinbeckwith/typedoc": "^0.10.1", @@ -37,8 +38,9 @@ "@types/mv": "^2.1.0", "@types/ncp": "^2.0.1", "@types/nock": "^9.1.3", - "@types/node": "^10.3.0", + "@types/node": "^10.5.1", "@types/pify": "^3.0.2", + "@types/semver": "^5.5.0", "@types/sinon": "^5.0.1", "@types/tmp": "^0.0.33", "clang-format": "^1.2.3", diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 6a02f33c..24a308c4 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -85,9 +85,6 @@ export const CLOUD_SDK_CLIENT_ID = export class GoogleAuth { transporter?: Transporter; - // This shim is in place for compatibility with google-auto-auth. - getProjectId = this.getDefaultProjectId; - /** * Caches a value indicating whether the auth layer is running on Google * Compute Engine. @@ -126,23 +123,36 @@ export class GoogleAuth { } /** - * Obtains the default project ID for the application. - * @param callback Optional callback - * @returns Promise that resolves with project Id (if used without callback) + * THIS METHOD HAS BEEN DEPRECATED. + * It will be removed in 3.0. Please use getProjectId instead. */ getDefaultProjectId(): Promise; getDefaultProjectId(callback: ProjectIdCallback): void; getDefaultProjectId(callback?: ProjectIdCallback): Promise|void { + messages.warn(messages.DEFAULT_PROJECT_ID_DEPRECATED); if (callback) { - this.getDefaultProjectIdAsync() - .then(r => callback(null, r)) - .catch(callback); + this.getProjectIdAsync().then(r => callback(null, r)).catch(callback); + } else { + return this.getProjectIdAsync(); + } + } + + /** + * Obtains the default project ID for the application. + * @param callback Optional callback + * @returns Promise that resolves with project Id (if used without callback) + */ + getProjectId(): Promise; + getProjectId(callback: ProjectIdCallback): void; + getProjectId(callback?: ProjectIdCallback): Promise|void { + if (callback) { + this.getProjectIdAsync().then(r => callback(null, r)).catch(callback); } else { - return this.getDefaultProjectIdAsync(); + return this.getProjectIdAsync(); } } - private getDefaultProjectIdAsync(): Promise { + private getProjectIdAsync(): Promise { if (this._cachedProjectId) { return Promise.resolve(this._cachedProjectId); } @@ -205,7 +215,7 @@ export class GoogleAuth { if (this.cachedCredential) { return { credential: this.cachedCredential as JWT | UserRefreshClient, - projectId: await this.getDefaultProjectIdAsync() + projectId: await this.getProjectIdAsync() }; } @@ -222,7 +232,7 @@ export class GoogleAuth { credential.scopes = this.scopes; } this.cachedCredential = credential; - projectId = await this.getDefaultProjectId(); + projectId = await this.getProjectId(); return {credential, projectId}; } @@ -234,7 +244,7 @@ export class GoogleAuth { credential.scopes = this.scopes; } this.cachedCredential = credential; - projectId = await this.getDefaultProjectId(); + projectId = await this.getProjectId(); return {credential, projectId}; } @@ -256,7 +266,7 @@ export class GoogleAuth { // For GCE, just return a default ComputeClient. It will take care of // the rest. this.cachedCredential = new Compute(options); - projectId = await this.getDefaultProjectId(); + projectId = await this.getProjectId(); return {projectId, credential: this.cachedCredential}; } @@ -382,7 +392,7 @@ export class GoogleAuth { */ protected warnOnProblematicCredentials(client: JWT) { if (client.email === CLOUD_SDK_CLIENT_ID) { - process.emitWarning(messages.PROBLEMATIC_CREDENTIALS_WARNING); + messages.warn(messages.PROBLEMATIC_CREDENTIALS_WARNING); } } diff --git a/src/messages.ts b/src/messages.ts index a8dd2b52..b8827ee8 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -14,10 +14,58 @@ * limitations under the License. */ -export const PROBLEMATIC_CREDENTIALS_WARNING = - `Your application has authenticated using end user credentials from Google - Cloud SDK. We recommend that most server applications use service accounts - instead. If your application continues to use end user credentials from Cloud - SDK, you might receive a "quota exceeded" or "API not enabled" error. For - more information about service accounts, see - https://cloud.google.com/docs/authentication/.`; +import * as semver from 'semver'; + +export enum WarningTypes { + WARNING = 'Warning', + DEPRECATION = 'DeprecationWarning' +} + +export function warn(warning: Warning) { + // Only show a given warning once + if (warning.warned) { + return; + } + warning.warned = true; + if (semver.satisfies(process.version, '>=8')) { + // @types/node doesn't recognize the emitWarning syntax which + // accepts a config object, so `as any` it is + // https://nodejs.org/docs/latest-v8.x/api/process.html#process_process_emitwarning_warning_options + // tslint:disable-next-line no-any + process.emitWarning(warning.message, warning as any); + } else { + // This path can be removed once we drop support for Node 6. + // https://nodejs.org/docs/latest-v6.x/api/process.html#process_process_emitwarning_warning_name_ctor + process.emitWarning(warning.message, warning.type); + } +} + +export interface Warning { + code: string; + type: WarningTypes; + message: string; + warned?: boolean; +} + +export const PROBLEMATIC_CREDENTIALS_WARNING = { + code: 'google-auth-library:00001', + type: WarningTypes.WARNING, + message: [ + 'Your application has authenticated using end user credentials from Google', + 'Cloud SDK. We recommend that most server applications use service accounts', + 'instead. If your application continues to use end user credentials from', + 'Cloud SDK, you might receive a "quota exceeded" or "API not enabled" error.', + 'For more information about service accounts, see', + 'https://cloud.google.com/docs/authentication/.' + ].join(' ') +}; + +export const DEFAULT_PROJECT_ID_DEPRECATED = { + code: 'google-auth-library:DEP002', + type: WarningTypes.DEPRECATION, + message: [ + 'The `getDefaultProjectId` method has been deprecated, and will be removed', + 'in the 3.0 release of google-auth-library. Please use the `getProjectId`', + 'method instead.' + ].join(' ') +}; diff --git a/test/fixtures/kitchen/src/index.ts b/test/fixtures/kitchen/src/index.ts index 025b18f7..772dd25c 100644 --- a/test/fixtures/kitchen/src/index.ts +++ b/test/fixtures/kitchen/src/index.ts @@ -5,7 +5,7 @@ const jwt = new JWT(); const auth = new GoogleAuth(); async function getToken() { const token = await jwt.getToken('token'); - const projectId = await auth.getDefaultProjectId(); + const projectId = await auth.getProjectId(); const creds = await auth.getApplicationDefault(); return token; } diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 8687c6b2..56022fc1 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -51,18 +51,16 @@ const fixedProjectId = 'my-awesome-project'; const privateKey = fs.readFileSync('./test/fixtures/private.pem', 'utf-8'); let auth: GoogleAuth; -let sandbox: sinon.SinonSandbox|undefined; +let sandbox: sinon.SinonSandbox; beforeEach(() => { auth = new GoogleAuth(); + sandbox = sinon.createSandbox(); }); afterEach(() => { nock.cleanAll(); // after each test, reset the env vars - if (sandbox) { - sandbox.restore(); - sandbox = undefined; - } + sandbox.restore(); }); function nockIsGCE() { @@ -144,9 +142,6 @@ function blockGoogleApplicationCredentialEnvironmentVariable() { // Intercepts the specified environment variable, returning the specified value. function mockEnvVar(name: string, value = '') { - if (!sandbox) { - sandbox = sinon.createSandbox(); - } const envVars = Object.assign({}, process.env, {[name]: value}); const stub = sandbox.stub(process, 'env').value(envVars); return stub; @@ -745,7 +740,7 @@ it('_tryGetApplicationCredentialsFromWellKnownFile should pass along a failure o assert.fail('failed to throw'); }); -it('getDefaultProjectId should return a new projectId the first time and a cached projectId the second time', +it('getProjectId should return a new projectId the first time and a cached projectId the second time', async () => { // Create a function which will set up a GoogleAuth instance to match // on an environment variable json file, but not on anything else. @@ -757,7 +752,7 @@ it('getDefaultProjectId should return a new projectId the first time and a cache setUpAuthForEnvironmentVariable(auth); // Ask for credentials, the first time. - const projectIdPromise = auth.getDefaultProjectId(); + const projectIdPromise = auth.getProjectId(); const projectId = await projectIdPromise; assert.equal(projectId, fixedProjectId); @@ -771,7 +766,7 @@ it('getDefaultProjectId should return a new projectId the first time and a cache // Ask for projectId again, from the same auth instance. If it isn't // cached, this will crash. - const projectId2 = await auth.getDefaultProjectId(); + const projectId2 = await auth.getProjectId(); // Make sure we get the original cached projectId back assert.equal(fixedProjectId, projectId2); @@ -781,34 +776,34 @@ it('getDefaultProjectId should return a new projectId the first time and a cache const auth2 = new GoogleAuth(); setUpAuthForEnvironmentVariable(auth2); - const getProjectIdPromise = auth2.getDefaultProjectId(); + const getProjectIdPromise = auth2.getProjectId(); assert.notEqual(getProjectIdPromise, projectIdPromise); }); -it('getDefaultProjectId should use GCLOUD_PROJECT environment variable when it is set', +it('getProjectId should use GCLOUD_PROJECT environment variable when it is set', async () => { mockEnvVar('GCLOUD_PROJECT', fixedProjectId); - const projectId = await auth.getDefaultProjectId(); + const projectId = await auth.getProjectId(); assert.equal(projectId, fixedProjectId); }); -it('getDefaultProjectId should use GOOGLE_CLOUD_PROJECT environment variable when it is set', +it('getProjectId should use GOOGLE_CLOUD_PROJECT environment variable when it is set', async () => { mockEnvVar('GOOGLE_CLOUD_PROJECT', fixedProjectId); - const projectId = await auth.getDefaultProjectId(); + const projectId = await auth.getProjectId(); assert.equal(projectId, fixedProjectId); }); -it('getDefaultProjectId should use GOOGLE_APPLICATION_CREDENTIALS file when it is available', +it('getProjectId should use GOOGLE_APPLICATION_CREDENTIALS file when it is available', async () => { mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', path.join(__dirname, '../../test/fixtures/private2.json')); - const projectId = await auth.getDefaultProjectId(); + const projectId = await auth.getProjectId(); assert.equal(projectId, fixedProjectId); }); -it('getDefaultProjectId should prefer configured projectId', async () => { +it('getProjectId should prefer configured projectId', async () => { mockEnvVar('GCLOUD_PROJECT', fixedProjectId); mockEnvVar('GOOGLE_CLOUD_PROJECT', fixedProjectId); mockEnvVar( @@ -818,11 +813,11 @@ it('getDefaultProjectId should prefer configured projectId', async () => { const PROJECT_ID = 'configured-project-id-should-be-preferred'; const auth = new GoogleAuth({projectId: PROJECT_ID}); - const projectId = await auth.getDefaultProjectId(); + const projectId = await auth.getProjectId(); assert.strictEqual(projectId, PROJECT_ID); }); -it('getProjectId should work the same as getDefaultProjectId', async () => { +it('getProjectId should work the same as getProjectId', async () => { mockEnvVar( 'GOOGLE_APPLICATION_CREDENTIALS', path.join(__dirname, '../../test/fixtures/private2.json')); @@ -830,31 +825,29 @@ it('getProjectId should work the same as getDefaultProjectId', async () => { assert.equal(projectId, fixedProjectId); }); -it('getDefaultProjectId should use Cloud SDK when it is available and env vars are not set', +it('getProjectId should use Cloud SDK when it is available and env vars are not set', async () => { // Set up the creds. // * Environment variable is not set. // * Well-known file is set up to point to private2.json // * Running on GCE is set to true. - sandbox = sinon.createSandbox(); blockGoogleApplicationCredentialEnvironmentVariable(); const stdout = JSON.stringify( {configuration: {properties: {core: {project: fixedProjectId}}}}); const stub = sandbox.stub(child_process, 'exec') .callsArgWith(1, null, stdout, null); - const projectId = await auth.getDefaultProjectId(); + const projectId = await auth.getProjectId(); assert(stub.calledOnce); assert.equal(projectId, fixedProjectId); }); -it('getDefaultProjectId should use GCE when well-known file and env const are not set', +it('getProjectId should use GCE when well-known file and env const are not set', async () => { blockGoogleApplicationCredentialEnvironmentVariable(); - sandbox = sinon.createSandbox(); const stub = sandbox.stub(child_process, 'exec').callsArgWith(1, null, '', null); const scope = createGetProjectIdNock(fixedProjectId); - const projectId = await auth.getDefaultProjectId(); + const projectId = await auth.getProjectId(); assert(stub.calledOnce); assert.equal(projectId, fixedProjectId); scope.done(); @@ -1388,15 +1381,37 @@ it('should warn the user if using default Cloud SDK credentials', done => { auth._getApplicationCredentialsFromFilePath = () => { return Promise.resolve(new JWT(CLOUD_SDK_CLIENT_ID)); }; - let warned = false; - process.on('warning', (warning) => { - assert.equal(warning.message, messages.PROBLEMATIC_CREDENTIALS_WARNING); - warned = true; - }); - auth._tryGetApplicationCredentialsFromWellKnownFile().then(() => { - setImmediate(() => { - assert(warned); - done(); - }); - }); + sandbox.stub(process, 'emitWarning') + .callsFake((message: string, warningOrType: messages.Warning|string) => { + assert.equal(message, messages.PROBLEMATIC_CREDENTIALS_WARNING.message); + const warningType = typeof warningOrType === 'string' ? + warningOrType : + warningOrType.type; + assert.equal(warningType, messages.WarningTypes.WARNING); + done(); + }); + auth._tryGetApplicationCredentialsFromWellKnownFile(); +}); + +it('should warn the user if using the getDefaultProjectId method', done => { + mockEnvVar('GCLOUD_PROJECT', fixedProjectId); + sandbox.stub(process, 'emitWarning') + .callsFake((message: string, warningOrType: messages.Warning|string) => { + assert.equal(message, messages.DEFAULT_PROJECT_ID_DEPRECATED.message); + const warningType = typeof warningOrType === 'string' ? + warningOrType : + warningOrType.type; + assert.equal(warningType, messages.WarningTypes.DEPRECATION); + done(); + }); + auth.getDefaultProjectId(); +}); + +it('should only emit warnings once', async () => { + // The warning was used above, so invoking it here should have no effect. + mockEnvVar('GCLOUD_PROJECT', fixedProjectId); + let count = 0; + sandbox.stub(process, 'emitWarning').callsFake(() => count++); + await auth.getDefaultProjectId(); + assert.equal(count, 0); });