From 6921be61e330c1e680f24150b193eee3b5b85457 Mon Sep 17 00:00:00 2001 From: Jason Dobry Date: Sat, 1 Oct 2016 00:09:30 -0700 Subject: [PATCH] Add support for detecting project ID. Fixes #97 --- lib/auth/googleauth.js | 179 +++++++++++++++++++++++++++++++++++- lib/auth/jwtaccess.js | 1 + lib/auth/jwtclient.js | 1 + package.json | 4 + test/fixtures/private2.json | 3 +- test/test.googleauth.js | 158 +++++++++++++++++++++++++++++++ 6 files changed, 341 insertions(+), 5 deletions(-) diff --git a/lib/auth/googleauth.js b/lib/auth/googleauth.js index 625357ff..0568fad5 100644 --- a/lib/auth/googleauth.js +++ b/lib/auth/googleauth.js @@ -18,6 +18,7 @@ var JWTClient = require('./jwtclient.js'); var ComputeClient = require('./computeclient.js'); +var exec = require('child_process').exec; var fs = require('fs'); var os = require('os'); var path = require('path'); @@ -101,6 +102,169 @@ GoogleAuth.prototype._isGCE = false; */ GoogleAuth.prototype._checked_isGCE = false; +/** + * Obtains the default project ID for the application.. + * @param {function=} opt_callback Optional callback. + */ +GoogleAuth.prototype.getDefaultProjectId = function(opt_callback) { + var that = this; + + // In implicit case, supports three environments. In order of precedence, the + // implicit environments are: + // + // * GCLOUD_PROJECT or GOOGLE_CLOUD_PROJECT environment variable + // * GOOGLE_APPLICATION_CREDENTIALS JSON file + // * Get default service project from + // ``$ gcloud beta auth application-default login`` + // * Google App Engine application ID (Not implemented yet) + // * Google Compute Engine project ID (from metadata server) (Not implemented yet) + + if (that._cachedProjectId) { + process.nextTick(function() { + callback(opt_callback, null, that._cachedProjectId); + }); + } else { + var my_callback = function(err, projectId) { + if (!err && projectId) { + that._cachedprojectId = projectId; + } + process.nextTick(function() { + callback(opt_callback, err, projectId); + }); + }; + + // environment variable + if (that._getProductionProjectId(my_callback)) { + return; + } + + // json file + that._getFileProjectId(function(err, projectId) { + if (err || projectId) { + my_callback(err, projectId); + return; + } + + // Google Cloud SDK default project id + that._getDefaultServiceProjectId(function(err, projectId) { + if (err || projectId) { + my_callback(err, projectId); + return; + } + + // Get project ID from Compute Engine metadata server + that._getGCEProjectId(my_callback); + }); + }); + } +}; + +/** + * Loads the project id from environment variables. + * @param {function} _callback Callback. + * @api private + */ +GoogleAuth.prototype._getProductionProjectId = function(_callback) { + var projectId = this._getEnv('GCLOUD_PROJECT') || this._getEnv('GOOGLE_CLOUD_PROJECT'); + if (projectId) { + process.nextTick(function() { + callback(_callback, null, projectId); + }); + } + return projectId; +}; + +/** + * Loads the project id from the GOOGLE_APPLICATION_CREDENTIALS json file. + * @param {function} _callback Callback. + * @api private + */ +GoogleAuth.prototype._getFileProjectId = function(_callback) { + var that = this; + if (that._cachedCredential) { + // Try to read the project ID from the cached credentials file + process.nextTick(function() { + callback(_callback, null, that._cachedCredential.projectId); + }); + return; + } + + // Try to load a credentials file and read its project ID + var pathExists = that._tryGetApplicationCredentialsFromEnvironmentVariable(function(err, result) { + if (!err && result) { + callback(_callback, null, result.projectId); + return; + } + callback(_callback, err); + }); + + if (!pathExists) { + callback(_callback, null); + } +}; + +/** + * Loads the default project of the Google Cloud SDK. + * @param {function} _callback Callback. + * @api private + */ +GoogleAuth.prototype._getDefaultServiceProjectId = function(_callback) { + this._getSDKDefaultProjectId(function(err, stdout) { + var projectId; + if (!err && stdout) { + try { + projectId = JSON.parse(stdout).core.project; + } catch (err) { + projectId = null; + } + } + // Ignore any errors + callback(_callback, null, projectId); + }); +}; + +/** + * Run the Google Cloud SDK command that prints the default project ID + * @param {function} _callback Callback. + * @api private + */ +GoogleAuth.prototype._getSDKDefaultProjectId = function(_callback) { + exec('gcloud -q config list core/project --format=json', _callback); +}; + +/** + * Gets the Compute Engine project ID if it can be inferred. + * Uses 169.254.169.254 for the metadata server to avoid request + * latency from DNS lookup. + * See https://cloud.google.com/compute/docs/metadata#metadataserver + * for information about this IP address. (This IP is also used for + * Amazon EC2 instances, so the metadata flavor is crucial.) + * See https://github.com/google/oauth2client/issues/93 for context about + * DNS latency. + * + * @param {function} _callback Callback. + * @api private + */ +GoogleAuth.prototype._getGCEProjectId = function(_callback) { + if (!this.transporter) { + this.transporter = new DefaultTransporter(); + } + this.transporter.request({ + method: 'GET', + uri: 'http://169.254.169.254/computeMetadata/v1/project/project-id', + headers: { + 'Metadata-Flavor': 'Google' + } + }, function(err, body, res) { + if (err || !res || res.statusCode !== 200 || !body) { + callback(_callback, null); + return; + } + // Ignore any errors + callback(_callback, null, body); + }); +}; + /** * Obtains the default service-level credentials for the application.. * @param {function=} opt_callback Optional callback. @@ -111,7 +275,7 @@ GoogleAuth.prototype.getApplicationDefault = function(opt_callback) { // If we've already got a cached credential, just return it. if (that._cachedCredential) { process.nextTick(function() { - callback(opt_callback, null, that._cachedCredential); + callback(opt_callback, null, that._cachedCredential, that._cachedProjectId); }); } else { // Inject our own callback routine, which will cache the credential once it's been created. @@ -119,10 +283,17 @@ GoogleAuth.prototype.getApplicationDefault = function(opt_callback) { var my_callback = function(err, result) { if (!err && result) { that._cachedCredential = result; + that.getDefaultProjectId(function(err, projectId) { + process.nextTick(function() { + // Ignore default project error + callback(opt_callback, null, result, projectId); + }); + }); + } else { + process.nextTick(function() { + callback(opt_callback, err, result); + }); } - process.nextTick(function() { - callback(opt_callback, err, result); - }); }; // Check for the existence of a local environment variable pointing to the // location of the credential file. This is typically used in local developer scenarios. diff --git a/lib/auth/jwtaccess.js b/lib/auth/jwtaccess.js index df2ff8e2..90d52431 100644 --- a/lib/auth/jwtaccess.js +++ b/lib/auth/jwtaccess.js @@ -111,6 +111,7 @@ JWTAccess.prototype.fromJSON = function(json, opt_callback) { // Extract the relevant information from the json key file. that.email = json.client_email; that.key = json.private_key; + that.projectId = json.project_id; done(); }; diff --git a/lib/auth/jwtclient.js b/lib/auth/jwtclient.js index 37bc4690..59ca53ed 100644 --- a/lib/auth/jwtclient.js +++ b/lib/auth/jwtclient.js @@ -172,6 +172,7 @@ JWT.prototype.fromJSON = function(json, opt_callback) { // Extract the relevant information from the json key file. that.email = json.client_email; that.key = json.private_key; + that.projectId = json.project_id; done(); }; diff --git a/package.json b/package.json index cc966eb1..a7b86000 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,10 @@ "name": "Jason Allor", "email": "jasonall@google.com" }, + { + "name": "Jason Dobry", + "email": "jason.dobry@gmail.com" + }, { "name": "Tim Emiola", "email": "temiola@google.com" diff --git a/test/fixtures/private2.json b/test/fixtures/private2.json index e61d129c..88ab26e0 100644 --- a/test/fixtures/private2.json +++ b/test/fixtures/private2.json @@ -3,5 +3,6 @@ "private_key": "privatekey2", "client_email": "goodbye@youarecool.com", "client_id": "client456", - "type": "service_account" + "type": "service_account", + "project_id": "my-awesome-project" } \ No newline at end of file diff --git a/test/test.googleauth.js b/test/test.googleauth.js index 94f44bc4..0d2aed7a 100644 --- a/test/test.googleauth.js +++ b/test/test.googleauth.js @@ -20,6 +20,7 @@ var assert = require('assert'); var GoogleAuth = require('../lib/auth/googleauth.js'); var nock = require('nock'); var fs = require('fs'); +var path = require('path'); nock.disableNetConnect(); @@ -789,6 +790,163 @@ describe('GoogleAuth', function() { step(); }); + describe('.getDefaultProjectId', function () { + + it('should return a new projectId the first time and a cached projectId the second time', + function (done) { + + var projectId = 'my-awesome-project'; + // The test ends successfully after 3 steps have completed. + var step = doneWhen(done, 3); + + // Create a function which will set up a GoogleAuth instance to match on + // an environment variable json file, but not on anything else. + var setUpAuthForEnvironmentVariable = function(creds) { + insertEnvironmentVariableIntoAuth(creds, 'GCLOUD_PROJECT', projectId); + + creds._fileExists = returns(false); + creds._checkIsGCE = callsBack(false); + }; + + // Set up a new GoogleAuth and prepare it for local environment variable handling. + var auth = new GoogleAuth(); + setUpAuthForEnvironmentVariable(auth); + + // Ask for credentials, the first time. + auth.getDefaultProjectId(function (err, _projectId) { + assert.equal(null, err); + assert.equal(_projectId, projectId); + + // Manually change the value of the cached projectId + auth._cachedProjectId = 'monkey'; + + // Step 1 has completed. + step(); + + // Ask for projectId again, from the same auth instance. We expect a cached instance + // this time. + auth.getDefaultProjectId(function (err2, _projectId2) { + assert.equal(null, err2); + + // Make sure we get the changed cached projectId back + assert.equal('monkey', _projectId2); + + // Now create a second GoogleAuth instance, and ask for projectId. We should + // get a new projectId instance this time. + var auth2 = new GoogleAuth(); + setUpAuthForEnvironmentVariable(auth2); + + // Step 2 has completed. + step(); + + auth2.getDefaultProjectId(function (err3, _projectId3) { + assert.equal(null, err3); + assert.equal(_projectId3, projectId); + + // Make sure we get a new (non-cached) projectId instance back. + assert.equal(_projectId3.specialTestBit, undefined); + + // Step 3 has completed. + step(); + }); + }); + }); + }); + + it('should use GCLOUD_PROJECT environment variable when it is set', function (done) { + var projectId = 'my-awesome-project'; + + var auth = new GoogleAuth(); + insertEnvironmentVariableIntoAuth(auth, 'GCLOUD_PROJECT', projectId); + + // Execute. + auth.getDefaultProjectId(function (err, _projectId) { + assert.equal(err, null); + assert.equal(_projectId, projectId); + done(); + }); + }); + + it('should use GOOGLE_CLOUD_PROJECT environment variable when it is set', function (done) { + var projectId = 'my-awesome-project'; + + var auth = new GoogleAuth(); + insertEnvironmentVariableIntoAuth(auth, 'GOOGLE_CLOUD_PROJECT', projectId); + + // Execute. + auth.getDefaultProjectId(function (err, _projectId) { + assert.equal(err, null); + assert.equal(_projectId, projectId); + done(); + }); + }); + + it('should use GOOGLE_APPLICATION_CREDENTIALS file when it is available', function (done) { + var projectId = 'my-awesome-project'; + + var auth = new GoogleAuth(); + insertEnvironmentVariableIntoAuth( + auth, + 'GOOGLE_APPLICATION_CREDENTIALS', + path.join(__dirname, 'fixtures/private2.json') + ); + + // Execute. + auth.getDefaultProjectId(function (err, _projectId) { + assert.equal(err, null); + assert.equal(_projectId, projectId); + done(); + }); + }); + + it('should use well-known file when it is available and env vars are not set', function (done) { + var projectId = 'my-awesome-project'; + + // 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. + var auth = new GoogleAuth(); + blockGoogleApplicationCredentialEnvironmentVariable(auth); + auth._getSDKDefaultProjectId = function(callback) { + callback(null, JSON.stringify({ + core: { + project: projectId + } + })); + }; + + // Execute. + auth.getDefaultProjectId(function (err, _projectId) { + assert.equal(err, null); + assert.equal(_projectId, projectId); + done(); + }); + }); + + it('should use GCE when well-known file and env var are not set', function (done) { + var projectId = 'my-awesome-project'; + var auth = new GoogleAuth(); + blockGoogleApplicationCredentialEnvironmentVariable(auth); + auth._getSDKDefaultProjectId = function(callback) { + callback(null, ''); + }; + auth.transporter = { + request: function(reqOpts, callback) { + callback(null, projectId, { body: projectId, statusCode: 200 }); + } + }; + + + // Execute. + auth.getDefaultProjectId(function (err, _projectId) { + assert.equal(err, null); + assert.equal(_projectId, projectId); + done(); + }); + }); + }); + describe('.getApplicationDefault', function () { it('should return a new credential the first time and a cached credential the second time',