Add support for detecting project ID. Fixes #97

This commit is contained in:
Jason Dobry
2016-10-01 00:09:30 -07:00
parent 02f587472e
commit 6921be61e3
6 changed files with 341 additions and 5 deletions

View File

@@ -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.

View File

@@ -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();
};

View File

@@ -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();
};

View File

@@ -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"

View File

@@ -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"
}

View File

@@ -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',