diff --git a/ext/csasl/csasl.c b/ext/csasl/csasl.c new file mode 100644 index 0000000000..50564ca48e --- /dev/null +++ b/ext/csasl/csasl.c @@ -0,0 +1,167 @@ +// Copyright (C) 2014 MongoDB, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +static void mongo_sasl_conn_free(void* data) { + sasl_conn_t *conn = (sasl_conn_t*) data; + // Ideally we would use sasl_client_done() but that's only available as of cyrus sasl 2.1.25 + if(conn) sasl_done(); +} + +static sasl_conn_t* mongo_sasl_context(VALUE self) { + sasl_conn_t* conn; + VALUE context; + context = rb_iv_get(self, "@context"); + Data_Get_Struct(context, sasl_conn_t, conn); + return conn; +} + +static VALUE a_init(VALUE self, VALUE user_name, VALUE host_name, VALUE service_name, VALUE canonicalize_host_name) +{ + if (sasl_client_init(NULL) == SASL_OK) { + rb_iv_set(self, "@valid", Qtrue); + rb_iv_set(self, "@user_name", user_name); + rb_iv_set(self, "@host_name", host_name); + rb_iv_set(self, "@service_name", service_name); + rb_iv_set(self, "@canonicalize_host_name", canonicalize_host_name); + } + + else { + rb_iv_set(self, "@valid", Qfalse); + } + + return self; +} + +static VALUE valid(VALUE self) { + return rb_iv_get(self, "@valid"); +} + +int is_sasl_failure(int result) +{ + if (result < 0) { + return 1; + } + + return 0; +} + +static int sasl_interact(VALUE self, int id, const char **result, unsigned *len) { + switch (id) { + case SASL_CB_AUTHNAME: + case SASL_CB_USER: + { + VALUE user_name; + user_name = rb_iv_get(self, "@user_name"); + *result = RSTRING_PTR(user_name); + if (len) { + *len = RSTRING_LEN(user_name); + } + return SASL_OK; + } + } + + return SASL_FAIL; +} + +static VALUE initialize_challenge(VALUE self) { + int result; + char encoded_payload[4096]; + const char *raw_payload; + unsigned int raw_payload_len, encoded_payload_len; + const char *mechanism_list = "GSSAPI"; + const char *mechanism_selected = "GSSAPI"; + VALUE context; + sasl_conn_t *conn; + sasl_callback_t client_interact [] = { + { SASL_CB_AUTHNAME, (int (*)(void))sasl_interact, (void*)self }, + { SASL_CB_USER, (int (*)(void))sasl_interact, (void*)self }, + { SASL_CB_LIST_END, NULL, NULL } + }; + + const char *servicename = RSTRING_PTR(rb_iv_get(self, "@service_name")); + const char *hostname = RSTRING_PTR(rb_iv_get(self, "@host_name")); + + result = sasl_client_new(servicename, hostname, NULL, NULL, client_interact, 0, &conn); + + if (result != SASL_OK) { + sasl_dispose(&conn); + return Qfalse; + } + + context = Data_Wrap_Struct(rb_cObject, NULL, mongo_sasl_conn_free, conn); + rb_iv_set(self, "@context", context); + + result = sasl_client_start(conn, mechanism_list, NULL, &raw_payload, &raw_payload_len, &mechanism_selected); + if (is_sasl_failure(result)) { + return Qfalse; + } + + if (result != SASL_CONTINUE) { + return Qfalse; + } + + result = sasl_encode64(raw_payload, raw_payload_len, encoded_payload, sizeof(encoded_payload), &encoded_payload_len); + if (is_sasl_failure(result)) { + return Qfalse; + } + + encoded_payload[encoded_payload_len] = 0; + return rb_str_new(encoded_payload, encoded_payload_len); +} + +static VALUE evaluate_challenge(VALUE self, VALUE rb_payload) { + char base_payload[4096], payload[4096]; + const char *step_payload, *out; + unsigned int step_payload_len, payload_len, base_payload_len, outlen; + int result; + sasl_conn_t *conn = mongo_sasl_context(self); + + StringValue(rb_payload); + step_payload = RSTRING_PTR(rb_payload); + step_payload_len = RSTRING_LEN(rb_payload); + + result = sasl_decode64(step_payload, step_payload_len, base_payload, sizeof(base_payload), &base_payload_len); + if (is_sasl_failure(result)) { + return Qfalse; + } + + result = sasl_client_step(conn, base_payload, base_payload_len, NULL, &out, &outlen); + if (is_sasl_failure(result)) { + return Qfalse; + } + + result = sasl_encode64(out, outlen, payload, sizeof(payload), &payload_len); + if (is_sasl_failure(result)) { + return Qfalse; + } + + return rb_str_new(payload, payload_len); +} + +VALUE c_GSSAPI_authenticator; + +void Init_csasl() { + VALUE mongo, sasl; + mongo = rb_const_get(rb_cObject, rb_intern("Mongo")); + sasl = rb_const_get(mongo, rb_intern("Sasl")); + c_GSSAPI_authenticator = rb_define_class_under(sasl, "GSSAPIAuthenticator", rb_cObject); + rb_define_method(c_GSSAPI_authenticator, "initialize", a_init, 4); + rb_define_method(c_GSSAPI_authenticator, "initialize_challenge", initialize_challenge, 0); + rb_define_method(c_GSSAPI_authenticator, "evaluate_challenge", evaluate_challenge, 1); + rb_define_method(rb_cObject, "valid?", valid, 0); +} diff --git a/ext/csasl/extconf.rb b/ext/csasl/extconf.rb new file mode 100644 index 0000000000..a28355760e --- /dev/null +++ b/ext/csasl/extconf.rb @@ -0,0 +1,5 @@ +require 'mkmf' +find_header('sasl/sasl.h') +have_library('sasl2', 'sasl_version') + +create_makefile('csasl/csasl') diff --git a/lib/mongo/functional.rb b/lib/mongo/functional.rb index 9949555139..54908a2e16 100644 --- a/lib/mongo/functional.rb +++ b/lib/mongo/functional.rb @@ -18,4 +18,14 @@ require 'mongo/functional/write_concern' require 'mongo/functional/uri_parser' -require 'mongo/functional/sasl_java' if RUBY_PLATFORM =~ /java/ +begin + if RUBY_PLATFORM =~ /java/ + require 'mongo/functional/sasl_java' + else + require 'mongo/functional/sasl_c' + require "csasl/csasl" + end + Mongo::HAS_SASL = true +rescue LoadError + Mongo::HAS_SASL = false +end \ No newline at end of file diff --git a/lib/mongo/functional/authentication.rb b/lib/mongo/functional/authentication.rb index faaa15dfb5..c8d19b8b9e 100644 --- a/lib/mongo/functional/authentication.rb +++ b/lib/mongo/functional/authentication.rb @@ -284,9 +284,6 @@ def issue_plain(auth, opts={}) # # @private def issue_gssapi(auth, opts={}) - raise NotImplementedError, - "The #{auth[:mechanism]} authentication mechanism is only supported " + - "for JRuby." unless RUBY_PLATFORM =~ /java/ Mongo::Sasl::GSSAPI.authenticate(auth[:username], self, opts[:socket], auth[:extra] || {}) end diff --git a/lib/mongo/functional/sasl_c.rb b/lib/mongo/functional/sasl_c.rb new file mode 100644 index 0000000000..b22aad471f --- /dev/null +++ b/lib/mongo/functional/sasl_c.rb @@ -0,0 +1,47 @@ +# Copyright (C) 2009-2014 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + module Sasl + + module GSSAPI + + def self.authenticate(username, client, socket, opts={}) + raise LoadError, + "The Sasl GSSAPI authentication mechanism cannot be used because " + + "its extension did not load properly" unless Mongo::HAS_SASL + + db = client.db('$external') + hostname = socket.pool.host + servicename = opts[:gssapi_service_name] || 'mongodb' + canonicalize = opts[:canonicalize_host_name] ? opts[:canonicalize_host_name] : false + authenticator = Mongo::Sasl::GSSAPIAuthenticator.new(username, hostname, servicename, canonicalize) + + return { } unless authenticator.valid? + + token = authenticator.initialize_challenge + cmd = BSON::OrderedHash['saslStart', 1, 'mechanism', 'GSSAPI', 'payload', token, 'autoAuthorize', 1] + response = db.command(cmd, :check_response => false, :socket => socket) + + until response['done'] do + break unless Support.ok?(response) + token = authenticator.evaluate_challenge(response['payload']) + cmd = BSON::OrderedHash['saslContinue', 1, 'conversationId', response['conversationId'], 'payload', token] + response = db.command(cmd, :check_response => false, :socket => socket) + end + response + end + end + end +end diff --git a/lib/mongo/functional/sasl_java.rb b/lib/mongo/functional/sasl_java.rb index a54144a9f0..0bcc8b703f 100644 --- a/lib/mongo/functional/sasl_java.rb +++ b/lib/mongo/functional/sasl_java.rb @@ -1,4 +1,4 @@ -# Copyright (C) 2009-2013 MongoDB, Inc. +# Copyright (C) 2009-2014 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,22 +25,30 @@ module Sasl module GSSAPI def self.authenticate(username, client, socket, opts={}) + raise LoadError, + "The Sasl GSSAPI authentication mechanism cannot be used because " + + "its extension did not load properly" unless Mongo::HAS_SASL db = client.db('$external') hostname = socket.pool.host servicename = opts[:gssapi_service_name] || 'mongodb' canonicalize = opts[:canonicalize_host_name] ? opts[:canonicalize_host_name] : false - authenticator = org.mongodb.sasl.GSSAPIAuthenticator.new(JRuby.runtime, username, hostname, servicename, canonicalize) - token = BSON::Binary.new(authenticator.initialize_challenge) - cmd = BSON::OrderedHash['saslStart', 1, 'mechanism', 'GSSAPI', 'payload', token, 'autoAuthorize', 1] - response = db.command(cmd, :check_response => false, :socket => socket) - - until response['done'] do - token = BSON::Binary.new(authenticator.evaluate_challenge(response['payload'].to_s)) - cmd = BSON::OrderedHash['saslContinue', 1, 'conversationId', response['conversationId'], 'payload', token] - response = db.command(cmd, :check_response => false, :socket => socket) + begin + authenticator = org.mongodb.sasl.GSSAPIAuthenticator.new(JRuby.runtime, username, hostname, servicename, canonicalize) + token = BSON::Binary.new(authenticator.initialize_challenge) + cmd = BSON::OrderedHash['saslStart', 1, 'mechanism', 'GSSAPI', 'payload', token, 'autoAuthorize', 1] + response = db.command(cmd, :check_response => false, :socket => socket) + + until response['done'] do + break unless Support.ok?(response) + token = BSON::Binary.new(authenticator.evaluate_challenge(response['payload'].to_s)) + cmd = BSON::OrderedHash['saslContinue', 1, 'conversationId', response['conversationId'], 'payload', token] + response = db.command(cmd, :check_response => false, :socket => socket) + end + response + rescue Java::OrgMongodbSasl::MongoSecurityException + return { } end - response end end diff --git a/mongo.gemspec b/mongo.gemspec index 4effc8a7c1..59c4d1354c 100644 --- a/mongo.gemspec +++ b/mongo.gemspec @@ -25,6 +25,9 @@ Gem::Specification.new do |s| if RUBY_PLATFORM =~ /java/ s.platform = 'java' s.files << 'ext/jsasl/target/jsasl.jar' + else + s.files += Dir.glob('ext/**/*.{c,h,rb}') + s.extensions = ['ext/csasl/extconf.rb'] end s.test_files = Dir['test/**/*.rb'] - Dir['test/bson/*'] diff --git a/tasks/compile.rake b/tasks/compile.rake index 5f562aed99..29ab4c78c7 100644 --- a/tasks/compile.rake +++ b/tasks/compile.rake @@ -31,7 +31,12 @@ else ext.lib_dir = "lib/bson_ext" Rake::Task['clean'].invoke end + Rake::ExtensionTask.new('csasl') do |ext| + ext.name = "csasl" + ext.ext_dir = "ext/csasl" + ext.lib_dir = "lib/csasl" + end end desc "Run the default compile task" -task :compile => RUBY_PLATFORM =~ /java/ ? ['compile:jbson', 'compile:jsasl'] : 'compile:cbson' \ No newline at end of file +task :compile => RUBY_PLATFORM =~ /java/ ? ['compile:jbson', 'compile:jsasl'] : ['compile:cbson', 'compile:csasl'] \ No newline at end of file diff --git a/tasks/testing.rake b/tasks/testing.rake index e6428a3d78..5187ee6697 100644 --- a/tasks/testing.rake +++ b/tasks/testing.rake @@ -67,6 +67,12 @@ namespace :test do end task :commit => :default + # Both the functional and replica_set tests will use the kerberos C ext + # when testing GSSAPI. So we must compile when on MRI. + task :default => 'compile:csasl' unless RUBY_PLATFORM =~ /java/ + task :functional => 'compile:csasl' unless RUBY_PLATFORM =~ /java/ + task :replica_set => 'compile:csasl' unless RUBY_PLATFORM =~ /java/ + desc 'Outputs diagnostic information for troubleshooting test failures.' task :diagnostic do puts <<-MSG @@ -107,4 +113,15 @@ namespace :test do t.libs << 'test' end end + + task :cleanup do |t| + %w(data tmp coverage lib/bson_ext lib/csasl).each do |dir| + if File.directory?(dir) + puts "[CLEAN-UP] Removing '#{dir}'..." + FileUtils.rm_rf(dir) + end + end + t.reenable + end + Rake.application.top_level_tasks << 'test:cleanup' end diff --git a/test/shared/authentication/gssapi_shared.rb b/test/shared/authentication/gssapi_shared.rb index 7d0f5b82a2..7400c7f0b2 100644 --- a/test/shared/authentication/gssapi_shared.rb +++ b/test/shared/authentication/gssapi_shared.rb @@ -28,7 +28,8 @@ module GSSAPITests # export MONGODB_GSSAPI_REALM='applicationuser@example.com' # export MONGODB_GSSAPI_KDC='SERVER.DOMAIN.COM' # - # You must either use kinit or provide a config file that references a keytab file: + # You must use kinit when on MRI. + # You have the option of providing a config file that references a keytab file on JRuby: # # export JAAS_LOGIN_CONFIG_FILE='file:///path/to/config/file' # @@ -37,10 +38,12 @@ module GSSAPITests MONGODB_GSSAPI_REALM = ENV['MONGODB_GSSAPI_REALM'] MONGODB_GSSAPI_KDC = ENV['MONGODB_GSSAPI_KDC'] MONGODB_GSSAPI_PORT = ENV['MONGODB_GSSAPI_PORT'] || '27017' - JAAS_LOGIN_CONFIG_FILE = ENV['JAAS_LOGIN_CONFIG_FILE'] + MONGODB_GSSAPI_DB = ENV['MONGODB_GSSAPI_DB'] + JAAS_LOGIN_CONFIG_FILE = ENV['JAAS_LOGIN_CONFIG_FILE'] # only JRuby if ENV.key?('MONGODB_GSSAPI_HOST') && ENV.key?('MONGODB_GSSAPI_USER') && - ENV.key?('MONGODB_GSSAPI_REALM') && ENV.key?('MONGODB_GSSAPI_KDC') && RUBY_PLATFORM =~ /java/ + ENV.key?('MONGODB_GSSAPI_REALM') && ENV.key?('MONGODB_GSSAPI_KDC') && + ENV.key?('MONGODB_GSSAPI_DB') def test_gssapi_authenticate client = Mongo::MongoClient.new(MONGODB_GSSAPI_HOST, MONGODB_GSSAPI_PORT) if client['admin'].command(:isMaster => 1)['setName'] @@ -48,7 +51,7 @@ def test_gssapi_authenticate end set_system_properties - db = client['kerberos'] + db = client[MONGODB_GSSAPI_DB] db.authenticate(MONGODB_GSSAPI_USER, nil, nil, nil, 'GSSAPI') assert db.command(:dbstats => 1) @@ -68,7 +71,7 @@ def test_gssapi_authenticate_uri uri = "mongodb://#{username}@#{ENV['MONGODB_GSSAPI_HOST']}:#{ENV['MONGODB_GSSAPI_PORT']}/?" + "authMechanism=GSSAPI" client = @client.class.from_uri(uri) - assert client['kerberos'].command(:dbstats => 1) + assert client[MONGODB_GSSAPI_DB].command(:dbstats => 1) end def test_wrong_service_name_fails @@ -79,8 +82,8 @@ def test_wrong_service_name_fails end set_system_properties - assert_raise_error Java::OrgMongodbSasl::MongoSecurityException do - client['kerberos'].authenticate(MONGODB_GSSAPI_USER, nil, nil, nil, 'GSSAPI', extra_opts) + assert_raise_error Mongo::AuthenticationError do + client[MONGODB_GSSAPI_DB].authenticate(MONGODB_GSSAPI_USER, nil, nil, nil, 'GSSAPI', extra_opts) end end @@ -92,8 +95,8 @@ def test_wrong_service_name_fails_uri uri = "mongodb://#{username}@#{ENV['MONGODB_GSSAPI_HOST']}:#{ENV['MONGODB_GSSAPI_PORT']}/?" + "authMechanism=GSSAPI&gssapiServiceName=example" client = @client.class.from_uri(uri) - assert_raise_error Java::OrgMongodbSasl::MongoSecurityException do - client['kerberos'].command(:dbstats => 1) + assert_raise_error Mongo::AuthenticationError do + client[MONGODB_GSSAPI_DB].command(:dbstats => 1) end end @@ -103,10 +106,11 @@ def test_extra_opts set_system_properties Mongo::Sasl::GSSAPI.expects(:authenticate).with do |username, client, socket, opts| - opts[:gssapi_service_name] == extra_opts[:gssapi_service_name] - opts[:canonicalize_host_name] == extra_opts[:canonicalize_host_name] + assert_equal opts[:gssapi_service_name], extra_opts[:gssapi_service_name] + assert_equal opts[:canonicalize_host_name], extra_opts[:canonicalize_host_name] + [ username, client, socket, opts ] end.returns('ok' => true ) - client['kerberos'].authenticate(MONGODB_GSSAPI_USER, nil, nil, nil, 'GSSAPI', extra_opts) + client[MONGODB_GSSAPI_DB].authenticate(MONGODB_GSSAPI_USER, nil, nil, nil, 'GSSAPI', extra_opts) end def test_extra_opts_uri @@ -114,8 +118,9 @@ def test_extra_opts_uri set_system_properties Mongo::Sasl::GSSAPI.expects(:authenticate).with do |username, client, socket, opts| - opts[:gssapi_service_name] == extra_opts[:gssapi_service_name] - opts[:canonicalize_host_name] == extra_opts[:canonicalize_host_name] + assert_equal opts[:gssapi_service_name], extra_opts[:gssapi_service_name] + assert_equal opts[:canonicalize_host_name], extra_opts[:canonicalize_host_name] + [ username, client, socket, opts ] end.returns('ok' => true) require 'cgi' @@ -124,7 +129,7 @@ def test_extra_opts_uri "authMechanism=GSSAPI&gssapiServiceName=example&canonicalizeHostName=true" client = @client.class.from_uri(uri) client.expects(:receive_message).returns([[{ 'ok' => 1 }], 1, 1]) - client['kerberos'].command(:dbstats => 1) + client[MONGODB_GSSAPI_DB].command(:dbstats => 1) end # In order to run this test, you must set the following environment variable: @@ -137,7 +142,7 @@ def test_canonicalize_host_name set_system_properties client = Mongo::MongoClient.new(ENV['MONGODB_GSSAPI_HOST_IP'], MONGODB_GSSAPI_PORT) - db = client['kerberos'] + db = client[MONGODB_GSSAPI_DB] db.authenticate(MONGODB_GSSAPI_USER, nil, nil, nil, 'GSSAPI', extra_opts) assert db.command(:dbstats => 1) end @@ -148,16 +153,18 @@ def test_invalid_extra_options client = Mongo::MongoClient.new(MONGODB_GSSAPI_HOST) assert_raise Mongo::MongoArgumentError do - client['kerberos'].authenticate(MONGODB_GSSAPI_USER, nil, nil, nil, 'GSSAPI', extra_opts) + client[MONGODB_GSSAPI_DB].authenticate(MONGODB_GSSAPI_USER, nil, nil, nil, 'GSSAPI', extra_opts) end end private def set_system_properties - java.lang.System.set_property 'javax.security.auth.useSubjectCredsOnly', 'false' - java.lang.System.set_property "java.security.krb5.realm", MONGODB_GSSAPI_REALM - java.lang.System.set_property "java.security.krb5.kdc", MONGODB_GSSAPI_KDC - java.lang.System.set_property "java.security.auth.login.config", JAAS_LOGIN_CONFIG_FILE if JAAS_LOGIN_CONFIG_FILE + if RUBY_PLATFORM =~ /java/ + java.lang.System.set_property 'javax.security.auth.useSubjectCredsOnly', 'false' + java.lang.System.set_property "java.security.krb5.realm", MONGODB_GSSAPI_REALM + java.lang.System.set_property "java.security.krb5.kdc", MONGODB_GSSAPI_KDC + java.lang.System.set_property "java.security.auth.login.config", JAAS_LOGIN_CONFIG_FILE if JAAS_LOGIN_CONFIG_FILE + end end end