Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0577968
Basic structure for Sasl GSSAPI c ext
estolfo Jul 24, 2014
31bd049
csasl C code
estolfo Jul 24, 2014
1db171a
fix typo
estolfo Jul 25, 2014
af23133
c code fixes
estolfo Jul 25, 2014
71c97b2
Put csasl bundle in its own directory, then clean it up
estolfo Jul 25, 2014
c86d810
Compile csasl extension if not on jruby
estolfo Jul 29, 2014
d2167aa
RUBY-530 Kerberos support for MRI c ext and Ruby code
estolfo Jul 29, 2014
ab9b6ec
RUBY-530 GSSAPI tests should test both MRI and JRuby
estolfo Aug 4, 2014
1f4d91d
RUBY-530 Fix spacing and clean up C code
estolfo Aug 4, 2014
0e67ff8
RUBY-530 Check to make sure the GSSAPIAuthentication initialized prop…
estolfo Aug 4, 2014
7a59f76
RUBY-530 make db to authenticate to an ENV variable
estolfo Aug 4, 2014
10d647d
RUBY-530 Make java ext and c ext throw the same auth error from GSSAP…
estolfo Aug 4, 2014
ce2c5d6
RUBY-530 Compile csasl c extension into its own directory
estolfo Aug 5, 2014
ec40832
Use HAS_SASL constant
estolfo Aug 6, 2014
13bcbaa
Remove uneeded platform definition
estolfo Aug 6, 2014
247f927
Cleanup after code review
estolfo Aug 6, 2014
4486548
RUBY-530 Use sasl_done and change copyright
estolfo Aug 7, 2014
8296363
RUBY-530 Adding rake cleanup task back in
estolfo Sep 3, 2014
02ec836
RUBY-530 Require gssapi test db in env for test
estolfo Sep 3, 2014
90c027e
RUBY-530 Note that we'd ideally use sasl_client_done for future refer…
estolfo Sep 3, 2014
ac72aba
RUBY-530 Use lib/csasl directory
estolfo Sep 5, 2014
6c83ce8
RUBY-530 Compile csasl for certain test suites
estolfo Sep 5, 2014
058ff27
RUBY-530 Test args to gssapi authenticate correctly
estolfo Sep 5, 2014
9bf6131
RUBY-530 Fix compile warnings on C90
estolfo Sep 5, 2014
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions ext/csasl/csasl.c
Original file line number Diff line number Diff line change
@@ -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 <ruby.h>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file is missing a license header

#include <sasl/sasl.h>
#include <sasl/saslutil.h>

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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this normal in ruby to do these "if not ok - otherwise"?

My brain strongly prefers if (true) {} else {/* failure branch_/ } over if (not true) { /_ failure branch */ } else { }

So I personally would have done:
if (sasl_client..() == SASL_OK) {
rb_iv_set(..... true);
/* and all the other variables /
} else {
/
set it to false */
}

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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one (canonicalize_host_name) seems unused?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, good eye = )
The java extension allows you to pass in canonicalize_host_name, so I set it here although it is not used so that the API is consistent between the two extensions. I figured it would be better to make it available to the c ext and perhaps make use of it later on, if I need to.

}

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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

result isn't checked for error...

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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little unsure how ruby land works... but a_init is only allowed to be called once per process (sasl_client_init()).
Also, there is no cleanup for the sasl routine here either.. sasl_done() should be called during shutdown (if sasl_client_init() was called) to cleanup the cyrus sasl state

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, ok. I've been able to create multiple authenticator objects (and thus call (a_init) / (sasl_client_init()) more than once per process when testing. Maybe it has only worked because the sasl context is identical between tests ? In other words, I'm always authenticating to the same instance with the same credentials.

I need to find out about a cleanup hook for a Ruby C extension. I'd imagine there's a function that is called when garbage collecting the Ruby object from which I can call sasl_done()... thanks for pointing that out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the sasl state is cleaned up. The context is wrapped in a struct here
https://github.com/mongodb/mongo-ruby-driver/pull/459/files#diff-2480dc4faaa354322f078919b8966823R87

and a function, mongo_sasl_conn_free is called when it is garbage collected:
https://github.com/mongodb/mongo-ruby-driver/pull/459/files#diff-2480dc4faaa354322f078919b8966823R5

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the implementation: http://git.cyrusimap.org/cyrus-sasl/tree/lib/client.c#n271
I guess its ok to call _init() multiple times, as long as _done() is called as often.

This may however be an implementation detail that can change in the future and not to be relied upon... unsure... The manpage doesn't explicitly cover this.. The norm for libs that need to init/cleanedup is to only call these once :]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense. I'm going to put sasl_done() in the cleanup function (mongo_sasl_conn_free) since it will be called when the Ruby object is gc-ed. This seems to make sense since it would parallel the initialization of the Ruby object, which calls sasl_client_init()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, sasl_done() cleans up the conn even though it's not passed in, right?
sasl_dispose takes the conn as an arg, so maybe I have to do both.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have to be careful with sasl_dispose(). Today we do not use SASL to negotiate a crypto connection so you can call it right after authenticating or to cleanup a failed authentication.
However, if we ever actually use SASL for its full capabilities (e.g. negotiating a real security transport layer, not only authentication) then you'll have to be careful of only sasl_dispose() the connection once you close the connection or have no longer need for it.

Note that sasl_client_done() (the recommended way) was introduced in 2.1.25, while sasl_done() has been available forever.

See also: https://cyrusimap.org/docs/cyrus-sasl/2.1.25/programming.php
Its a very nice resource

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, that is a good resource. They say sasl_client_done() is recommended but don't say why.
I guess for now, since we are only using SASL for authentication, it's ok to use sasl_client_done()

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);
}
5 changes: 5 additions & 0 deletions ext/csasl/extconf.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
require 'mkmf'
find_header('sasl/sasl.h')
have_library('sasl2', 'sasl_version')

create_makefile('csasl/csasl')
12 changes: 11 additions & 1 deletion lib/mongo/functional.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 0 additions & 3 deletions lib/mongo/functional/authentication.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
47 changes: 47 additions & 0 deletions lib/mongo/functional/sasl_c.rb
Original file line number Diff line number Diff line change
@@ -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
30 changes: 19 additions & 11 deletions lib/mongo/functional/sasl_java.rb
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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

Expand Down
3 changes: 3 additions & 0 deletions mongo.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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/*']
Expand Down
7 changes: 6 additions & 1 deletion tasks/compile.rake
Original file line number Diff line number Diff line change
Expand Up @@ -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'
task :compile => RUBY_PLATFORM =~ /java/ ? ['compile:jbson', 'compile:jsasl'] : ['compile:cbson', 'compile:csasl']
17 changes: 17 additions & 0 deletions tasks/testing.rake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Loading