Merge pull request #590 from murgatroid99/node_auth_integration

Node auth implementation
pull/604/head
Tim Emiola 10 years ago
commit bbf11f77a2
  1. 33
      src/node/index.js
  2. 62
      src/node/interop/interop_client.js
  3. 10
      src/node/interop/messages.proto
  4. 3
      src/node/package.json
  5. 193
      src/node/src/client.js
  6. 21
      src/node/test/interop_sanity_test.js
  7. 7
      tools/dockerfile/grpc_node/Dockerfile
  8. 30
      tools/gce_setup/grpc_docker.sh
  9. 4
      tools/gce_setup/shared_startup_funcs.sh

@ -73,6 +73,37 @@ function load(filename) {
return loadObject(builder.ns); return loadObject(builder.ns);
} }
/**
* Get a function that a client can use to update metadata with authentication
* information from a Google Auth credential object, which comes from the
* googleauth library.
* @param {Object} credential The credential object to use
* @return {function(Object, callback)} Metadata updater function
*/
function getGoogleAuthDelegate(credential) {
/**
* Update a metadata object with authentication information.
* @param {Object} metadata Metadata object
* @param {function(Error, Object)} callback
*/
return function updateMetadata(metadata, callback) {
metadata = _.clone(metadata);
if (metadata.Authorization) {
metadata.Authorization = _.clone(metadata.Authorization);
} else {
metadata.Authorization = [];
}
credential.getAccessToken(function(err, token) {
if (err) {
callback(err);
return;
}
metadata.Authorization.push('Bearer ' + token);
callback(null, metadata);
});
};
}
/** /**
* See docs for loadObject * See docs for loadObject
*/ */
@ -106,3 +137,5 @@ exports.Credentials = grpc.Credentials;
* ServerCredentials factories * ServerCredentials factories
*/ */
exports.ServerCredentials = grpc.ServerCredentials; exports.ServerCredentials = grpc.ServerCredentials;
exports.getGoogleAuthDelegate = getGoogleAuthDelegate;

@ -35,9 +35,14 @@ var fs = require('fs');
var path = require('path'); var path = require('path');
var grpc = require('..'); var grpc = require('..');
var testProto = grpc.load(__dirname + '/test.proto').grpc.testing; var testProto = grpc.load(__dirname + '/test.proto').grpc.testing;
var GoogleAuth = require('googleauth');
var assert = require('assert'); var assert = require('assert');
var AUTH_SCOPE = 'https://www.googleapis.com/auth/xapi.zoo';
var AUTH_SCOPE_RESPONSE = 'xapi.zoo';
var AUTH_USER = '155450119199-3psnrh1sdr3d8cpj1v46naggf81mhdnk@developer.gserviceaccount.com';
/** /**
* Create a buffer filled with size zeroes * Create a buffer filled with size zeroes
* @param {number} size The length of the buffer * @param {number} size The length of the buffer
@ -255,6 +260,45 @@ function cancelAfterFirstResponse(client, done) {
}); });
} }
/**
* Run one of the authentication tests.
* @param {Client} client The client to test against
* @param {function} done Callback to call when the test is completed. Included
* primarily for use with mocha
*/
function authTest(client, done) {
(new GoogleAuth()).getApplicationDefault(function(err, credential) {
assert.ifError(err);
if (credential.createScopedRequired()) {
credential = credential.createScoped(AUTH_SCOPE);
}
client.updateMetadata = grpc.getGoogleAuthDelegate(credential);
var arg = {
response_type: testProto.PayloadType.COMPRESSABLE,
response_size: 314159,
payload: {
body: zeroBuffer(271828)
},
fill_username: true,
fill_oauth_scope: true
};
var call = client.unaryCall(arg, function(err, resp) {
assert.ifError(err);
assert.strictEqual(resp.payload.type, testProto.PayloadType.COMPRESSABLE);
assert.strictEqual(resp.payload.body.limit - resp.payload.body.offset,
314159);
assert.strictEqual(resp.username, AUTH_USER);
assert.strictEqual(resp.oauth_scope, AUTH_SCOPE_RESPONSE);
});
call.on('status', function(status) {
assert.strictEqual(status.code, grpc.status.OK);
if (done) {
done();
}
});
});
}
/** /**
* Map from test case names to test functions * Map from test case names to test functions
*/ */
@ -266,7 +310,9 @@ var test_cases = {
ping_pong: pingPong, ping_pong: pingPong,
empty_stream: emptyStream, empty_stream: emptyStream,
cancel_after_begin: cancelAfterBegin, cancel_after_begin: cancelAfterBegin,
cancel_after_first_response: cancelAfterFirstResponse cancel_after_first_response: cancelAfterFirstResponse,
compute_engine_creds: authTest,
service_account_creds: authTest
}; };
/** /**
@ -280,11 +326,16 @@ var test_cases = {
* @param {function} done Callback to call when the test is completed. Included * @param {function} done Callback to call when the test is completed. Included
* primarily for use with mocha * primarily for use with mocha
*/ */
function runTest(address, host_override, test_case, tls, done) { function runTest(address, host_override, test_case, tls, test_ca, done) {
// TODO(mlumish): enable TLS functionality // TODO(mlumish): enable TLS functionality
var options = {}; var options = {};
if (tls) { if (tls) {
var ca_path = path.join(__dirname, '../test/data/ca.pem'); var ca_path;
if (test_ca) {
ca_path = path.join(__dirname, '../test/data/ca.pem');
} else {
ca_path = process.env.SSL_CERT_FILE;
}
var ca_data = fs.readFileSync(ca_path); var ca_data = fs.readFileSync(ca_path);
var creds = grpc.Credentials.createSsl(ca_data); var creds = grpc.Credentials.createSsl(ca_data);
options.credentials = creds; options.credentials = creds;
@ -304,7 +355,10 @@ if (require.main === module) {
'use_tls', 'use_test_ca'] 'use_tls', 'use_test_ca']
}); });
runTest(argv.server_host + ':' + argv.server_port, argv.server_host_override, runTest(argv.server_host + ':' + argv.server_port, argv.server_host_override,
argv.test_case, argv.use_tls === 'true'); argv.test_case, argv.use_tls === 'true', argv.use_test_ca === 'true',
function () {
console.log('OK:', argv.test_case);
});
} }
/** /**

@ -66,6 +66,12 @@ message SimpleRequest {
// Optional input payload sent along with the request. // Optional input payload sent along with the request.
optional Payload payload = 3; optional Payload payload = 3;
// Whether SimpleResponse should include username.
optional bool fill_username = 4;
// Whether SimpleResponse should include OAuth scope.
optional bool fill_oauth_scope = 5;
} }
// Unary response, as configured by the request. // Unary response, as configured by the request.
@ -74,7 +80,9 @@ message SimpleResponse {
optional Payload payload = 1; optional Payload payload = 1;
// The user the request came from, for verifying authentication was // The user the request came from, for verifying authentication was
// successful when the client expected it. // successful when the client expected it.
optional int64 effective_gaia_user_id = 2; optional string username = 2;
// OAuth scope.
optional string oauth_scope = 3;
} }
// Client-streaming request. // Client-streaming request.

@ -14,7 +14,8 @@
}, },
"devDependencies": { "devDependencies": {
"mocha": "~1.21.0", "mocha": "~1.21.0",
"minimist": "^1.1.0" "minimist": "^1.1.0",
"googleauth": "google/google-auth-library-nodejs"
}, },
"main": "index.js" "main": "index.js"
} }

@ -224,25 +224,32 @@ function makeUnaryRequestFunction(method, serialize, deserialize) {
emitter.cancel = function cancel() { emitter.cancel = function cancel() {
call.cancel(); call.cancel();
}; };
var client_batch = {}; this.updateMetadata(metadata, function(error, metadata) {
client_batch[grpc.opType.SEND_INITIAL_METADATA] = metadata; if (error) {
client_batch[grpc.opType.SEND_MESSAGE] = serialize(argument); call.cancel();
client_batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true; callback(error);
client_batch[grpc.opType.RECV_INITIAL_METADATA] = true;
client_batch[grpc.opType.RECV_MESSAGE] = true;
client_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
call.startBatch(client_batch, function(err, response) {
if (err) {
callback(err);
return; return;
} }
if (response.status.code != grpc.status.OK) { var client_batch = {};
callback(response.status); client_batch[grpc.opType.SEND_INITIAL_METADATA] = metadata;
return; client_batch[grpc.opType.SEND_MESSAGE] = serialize(argument);
} client_batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true;
emitter.emit('status', response.status); client_batch[grpc.opType.RECV_INITIAL_METADATA] = true;
emitter.emit('metadata', response.metadata); client_batch[grpc.opType.RECV_MESSAGE] = true;
callback(null, deserialize(response.read)); client_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
call.startBatch(client_batch, function(err, response) {
if (err) {
callback(err);
return;
}
if (response.status.code != grpc.status.OK) {
callback(response.status);
return;
}
emitter.emit('status', response.status);
emitter.emit('metadata', response.metadata);
callback(null, deserialize(response.read));
});
}); });
return emitter; return emitter;
} }
@ -279,30 +286,37 @@ function makeClientStreamRequestFunction(method, serialize, deserialize) {
metadata = {}; metadata = {};
} }
var stream = new ClientWritableStream(call, serialize); var stream = new ClientWritableStream(call, serialize);
var metadata_batch = {}; this.updateMetadata(metadata, function(error, metadata) {
metadata_batch[grpc.opType.SEND_INITIAL_METADATA] = metadata; if (error) {
metadata_batch[grpc.opType.RECV_INITIAL_METADATA] = true; call.cancel();
call.startBatch(metadata_batch, function(err, response) { callback(error);
if (err) {
callback(err);
return;
}
stream.emit('metadata', response.metadata);
});
var client_batch = {};
client_batch[grpc.opType.RECV_MESSAGE] = true;
client_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
call.startBatch(client_batch, function(err, response) {
if (err) {
callback(err);
return;
}
if (response.status.code != grpc.status.OK) {
callback(response.status);
return; return;
} }
stream.emit('status', response.status); var metadata_batch = {};
callback(null, deserialize(response.read)); metadata_batch[grpc.opType.SEND_INITIAL_METADATA] = metadata;
metadata_batch[grpc.opType.RECV_INITIAL_METADATA] = true;
call.startBatch(metadata_batch, function(err, response) {
if (err) {
callback(err);
return;
}
stream.emit('metadata', response.metadata);
});
var client_batch = {};
client_batch[grpc.opType.RECV_MESSAGE] = true;
client_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
call.startBatch(client_batch, function(err, response) {
if (err) {
callback(err);
return;
}
if (response.status.code != grpc.status.OK) {
callback(response.status);
return;
}
stream.emit('status', response.status);
callback(null, deserialize(response.read));
});
}); });
return stream; return stream;
} }
@ -339,24 +353,31 @@ function makeServerStreamRequestFunction(method, serialize, deserialize) {
metadata = {}; metadata = {};
} }
var stream = new ClientReadableStream(call, deserialize); var stream = new ClientReadableStream(call, deserialize);
var start_batch = {}; this.updateMetadata(metadata, function(error, metadata) {
start_batch[grpc.opType.SEND_INITIAL_METADATA] = metadata; if (error) {
start_batch[grpc.opType.RECV_INITIAL_METADATA] = true; call.cancel();
start_batch[grpc.opType.SEND_MESSAGE] = serialize(argument); stream.emit('error', error);
start_batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true; return;
call.startBatch(start_batch, function(err, response) {
if (err) {
throw err;
}
stream.emit('metadata', response.metadata);
});
var status_batch = {};
status_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
call.startBatch(status_batch, function(err, response) {
if (err) {
throw err;
} }
stream.emit('status', response.status); var start_batch = {};
start_batch[grpc.opType.SEND_INITIAL_METADATA] = metadata;
start_batch[grpc.opType.RECV_INITIAL_METADATA] = true;
start_batch[grpc.opType.SEND_MESSAGE] = serialize(argument);
start_batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true;
call.startBatch(start_batch, function(err, response) {
if (err) {
throw err;
}
stream.emit('metadata', response.metadata);
});
var status_batch = {};
status_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
call.startBatch(status_batch, function(err, response) {
if (err) {
throw err;
}
stream.emit('status', response.status);
});
}); });
return stream; return stream;
} }
@ -391,22 +412,29 @@ function makeBidiStreamRequestFunction(method, serialize, deserialize) {
metadata = {}; metadata = {};
} }
var stream = new ClientDuplexStream(call, serialize, deserialize); var stream = new ClientDuplexStream(call, serialize, deserialize);
var start_batch = {}; this.updateMetadata(metadata, function(error, metadata) {
start_batch[grpc.opType.SEND_INITIAL_METADATA] = metadata; if (error) {
start_batch[grpc.opType.RECV_INITIAL_METADATA] = true; call.cancel();
call.startBatch(start_batch, function(err, response) { stream.emit('error', error);
if (err) { return;
throw err;
}
stream.emit('metadata', response.metadata);
});
var status_batch = {};
status_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
call.startBatch(status_batch, function(err, response) {
if (err) {
throw err;
} }
stream.emit('status', response.status); var start_batch = {};
start_batch[grpc.opType.SEND_INITIAL_METADATA] = metadata;
start_batch[grpc.opType.RECV_INITIAL_METADATA] = true;
call.startBatch(start_batch, function(err, response) {
if (err) {
throw err;
}
stream.emit('metadata', response.metadata);
});
var status_batch = {};
status_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
call.startBatch(status_batch, function(err, response) {
if (err) {
throw err;
}
stream.emit('status', response.status);
});
}); });
return stream; return stream;
} }
@ -438,8 +466,17 @@ function makeClientConstructor(service) {
* @constructor * @constructor
* @param {string} address The address of the server to connect to * @param {string} address The address of the server to connect to
* @param {Object} options Options to pass to the underlying channel * @param {Object} options Options to pass to the underlying channel
* @param {function(Object, function)=} updateMetadata function to update the
* metadata for each request
*/ */
function Client(address, options) { function Client(address, options, updateMetadata) {
if (updateMetadata) {
this.updateMetadata = updateMetadata;
} else {
this.updateMetadata = function(metadata, callback) {
callback(null, metadata);
};
}
this.channel = new grpc.Channel(address, options); this.channel = new grpc.Channel(address, options);
} }
@ -458,11 +495,13 @@ function makeClientConstructor(service) {
method_type = 'unary'; method_type = 'unary';
} }
} }
Client.prototype[decapitalize(method.name)] = var serialize = common.serializeCls(method.resolvedRequestType.build());
requester_makers[method_type]( var deserialize = common.deserializeCls(
prefix + capitalize(method.name), method.resolvedResponseType.build());
common.serializeCls(method.resolvedRequestType.build()), Client.prototype[decapitalize(method.name)] = requester_makers[method_type](
common.deserializeCls(method.resolvedResponseType.build())); prefix + capitalize(method.name), serialize, deserialize);
Client.prototype[decapitalize(method.name)].serialize = serialize;
Client.prototype[decapitalize(method.name)].deserialize = deserialize;
}); });
Client.service = service; Client.service = service;

@ -53,30 +53,35 @@ describe('Interop tests', function() {
}); });
// This depends on not using a binary stream // This depends on not using a binary stream
it('should pass empty_unary', function(done) { it('should pass empty_unary', function(done) {
interop_client.runTest(port, name_override, 'empty_unary', true, done); interop_client.runTest(port, name_override, 'empty_unary', true, true,
done);
}); });
// This fails due to an unknown bug // This fails due to an unknown bug
it('should pass large_unary', function(done) { it('should pass large_unary', function(done) {
interop_client.runTest(port, name_override, 'large_unary', true, done); interop_client.runTest(port, name_override, 'large_unary', true, true,
done);
}); });
it('should pass client_streaming', function(done) { it('should pass client_streaming', function(done) {
interop_client.runTest(port, name_override, 'client_streaming', true, done); interop_client.runTest(port, name_override, 'client_streaming', true, true,
done);
}); });
it('should pass server_streaming', function(done) { it('should pass server_streaming', function(done) {
interop_client.runTest(port, name_override, 'server_streaming', true, done); interop_client.runTest(port, name_override, 'server_streaming', true, true,
done);
}); });
it('should pass ping_pong', function(done) { it('should pass ping_pong', function(done) {
interop_client.runTest(port, name_override, 'ping_pong', true, done); interop_client.runTest(port, name_override, 'ping_pong', true, true, done);
}); });
it('should pass empty_stream', function(done) { it('should pass empty_stream', function(done) {
interop_client.runTest(port, name_override, 'empty_stream', true, done); interop_client.runTest(port, name_override, 'empty_stream', true, true,
done);
}); });
it('should pass cancel_after_begin', function(done) { it('should pass cancel_after_begin', function(done) {
interop_client.runTest(port, name_override, 'cancel_after_begin', true, interop_client.runTest(port, name_override, 'cancel_after_begin', true,
done); true, done);
}); });
it('should pass cancel_after_first_response', function(done) { it('should pass cancel_after_first_response', function(done) {
interop_client.runTest(port, name_override, 'cancel_after_first_response', interop_client.runTest(port, name_override, 'cancel_after_first_response',
true, done); true, true, done);
}); });
}); });

@ -11,4 +11,11 @@ RUN make install_c -C /var/local/git/grpc
RUN cd /var/local/git/grpc/src/node && npm install && node-gyp rebuild RUN cd /var/local/git/grpc/src/node && npm install && node-gyp rebuild
# Add a cacerts directory containing the Google root pem file, allowing the
# ruby client to access the production test instance
ADD cacerts cacerts
# Add a service_account directory containing the auth creds file
ADD service_account service_account
CMD ["/usr/bin/nodejs", "/var/local/git/grpc/src/node/interop/interop_server.js", "--use_tls=true", "--port=8040"] CMD ["/usr/bin/nodejs", "/var/local/git/grpc/src/node/interop/interop_server.js", "--use_tls=true", "--port=8040"]

@ -1044,11 +1044,39 @@ grpc_interop_gen_php_cmd() {
# cmd=$($grpc_gen_test_cmd $flags) # cmd=$($grpc_gen_test_cmd $flags)
grpc_interop_gen_node_cmd() { grpc_interop_gen_node_cmd() {
local cmd_prefix="sudo docker run grpc/node"; local cmd_prefix="sudo docker run grpc/node";
local test_script="/usr/bin/nodejs /var/local/git/grpc/src/node/interop/interop_client.js --use_tls=true"; local test_script="/usr/bin/nodejs /var/local/git/grpc/src/node/interop/interop_client.js --use_tls=true --use_test_ca=true";
local the_cmd="$cmd_prefix $test_script $@"; local the_cmd="$cmd_prefix $test_script $@";
echo $the_cmd echo $the_cmd
} }
# constructs the full dockerized node gce=>prod interop test cmd.
#
# call-seq:
# flags= .... # generic flags to include the command
# cmd=$($grpc_gen_test_cmd $flags)
grpc_cloud_prod_gen_node_cmd() {
local cmd_prefix="sudo docker run grpc/node";
local test_script="/usr/bin/nodejs /var/local/git/grpc/src/node/interop/interop_client.js --use_tls=true";
local gfe_flags=$(_grpc_prod_gfe_flags);
local the_cmd="$cmd_prefix $test_script $gfe_flags $@";
echo $the_cmd
}
# constructs the full dockerized node service_account auth interop test cmd.
#
# call-seq:
# flags= .... # generic flags to include the command
# cmd=$($grpc_gen_test_cmd $flags)
grpc_cloud_prod_auth_service_account_creds_gen_node_cmd() {
local cmd_prefix="sudo docker run grpc/node";
local test_script="/usr/bin/nodejs /var/local/git/grpc/src/node/interop/interop_client.js --use_tls=true";
local gfe_flags=$(_grpc_prod_gfe_flags);
local env_prefix="SSL_CERT_FILE=/cacerts/roots.pem"
env_prefix+=" GOOGLE_APPLICATION_CREDENTIALS=/service_account/stubbyCloudTestingTest-7dd63462c60c.json"
local the_cmd="$env_prefix $cmd_prefix $test_script $gfe_flags $@";
echo $the_cmd
}
# constructs the full dockerized cpp interop test cmd. # constructs the full dockerized cpp interop test cmd.
# #
# call-seq: # call-seq:

@ -416,6 +416,10 @@ grpc_dockerfile_install() {
grpc_docker_sync_roots_pem $dockerfile_dir/cacerts || return 1; grpc_docker_sync_roots_pem $dockerfile_dir/cacerts || return 1;
grpc_docker_sync_service_account $dockerfile_dir/service_account || return 1; grpc_docker_sync_service_account $dockerfile_dir/service_account || return 1;
} }
[[ $image_label == "grpc/node" ]] && {
grpc_docker_sync_roots_pem $dockerfile_dir/cacerts || return 1;
grpc_docker_sync_service_account $dockerfile_dir/service_account || return 1;
}
[[ $image_label == "grpc/cxx" ]] && { [[ $image_label == "grpc/cxx" ]] && {
grpc_docker_sync_roots_pem $dockerfile_dir/cacerts || return 1; grpc_docker_sync_roots_pem $dockerfile_dir/cacerts || return 1;
grpc_docker_sync_service_account $dockerfile_dir/service_account || return 1; grpc_docker_sync_service_account $dockerfile_dir/service_account || return 1;

Loading…
Cancel
Save